12 Commits

12 changed files with 211 additions and 67 deletions

View File

@@ -1,6 +1,6 @@
[Application]
name=Solo Auswertung
version=2.0.0
version=2.1.1
# How to launch the app - this calls the 'main' function from the 'myapp' package:
entry_point=main:main
# icon=myapp.ico
@@ -17,10 +17,18 @@ console=true
# Packages from PyPI that your application requires, one per line
# These must have wheels on PyPI:
pypi_wheels = beautifulsoup4==4.12.3
coloredlogs==15.0.1
flask==3.0.2
tabulate==0.9.0
blinker==1.8.2
click==8.1.7
colorama==0.4.6
coloredlogs==15.0.1
flask==3.0.3
humanfriendly==10.0
itsdangerous==2.2.0
jinja2==3.1.4
markupsafe==2.1.5
soupsieve==2.5
tabulate==0.9.0
werkzeug==3.0.3
packages = solo_turnier

View File

@@ -1,8 +1,6 @@
import argparse
import logging
import debugpy
class Cli:
def __init__(self, l: logging.Logger):
@@ -50,6 +48,8 @@ class Cli:
self.__args = parser.parse_args()
if self.__args.debug:
import debugpy
debugpy.listen(5678)
debugpy.wait_for_client()

View File

@@ -15,6 +15,11 @@ class IncompleteRoundException(Exception):
super(IncompleteRoundException, self).__init__(*args)
class CannotParseRowException(Exception):
def __init__(self, *args):
super(CannotParseRowException, self).__init__(*args)
class HtmlParser:
def __init__(self, text: str, fileName: str = None):
self.l = logging.getLogger("solo_turnier.html_parser")
@@ -37,6 +42,12 @@ class HtmlParser:
title = self.getEventTitle()
match = re.compile('.*?OT, Solos (.*?)(?: ".*")?').fullmatch(title)
if match is None:
self.l.debug(
'Parsing HTML page title "%s" as OT failed. Falling back to legacy ETW.',
title,
)
match = re.compile('.*?ETW, Solos (.*?)(?: ".*")?').fullmatch(title)
if match is None:
self.l.info(
'Cannot parse html title "%s". Is it a solo competition? Possible bug.',
@@ -56,34 +67,50 @@ class HtmlParser:
def parseResult(self) -> HtmlResultImport:
participants = {}
def __parseRows(rows, finalist: bool):
def __parseRow(row):
tds = row.find_all("td")
nameRegex = re.compile("(.*) \\(([0-9]+)\\)")
if len(tds) != 2:
return
def __parseNameAndId(string: str, tds) -> tuple[str, str]:
match = nameRegex.fullmatch(string)
if match is None:
self.l.error("Could not match %s to regex search pattern", str(tds))
raise CannotParseRowException(
f"Could not match {tds} to regex search pattern for 'name (id)'"
)
name = match.group(1)
number = match.group(2)
return name, number
if tds[1].contents[0].startswith("Alle Starter weiter genommen."):
self.l.info("No excluded starters found.")
return
def __parseRows(rows, parsers):
def parseRow(row):
for parser in parsers:
try:
parser(row("td"))
return
except CannotParseRowException:
pass
regex = re.compile("(.*) \\(([0-9]+)\\)")
place = tds[0].contents[0]
match = regex.fullmatch(tds[1].contents[0])
if match is None:
self.l.error("Could not match %s to regex search pattern", str(tds))
raise Exception(f"Could not match {tds} to regex search pattern")
name = match.group(1)
number = match.group(2)
participant = HtmlParticipant(name, number)
participant.finalist = finalist
participants[participant] = place
# No parser was found if we get here.
self.l.error("Cannot parse row in table.")
for row in rows:
__parseRow(row)
parseRow(row)
def __ensureLength(tds, length):
if len(tds) != length:
raise CannotParseRowException(
"The row has %d entries but %d are expected." % (len(tds), length)
)
def __parseFormationRowGeneric(tds, finalist):
__ensureLength(tds, 2)
place = tds[0].contents[0]
name, number = __parseNameAndId(tds[1].contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = finalist
participant.club = ""
participants[participant] = place
def __parseFirstTable(table):
roundName = table.tr.td.contents[0]
@@ -91,11 +118,93 @@ class HtmlParser:
self.l.warning("Found table with round name %s.", roundName)
raise IncompleteRoundException("Could not parse HTML file")
__parseRows(table.find_all("tr")[2:], True)
def __parseFormationRow(tds):
__parseFormationRowGeneric(tds, True)
def __parsePairRow(tds):
__ensureLength(tds, 4)
place = tds[0].contents[0]
tdNameClub = tds[1]
tdClub = tdNameClub.i.extract()
name, number = __parseNameAndId(tdNameClub.contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = True
participant.club = tdClub.contents[0]
participants[participant] = place
__parseRows(
table.find_all("tr")[2:],
[
__parsePairRow,
__parseFormationRow,
],
)
def __parseRemainingTables(tables):
def __parseFormationRow(tds):
__parseFormationRowGeneric(tds, False)
def __parsePairRow(tds):
__ensureLength(tds, 3)
place = tds[0].contents[0]
name, number = __parseNameAndId(tds[1].contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = False
participant.club = tds[2].contents[0]
participants[participant] = place
def __parseSeparatorRow(tds):
__ensureLength(tds, 1)
if len(list(tds[0].stripped_strings)) == 0:
return
raise CannotParseRowException("No empty string")
regexZwischenRunde = re.compile("[1-9]\. Zwischenrunde")
def __parseRoundHeading(tds):
__ensureLength(tds, 1)
s = "".join(tds[0].stripped_strings)
if s.startswith("Vorrunde"):
return
if regexZwischenRunde.match(s) is not None:
return
raise CannotParseRowException("Kein Header einer Runde gefunden.")
def __parseAllSolosQualifiedFormation(tds):
__ensureLength(tds, 2)
if tds[1].contents[0].startswith("Alle Starter weiter genommen."):
return
raise CannotParseRowException(
'Not found the text "Alle Starter weiter genommen"'
)
def __parseAllSolosQualifiedPair(tds):
__ensureLength(tds, 3)
if tds[1].contents[0].startswith("Alle Mannschaften weiter genommen."):
return
raise CannotParseRowException(
'Not found the text "Alle Mannschaften weiter genommen"'
)
for table in tables:
__parseRows(table.find_all("tr"), False)
__parseRows(
table.find_all("tr"),
[
__parseAllSolosQualifiedFormation,
__parseAllSolosQualifiedPair,
__parsePairRow,
__parseFormationRow,
__parseSeparatorRow,
__parseRoundHeading,
],
)
tables = self.soup.find("div", class_="extract").find_all("table")

View File

@@ -117,16 +117,26 @@ class ConsoleOutputter(AbstractOutputter):
placeNative = str(result.nativePlace)
place = str(result.place)
lineOne = f"{placeNative}"
# lineTwo = f"[{place} in {result.competitionClass}]"
lines = [lineOne]
groupCompetition = result.competitionGroup
if isinstance(groupCompetition, solo_turnier.group.CombinedGroup):
lineTwo = f"[{place} in {groupCompetition}]"
lines.append(lineTwo)
if not result.finalist:
lines = ["kein/e Finalist/in"] + lines
return "\n".join(lines)
mappedResults = map(mapResultColumn, results)
tableRow = [f"{participant.name} ({participant.id})"] + list(mappedResults)
participantName = f"{participant.name} ({participant.id})"
if participant.club is not None:
participantName = f"{participantName}, {participant.club}"
tableRow = [f"{participantName}"] + list(mappedResults)
tableData.append(tableRow)
self.l.log(5, "table data: %s", pprint.pformat(tableData))

View File

@@ -1,53 +1,51 @@
.tab-summary {
width: 100%;
border-collapse: collapse;
width: 100%;
border-collapse: collapse;
}
.tab-summary tr:nth-of-type(even) {
background-color: cyan;
background-color: cyan;
}
.tab-summary td {
text-align: center;
text-align: center;
}
.tab-summary td .competition-place {
font-size: smaller;
font-weight: 300;
font-style: italic;
font-size: smaller;
font-weight: 300;
font-style: italic;
}
.tab-summary .no-finalist {
color: gray;
color: gray;
}
.tab-summary .no-finalist-dance {
color: gray;
text-decoration-style: solid;
text-decoration-line: line-through;
color: gray;
text-decoration-style: solid;
text-decoration-line: line-through;
}
@media print {
@page {
size: landscape;
}
@page portrait {
size: portrait;
}
/* body {
@page {
size: landscape;
}
@page portrait {
size: portrait;
}
/* body {
size: landscape;
page-orientation: rotate-right;
} */
.section,
.section table tr,
.section table td
{
page-break-inside: avoid;
}
.section,
.section table tr,
.section table td {
page-break-inside: avoid;
}
.tab-summary .no-finalist {
color: gray;
}
.tab-summary .no-finalist {
color: gray;
}
}

View File

@@ -34,7 +34,12 @@
{% endif %}
{% if participant.finalist or not onlyFinalists %}
<tr class="{{ rowCls }}">
<td>{{ participant.name }} ({{ participant.id }})</td>
<td>
{{ participant.name }} ({{ participant.id }})
{% if participant.club is not none %}
, {{ participant.club}}
{% endif %}
</td>
{% for dance in data.resultsPerGroup[group].dances %}
{% block danceResult scoped %}
{% set res = activeGroup[participant][loop.index0] %}
@@ -45,6 +50,10 @@
{% endif %}
<span class="{% if not res.finalist %}no-finalist-dance{% endif %}">
{{ res.getNativePlace() }}
{% if res.isCombinedGroup() %}
<br />
({{ res.place }} {{ res.competitionGroup }})
{% endif %}
</span>
{% endif %}
</td>

View File

@@ -3,6 +3,7 @@ class HtmlParticipant:
self.name = name
self.id = id
self.finalist = None
self.club = None
def __eq__(self, o):
if type(o) != HtmlParticipant:

View File

@@ -2,10 +2,11 @@ from .place import Place
class HtmlSingleCompetitionResult:
def __init__(self, name: str, place: Place, finalist: bool):
def __init__(self, name: str, place: Place, finalist: bool, club: str):
self.name = name
self.place = place
self.finalist = finalist
self.club = club
def __repr__(self):
place = self.place

View File

@@ -9,10 +9,12 @@ class Participant(Person):
name: str,
id: int,
finalist: bool = None,
club: str = None,
):
super().__init__(name)
self.id = id
self.finalist = finalist
self.club = club
def __repr__(self):
if self.finalist == True:

View File

@@ -7,6 +7,7 @@ class SingleParticipantResult:
def __init__(
self,
competitionClass: solo_turnier.competition_class.Class_t,
competitionGroup: solo_turnier.group.Group_t,
nativeClass: solo_turnier.competition_class.CompetitionClass,
dance: str,
finalist: bool,
@@ -14,6 +15,7 @@ class SingleParticipantResult:
nativePlace: Place = None,
):
self.competitionClass = competitionClass
self.competitionGroup = competitionGroup
self.nativeClass = nativeClass
self.dance = dance
self.finalist = finalist
@@ -27,3 +29,6 @@ class SingleParticipantResult:
def getNativePlace(self) -> str:
return str(self.nativePlace)
def isCombinedGroup(self) -> bool:
return isinstance(self.competitionGroup, solo_turnier.group.CombinedGroup)

View File

@@ -85,7 +85,7 @@ class ResultExtractor:
placeStr = result.results[person]
place = self._extractPlace(placeStr)
competitionResult = types.HtmlSingleCompetitionResult(
person.name, place, person.finalist
person.name, place, person.finalist, person.club
)
results.add(
competitionGroup,

View File

@@ -42,10 +42,10 @@ class Worker:
self.l.debug("Found groups in the dataset: %s", groups)
invertedGroupMapping = self._invertGroupMapping(groupMapping, groups)
self.l.log(5, "Inverted group maping: %s", invertedGroupMapping)
self.l.log(5, "Inverted group mapping: %s", invertedGroupMapping)
idToParticipantMapping = self._invertIdMapping(importedData.htmlResults)
self.l.log(5, "Id to participant mappting: %s", idToParticipantMapping)
self.l.log(5, "Id to participant mapping: %s", idToParticipantMapping)
totalResult = {}
@@ -96,6 +96,7 @@ class Worker:
singleResult = solo_turnier.types.SingleParticipantResult(
competitionClass=tup.class_,
nativeClass=tup.class_,
competitionGroup=tup.group,
dance=tup.dance,
finalist=singleHtmlResult.finalist,
place=singleHtmlResult.place,
@@ -329,7 +330,7 @@ class Worker:
if id not in mapping:
mapping[id] = solo_turnier.types.Participant(
name=results[0].name, id=id
name=results[0].name, id=id, club=results[0].club
)
else:
if mapping[id].name != results[0].name or mapping[id].id != id: