Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0198ecf752 | |||
| 801c7f2bbd | |||
| 16ed0280cd | |||
| 5712c3da63 | |||
| 3b98cef590 | |||
| 4bc626b163 | |||
| 533f3ef237 | |||
| 61a607e516 | |||
| cd8ab716b8 | |||
| a7f8bad0ec | |||
| 7ec359d2f2 | |||
| 5c4b0106fc |
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,46 +67,144 @@ 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
|
||||
|
||||
if tds[1].contents[0].startswith("Alle Starter weiter genommen."):
|
||||
self.l.info("No excluded starters found.")
|
||||
return
|
||||
|
||||
regex = re.compile("(.*) \\(([0-9]+)\\)")
|
||||
|
||||
place = tds[0].contents[0]
|
||||
|
||||
match = regex.fullmatch(tds[1].contents[0])
|
||||
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 Exception(f"Could not match {tds} to regex search pattern")
|
||||
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
|
||||
|
||||
def __parseRows(rows, parsers):
|
||||
def parseRow(row):
|
||||
for parser in parsers:
|
||||
try:
|
||||
parser(row("td"))
|
||||
return
|
||||
except CannotParseRowException:
|
||||
pass
|
||||
|
||||
# No parser was found if we get here.
|
||||
self.l.error("Cannot parse row in table.")
|
||||
|
||||
for row in rows:
|
||||
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
|
||||
|
||||
for row in rows:
|
||||
__parseRow(row)
|
||||
|
||||
def __parseFirstTable(table):
|
||||
roundName = table.tr.td.contents[0]
|
||||
if roundName != "Endrunde":
|
||||
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")
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.tab-summary {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -42,8 +41,7 @@
|
||||
|
||||
.section,
|
||||
.section table tr,
|
||||
.section table td
|
||||
{
|
||||
.section table td {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user