From 8298cd85a860b3008920c59697aec315d0bddb41 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 13 Nov 2023 09:31:43 +0100 Subject: [PATCH 01/31] Added file changes made live during competition to make things run smoothly --- src/solo_turnier/types.py | 12 +++++++ src/solo_turnier/worker.py | 70 +++++++++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/solo_turnier/types.py b/src/solo_turnier/types.py index 94f08ac..35e37c0 100644 --- a/src/solo_turnier/types.py +++ b/src/solo_turnier/types.py @@ -60,6 +60,9 @@ class HtmlParticipant: def __hash__(self): return hash((self.id, self.name)) + + def __gt__(self, other): + return self.id >= other.id # class PreviewParticipationData: # def __init__(self, dance: str, class_: competition_class.CompetitionClass): @@ -144,6 +147,15 @@ class HtmlSingleCompetitionResult: return f'Res({self.name} [F], placed {place})' else: return f'Res({self.name}, placed {place})' + + def __gt__(self,other): + return self.id > other.id + + def __eq__(self, other): + return self.id == other.id + + def __hash__(self): + return hash(self.id) class HtmlCompetitionTotalResults: def __init__(self): diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index b5aa7ba..a8e2224 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -456,7 +456,7 @@ class Worker: dances = self._extractDancesPerGroup(importedData, group) self.l.log(5, 'Found dances in group %s: %s', group, dances) - participants = self._extractParticipantsPerGroup(importedData.previewImport, group) + participants = self._extractParticipantsPerGroup(importedData, group) self.l.log(5, 'Related participants %s', participants) results = {} @@ -485,24 +485,35 @@ class Worker: def _extractGroups(self, data: types.State3): + groupParser = solo_turnier.group.GroupParser() + groupSet = set([]) - for id in data.previewImport.participants: - participants = data.previewImport.participants[id] - for participant in participants: - groupSet.add(participant.group) + # for id in data.previewImport.participants: + # participants = data.previewImport.participants[id] + # for participant in participants: + # groupSet.add(participant.group) + for tup in data.htmlResults.results.keys(): + gr = groupParser.parseClass(tup[0]) + groupSet.add(gr) + # self.l.log(5, 'Group type %s', type(gr)) self.l.log(5, 'Set of active groups: %s', groupSet) - groupParser = solo_turnier.group.GroupParser() groups = groupParser.getGroupsAsSortedList(groupSet) return groups def _extractDancesPerGroup(self, data: types.State3, group: solo_turnier.group.Group): + groupParser = solo_turnier.group.GroupParser() + dances = set() additionalDances = set() - for part in data.previewImport.results.keys(): - allFoundDances = set(data.previewImport.results[part].keys()) - dances.update(allFoundDances.intersection(self._allDances)) - additionalDances.update(allFoundDances.difference(self._allDances)) + for tup in data.htmlResults.results.keys(): + if not groupParser.parseClass(tup[0]) == group: + continue + dances.add(tup[2]) + # for part in data.previewImport.results.keys(): + # allFoundDances = set(data.previewImport.results[part].keys()) + # dances.update(allFoundDances.intersection(self._allDances)) + # additionalDances.update(allFoundDances.difference(self._allDances)) if len(additionalDances) > 0: self.l.warning('There were dances found, that are not registered. A bug? The dances were: %s', additionalDances) @@ -514,27 +525,45 @@ class Worker: def _extractParticipantsPerGroup( self, - previewData: types.HtmlPreviewImport, + importedData: types.State3, + # previewData: types.HtmlPreviewImport, group: solo_turnier.group.Group ) -> list[types.HtmlPreviewParticipant]: + groupParser = types.group.GroupParser() + ret = [] - for id in previewData.participants: - participantList = previewData.participants[id] - for participant in participantList: - if participant.group == group: - ret.append(participant) + + self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) + self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) + + for tup in importedData.htmlResults.results.keys(): + gr = groupParser.parseClass(tup[0]) + if not gr == group: + continue + part = importedData.htmlResults.results[tup][0] + part.id = int(tup[3]) + ret.append(part) + + self.l.log(5, 'ret %s', ret) + # raise Exception('Test') + + # for id in previewData.participants: + # participantList = previewData.participants[id] + # for participant in participantList: + # if participant.group == group: + # ret.append(participant) return ret def _getResultOfSingleParticipant( self, - participant: types.HtmlPreviewParticipant, + participant: types.HtmlParticipant, nominalGroup: solo_turnier.group.Group, previewResults: types.HtmlPreviewImport, totalResults: types.HtmlCompetitionTotalResults, allDances: list[str] ) -> list[types.SingleParticipantResult|None]: rawResults = totalResults.getById(participant.id) - self.l.log(5, 'Found result data (raw): %s', rawResults) + self.l.log(5, 'Found result data for id %i (raw): %s', participant.id, rawResults) results = [None for x in allDances] @@ -589,7 +618,10 @@ class Worker: keys = list(importedData.tabges.keys()) selected = list(map(selectEntry, keys)) - selectedIndex = selected.index(True) + try: + selectedIndex = selected.index(True) + except: + continue raw = importedData.tabges[keys[selectedIndex]] self.l.log(5,'Raw %s', raw) From 638b2a300269e3abb4861b1f2f6234c0c4cb393c Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 13:04:01 +0100 Subject: [PATCH 02/31] Reenable checking of additional dance to make bugs more prominent --- src/solo_turnier/worker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index a8e2224..80ed776 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -506,17 +506,17 @@ class Worker: dances = set() additionalDances = set() + foundDances = set() for tup in data.htmlResults.results.keys(): if not groupParser.parseClass(tup[0]) == group: continue - dances.add(tup[2]) - # for part in data.previewImport.results.keys(): - # allFoundDances = set(data.previewImport.results[part].keys()) - # dances.update(allFoundDances.intersection(self._allDances)) - # additionalDances.update(allFoundDances.difference(self._allDances)) + foundDances.add(tup[2]) + dances.update(foundDances.intersection(self._allDances)) + additionalDances.update(foundDances.difference(self._allDances)) + if len(additionalDances) > 0: - self.l.warning('There were dances found, that are not registered. A bug? The dances were: %s', additionalDances) + self.l.error('There were dances found, that are not registered. A bug? The dances were: %s', additionalDances) dancesList = [x for x in self._allDances if x in dances] additionalDancesList = list(additionalDances) From f075e1e1679cb4b25d126f250a8fae9c127c8d6b Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 13:18:12 +0100 Subject: [PATCH 03/31] Reducing log level of information about dropped non-finalists --- src/solo_turnier/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index 80ed776..1973632 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -721,7 +721,7 @@ class Worker: participant.finalist = True else: participant.finalist = False - self.l.warning('Dropping %s from the output as no finalist', participant) + self.l.info('Dropping %s from the output as no finalist', participant) droppedParticipants.append(participant) if filterOut: From c4b2d0a23b74109ec4241831e8d259ee74db79ef Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 13:19:32 +0100 Subject: [PATCH 04/31] Enhanced matching of HTML title w.r.t custom alias in TT --- src/solo_turnier/html_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index 45b3f32..9eb97f4 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -34,8 +34,9 @@ class HtmlParser: if title is None: title = self.getEventTitle() - match = re.compile('.*?ETW, Solos (.*)').match(title) + match = re.compile('.*?ETW, Solos (.*?)(?: ".*")?').fullmatch(title) if match is None: + self.l.error('Cannot parse html title "%s". Possible bug?', title) raise Exception(f'Cannot parse title "{title}"') rest = match.group(1) From 727ce0ca3a5f58972ce9f8a56026f09f18261a99 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 14:03:48 +0100 Subject: [PATCH 05/31] Automation of test to compare with previous runs --- run-all-examples.sh | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 run-all-examples.sh diff --git a/run-all-examples.sh b/run-all-examples.sh new file mode 100755 index 0000000..cf2a5a6 --- /dev/null +++ b/run-all-examples.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +if [ $# -ne 1 ] +then + echo "Please give path of test cases as parameter." + exit 1 +fi + +rundir=$(realpath "$(dirname "$0")") + +cd "$1" + +for i in Turnier\ * +do + echo "Running on data in $i" + + if [ ! -r "$i/result.table" ] + then + echo "No result file is found. Skipping." + continue + fi + + tmp=$(mktemp) + "$rundir/solo_runner.sh" --no-flask -a "$i/HTML" > "$tmp" + + if diff -u "$i/result.table" "$tmp" > /dev/null + then + rm "$tmp" + else + echo "Differences found in competition $i" + mv "$tmp" "$i/result2.table" + fi + +done From 30e4a43a9fcc64a181ecac4e665f2d3559cfa70a Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 14:20:12 +0100 Subject: [PATCH 06/31] Extract groups for pure combined competitions --- src/solo_turnier/group.py | 6 ++++++ src/solo_turnier/worker.py | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index 7e0f13d..0f10ec5 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -6,6 +6,9 @@ class Group: def __repr__(self): return self.name + + def getContainedGroups(self): + return (self,) class CombinedGroup: def __init__(self, grpA: Group, grpB: Group): @@ -14,6 +17,9 @@ class CombinedGroup: def __repr__(self): return f'{self.clsA}/{self.clsB}' + + def getContainedGroups(self): + return (self.clsA, self.clsB) Group_t = Group | CombinedGroup diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index 1973632..c4527b0 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -494,7 +494,8 @@ class Worker: # groupSet.add(participant.group) for tup in data.htmlResults.results.keys(): gr = groupParser.parseClass(tup[0]) - groupSet.add(gr) + # groupSet.add(gr) + groupSet.update(gr.getContainedGroups()) # self.l.log(5, 'Group type %s', type(gr)) self.l.log(5, 'Set of active groups: %s', groupSet) @@ -508,7 +509,8 @@ class Worker: additionalDances = set() foundDances = set() for tup in data.htmlResults.results.keys(): - if not groupParser.parseClass(tup[0]) == group: + currentGroup = groupParser.parseClass(tup[0]) + if group not in currentGroup.getContainedGroups(): continue foundDances.add(tup[2]) From ee3789aab9026e01c87d5fd7184489bca987f402 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 16:55:42 +0100 Subject: [PATCH 07/31] Increase consistency of chceks --- src/solo_turnier/worker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index c4527b0..e87d48d 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -535,13 +535,20 @@ class Worker: ret = [] - self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) - self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) + # self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) + # self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) for tup in importedData.htmlResults.results.keys(): - gr = groupParser.parseClass(tup[0]) - if not gr == group: + currentGroup = groupParser.parseClass(tup[0]) + activeGroups = currentGroup.getContainedGroups() + if group not in activeGroups: continue + + fixture = importedData.htmlResults.tabges[tup] + if fixture[2] is not None and fixture[2] != group: + self.l.log(5, 'Skipping id %s in group %s as not part', tup[3], group) + continue + part = importedData.htmlResults.results[tup][0] part.id = int(tup[3]) ret.append(part) From ca9164552ca040123c89094b5a35668720514517 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 17:04:35 +0100 Subject: [PATCH 08/31] Install black as dev dependency --- requiremnts.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requiremnts.txt b/requiremnts.txt index 623b37c..0a75558 100644 --- a/requiremnts.txt +++ b/requiremnts.txt @@ -1,5 +1,6 @@ attrs==22.1.0 beautifulsoup4==4.11.1 +black==23.11.0 blinker==1.6.2 certifi==2023.7.22 charset-normalizer==3.2.0 @@ -17,7 +18,10 @@ iniconfig==1.1.1 itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.3 -packaging==21.3 +mypy-extensions==1.0.0 +packaging==23.2 +pathspec==0.11.2 +platformdirs==4.0.0 pluggy==1.0.0 pynsist==2.8 pyparsing==3.0.9 From 83a0003024d95d551f7dac1258ce5f090367cd53 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 17:07:20 +0100 Subject: [PATCH 09/31] Make style code conformant with black --- src/main.py | 15 +- src/solo_turnier/__init__.py | 1 - src/solo_turnier/batch.py | 28 +- src/solo_turnier/cli.py | 63 +- src/solo_turnier/competition_class.py | 70 +-- src/solo_turnier/flask.py | 24 +- src/solo_turnier/group.py | 123 ++-- src/solo_turnier/html_locator.py | 21 +- src/solo_turnier/html_parser.py | 176 +++--- src/solo_turnier/output.py | 75 +-- src/solo_turnier/participant.py | 36 +- src/solo_turnier/reader.py | 77 ++- .../tests/test_competition_class.py | 42 +- src/solo_turnier/tests/test_csvReader.py | 5 +- src/solo_turnier/tests/test_group.py | 30 +- src/solo_turnier/tests/test_html_locator.py | 10 +- src/solo_turnier/tests/test_html_parser.py | 114 ++-- src/solo_turnier/tests/test_types.py | 9 +- src/solo_turnier/tests/test_worker.py | 506 +++++++++++----- src/solo_turnier/types.py | 264 +++++---- src/solo_turnier/worker.py | 539 +++++++++++------- 21 files changed, 1337 insertions(+), 891 deletions(-) diff --git a/src/main.py b/src/main.py index fcff5d8..90fcbd9 100644 --- a/src/main.py +++ b/src/main.py @@ -2,15 +2,17 @@ import solo_turnier import logging import coloredlogs + def __initLogging(): logging.basicConfig() logging.root.setLevel(logging.NOTSET) - logger = logging.getLogger('solo_turnier') - + logger = logging.getLogger("solo_turnier") + coloredlogs.install(level=5, logger=logger) return logger + def main(): l = __initLogging() cli = solo_turnier.cli.Cli(l) @@ -18,14 +20,14 @@ def main(): batchWorker = solo_turnier.batch.BatchWorker(cli) if cli.showGUI(): - raise Exception('Not yet implemented') + raise Exception("Not yet implemented") elif cli.startFlaskServer(): solo_turnier.flask.startFlask( - batchWorker, + batchWorker, debug=cli.getLogLevel() > 0, port=cli.getPort(), showOnlyFinalists=not cli.showAllParticipants(), - externalDebugger=cli.externalDebugger + externalDebugger=cli.externalDebugger, ) else: combinedData = batchWorker.run( @@ -35,5 +37,6 @@ def main(): consoleOutputtter = solo_turnier.output.ConsoleOutputter() consoleOutputtter.output(combinedData) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/src/solo_turnier/__init__.py b/src/solo_turnier/__init__.py index e06c62f..a321b6d 100644 --- a/src/solo_turnier/__init__.py +++ b/src/solo_turnier/__init__.py @@ -1,4 +1,3 @@ - from . import competition_class from . import group from . import types diff --git a/src/solo_turnier/batch.py b/src/solo_turnier/batch.py index 99de611..ad489e3 100644 --- a/src/solo_turnier/batch.py +++ b/src/solo_turnier/batch.py @@ -1,4 +1,3 @@ - import solo_turnier import logging import os @@ -6,24 +5,31 @@ import pprint import tabulate + class BatchWorker: - def __init__( - self, - config: solo_turnier.cli.Cli - ): - self.l = logging.getLogger('solo_turnier.batch') + def __init__(self, config: solo_turnier.cli.Cli): + self.l = logging.getLogger("solo_turnier.batch") self.config = config def run(self, removeFilteredParicipants=True): self.l.debug(self.config.__dict__) - + locator = solo_turnier.html_locator.HtmlLocator() - self.l.info('Checking for feasible preview HTML export files in "%s"', self.config.importHtmlPath()) - htmlCandidatesPreview = locator.findPreviewRoundCandidates(self.config.importHtmlPath()) - self.l.debug('Found HTML file candidates for preview rounds: %s', htmlCandidatesPreview) + self.l.info( + 'Checking for feasible preview HTML export files in "%s"', + self.config.importHtmlPath(), + ) + htmlCandidatesPreview = locator.findPreviewRoundCandidates( + self.config.importHtmlPath() + ) + self.l.debug( + "Found HTML file candidates for preview rounds: %s", htmlCandidatesPreview + ) htmlResultFiles = locator.findCandidates(self.config.importHtmlPath()) - self.l.debug('Using HTML result files for result extraction: %s', htmlResultFiles) + self.l.debug( + "Using HTML result files for result extraction: %s", htmlResultFiles + ) worker = solo_turnier.worker.Worker() importedData = worker.collectAllData(htmlCandidatesPreview, htmlResultFiles) diff --git a/src/solo_turnier/cli.py b/src/solo_turnier/cli.py index b190663..2d240c5 100644 --- a/src/solo_turnier/cli.py +++ b/src/solo_turnier/cli.py @@ -3,27 +3,58 @@ import logging import debugpy + class Cli: def __init__(self, l: logging.Logger): parser = argparse.ArgumentParser() # parser.add_argument('--gui', help='Show the GUI', action='store_true') - parser.add_argument('--no-flask', action='store_false', dest='flask', help='Disable the internal flask web server') - parser.add_argument('--port', help='The port to listen for incoming requests', default='8082') + parser.add_argument( + "--no-flask", + action="store_false", + dest="flask", + help="Disable the internal flask web server", + ) + parser.add_argument( + "--port", help="The port to listen for incoming requests", default="8082" + ) - parser.add_argument('html', help='The path from where to look for HTML export files', nargs=1, default=['.']) - parser.add_argument('-o', '--output', help='Set the output path of the script', nargs=1, default=[None]) - parser.add_argument('--all-participants', '-a', action='store_true', help='Show all participants not only finalists') + parser.add_argument( + "html", + help="The path from where to look for HTML export files", + nargs=1, + default=["."], + ) + parser.add_argument( + "-o", + "--output", + help="Set the output path of the script", + nargs=1, + default=[None], + ) + parser.add_argument( + "--all-participants", + "-a", + action="store_true", + help="Show all participants not only finalists", + ) - parser.add_argument('-v', '--verbose', help='Increase verbosity', action='count', default=0) - parser.add_argument('-d', '--debug', action='store_true', help='Activate debugging during startup') + parser.add_argument( + "-v", "--verbose", help="Increase verbosity", action="count", default=0 + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="Activate debugging during startup", + ) self.__args = parser.parse_args() if self.__args.debug: debugpy.listen(5678) debugpy.wait_for_client() - + self.externalDebugger = self.__args.debug - + map = { 0: logging.ERROR, 1: logging.WARN, @@ -33,28 +64,28 @@ class Cli: } logLevel = map.get(self.__args.verbose, logging.DEBUG) l.setLevel(logLevel) - + def showGUI(self): # return self.__args.gui return False - + def startFlaskServer(self): return self.__args.flask def importHtmlPath(self): return self.__args.html[0] - + def importCSVPath(self): return self.__args.import_from[0] - + def output(self): return self.__args.output[0] - + def getLogLevel(self): return self.__args.verbose - + def showAllParticipants(self): return self.__args.all_participants - + def getPort(self): return int(self.__args.port) diff --git a/src/solo_turnier/competition_class.py b/src/solo_turnier/competition_class.py index 20e1107..d24f0e1 100644 --- a/src/solo_turnier/competition_class.py +++ b/src/solo_turnier/competition_class.py @@ -1,74 +1,80 @@ import re + class CompetitionClass: def __init__(self, text: str): self.name = text - + def __repr__(self): return self.name + class CombinedCompetitionClass: - def __init__(self, clsA: CompetitionClass, clsB: CompetitionClass, clsC: CompetitionClass = None): + def __init__( + self, + clsA: CompetitionClass, + clsB: CompetitionClass, + clsC: CompetitionClass = None, + ): self.clsA = clsA self.clsB = clsB self.clsC = clsC - + def __repr__(self): if self.clsC is None: - return f'{self.clsA}/{self.clsB}' + return f"{self.clsA}/{self.clsB}" else: - return f'{self.clsA}/{self.clsB}/{self.clsC}' + return f"{self.clsA}/{self.clsB}/{self.clsC}" + Class_t = CompetitionClass | CombinedCompetitionClass + class CompetitionClassParser: - NEWC = CompetitionClass('Newc.') - BEG = CompetitionClass('Beg.') - ADV = CompetitionClass('Adv.') + NEWC = CompetitionClass("Newc.") + BEG = CompetitionClass("Beg.") + ADV = CompetitionClass("Adv.") + + PREVIEW = CompetitionClass("Sichtung") - PREVIEW = CompetitionClass('Sichtung') - def __init__(self): - self.mapNames = { - 'Newc': self.NEWC, - 'Newc.': self.NEWC, - 'Newcomer': self.NEWC, - 'Beg': self.BEG, - 'Beg.': self.BEG, - 'Beginner': self.BEG, - 'Adv': self.ADV, - 'Adv.': self.ADV, - 'Advanced': self.ADV, + "Newc": self.NEWC, + "Newc.": self.NEWC, + "Newcomer": self.NEWC, + "Beg": self.BEG, + "Beg.": self.BEG, + "Beginner": self.BEG, + "Adv": self.ADV, + "Adv.": self.ADV, + "Advanced": self.ADV, } - self.namesPreview = [ - 'Sichtung' - ] + self.namesPreview = ["Sichtung"] self.mapShortNames = { - 'N': self.NEWC, - 'B': self.BEG, - 'A': self.ADV, + "N": self.NEWC, + "B": self.BEG, + "A": self.ADV, } def parseClass(self, cls: str, allowPreview: bool = False) -> Class_t: if allowPreview and cls in self.namesPreview: - return self.PREVIEW - - match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls) + return self.PREVIEW + + match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls) if match is not None: clsA = self.mapNames[match.group(1)] clsB = self.mapNames[match.group(2)] return CombinedCompetitionClass(clsA, clsB) else: return self.mapNames[cls] - + def parseAbbreviatedClass(self, cls: str) -> Class_t: return self.mapShortNames[cls] - + def isPureClass(self, cls: str, allowPreview: bool = False) -> bool: parsedClass = self.parseClass(cls, allowPreview) return isinstance(parsedClass, CompetitionClass) - + def getAllClasses(self) -> list[CompetitionClass]: return [self.NEWC, self.BEG, self.ADV] diff --git a/src/solo_turnier/flask.py b/src/solo_turnier/flask.py index ab5723c..5864a50 100644 --- a/src/solo_turnier/flask.py +++ b/src/solo_turnier/flask.py @@ -4,29 +4,29 @@ import logging _l = logging.getLogger(__name__) + def startFlask( batchWorker: solo_turnier.batch.BatchWorker, debug: bool = False, port: int = 8082, showOnlyFinalists: bool = True, externalDebugger: bool = False, - ): +): app = flask.Flask(__name__) - @app.route('/') + @app.route("/") def index(): combinedData = batchWorker.run(False) - _l.debug('Show only finalists %s', showOnlyFinalists) + _l.debug("Show only finalists %s", showOnlyFinalists) - return flask.render_template('index.html', data=combinedData, onlyFinalists=showOnlyFinalists) - - @app.get('/custom.css') - def css(): - ret = flask.render_template( - 'custom.css', - onlyFinalists=showOnlyFinalists + return flask.render_template( + "index.html", data=combinedData, onlyFinalists=showOnlyFinalists ) - return flask.Response(ret, mimetype='text/css') + + @app.get("/custom.css") + def css(): + ret = flask.render_template("custom.css", onlyFinalists=showOnlyFinalists) + return flask.Response(ret, mimetype="text/css") useReloader = debug and not externalDebugger - app.run(host='0.0.0.0', port=port, debug=debug, use_reloader=useReloader) + app.run(host="0.0.0.0", port=port, debug=debug, use_reloader=useReloader) diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index 0f10ec5..bd6f813 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -1,102 +1,101 @@ import re + class Group: def __init__(self, text: str): self.name = text - + def __repr__(self): return self.name - + def getContainedGroups(self): return (self,) + class CombinedGroup: def __init__(self, grpA: Group, grpB: Group): self.clsA = grpA self.clsB = grpB - + def __repr__(self): - return f'{self.clsA}/{self.clsB}' - + return f"{self.clsA}/{self.clsB}" + def getContainedGroups(self): return (self.clsA, self.clsB) + Group_t = Group | CombinedGroup + class GroupParser: - KIN = Group('Kin.') - JUN = Group('Jun.') - JUG = Group('Jug.') - HGR = Group('Hgr.') - MAS1 = Group('Mas. I') - MAS2 = Group('Mas. II') - MAS3 = Group('Mas. III') - MAS4 = Group('Mas. IV') - MAS5 = Group('Mas. V') + KIN = Group("Kin.") + JUN = Group("Jun.") + JUG = Group("Jug.") + HGR = Group("Hgr.") + MAS1 = Group("Mas. I") + MAS2 = Group("Mas. II") + MAS3 = Group("Mas. III") + MAS4 = Group("Mas. IV") + MAS5 = Group("Mas. V") def __init__(self): - self.mapNames = { - 'Kin': self.KIN, - 'Kin.': self.KIN, - 'Kinder': self.KIN, - - 'Jun': self.JUN, - 'Jun.': self.JUN, - 'Junioren': self.JUN, - - 'Jug': self.JUG, - 'Jug.': self.JUG, - 'Jugend': self.JUG, - - 'Hgr': self.HGR, - 'HGr': self.HGR, - 'Hgr.': self.HGR, - 'HGr.': self.HGR, - 'Hauptgruppe': self.HGR, - - 'Mas. I': self.MAS1, - 'Mas. II': self.MAS2, - 'Mas. III': self.MAS3, - 'Mas. IV': self.MAS4, - 'Mas. V': self.MAS5, - 'Mas I': self.MAS1, - 'Mas II': self.MAS2, - 'Mas III': self.MAS3, - 'Mas IV': self.MAS4, - 'Mas V': self.MAS5, - 'Masters I': self.MAS1, - 'Masters II': self.MAS2, - 'Masters III': self.MAS3, - 'Masters IV': self.MAS4, - 'Masters V': self.MAS5, + "Kin": self.KIN, + "Kin.": self.KIN, + "Kinder": self.KIN, + "Jun": self.JUN, + "Jun.": self.JUN, + "Junioren": self.JUN, + "Jug": self.JUG, + "Jug.": self.JUG, + "Jugend": self.JUG, + "Hgr": self.HGR, + "HGr": self.HGR, + "Hgr.": self.HGR, + "HGr.": self.HGR, + "Hauptgruppe": self.HGR, + "Mas. I": self.MAS1, + "Mas. II": self.MAS2, + "Mas. III": self.MAS3, + "Mas. IV": self.MAS4, + "Mas. V": self.MAS5, + "Mas I": self.MAS1, + "Mas II": self.MAS2, + "Mas III": self.MAS3, + "Mas IV": self.MAS4, + "Mas V": self.MAS5, + "Masters I": self.MAS1, + "Masters II": self.MAS2, + "Masters III": self.MAS3, + "Masters IV": self.MAS4, + "Masters V": self.MAS5, } def parseClass(self, cls: str) -> Group_t: - match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls) + match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls) if match is not None: grpA = self.mapNames[match.group(1)] grpB = self.mapNames[match.group(2)] return CombinedGroup(grpA, grpB) else: return self.mapNames[cls] - + def isPureClass(self, cls: str) -> bool: parsedClass = self.parseClass(cls) return isinstance(parsedClass, Group) - + def getGroups(self) -> list[Group]: - return[ - GroupParser.KIN, - GroupParser.JUN, - GroupParser.JUG, - GroupParser.HGR, - GroupParser.MAS1, - GroupParser.MAS2, - GroupParser.MAS3, - GroupParser.MAS4, - GroupParser.MAS5 + return [ + GroupParser.KIN, + GroupParser.JUN, + GroupParser.JUG, + GroupParser.HGR, + GroupParser.MAS1, + GroupParser.MAS2, + GroupParser.MAS3, + GroupParser.MAS4, + GroupParser.MAS5, ] - + def getGroupsAsSortedList(self, groups) -> list[Group]: return [x for x in self.getGroups() if x in groups] diff --git a/src/solo_turnier/html_locator.py b/src/solo_turnier/html_locator.py index 4adbe7d..429ab6b 100644 --- a/src/solo_turnier/html_locator.py +++ b/src/solo_turnier/html_locator.py @@ -1,36 +1,37 @@ import os import logging + class HtmlLocator: def __init__(self): - self.l = logging.getLogger('solo_turnier.html_locator') - + self.l = logging.getLogger("solo_turnier.html_locator") + def __findRecursivelyCandidates(self, path: str, fileName: str): ret = [] ls = os.listdir(path) - + if fileName in ls and os.path.isfile(os.path.join(path, fileName)): ret.append(os.path.join(path, fileName)) - + for p in ls: subPath = os.path.join(path, p) if os.path.isdir(subPath): ret = ret + self.__findRecursivelyCandidates(subPath, fileName) - + return ret def __fingMatchingTabs(self, ergCandidate): path = os.path.dirname(ergCandidate) - tabPath = os.path.join(path, 'tabges.htm') + tabPath = os.path.join(path, "tabges.htm") if not os.path.exists(tabPath): tabPath = None return (ergCandidate, tabPath) - + def findCandidates(self, path: str): - candidatesErg = self.__findRecursivelyCandidates(path, 'erg.htm') + candidatesErg = self.__findRecursivelyCandidates(path, "erg.htm") candidates = [self.__fingMatchingTabs(x) for x in candidatesErg] return candidates - + def findPreviewRoundCandidates(self, path: str): - candidates = self.__findRecursivelyCandidates(path, 'tabges.htm') + candidates = self.__findRecursivelyCandidates(path, "tabges.htm") return candidates diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index 9eb97f4..f74f795 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -8,44 +8,45 @@ from .types import HtmlPreviewImport as HtmlImport, HtmlResultImport from .group import GroupParser from .competition_class import CompetitionClassParser + class IncompleteRoundException(Exception): def __init__(self, *args): super(IncompleteRoundException, self).__init__(*args) -class HtmlParser: +class HtmlParser: def __init__(self, text: str, fileName: str = None): - self.l = logging.getLogger('solo_turnier.html_parser') - self.soup = BeautifulSoup(text, 'html.parser') + self.l = logging.getLogger("solo_turnier.html_parser") + self.soup = BeautifulSoup(text, "html.parser") self.fileName = fileName self.groupParser = GroupParser() self.classParser = CompetitionClassParser() def __repr__(self): if self.fileName is None: - return 'HtmlParser(direct text)' + return "HtmlParser(direct text)" else: - return f'HtmlParser({self.fileName})' - - def getEventTitle(self): - return self.soup.find('div', class_='eventhead').table.tr.td.contents[0] + return f"HtmlParser({self.fileName})" - def guessDataFromHtmlTitle(self, title = None): + def getEventTitle(self): + return self.soup.find("div", class_="eventhead").table.tr.td.contents[0] + + def guessDataFromHtmlTitle(self, title=None): if title is None: title = self.getEventTitle() - + match = re.compile('.*?ETW, Solos (.*?)(?: ".*")?').fullmatch(title) if match is None: self.l.error('Cannot parse html title "%s". Possible bug?', title) raise Exception(f'Cannot parse title "{title}"') - + rest = match.group(1) - rawGroup, rawClass, dance = rest.split(' ', 2) + rawGroup, rawClass, dance = rest.split(" ", 2) return { - 'dance': dance.strip(), - 'class_': str(self.classParser.parseClass(rawClass, True)), - 'group': str(self.groupParser.parseClass(rawGroup)) + "dance": dance.strip(), + "class_": str(self.classParser.parseClass(rawClass, True)), + "group": str(self.groupParser.parseClass(rawGroup)), } def parseResult(self): @@ -53,47 +54,47 @@ class HtmlParser: def __parseRows(rows, finalist: bool): def __parseRow(row): - tds = row.find_all('td') + tds = row.find_all("td") if len(tds) != 2: return - if tds[1].contents[0].startswith('Alle Starter weiter genommen.'): - self.l.info('No excluded starters found.') + if tds[1].contents[0].startswith("Alle Starter weiter genommen."): + self.l.info("No excluded starters found.") return - regex = re.compile('(.*) \\(([0-9]+)\\)') - + 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') + 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 - + 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) + 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 __parseRemainingTables(tables): for table in tables: - __parseRows(table.find_all('tr'), False) + __parseRows(table.find_all("tr"), False) + + tables = self.soup.find("div", class_="extract").find_all("table") - tables = self.soup.find('div', class_='extract').find_all('table') - try: if len(tables) > 0: __parseFirstTable(tables[0]) @@ -105,61 +106,58 @@ class HtmlParser: # title = self.soup.find('div', class_='eventhead').table.tr.td.contents[0] # ret = HtmlImport(title, participants) - ret = HtmlResultImport(participants) + ret = HtmlResultImport(participants) return ret def parsePreparationRound(self): - title = self.soup.find('div', class_='eventhead').table.tr.td.contents[0] + title = self.soup.find("div", class_="eventhead").table.tr.td.contents[0] tableData = [] rowTitles = [] def __mapBr(td): - for br in td.find_all('br'): - br.replace_with('\n') + for br in td.find_all("br"): + br.replace_with("\n") td.smooth() return td def __extractTitles(table): - for row in table.find_all('tr')[1:]: + for row in table.find_all("tr")[1:]: rowTitles.append(__mapBr(row.td).string) - + def __extractColumns(table): content = [] def __extractContent(td): - for br in td.find_all('br'): - br.replace_with('\n') - + for br in td.find_all("br"): + br.replace_with("\n") + span = td.span if span is not None: span = span.extract() meta = span.string else: meta = None - + td.smooth() - return { - 'text': td.string.replace('\xa0', ' ').strip(), - 'meta': meta - } + return {"text": td.string.replace("\xa0", " ").strip(), "meta": meta} def __extractRow(row): entries = [] - for entry in row.find_all('td')[1:]: + for entry in row.find_all("td")[1:]: entries.append(__extractContent(entry)) return entries - - for row in table.find_all('tr')[1:]: + + for row in table.find_all("tr")[1:]: content.append(__extractRow(row)) - + return content - + def __mergeColumns(columns1, columns2): return list(map(lambda x, y: x + y, columns1, columns2)) - extract = self.soup.find('div', class_='extract') - tables = extract.find_all('table', class_='tab1') + extract = self.soup.find("div", class_="extract") + tables = extract.find_all("table", class_="tab1") __extractTitles(tables[0]) tableData = __extractColumns(tables[0]) @@ -167,94 +165,96 @@ class HtmlParser: for table in tables[1:]: tableData = __mergeColumns(tableData, __extractColumns(table)) - data = { - 'titles': rowTitles, - 'table': tableData - } + data = {"titles": rowTitles, "table": tableData} + + return {"title": title, "data": data} - return {'title': title, 'data': data} - def cleanPreparationRoundImport(self, data): def __cleanTable(table): def __cleanText(s: str): # print("cleaning string ", s) - return s.strip(' \n\xa0') - + return s.strip(" \n\xa0") + def __cleanEntry(entry): - entry['text'] = __cleanText(entry['text']) - if entry['meta'] is not None: - entry['meta'] = __cleanText(entry['meta']) - + entry["text"] = __cleanText(entry["text"]) + if entry["meta"] is not None: + entry["meta"] = __cleanText(entry["meta"]) + for row in table: for entry in row: # print(entry) __cleanEntry(entry) - data['title'] = data['title'].strip() - __cleanTable(data['data']['table']) + data["title"] = data["title"].strip() + __cleanTable(data["data"]["table"]) def parseIndividualResult(self, competitionGroup, competitionClass, dance): - participants = {} def __parseTable(table): - rows = table.find_all('tr') + rows = table.find_all("tr") def __getIds(): row = rows[1] - entries = row('td') + entries = row("td") entries = entries[1:] entries = [x for x in entries if len(x.contents[0].strip()) > 0] return [x.contents[0].strip() for x in entries] - + ids = __getIds() numIds = len(ids) - self.l.log(5, 'Found ids in dataset: %s', ids) + self.l.log(5, "Found ids in dataset: %s", ids) def findRowIndex(prefixStr): def isRowMatchingCriteria(row): if row.td.contents[0].startswith(prefixStr): return True return False + l = list(map(isRowMatchingCriteria, rows)) if True not in l: return None return l.index(True) - + def getPlaces(): - placeRowIdx = findRowIndex('Platz von') - placeTags = rows[placeRowIdx]('td')[1:(numIds+1)] + placeRowIdx = findRowIndex("Platz von") + placeTags = rows[placeRowIdx]("td")[1 : (numIds + 1)] + def getSinglePlaceStr(tag): - for br in tag('br'): - br.replace_with('-') + for br in tag("br"): + br.replace_with("-") tag.smooth() rawStr = tag.contents[0].strip() - if rawStr.endswith('-'): + if rawStr.endswith("-"): rawStr = rawStr[:-1] return rawStr + places = list(map(getSinglePlaceStr, placeTags)) return places + places = getPlaces() - self.l.log(5, 'Found places: %s', places) + self.l.log(5, "Found places: %s", places) def getClass(): - classRow = findRowIndex('Startklasse') + classRow = findRowIndex("Startklasse") if classRow is not None: - classTags = rows[classRow]('td')[1:(numIds+1)] + classTags = rows[classRow]("td")[1 : (numIds + 1)] return list(map(lambda x: x.contents[0], classTags)) return None + classes = getClass() - self.l.log(5, 'Classes: %s', classes) + self.l.log(5, "Classes: %s", classes) def getGroups(): - groupRow = findRowIndex('Startgruppe') + groupRow = findRowIndex("Startgruppe") if groupRow is not None: - classTags = rows[groupRow]('td')[1:(numIds+1)] + classTags = rows[groupRow]("td")[1 : (numIds + 1)] return list(map(lambda x: x.contents[0], classTags)) return None + groups = getGroups() - self.l.log(5, 'Groups: %s', groups) + self.l.log(5, "Groups: %s", groups) for idx, id in enumerate(ids): cls = classes[idx] if classes is not None else None @@ -263,8 +263,8 @@ class HtmlParser: tup = (competitionGroup, competitionClass, dance, id) participants[tup] = (places[idx], cls, grp) - tables = self.soup.find('div', class_='extract').find_all('table') + tables = self.soup.find("div", class_="extract").find_all("table") for table in tables: __parseTable(table) - - return HtmlResultTotalTable( participants) + + return HtmlResultTotalTable(participants) diff --git a/src/solo_turnier/output.py b/src/solo_turnier/output.py index 093ab7b..3ee874a 100644 --- a/src/solo_turnier/output.py +++ b/src/solo_turnier/output.py @@ -1,4 +1,3 @@ - import logging from tabulate import tabulate import pprint @@ -6,14 +5,15 @@ import pprint import solo_turnier from solo_turnier import types -sections = ('Kin.', 'Jun.', 'Jug.', 'Sonst') +sections = ("Kin.", "Jun.", "Jug.", "Sonst") sectionMap = { - 'Kin.': 'Kinder', - 'Jun.': 'Junioren', - 'Jug.': 'Jugend', - 'Sonst': 'Undefiniert' + "Kin.": "Kinder", + "Jun.": "Junioren", + "Jug.": "Jugend", + "Sonst": "Undefiniert", } + class AbstractOutputter: def __init__(self): self.worker = solo_turnier.worker.DataWorker() @@ -24,24 +24,26 @@ class AbstractOutputter: def getRowData(self, person: solo_turnier.worker.ResultPerson, results): mappedResults = self.worker.mapPersonResultsToDanceList(results, self.dances) if self.showIds: - name = f'{person.name} ({person.id})' + name = f"{person.name} ({person.id})" else: name = person.name ret = [name] for result in mappedResults: if result is None: - ret.append('') + ret.append("") elif result.finalist == False: - ret.append('x') + ret.append("x") elif result.place == result.placeTo: - ret.append(f'{result.place}. ({result.class_})') + ret.append(f"{result.place}. ({result.class_})") else: - ret.append(f'{result.place}.-{result.placeTo}. ({result.class_})') + ret.append(f"{result.place}.-{result.placeTo}. ({result.class_})") return ret def getTabularData(self, data, section): - sortedPersons, self.showIds = self.worker.sortPersonsInGroup(self.groups[section]) + sortedPersons, self.showIds = self.worker.sortPersonsInGroup( + self.groups[section] + ) tableData = [] for person in sortedPersons: @@ -49,63 +51,66 @@ class AbstractOutputter: return tableData + class ConsoleOutputter(AbstractOutputter): def __init__(self): super().__init__() - self.l = logging.getLogger('solo_turnier.output.console') - + self.l = logging.getLogger("solo_turnier.output.console") + def __outputSection(self, data, section): tableData = self.getTabularData(data, section) - tableData = [['Name'] + self.dances] + tableData + tableData = [["Name"] + self.dances] + tableData print(f"Einzeltanzwettbewerb der {sectionMap[section]}") - print(tabulate(tableData, headers='firstrow', tablefmt='fancy_grid')) + print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) print() - def _outputGroup(self, group: solo_turnier.group.Group, groupResults: types.TotalGroupResult): + def _outputGroup( + self, group: solo_turnier.group.Group, groupResults: types.TotalGroupResult + ): print(f"Einzeltanzwettbewerb der Gruppe {group}") - - tableData = [['Tanz'] + groupResults.dances] + + tableData = [["Tanz"] + groupResults.dances] participants = list(groupResults.results.keys()) participants.sort(key=lambda x: (x.id, x.name)) for participant in participants: results = groupResults.results[participant] + def mapResultColumn(result: types.SingleParticipantResult): def getPlace(place, placeTo): if placeTo is None: - return f'{place}.' + return f"{place}." else: - return f'{place}.-{placeTo}.' - + return f"{place}.-{placeTo}." + if result is None: - return '' - + return "" + placeNative = getPlace(result.placeNative, result.placeNativeTo) place = getPlace(result.place, result.placeTo) - lineOne = f'{placeNative} ({result.nativeClass})' - lineTwo = f'[{place} in {result.competitionClass}]' + lineOne = f"{placeNative} ({result.nativeClass})" + lineTwo = f"[{place} in {result.competitionClass}]" lines = [lineOne, lineTwo] if not result.finalist: - lines = ['kein/e Finalist/in'] + lines - - return '\n'.join(lines) + lines = ["kein/e Finalist/in"] + lines + + return "\n".join(lines) mappedResults = map(mapResultColumn, results) - tableRow = [f'{participant.name} ({participant.id})'] + list(mappedResults) + tableRow = [f"{participant.name} ({participant.id})"] + list(mappedResults) tableData.append(tableRow) - - self.l.log(5, 'table data: %s', pprint.pformat(tableData)) - print(tabulate(tableData, headers='firstrow', tablefmt='fancy_grid')) - + + self.l.log(5, "table data: %s", pprint.pformat(tableData)) + print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) def output(self, data: types.State4): for idx, group in enumerate(data.groups): if idx > 0: print() - self.l.debug('Output for group %s', group) + self.l.debug("Output for group %s", group) self._outputGroup(group, data.results[group]) # self.groups = self.worker.collectPersonsInGroups(data) diff --git a/src/solo_turnier/participant.py b/src/solo_turnier/participant.py index 26c8b94..578f1bb 100644 --- a/src/solo_turnier/participant.py +++ b/src/solo_turnier/participant.py @@ -1,34 +1,28 @@ - class Person: - def __init__( - self, - firstName: str, - lastName: str, - club: str, - group: str - ): + def __init__(self, firstName: str, lastName: str, club: str, group: str): self.firstName = firstName self.lastName = lastName self.club = club self.group = group - + def __eq__(self, o): if not isinstance(o, Person): False - + return ( - self.firstName == o.firstName and - self.lastName == o.lastName and - self.club == o.club and - self.group == o.group + self.firstName == o.firstName + and self.lastName == o.lastName + and self.club == o.club + and self.group == o.group ) - + def getTuple(self): return (self.firstName, self.lastName, self.club) - - def __repr__(self): - return f'{self.firstName} {self.lastName} ({self.club}, {self.group})' - - def __hash__(self): - return self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__() + def __repr__(self): + return f"{self.firstName} {self.lastName} ({self.club}, {self.group})" + + def __hash__(self): + return ( + self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__() + ) diff --git a/src/solo_turnier/reader.py b/src/solo_turnier/reader.py index d139a22..28501ab 100644 --- a/src/solo_turnier/reader.py +++ b/src/solo_turnier/reader.py @@ -1,4 +1,3 @@ - import solo_turnier import csv import os @@ -7,34 +6,32 @@ import re from pprint import pformat from .types import CSVResultRow as ResultRow + class CSVResultReader: def __init__(self, fileName: str): self.fileName = fileName - self.l = logging.getLogger('solo_turnier.reader.CSVResultReader') - + self.l = logging.getLogger("solo_turnier.reader.CSVResultReader") + def readFile(self): - with open(self.fileName, 'r') as fp: + with open(self.fileName, "r") as fp: dialect = csv.Sniffer().sniff(fp.read(1024)) fp.seek(0) - + csvReader = csv.reader(fp, dialect) - + rows = [] for row in csvReader: rows.append(row) - - ret = { - 'header': rows[0], - 'data': rows[1:] - } - - self.l.log(5, 'Imported results from allresults.csv file: %s', (ret)) + + ret = {"header": rows[0], "data": rows[1:]} + + self.l.log(5, "Imported results from allresults.csv file: %s", (ret)) return ret - def extractResult(self, entries = None) -> list[ResultRow]: + def extractResult(self, entries=None) -> list[ResultRow]: if entries is None: entries = self.readFile() - + groupParser = solo_turnier.group.GroupParser() classParser = solo_turnier.competition_class.CompetitionClassParser() @@ -44,40 +41,35 @@ class CSVResultReader: competitionClass=classParser.parseClass(row[3]), dance=row[4], id=row[5], - firstName=row[6], lastName=row[7], + firstName=row[6], + lastName=row[7], club=row[10], - place=row[12], placeTo=row[13], + place=row[12], + placeTo=row[13], group=groupParser.parseClass(row[15]), - class_=classParser.parseClass(row[16]) + class_=classParser.parseClass(row[16]), ) - self.l.log(5, 'Found row in CSV: %s', result) + self.l.log(5, "Found row in CSV: %s", result) return result - ret = list(map(__processRow, entries['data'])) - - self.l.log(5, 'Extracted rows from CSV data: %s', ret) + ret = list(map(__processRow, entries["data"])) + + self.l.log(5, "Extracted rows from CSV data: %s", ret) return ret + class CSVExtractor: def __init__(self): - self.l = logging.getLogger('solo_turnier.worker') - self.__groupMaps = { - 'Kinder': 'Kin.', - 'Junioren': 'Jun.', - 'Jugend': 'Jug.' - } - self.__classMaps = { - 'Newcomer': 'Newc.', - 'Beginner': 'Beg.', - 'Advanced': 'Adv.' - } + self.l = logging.getLogger("solo_turnier.worker") + self.__groupMaps = {"Kinder": "Kin.", "Junioren": "Jun.", "Jugend": "Jug."} + self.__classMaps = {"Newcomer": "Newc.", "Beginner": "Beg.", "Advanced": "Adv."} def __mapGroup(self, group): return self.__groupMaps.get(group, group) - + def __mapClass(self, class_): return self.__classMaps.get(class_, class_) - + def mapCSVImport(self, imported) -> list[ResultRow]: ret = [] @@ -87,15 +79,18 @@ class CSVExtractor: competitionClass=self.__mapClass(row[3]), dance=row[4], id=row[5], - firstName=row[6], lastName=row[7], + firstName=row[6], + lastName=row[7], club=row[10], - place=row[12], placeTo=row[13], - group=self.__mapGroup(row[15]), class_=self.__mapClass(row[16]) + place=row[12], + placeTo=row[13], + group=self.__mapGroup(row[15]), + class_=self.__mapClass(row[16]), ) ret.append(result) - self.l.log(5, 'Found row in CSV: %s', result) + self.l.log(5, "Found row in CSV: %s", result) - for row in imported['data']: + for row in imported["data"]: __processRow(row) - + return ret diff --git a/src/solo_turnier/tests/test_competition_class.py b/src/solo_turnier/tests/test_competition_class.py index 88287ef..2feb016 100644 --- a/src/solo_turnier/tests/test_competition_class.py +++ b/src/solo_turnier/tests/test_competition_class.py @@ -1,21 +1,23 @@ import solo_turnier.competition_class import pytest + @pytest.fixture(params=range(9)) def fix_pureClass(request): cases = ( - ('Newc', 'Newc.'), - ('Newc.', 'Newc.'), - ('Newcomer', 'Newc.'), - ('Beg', 'Beg.'), - ('Beg.', 'Beg.'), - ('Beginner', 'Beg.'), - ('Adv', 'Adv.'), - ('Adv.', 'Adv.'), - ('Advanced', 'Adv.'), + ("Newc", "Newc."), + ("Newc.", "Newc."), + ("Newcomer", "Newc."), + ("Beg", "Beg."), + ("Beg.", "Beg."), + ("Beginner", "Beg."), + ("Adv", "Adv."), + ("Adv.", "Adv."), + ("Advanced", "Adv."), ) return cases[request.param] + def test_pureClassParsing(fix_pureClass): className = fix_pureClass[0] expected = fix_pureClass[1] @@ -28,39 +30,43 @@ def test_pureClassParsing(fix_pureClass): assert parser.isPureClass(className) + def test_classParsingWithPreview(): parser = solo_turnier.competition_class.CompetitionClassParser() - ret = parser.parseClass('Sichtung', True) + ret = parser.parseClass("Sichtung", True) assert isinstance(ret, solo_turnier.competition_class.CompetitionClass) - assert str(ret) == 'Sichtung' + assert str(ret) == "Sichtung" + + assert parser.isPureClass("Sichtung", True) - assert parser.isPureClass('Sichtung', True) def test_classParsingInvalidPreview(): parser = solo_turnier.competition_class.CompetitionClassParser() try: - parser.parseClass('Sichtung') + parser.parseClass("Sichtung") assert False except: assert True try: - parser.isPureClass('Sichtung') + parser.isPureClass("Sichtung") assert False except: assert True + @pytest.fixture(params=range(4)) def fix_combinedClass(request): cases = ( - ('Newc/Beg', 'Newc./Beg.'), - ('Newc./Beg', 'Newc./Beg.'), - ('Beginner/Adv', 'Beg./Adv.'), - ('Beg/Adv', 'Beg./Adv.'), + ("Newc/Beg", "Newc./Beg."), + ("Newc./Beg", "Newc./Beg."), + ("Beginner/Adv", "Beg./Adv."), + ("Beg/Adv", "Beg./Adv."), ) return cases[request.param] + def test_combinedClassParsing(fix_combinedClass): className = fix_combinedClass[0] expected = fix_combinedClass[1] diff --git a/src/solo_turnier/tests/test_csvReader.py b/src/solo_turnier/tests/test_csvReader.py index 80436a0..3b6cd87 100644 --- a/src/solo_turnier/tests/test_csvReader.py +++ b/src/solo_turnier/tests/test_csvReader.py @@ -2,12 +2,13 @@ import solo_turnier.reader import os import json + def test_import(): - fileName = os.path.join(os.path.dirname(__file__), 'reader', 'test.csv') + fileName = os.path.join(os.path.dirname(__file__), "reader", "test.csv") reader = solo_turnier.reader.CSVResultReader(fileName) ret = reader.readFile() - with open(os.path.join(os.path.dirname(__file__), 'reader', 'expected.json')) as fp: + with open(os.path.join(os.path.dirname(__file__), "reader", "expected.json")) as fp: expected = json.load(fp) assert ret == expected diff --git a/src/solo_turnier/tests/test_group.py b/src/solo_turnier/tests/test_group.py index 09eb5a8..d0e5e47 100644 --- a/src/solo_turnier/tests/test_group.py +++ b/src/solo_turnier/tests/test_group.py @@ -1,21 +1,23 @@ import solo_turnier.group import pytest + @pytest.fixture(params=range(9)) def fix_pureClass(request): cases = ( - ('Kin', 'Kin.'), - ('Kin.', 'Kin.'), - ('Kinder', 'Kin.'), - ('Jun', 'Jun.'), - ('Jun.', 'Jun.'), - ('Junioren', 'Jun.'), - ('Jug', 'Jug.'), - ('Jug.', 'Jug.'), - ('Jugend', 'Jug.'), + ("Kin", "Kin."), + ("Kin.", "Kin."), + ("Kinder", "Kin."), + ("Jun", "Jun."), + ("Jun.", "Jun."), + ("Junioren", "Jun."), + ("Jug", "Jug."), + ("Jug.", "Jug."), + ("Jugend", "Jug."), ) return cases[request.param] + def test_pureClassParsing(fix_pureClass): className = fix_pureClass[0] expected = fix_pureClass[1] @@ -28,16 +30,18 @@ def test_pureClassParsing(fix_pureClass): assert parser.isPureClass(className) + @pytest.fixture(params=range(4)) def fix_combinedClass(request): cases = ( - ('Kin/Jun', 'Kin./Jun.'), - ('Kin./Jun', 'Kin./Jun.'), - ('Junioren/Jug', 'Jun./Jug.'), - ('Jun/Jug', 'Jun./Jug.'), + ("Kin/Jun", "Kin./Jun."), + ("Kin./Jun", "Kin./Jun."), + ("Junioren/Jug", "Jun./Jug."), + ("Jun/Jug", "Jun./Jug."), ) return cases[request.param] + def test_combinedClassParsing(fix_combinedClass): className = fix_combinedClass[0] expected = fix_combinedClass[1] diff --git a/src/solo_turnier/tests/test_html_locator.py b/src/solo_turnier/tests/test_html_locator.py index a24a514..757edd6 100644 --- a/src/solo_turnier/tests/test_html_locator.py +++ b/src/solo_turnier/tests/test_html_locator.py @@ -1,17 +1,17 @@ - import os import solo_turnier.html_locator + def test_fetchLocationCandidates(): - folder = os.path.join(os.path.dirname(__file__), 'html_locator', 'export') + folder = os.path.join(os.path.dirname(__file__), "html_locator", "export") relFolder = os.path.relpath(folder) locator = solo_turnier.html_locator.HtmlLocator() candidates = locator.findCandidates(relFolder) expected = [ - 'solo_turnier/tests/html_locator/export/2-bar/erg.htm', - 'solo_turnier/tests/html_locator/export/3-baz/erg.htm', - 'solo_turnier/tests/html_locator/export/3-baz/subfolder/4-baz/erg.htm' + "solo_turnier/tests/html_locator/export/2-bar/erg.htm", + "solo_turnier/tests/html_locator/export/3-baz/erg.htm", + "solo_turnier/tests/html_locator/export/3-baz/subfolder/4-baz/erg.htm", ] assert set(candidates) == set(expected) diff --git a/src/solo_turnier/tests/test_html_parser.py b/src/solo_turnier/tests/test_html_parser.py index d5ecd66..dbccb44 100644 --- a/src/solo_turnier/tests/test_html_parser.py +++ b/src/solo_turnier/tests/test_html_parser.py @@ -4,20 +4,22 @@ import json import solo_turnier.html_parser -@pytest.fixture(scope='module', params=range(2)) + +@pytest.fixture(scope="module", params=range(2)) def dataProviderHtmlParser(request): - variant = str(request.param+1) - dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'erg', variant) - htmlFile = os.path.join(dir, 'erg.htm') - jsonFile = os.path.join(dir, 'expected.json') - - with open(htmlFile, 'r') as fp: + variant = str(request.param + 1) + dir = os.path.join(os.path.dirname(__file__), "html_parser", "erg", variant) + htmlFile = os.path.join(dir, "erg.htm") + jsonFile = os.path.join(dir, "expected.json") + + with open(htmlFile, "r") as fp: html = fp.read() - with open(jsonFile, 'r') as fp: + with open(jsonFile, "r") as fp: jsonContent = json.load(fp) - + return (html, jsonContent) + def test_extractDataFromHtml(dataProviderHtmlParser): htmlString = dataProviderHtmlParser[0] expected = dataProviderHtmlParser[1] @@ -29,67 +31,71 @@ def test_extractDataFromHtml(dataProviderHtmlParser): for i in actualResult.participants: participants[i] = actualResult.participants[i].__dict__ - assert actualResult.title == expected['title'] - assert participants == expected['participants'] + assert actualResult.title == expected["title"] + assert participants == expected["participants"] + @pytest.fixture(params=range(6)) def fixture_guessDataFromTitle(request): cases = { - '09.07.2022 - ETW, Solos Jun. Beginner Jive': { - 'class_': 'Beg.', - 'dance': 'Jive', - 'group': 'Jun.' + "09.07.2022 - ETW, Solos Jun. Beginner Jive": { + "class_": "Beg.", + "dance": "Jive", + "group": "Jun.", }, - '09.07.2022 - ETW, Solos Jun. Newc./Beg. Rumba': { - 'class_': 'Newc./Beg.', - 'dance': 'Rumba', - 'group': 'Jun.' + "09.07.2022 - ETW, Solos Jun. Newc./Beg. Rumba": { + "class_": "Newc./Beg.", + "dance": "Rumba", + "group": "Jun.", }, - '09.07.2022 - ETW, Solos Kin./Jun. Beginner Cha Cha': { - 'class_': 'Beg.', - 'dance': 'Cha Cha', - 'group': 'Kin./Jun.' + "09.07.2022 - ETW, Solos Kin./Jun. Beginner Cha Cha": { + "class_": "Beg.", + "dance": "Cha Cha", + "group": "Kin./Jun.", }, - '09.07.2022 - ETW, Solos Kin. Newcomer Samba': { - 'class_': 'Newc.', - 'dance': 'Samba', - 'group': 'Kin.' + "09.07.2022 - ETW, Solos Kin. Newcomer Samba": { + "class_": "Newc.", + "dance": "Samba", + "group": "Kin.", }, - '09.07.2022 - ETW, Solos Jugend Beg./Adv. Wiener Walzer': { - 'class_': 'Beg./Adv.', - 'dance': 'Wiener Walzer', - 'group': 'Jug.' + "09.07.2022 - ETW, Solos Jugend Beg./Adv. Wiener Walzer": { + "class_": "Beg./Adv.", + "dance": "Wiener Walzer", + "group": "Jug.", }, - '09.07.2022 - ETW, Solos Jugend Sichtung Wiener Walzer': { - 'class_': 'Sichtung', - 'dance': 'Wiener Walzer', - 'group': 'Jug.' + "09.07.2022 - ETW, Solos Jugend Sichtung Wiener Walzer": { + "class_": "Sichtung", + "dance": "Wiener Walzer", + "group": "Jug.", }, } keys = list(cases.keys()) key = keys[request.param] return (key, cases[key]) + def test_guessDataFromTitle(fixture_guessDataFromTitle): - parser = solo_turnier.html_parser.HtmlParser('') + parser = solo_turnier.html_parser.HtmlParser("") ret = parser.guessDataFromHtmlTitle(fixture_guessDataFromTitle[0]) assert ret == fixture_guessDataFromTitle[1] + @pytest.fixture(params=range(1)) def fixture_parsePreparationResult(request): - variant = str(request.param+1) - dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'tabges', variant) - htmlFile = os.path.join(dir, 'tabges.htm') - jsonFile = os.path.join(dir, 'expected.json') - - with open(htmlFile, 'r') as fp: + variant = str(request.param + 1) + dir = os.path.join(os.path.dirname(__file__), "html_parser", "tabges", variant) + htmlFile = os.path.join(dir, "tabges.htm") + jsonFile = os.path.join(dir, "expected.json") + + with open(htmlFile, "r") as fp: html = fp.read() - with open(jsonFile, 'r') as fp: + with open(jsonFile, "r") as fp: jsonContent = json.load(fp) - + return (html, jsonContent) + def test_parsePreparationResult(fixture_parsePreparationResult): html = fixture_parsePreparationResult[0] jsonContent = fixture_parsePreparationResult[1] @@ -99,25 +105,27 @@ def test_parsePreparationResult(fixture_parsePreparationResult): assert ret == jsonContent + @pytest.fixture(params=range(1)) def fixture_cleanPreparationImport(request): - variant = str(request.param+1) - dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'tabges', variant) - srcFile = os.path.join(dir, 'expected.json') - expectedFile = os.path.join(dir, 'cleaned.json') - - with open(srcFile, 'r') as fp: + variant = str(request.param + 1) + dir = os.path.join(os.path.dirname(__file__), "html_parser", "tabges", variant) + srcFile = os.path.join(dir, "expected.json") + expectedFile = os.path.join(dir, "cleaned.json") + + with open(srcFile, "r") as fp: source = json.load(fp) - with open(expectedFile, 'r') as fp: + with open(expectedFile, "r") as fp: expected = json.load(fp) - + return (source, expected) + def test_cleanPreparationImport(fixture_cleanPreparationImport): src = fixture_cleanPreparationImport[0] expected = fixture_cleanPreparationImport[1] - parser = solo_turnier.html_parser.HtmlParser('') + parser = solo_turnier.html_parser.HtmlParser("") parser.cleanPreparationRoundImport(src) assert src == expected diff --git a/src/solo_turnier/tests/test_types.py b/src/solo_turnier/tests/test_types.py index ca6ffdc..939358d 100644 --- a/src/solo_turnier/tests/test_types.py +++ b/src/solo_turnier/tests/test_types.py @@ -1,10 +1,11 @@ import pytest import solo_turnier.types as types + def test_HtmlPreviewParticipant_eq(): - name = 'Max Mustermann' + name = "Max Mustermann" id = 123 - group = 'Kin' + group = "Kin" participant = types.HtmlPreviewParticipant(name, id, group) l = [] @@ -13,6 +14,6 @@ def test_HtmlPreviewParticipant_eq(): assert participant in l assert types.HtmlPreviewParticipant(name, id, group) in l - assert types.HtmlPreviewParticipant('Maxime Musterfrau', id, group) not in l + assert types.HtmlPreviewParticipant("Maxime Musterfrau", id, group) not in l assert types.HtmlPreviewParticipant(name, 234, group) not in l - assert types.HtmlPreviewParticipant(name, id, 'Jun') not in l + assert types.HtmlPreviewParticipant(name, id, "Jun") not in l diff --git a/src/solo_turnier/tests/test_worker.py b/src/solo_turnier/tests/test_worker.py index 5a8c18b..957b57c 100644 --- a/src/solo_turnier/tests/test_worker.py +++ b/src/solo_turnier/tests/test_worker.py @@ -4,178 +4,298 @@ import json import pytest import pytest_mock + def __importJSONData(name): - path = os.path.join(os.path.dirname(__file__), 'worker', name) - with open(path, 'r') as fp: + path = os.path.join(os.path.dirname(__file__), "worker", name) + with open(path, "r") as fp: return json.load(fp) + @pytest.fixture def fixture_csvExtractor(): - data = __importJSONData('csvImport.json') - expected = __importJSONData('csvImportResult.json') + data = __importJSONData("csvImport.json") + expected = __importJSONData("csvImportResult.json") return (data, expected) + def test_csvExtractor(fixture_csvExtractor): extractor = worker.CSVExtractor() mapped = extractor.mapCSVImport(fixture_csvExtractor[0]) assert len(mapped) == len(fixture_csvExtractor[1]) - for i,elem in enumerate(fixture_csvExtractor[1]): + for i, elem in enumerate(fixture_csvExtractor[1]): assert mapped[i].__dict__ == elem + def test_extractPersonFromRow(): - row = worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Beg./Adv.') + row = worker.ResultRow( + "Max", + "Mustermann", + "TSC Entenhausen", + "2", + "Kin", + "Adv.", + "Rumba", + "2", + "2", + "Kin.", + "Beg./Adv.", + ) person = worker.ResultPerson.extractFromResultRow(row) expected = { - 'firstName': 'Max', - 'lastName': 'Mustermann', - 'name': 'Max Mustermann', - 'club': 'TSC Entenhausen', - 'id': None, - 'group': None + "firstName": "Max", + "lastName": "Mustermann", + "name": "Max Mustermann", + "club": "TSC Entenhausen", + "id": None, + "group": None, } assert person.__dict__ == expected + def test_extractCompetitionFromRow(): - row = worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Beg./Adv.') + row = worker.ResultRow( + "Max", + "Mustermann", + "TSC Entenhausen", + "2", + "Kin", + "Adv.", + "Rumba", + "2", + "2", + "Kin.", + "Beg./Adv.", + ) competition = worker.CompetitionResult.extractFromResultRow(row) expected = { - 'dance': 'Rumba', - 'class_': 'Adv.', - 'group': 'Kin', - 'place': '2', - 'placeTo': '2', - 'id': 2, - 'finalist': None, - 'competitionGroup': 'Kin.', - 'competitionClass': 'Beg./Adv.' + "dance": "Rumba", + "class_": "Adv.", + "group": "Kin", + "place": "2", + "placeTo": "2", + "id": 2, + "finalist": None, + "competitionGroup": "Kin.", + "competitionClass": "Beg./Adv.", } assert competition.__dict__ == expected + def test_combineRowsByPerson(): rows = [ - worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Cha Cha', '-', '-', 'Kin.', 'Adv.'), - worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Adv.'), - worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Beg.', 'Jive', '1', '1', 'Kin.', 'Beg.'), - worker.ResultRow('Maxime', 'Musterfrau', '1. SC Entenhausen', '1', 'Kin', 'Adv.', 'Rumba', '1', '1', 'Kin.', 'Adv.') + worker.ResultRow( + "Max", + "Mustermann", + "TSC Entenhausen", + "2", + "Kin", + "Adv.", + "Cha Cha", + "-", + "-", + "Kin.", + "Adv.", + ), + worker.ResultRow( + "Max", + "Mustermann", + "TSC Entenhausen", + "2", + "Kin", + "Adv.", + "Rumba", + "2", + "2", + "Kin.", + "Adv.", + ), + worker.ResultRow( + "Max", + "Mustermann", + "TSC Entenhausen", + "2", + "Kin", + "Beg.", + "Jive", + "1", + "1", + "Kin.", + "Beg.", + ), + worker.ResultRow( + "Maxime", + "Musterfrau", + "1. SC Entenhausen", + "1", + "Kin", + "Adv.", + "Rumba", + "1", + "1", + "Kin.", + "Adv.", + ), ] dataWorker = worker.DataWorker() result = dataWorker.combineRowsByPerson(rows) expected = { - worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen'): [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', '2', 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', '2', 'Kin.', 'Beg.') + worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen"): [ + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "2", "2", "2", "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin", "Beg.", "1", "1", "2", "Kin.", "Beg." + ), + ], + worker.ResultPerson("Maxime", "Musterfrau", "1. SC Entenhausen"): [ + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "1", "1", "1", "Kin.", "Adv." + ) ], - worker.ResultPerson('Maxime', 'Musterfrau', '1. SC Entenhausen'): [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '1', '1', '1', 'Kin.', 'Adv.') - ] } assert result == expected + def test_checkUniqueIds_True(): - person1 = worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen') - person2 = worker.ResultPerson('Maxime', 'Musterfrau', '1. SC Entenhausen') + person1 = worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen") + person2 = worker.ResultPerson("Maxime", "Musterfrau", "1. SC Entenhausen") data = { person1: [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', 2, 'Kin.', 'Beg.') + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin", "Beg.", "1", "1", 2, "Kin.", "Beg." + ), ], person2: [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '1', '1', 1, 'Kin.', 'Adv.') - ] + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "1", "1", 1, "Kin.", "Adv." + ) + ], } dataWorker = worker.DataWorker() assert dataWorker.checkUniqueIds(data) == True assert person1.id == 2 assert person2.id == 1 + def test_checkUniqueIds_False(): - person1 = worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen') - person2 = worker.ResultPerson('Maxime', 'Musterfrau', '1. SC Entenhausen') + person1 = worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen") + person2 = worker.ResultPerson("Maxime", "Musterfrau", "1. SC Entenhausen") data = { person1: [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', 3, 'Kin.', 'Beg.') + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin", "Beg.", "1", "1", 3, "Kin.", "Beg." + ), ], person2: [ - worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '1', '1', 1, 'Kin.', 'Adv.') - ] + worker.CompetitionResult( + "Rumba", "Kin", "Adv.", "1", "1", 1, "Kin.", "Adv." + ) + ], } dataWorker = worker.DataWorker() assert dataWorker.checkUniqueIds(data) == False assert person1.id == None assert person2.id == 1 + @pytest.fixture(params=range(5)) def fixture_consolidateGroups(request): - person1 = worker.ResultPerson('Max 1', 'Mustermann', 'TSC Entenhausen') - person2 = worker.ResultPerson('Max 2', 'Mustermann', 'TSC Entenhausen') - person3 = worker.ResultPerson('Max 3', 'Mustermann', 'TSC Entenhausen') - person4 = worker.ResultPerson('Max 4', 'Mustermann', 'TSC Entenhausen') + person1 = worker.ResultPerson("Max 1", "Mustermann", "TSC Entenhausen") + person2 = worker.ResultPerson("Max 2", "Mustermann", "TSC Entenhausen") + person3 = worker.ResultPerson("Max 3", "Mustermann", "TSC Entenhausen") + person4 = worker.ResultPerson("Max 4", "Mustermann", "TSC Entenhausen") # persons = (person1, person2, person3, person4) dict1 = { person1: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '1', '1', 3, 'Kin.', 'Beg.') + worker.CompetitionResult( + "Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin.", "Beg.", "1", "1", 3, "Kin.", "Beg." + ), ] } dict2 = { person2: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin./Jun.', 'Beg.', '1', '1', 3, 'Kin./Jun.', 'Beg.') + worker.CompetitionResult( + "Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin./Jun.", "Beg.", "1", "1", 3, "Kin./Jun.", "Beg." + ), ] } dict3 = { person3: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.') + worker.CompetitionResult( + "Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ) ] } dict4 = { person4: [ - worker.CompetitionResult('Rumba', 'Kin./Jun.', 'Adv.', '2', '2', 2, 'Kin./Jun.', 'Adv.') + worker.CompetitionResult( + "Rumba", "Kin./Jun.", "Adv.", "2", "2", 2, "Kin./Jun.", "Adv." + ) ] } dict5 = { person4: [ - worker.CompetitionResult('Rumba', 'Kin./Jun.', 'Adv.', '2', '2', 2, 'Kin./Jun.', 'Adv.'), - worker.CompetitionResult('Cha Cha', 'Jun./Jug.', 'Beg.', '3', '4', 2, 'Jun./Jug.', 'Beg.') + worker.CompetitionResult( + "Rumba", "Kin./Jun.", "Adv.", "2", "2", 2, "Kin./Jun.", "Adv." + ), + worker.CompetitionResult( + "Cha Cha", "Jun./Jug.", "Beg.", "3", "4", 2, "Jun./Jug.", "Beg." + ), ] } cases = ( - (dict1|dict3, (True, False), {}), - (dict1|dict2|dict3, (True, True), {}), - (dict4, (False, False), {person4: 'Kin./Jun.'}), - (dict1|dict2|dict3|dict4, (False, True), {person4: 'Kin./Jun.'}), - (dict5, (True, True), {person4: 'Jun.'}), + (dict1 | dict3, (True, False), {}), + (dict1 | dict2 | dict3, (True, True), {}), + (dict4, (False, False), {person4: "Kin./Jun."}), + (dict1 | dict2 | dict3 | dict4, (False, True), {person4: "Kin./Jun."}), + (dict5, (True, True), {person4: "Jun."}), ) return cases[request.param] + @pytest.fixture(params=range(2)) def fixture_consolidateGroups_fail(request, fixture_consolidateGroups): - person = worker.ResultPerson('Max 5', 'Mustermann', 'TSC Entenhausen') + person = worker.ResultPerson("Max 5", "Mustermann", "TSC Entenhausen") dict1 = { person: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Jun.', 'Beg.', '1', '1', 3, 'Jun.', 'Adv.') + worker.CompetitionResult( + "Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Jun.", "Beg.", "1", "1", 3, "Jun.", "Adv." + ), ] } dict2 = { person: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Hgr', 'Beg.', '1', '1', 3, 'Hgr', 'Adv.') + worker.CompetitionResult( + "Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult("Jive", "Hgr", "Beg.", "1", "1", 3, "Hgr", "Adv."), ] } - cases = ( - dict1 | fixture_consolidateGroups[0], - dict2 | fixture_consolidateGroups[0] - ) + cases = (dict1 | fixture_consolidateGroups[0], dict2 | fixture_consolidateGroups[0]) return cases[request.param] + def test_consolidateGroups(fixture_consolidateGroups): data = fixture_consolidateGroups[0] dataWorker = worker.DataWorker() @@ -183,7 +303,8 @@ def test_consolidateGroups(fixture_consolidateGroups): assert dataWorker.consolidateGroups(data) == fixture_consolidateGroups[1] for person in data: - assert person.group == fixture_consolidateGroups[2].get(person, 'Kin.') + assert person.group == fixture_consolidateGroups[2].get(person, "Kin.") + def test_consolidateGroups_failing(fixture_consolidateGroups_fail): data = fixture_consolidateGroups_fail @@ -192,105 +313,143 @@ def test_consolidateGroups_failing(fixture_consolidateGroups_fail): with pytest.raises(Exception): dataWorker.consolidateGroups(data) + def test_createHtmlLUT(mocker): - mock = mocker.patch('solo_turnier.html_parser.HtmlParser.guessDataFromHtmlTitle') - mock.side_effect= [ - {'group': 'group1', 'class_': 'class1', 'dance': 'dance1'}, - {'group': 'group2', 'class_': 'class2', 'dance': 'dance2'}, - {'group': 'group3', 'class_': 'class3', 'dance': 'dance3'}, + mock = mocker.patch("solo_turnier.html_parser.HtmlParser.guessDataFromHtmlTitle") + mock.side_effect = [ + {"group": "group1", "class_": "class1", "dance": "dance1"}, + {"group": "group2", "class_": "class2", "dance": "dance2"}, + {"group": "group3", "class_": "class3", "dance": "dance3"}, ] - - importMock1 = mocker.patch('solo_turnier.html_parser.HtmlImport') - importMock2 = mocker.patch('solo_turnier.html_parser.HtmlImport') - importMock3 = mocker.patch('solo_turnier.html_parser.HtmlImport') - importMock1.title = 'Fake title 1' - importMock2.title = 'Fake title 2' - importMock3.title = 'Fake title 3' + + importMock1 = mocker.patch("solo_turnier.html_parser.HtmlImport") + importMock2 = mocker.patch("solo_turnier.html_parser.HtmlImport") + importMock3 = mocker.patch("solo_turnier.html_parser.HtmlImport") + importMock1.title = "Fake title 1" + importMock2.title = "Fake title 2" + importMock3.title = "Fake title 3" dataWorker = worker.DataWorker() structure = dataWorker._createHtmlLUT([importMock1, importMock2, importMock3]) expected = { - ('group1', 'class1', 'dance1'): importMock1, - ('group2', 'class2', 'dance2'): importMock2, - ('group3', 'class3', 'dance3'): importMock3, + ("group1", "class1", "dance1"): importMock1, + ("group2", "class2", "dance2"): importMock2, + ("group3", "class3", "dance3"): importMock3, } assert expected == structure + def test_mergeHtmlData(mocker): - person1 = worker.ResultPerson('Max 1', 'Mustermann', 'TSC Entenhausen') - person2 = worker.ResultPerson('Max 2', 'Mustermann', 'TSC Entenhausen') - person3 = worker.ResultPerson('Max 3', 'Mustermann', 'TSC Entenhausen') - person4 = worker.ResultPerson('Max 4', 'Mustermann', 'TSC Entenhausen') + person1 = worker.ResultPerson("Max 1", "Mustermann", "TSC Entenhausen") + person2 = worker.ResultPerson("Max 2", "Mustermann", "TSC Entenhausen") + person3 = worker.ResultPerson("Max 3", "Mustermann", "TSC Entenhausen") + person4 = worker.ResultPerson("Max 4", "Mustermann", "TSC Entenhausen") data = { person1: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '1', '1', 1, 'Kin./Jun.', 'Beg.'), - worker.CompetitionResult('Cha Cha', 'Kin.', 'Adv.', '1', '1', 1, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '1', '2', 1, 'Kin.', 'Beg.'), - worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Beg.', '1', '1', 1, 'Kin.', 'Newc./Beg.'), + worker.CompetitionResult( + "Rumba", "Kin.", "Beg.", "1", "1", 1, "Kin./Jun.", "Beg." + ), + worker.CompetitionResult( + "Cha Cha", "Kin.", "Adv.", "1", "1", 1, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin.", "Beg.", "1", "2", 1, "Kin.", "Beg." + ), + worker.CompetitionResult( + "Langs. Walzer", "Kin.", "Beg.", "1", "1", 1, "Kin.", "Newc./Beg." + ), ], person2: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '2', '2', 2, 'Kin./Jun.', 'Beg.'), - worker.CompetitionResult('Cha Cha', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'), - worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '1', '2', 2, 'Kin.', 'Beg.'), - worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Newc.', '1', '1', 2, 'Kin.', 'Newc./Beg.'), + worker.CompetitionResult( + "Rumba", "Kin.", "Beg.", "2", "2", 2, "Kin./Jun.", "Beg." + ), + worker.CompetitionResult( + "Cha Cha", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." + ), + worker.CompetitionResult( + "Jive", "Kin.", "Beg.", "1", "2", 2, "Kin.", "Beg." + ), + worker.CompetitionResult( + "Langs. Walzer", "Kin.", "Newc.", "1", "1", 2, "Kin.", "Newc./Beg." + ), ], person3: [ - worker.CompetitionResult('Rumba', 'Jun.', 'Beg.', '1', '1', 3, 'Kin./Jun.', 'Beg.'), + worker.CompetitionResult( + "Rumba", "Jun.", "Beg.", "1", "1", 3, "Kin./Jun.", "Beg." + ), # worker.CompetitionResult('Cha Cha', 'Jun.', 'Adv.', '1', '1', 3, 'Kin.', 'Adv.'), # worker.CompetitionResult('Jive', 'Jun.', 'Beg.', '2', '2', 3, 'Kin.', 'Beg.'), # worker.CompetitionResult('Langs. Walzer', 'Jun.', 'Newc./Beg.', '1', '1', 3, 'Kin.', 'Beg.'), ], person4: [ - worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '3', '3', 4, 'Kin./Jun.', 'Beg.'), + worker.CompetitionResult( + "Rumba", "Kin.", "Beg.", "3", "3", 4, "Kin./Jun.", "Beg." + ), # worker.CompetitionResult('Cha Cha', 'Kin.', 'Adv.', '1', '1', 4, 'Kin.', 'Adv.'), # worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '2', '2', 4, 'Kin.', 'Beg.'), # worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Newc./Beg.', '1', '1', 4, 'Kin.', 'Beg.'), ], } - htmlParticipant1Dance1 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.', True) - htmlParticipant1Dance2 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.', True) - htmlParticipant1Dance3 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.-2.', True) - htmlParticipant1Dance4 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.', True) - - htmlParticipant2Dance1 = html_parser.HtmlParticipant('Max 2 Mustermann', '2.', True) - htmlParticipant2Dance2 = html_parser.HtmlParticipant('Max 2 Mustermann', '2.', True) - htmlParticipant2Dance3 = html_parser.HtmlParticipant('Max 2 Mustermann', '1.-2.', True) - htmlParticipant2Dance4 = html_parser.HtmlParticipant('Max 2 Mustermann', '1.', True) - - htmlParticipant3Dance1 = html_parser.HtmlParticipant('Max 3 Mustermann', '1.', True) + htmlParticipant1Dance1 = html_parser.HtmlParticipant("Max 1 Mustermann", "1.", True) + htmlParticipant1Dance2 = html_parser.HtmlParticipant("Max 1 Mustermann", "1.", True) + htmlParticipant1Dance3 = html_parser.HtmlParticipant( + "Max 1 Mustermann", "1.-2.", True + ) + htmlParticipant1Dance4 = html_parser.HtmlParticipant("Max 1 Mustermann", "1.", True) - htmlParticipant4Dance1 = html_parser.HtmlParticipant('Max 4 Mustermann', '3.', False) + htmlParticipant2Dance1 = html_parser.HtmlParticipant("Max 2 Mustermann", "2.", True) + htmlParticipant2Dance2 = html_parser.HtmlParticipant("Max 2 Mustermann", "2.", True) + htmlParticipant2Dance3 = html_parser.HtmlParticipant( + "Max 2 Mustermann", "1.-2.", True + ) + htmlParticipant2Dance4 = html_parser.HtmlParticipant("Max 2 Mustermann", "1.", True) + + htmlParticipant3Dance1 = html_parser.HtmlParticipant("Max 3 Mustermann", "1.", True) + + htmlParticipant4Dance1 = html_parser.HtmlParticipant( + "Max 4 Mustermann", "3.", False + ) htmlParticipantsDance1 = { - '1': htmlParticipant1Dance1, - '2': htmlParticipant2Dance1, - '3': htmlParticipant3Dance1, - '4': htmlParticipant4Dance1 + "1": htmlParticipant1Dance1, + "2": htmlParticipant2Dance1, + "3": htmlParticipant3Dance1, + "4": htmlParticipant4Dance1, } htmlParticipantsDance2 = { - '1': htmlParticipant1Dance2, - '2': htmlParticipant2Dance2, + "1": htmlParticipant1Dance2, + "2": htmlParticipant2Dance2, } htmlParticipantsDance3 = { - '1': htmlParticipant1Dance3, - '2': htmlParticipant2Dance3, + "1": htmlParticipant1Dance3, + "2": htmlParticipant2Dance3, } htmlParticipantsDance4 = { - '1': htmlParticipant1Dance4, - '2': htmlParticipant2Dance4, + "1": htmlParticipant1Dance4, + "2": htmlParticipant2Dance4, } - htmlCompetition1 = html_parser.HtmlImport('ETW, Solos Kin./Jun. Beginner Rumba', htmlParticipantsDance1) - htmlCompetition2 = html_parser.HtmlImport('ETW, Solos Kin. Advanced Cha Cha', htmlParticipantsDance2) - htmlCompetition3 = html_parser.HtmlImport('ETW, Solos Kinder Beginner Jive', htmlParticipantsDance3) - htmlCompetition4 = html_parser.HtmlImport('ETW, Solos Kin. Newc./Beg. Langs. Walzer', htmlParticipantsDance4) - + htmlCompetition1 = html_parser.HtmlImport( + "ETW, Solos Kin./Jun. Beginner Rumba", htmlParticipantsDance1 + ) + htmlCompetition2 = html_parser.HtmlImport( + "ETW, Solos Kin. Advanced Cha Cha", htmlParticipantsDance2 + ) + htmlCompetition3 = html_parser.HtmlImport( + "ETW, Solos Kinder Beginner Jive", htmlParticipantsDance3 + ) + htmlCompetition4 = html_parser.HtmlImport( + "ETW, Solos Kin. Newc./Beg. Langs. Walzer", htmlParticipantsDance4 + ) + dataWorker = worker.DataWorker() - dataWorker.mergeHtmlData(data, [htmlCompetition1, htmlCompetition2, htmlCompetition3, htmlCompetition4]) + dataWorker.mergeHtmlData( + data, [htmlCompetition1, htmlCompetition2, htmlCompetition3, htmlCompetition4] + ) person1Finalist = [c.finalist for c in data[person1]] person2Finalist = [c.finalist for c in data[person2]] @@ -306,33 +465,59 @@ def test_mergeHtmlData(mocker): person1: [True, True, True, True], person2: [True, True, True, True], person3: [True], - person4: [False], + person4: [False], } assert finalists == expectedFinalists + @pytest.fixture(params=range(4)) def fixture_getAllDancesInCompetition(request, mocker): def mockCompetition(comp): def mockUser(): - return mocker.patch('solo_turnier.worker.ResultPerson') + return mocker.patch("solo_turnier.worker.ResultPerson") + def mockDances(dances): def mockDance(name): - mock = mocker.patch('solo_turnier.worker.CompetitionResult') + mock = mocker.patch("solo_turnier.worker.CompetitionResult") mock.dance = name return mock + return [mockDance(d) for d in dances] + return {mockUser(): mockDances(dances) for dances in comp} - + cases = ( - ([['Samba']], ['Samba']), - ([['Samba', 'Rumba'], ['Cha Cha']], ['Samba', 'Cha Cha', 'Rumba']), - ([['Samba', 'Rumba'], ['Cha Cha', 'Tango', 'Langs. Walzer']], ['Samba', 'Cha Cha', 'Rumba', 'Langs. Walzer', 'Tango']), - ([['Cha Cha', 'Rumba', 'Jive'], ['Quickstep', 'Tango', 'Wiener Walzer', 'Langs. Walzer'], ['Slowfox', 'Langs. Walzer', 'Paso Doble', 'Samba']], ['Samba', 'Cha Cha', 'Rumba', 'Paso Doble', 'Jive', 'Langs. Walzer', 'Tango', 'Wiener Walzer', 'Slowfox', 'Quickstep']) + ([["Samba"]], ["Samba"]), + ([["Samba", "Rumba"], ["Cha Cha"]], ["Samba", "Cha Cha", "Rumba"]), + ( + [["Samba", "Rumba"], ["Cha Cha", "Tango", "Langs. Walzer"]], + ["Samba", "Cha Cha", "Rumba", "Langs. Walzer", "Tango"], + ), + ( + [ + ["Cha Cha", "Rumba", "Jive"], + ["Quickstep", "Tango", "Wiener Walzer", "Langs. Walzer"], + ["Slowfox", "Langs. Walzer", "Paso Doble", "Samba"], + ], + [ + "Samba", + "Cha Cha", + "Rumba", + "Paso Doble", + "Jive", + "Langs. Walzer", + "Tango", + "Wiener Walzer", + "Slowfox", + "Quickstep", + ], + ), ) case = cases[request.param] return (mockCompetition(case[0]), case[1]) + def test_getAllDancesInCompetitions(fixture_getAllDancesInCompetition): print(fixture_getAllDancesInCompetition) data = fixture_getAllDancesInCompetition[0] @@ -340,57 +525,76 @@ def test_getAllDancesInCompetitions(fixture_getAllDancesInCompetition): ret = dataWorker.getAllDancesInCompetitions(data) assert ret == fixture_getAllDancesInCompetition[1] + def test_collectPersonsInGroups(mocker): def mockPerson(group): - mock = mocker.patch('solo_turnier.worker.ResultPerson') + mock = mocker.patch("solo_turnier.worker.ResultPerson") mock.group = group return mock + persons = ( - mockPerson('Kin.'), mockPerson('Kin.'), mockPerson('Jun.'), - mockPerson('Kin.'), mockPerson(None), mockPerson('Jug.'), - mockPerson(None), mockPerson('Kin./Jun.'), mockPerson('Jun.') + mockPerson("Kin."), + mockPerson("Kin."), + mockPerson("Jun."), + mockPerson("Kin."), + mockPerson(None), + mockPerson("Jug."), + mockPerson(None), + mockPerson("Kin./Jun."), + mockPerson("Jun."), ) data = {p: [] for p in persons} dataWorker = worker.DataWorker() groups = dataWorker.collectPersonsInGroups(data) - assert groups['Kin.'] == [persons[0], persons[1], persons[3]] - assert groups['Jun.'] == [persons[2], persons[8]] - assert groups['Jug.'] == [persons[5]] - assert groups['Sonst'] == [persons[4], persons[6], persons[7]] + assert groups["Kin."] == [persons[0], persons[1], persons[3]] + assert groups["Jun."] == [persons[2], persons[8]] + assert groups["Jug."] == [persons[5]] + assert groups["Sonst"] == [persons[4], persons[6], persons[7]] + def test_sortPersons_withId(mocker): def mockPerson(id): - mock = mocker.patch('solo_turnier.worker.ResultPerson') + mock = mocker.patch("solo_turnier.worker.ResultPerson") mock.id = id return mock + persons = [mockPerson(2), mockPerson(1), mockPerson(5), mockPerson(3)] dataWorker = worker.DataWorker() sorted, showIds = dataWorker.sortPersonsInGroup(persons) assert sorted == [persons[1], persons[0], persons[3], persons[2]] assert showIds == True + def test_sortPersons_withoutId(mocker): def mockPerson(name): - mock = mocker.patch('solo_turnier.worker.ResultPerson') + mock = mocker.patch("solo_turnier.worker.ResultPerson") mock.id = 3 mock.name = name - mock.club = 'TSC Entenhausen' + mock.club = "TSC Entenhausen" return mock - persons = [mockPerson('Max'), mockPerson('Isabel'), mockPerson('Reimund'), mockPerson('Anna')] + + persons = [ + mockPerson("Max"), + mockPerson("Isabel"), + mockPerson("Reimund"), + mockPerson("Anna"), + ] persons[2].id = None dataWorker = worker.DataWorker() sorted, showIds = dataWorker.sortPersonsInGroup(persons) assert sorted == [persons[3], persons[1], persons[0], persons[2]] assert showIds == False + def test_mapPersonResultsToDanceList(mocker): def mockResult(dance): - mock = mocker.patch('solo_turnier.worker.CompetitionResult') + mock = mocker.patch("solo_turnier.worker.CompetitionResult") mock.dance = dance return mock - dances = ['Cha Cha', 'Rumba', 'Langs. Walzer', 'Quickstep'] - results = [mockResult('Rumba'), mockResult('Quickstep'), mockResult('Cha Cha')] + + dances = ["Cha Cha", "Rumba", "Langs. Walzer", "Quickstep"] + results = [mockResult("Rumba"), mockResult("Quickstep"), mockResult("Cha Cha")] dataWorker = worker.DataWorker() mappedResults = dataWorker.mapPersonResultsToDanceList(results, dances) assert mappedResults == [results[2], results[0], None, results[1]] diff --git a/src/solo_turnier/types.py b/src/solo_turnier/types.py index 35e37c0..bfd2404 100644 --- a/src/solo_turnier/types.py +++ b/src/solo_turnier/types.py @@ -1,12 +1,25 @@ - from . import group from . import competition_class + class CSVResultRow: - def __init__(self, firstName, lastName, club, id, group, class_, dance, place, placeTo, competitionGroup, competitionClass): + def __init__( + self, + firstName, + lastName, + club, + id, + group, + class_, + dance, + place, + placeTo, + competitionGroup, + competitionClass, + ): self.firstName = firstName self.lastName = lastName - self.name = f'{firstName} {lastName}' + self.name = f"{firstName} {lastName}" self.club = club self.id = id self.group = group @@ -16,9 +29,10 @@ class CSVResultRow: self.placeTo = placeTo self.competitionGroup = competitionGroup self.competitionClass = competitionClass - + def __repr__(self): - return f'{self.name} ({self.id}, {self.club}) is in {self.group} {self.class_} and danced the {self.dance} in {self.competitionGroup} {self.competitionClass} getting place {self.place}-{self.placeTo}' + return f"{self.name} ({self.id}, {self.club}) is in {self.group} {self.class_} and danced the {self.dance} in {self.competitionGroup} {self.competitionClass} getting place {self.place}-{self.placeTo}" + class HtmlPreviewParticipant: def __init__(self, name, id, group_): @@ -27,74 +41,94 @@ class HtmlPreviewParticipant: groupParser = group.GroupParser() self.group = groupParser.parseClass(group_) self.finalist = None - + def __eq__(self, o): if type(o) != HtmlPreviewParticipant: return False - - return all(map(lambda x, y: x == y, (self.name, self.id, self.group), (o.name, o.id, o.group))) + + return all( + map( + lambda x, y: x == y, + (self.name, self.id, self.group), + (o.name, o.id, o.group), + ) + ) def __repr__(self): - return f'{self.id} ({self.name}, {self.group})' - + return f"{self.id} ({self.name}, {self.group})" + def __hash__(self): return hash((self.id, self.name, self.group)) def __gt__(self, other): return self.id >= other.id + class HtmlParticipant: def __init__(self, name, id): self.name = name self.id = id self.finalist = None - + def __eq__(self, o): if type(o) != HtmlPreviewParticipant: return False - - return all(map(lambda x, y: x == y, (self.name, self.id, self.group), (o.name, o.id, o.group))) + + return all( + map( + lambda x, y: x == y, + (self.name, self.id, self.group), + (o.name, o.id, o.group), + ) + ) def __repr__(self): - return f'{self.id}: {self.name}' - + return f"{self.id}: {self.name}" + def __hash__(self): return hash((self.id, self.name)) - + def __gt__(self, other): return self.id >= other.id + # class PreviewParticipationData: # def __init__(self, dance: str, class_: competition_class.CompetitionClass): # self.class_ = class_ # self.dance = dance + class HtmlPreviewImport: def __init__( self, participants: dict[int, list[HtmlPreviewParticipant]], - results: dict[HtmlPreviewParticipant, dict[str, competition_class.CompetitionClass]] + results: dict[ + HtmlPreviewParticipant, dict[str, competition_class.CompetitionClass] + ], ): self.participants = participants self.results = results - + def __repr__(self): return (str(self.participants), str(self.results)) + class HtmlResultImport: def __init__(self, results: dict[HtmlParticipant, str]): self.results = results - + def __repr__(self): return str(self.results) + class HtmlResultTotalTable: def __init__(self, participants): self.participants = participants - + def __repr__(self): return str(self.participants) - + + class HtmlCompetitionResultRow: def __init__(self, name, id, dance, group, class_, place, placeTo, finalist): self.dance = dance @@ -105,70 +139,90 @@ class HtmlCompetitionResultRow: self.id = int(id) self.name = name self.finalist = finalist - + def __repr__(self): if self.place == self.placeTo: - result = f'{self.place}.' + result = f"{self.place}." else: - result = f'{self.place}.-{self.placeTo}.' - + result = f"{self.place}.-{self.placeTo}." + if self.finalist == True: - finalist = '[F]' + finalist = "[F]" else: - finalist = '' - return f'Result[{self.id}]({self.group} {self.class_} {self.dance} as {result}{finalist})' + finalist = "" + return f"Result[{self.id}]({self.group} {self.class_} {self.dance} as {result}{finalist})" def __eq__(self, o): if not isinstance(o, CompetitionResult): return False - + return ( - self.dance == o.dance and - self.competitionClass == o.competitionClass and - self.competitionGroup == o.competitionGroup and - self.place == o.place and self.placeTo == o.placeTo and - self.id == o.id + self.dance == o.dance + and self.competitionClass == o.competitionClass + and self.competitionGroup == o.competitionGroup + and self.place == o.place + and self.placeTo == o.placeTo + and self.id == o.id ) + class HtmlSingleCompetitionResult: def __init__(self, name, place, placeTo, finalist): self.name = name self.place = place self.placeTo = placeTo self.finalist = finalist - + def __repr__(self): if self.placeTo is None: place = self.place else: - place = f'{self.place}-{self.placeTo}' + place = f"{self.place}-{self.placeTo}" if self.finalist: - return f'Res({self.name} [F], placed {place})' + return f"Res({self.name} [F], placed {place})" else: - return f'Res({self.name}, placed {place})' - - def __gt__(self,other): + return f"Res({self.name}, placed {place})" + + def __gt__(self, other): return self.id > other.id - + def __eq__(self, other): return self.id == other.id - + def __hash__(self): return hash(self.id) + class HtmlCompetitionTotalResults: def __init__(self): self.results = {} self.tabges = {} - - def __getTuple(self, group: group.Group_t, class_: competition_class.Class_t, dance: str, id: int): + + def __getTuple( + self, + group: group.Group_t, + class_: competition_class.Class_t, + dance: str, + id: int, + ): return (group, class_, dance, id) - - def get(self, group: group.Group_t, class_: competition_class.Class_t, dance: str, id: int) -> list[HtmlSingleCompetitionResult]: + + def get( + self, + group: group.Group_t, + class_: competition_class.Class_t, + dance: str, + id: int, + ) -> list[HtmlSingleCompetitionResult]: return self.results[self.__getTuple(group, class_, dance, id)] - - def getById(self, id: int) -> dict[tuple[str, group.Group_t, competition_class.Class_t], HtmlSingleCompetitionResult]: + + def getById( + self, id: int + ) -> dict[ + tuple[str, group.Group_t, competition_class.Class_t], + HtmlSingleCompetitionResult, + ]: ret = {} for k in self.results: @@ -178,15 +232,16 @@ class HtmlCompetitionTotalResults: # Dance, Group, Class key = (k[2], k[0], k[1]) ret[key] = self.results[k] - + return ret - + def add(self, group, class_, dance, id, result: HtmlSingleCompetitionResult): tup = self.__getTuple(group, class_, dance, id) l = self.results.get(tup, []) l.append(result) self.results[tup] = l + class SingleParticipantResult: def __init__( self, @@ -195,7 +250,7 @@ class SingleParticipantResult: dance: str, finalist: bool, place: int, - placeTo: int|None + placeTo: int | None, ): self.competitionClass = competitionClass self.nativeClass = nativeClass @@ -209,67 +264,83 @@ class SingleParticipantResult: self.placeNative = None self.placeNativeTo = None - + def __repr__(self): - asFinalist = ' as finalist' if self.finalist else '' + asFinalist = " as finalist" if self.finalist else "" if self.placeTo is None: - return f'SR[{self.place} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]' + return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" + + return f"SR[{self.place}-{self.placeTo} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" - return f'SR[{self.place}-{self.placeTo} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]' - def getPlace(self): if self.placeTo is None: - return f'{self.place}.' + return f"{self.place}." else: - return f'{self.place}.-{self.placeTo}.' - + return f"{self.place}.-{self.placeTo}." + def getNativePlace(self): if self.placeNativeTo is None: - return f'{self.placeNative}.' + return f"{self.placeNative}." else: - return f'{self.placeNative}.-{self.placeNativeTo}.' + return f"{self.placeNative}.-{self.placeNativeTo}." + class TotalGroupResult: - def __init__(self, dances: list[str], results: dict[HtmlPreviewParticipant, list[SingleParticipantResult]]): - self.dances = dances - self.results = results - def __repr__(self): - return f'TotalGroupResult({self.dances}, {self.results})' - -class State4: def __init__( self, - resultPerGroup: dict[group.Group, TotalGroupResult] + dances: list[str], + results: dict[HtmlPreviewParticipant, list[SingleParticipantResult]], ): + self.dances = dances + self.results = results + + def __repr__(self): + return f"TotalGroupResult({self.dances}, {self.results})" + + +class State4: + def __init__(self, resultPerGroup: dict[group.Group, TotalGroupResult]): parser = group.GroupParser() self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) self.results = resultPerGroup + class State3: def __init__( - self, - previewImport: HtmlPreviewImport, - htmlResults: HtmlCompetitionTotalResults + self, previewImport: HtmlPreviewImport, htmlResults: HtmlCompetitionTotalResults ): self.previewImport = previewImport self.htmlResults = htmlResults + class Participant: - def __init__(self, firstName: str, lastName: str, club: str, group: group.Group, class_: competition_class.CompetitionClass): + def __init__( + self, + firstName: str, + lastName: str, + club: str, + group: group.Group, + class_: competition_class.CompetitionClass, + ): self.firstName = firstName self.lastName = lastName self.club = club self.group = group self.class_ = class_ + class ParticipantResult: def __init__( - self, id: int, finalist: bool, cancelled: bool, + self, + id: int, + finalist: bool, + cancelled: bool, group: group.Group_t, class_: competition_class.Class_t, dance: str, - place, placeTo + place, + placeTo, ): self.id = id self.finalist = finalist @@ -280,10 +351,12 @@ class ParticipantResult: self.place = place self.placeTo = placeTo + class Stage2: def __init__(self, results: dict[Participant, list[ParticipantResult]]): self.results = results + class TableCompetitionEntry: def __init__( self, @@ -293,7 +366,7 @@ class TableCompetitionEntry: place: int = -1, placeTo: int = -1, group: group.Group_t = None, - id: int = None + id: int = None, ): self.finalist = finalist self.cancelled = cancelled @@ -301,61 +374,60 @@ class TableCompetitionEntry: self.class_ = class_ self.place = place self.placeTo = placeTo - + def __repr__(self): def paramMerging(l): - return ', '.join(filter(lambda x: x is not None, l)) + return ", ".join(filter(lambda x: x is not None, l)) if self.cancelled: params = paramMerging([self.group, self.class_, self.id]) if len(params) > 0: - return f'- ({params})' + return f"- ({params})" else: - return '-' + return "-" elif not self.finalist: params = paramMerging([self.group, self.class_, self.id]) if len(params) > 0: - return f'x ({params})' + return f"x ({params})" else: - return 'x' + return "x" else: if self.place == self.placeTo: - place = f'{self.place}.' + place = f"{self.place}." else: - place = f'{self.place}.-{self.placeTo}.' + place = f"{self.place}.-{self.placeTo}." params = paramMerging([self.group, self.class_, self.id]) - return f'{place} ({params})' + return f"{place} ({params})" + class TableEntry: def __init__(self, competitions: list[TableCompetitionEntry]): self.competitions = competitions - + def __repr__(self): - return ', '.join(self.competitions) + return ", ".join(self.competitions) + class TableRow: - def __init__( - self, - participant: Participant, - id: int, - entries: list[TableEntry] - ): + def __init__(self, participant: Participant, id: int, entries: list[TableEntry]): self.participant = participant self.id = id self.entries = entries - + def getRowList(self): if self.id is not None: - first = f'{self.id}. {self.participant.firstName} {self.participant.lastName} ({self.participant.club})' + first = f"{self.id}. {self.participant.firstName} {self.participant.lastName} ({self.participant.club})" else: - first = f'{self.participant.firstName} {self.participant.lastName} ({self.participant.club})' + first = f"{self.participant.firstName} {self.participant.lastName} ({self.participant.club})" return [first] + map(str, self.entries) + class OutputTable: def __init__(self, dances: list[str], rows: list[TableRow]): self.dances = dances self.rows = rows + class Stage1: def __init__(self, tables: dict[group.Group, OutputTable]): self.tables = tables diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index e87d48d..c4faa1c 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -10,15 +10,16 @@ from .types import HtmlCompetitionResultRow as CompetitionResult from . import types from . import competition_class + class HtmlPerson: def __init__(self, name, id, group): self.name = name self.id = id self.group = group - + def __repr__(self): - return f'{self.name} ({self.id}, {self.group})' - + return f"{self.name} ({self.id}, {self.group})" + def __eq__(self, o): if not isinstance(o, HtmlPerson): return False @@ -27,40 +28,39 @@ class HtmlPerson: def __hash__(self): return str(self).__hash__() + class ResultPerson: - def __init__(self, firstName, lastName, club, id = None, group = None): + def __init__(self, firstName, lastName, club, id=None, group=None): self.firstName = firstName self.lastName = lastName - self.name = f'{firstName} {lastName}' + self.name = f"{firstName} {lastName}" self.club = club self.id = id self.group = group - + @staticmethod def extractFromResultRow(row: ResultRow): return ResultPerson( - firstName=row.firstName, - lastName=row.lastName, - club=row.club + firstName=row.firstName, lastName=row.lastName, club=row.club ) - + def __eq__(self, o): if not isinstance(o, ResultPerson): return False - + return ( - self.firstName == o.firstName and - self.lastName == o.lastName and - self.club == o.club and - self.id == o.id + self.firstName == o.firstName + and self.lastName == o.lastName + and self.club == o.club + and self.id == o.id ) - + def __repr__(self): if self.id is None: - return f'{self.name} ({self.club})' + return f"{self.name} ({self.club})" else: - return f'{self.name} ({self.club}) [{self.id}]' - + return f"{self.name} ({self.club}) [{self.id}]" + def __hash__(self): text = str(self) return text.__hash__() @@ -69,83 +69,99 @@ class ResultPerson: class ImportNotParsableException(Exception): pass + ParserList_t = dict[str, html_parser.HtmlParser] + class PreviewWorker: def __init__(self): - self.l = logging.getLogger('solo_turnier.worker.PreviewWorker') + self.l = logging.getLogger("solo_turnier.worker.PreviewWorker") self.participants = {} self.previewResults = {} - + def filterFilesPreview(self, files: list[str]) -> ParserList_t: - self.l.debug('Filtering the list of parsers by removing all non preview entries.') + self.l.debug( + "Filtering the list of parsers by removing all non preview entries." + ) ret = {} for file in files: - with open(file, 'r') as fp: + with open(file, "r") as fp: text = fp.read() - + parser = html_parser.HtmlParser(text, file) try: data = parser.guessDataFromHtmlTitle() except: - self.l.error(f'Unable to parse html file in {file}. Please check manually.') + self.l.error( + f"Unable to parse html file in {file}. Please check manually." + ) continue - if data['class_'] == 'Sichtung': + if data["class_"] == "Sichtung": self.l.debug(f"Found candidate in {file}. Adding to the list.") ret[file] = parser else: - self.l.debug(f'Rejecting file {file} as the name {data["class_"]} did not match.') - + self.l.debug( + f'Rejecting file {file} as the name {data["class_"]} did not match.' + ) + return ret - + def __extractPersonsFromSinglePreview(self, parser: html_parser.HtmlParser): imported = parser.parsePreparationRound() parser.cleanPreparationRoundImport(imported) - data = imported['data'] + data = imported["data"] headerData = parser.guessDataFromHtmlTitle() - dance = headerData['dance'] + dance = headerData["dance"] classParser = solo_turnier.competition_class.CompetitionClassParser() def getRowIndexOfClass(): - return data['titles'].index('Platz von\nPlatz bis') - - self.l.log(5, data) - - if data['titles'][0] != 'Wertungsrichter': - self.l.fatal('Cannot parse the parsed content of the preview file.') - raise ImportNotParsableException('Incompatible export file') + return data["titles"].index("Platz von\nPlatz bis") - if data['titles'][-1] == 'Startgruppe': - self.l.debug('Combined competition found. Extracting group from table required.') + self.l.log(5, data) + + if data["titles"][0] != "Wertungsrichter": + self.l.fatal("Cannot parse the parsed content of the preview file.") + raise ImportNotParsableException("Incompatible export file") + + if data["titles"][-1] == "Startgruppe": + self.l.debug( + "Combined competition found. Extracting group from table required." + ) extractGroup = True else: - self.l.debug('Using group from the title.') - group = parser.guessDataFromHtmlTitle(imported['title'])['group'] + self.l.debug("Using group from the title.") + group = parser.guessDataFromHtmlTitle(imported["title"])["group"] extractGroup = False classRowIndex = getRowIndexOfClass() - for index, e in enumerate(data['table'][0]): - if e['text'] == '': + for index, e in enumerate(data["table"][0]): + if e["text"] == "": # Skip empty columns continue - - # Extract data from column - name = e['meta'] - id = int(e['text']) - if extractGroup: - group = data['table'][-1][index]['text'] - # dance = - class_ = classParser.parseClass(data['table'][classRowIndex][index]['text']) - + # Extract data from column + name = e["meta"] + id = int(e["text"]) + if extractGroup: + group = data["table"][-1][index]["text"] + + # dance = + class_ = classParser.parseClass(data["table"][classRowIndex][index]["text"]) + participant = types.HtmlPreviewParticipant(name, id, group) l = self.participants.get(id, []) - self.l.log(5, 'Checking for existence of %s in %s: %s', participant, l, participant in l) + self.l.log( + 5, + "Checking for existence of %s in %s: %s", + participant, + l, + participant in l, + ) if participant not in l: l.append(participant) self.participants[id] = l @@ -162,118 +178,146 @@ class PreviewWorker: try: self.__extractPersonsFromSinglePreview(parser) except: - self.l.error('Failed to parse preview round in file %s. Skipping this file\'s content.', parser.fileName) - + self.l.error( + "Failed to parse preview round in file %s. Skipping this file's content.", + parser.fileName, + ) + return types.HtmlPreviewImport(self.participants, self.previewResults) + class ResultExtractor: - def __init__(self): - self.l = logging.getLogger('solo_turnier.worker.ResultExtractor') - self.rePlaceSingle = re.compile(' *([0-9]+) *') - self.rePlaceDouble = re.compile(' *([0-9]+) *- *([0-9]+) *') - - def getAllParsers(self, files: list[tuple[str,str]]) -> ParserList_t: + self.l = logging.getLogger("solo_turnier.worker.ResultExtractor") + self.rePlaceSingle = re.compile(" *([0-9]+) *") + self.rePlaceDouble = re.compile(" *([0-9]+) *- *([0-9]+) *") + + def getAllParsers(self, files: list[tuple[str, str]]) -> ParserList_t: ret = {} classParser = competition_class.CompetitionClassParser() for filePair in files: - with open(filePair[0], 'r') as fp: + with open(filePair[0], "r") as fp: text = fp.read() parser = html_parser.HtmlParser(text, filePair[0]) if filePair[1] is None: parserTab = None else: - with open(filePair[1], 'r') as fp: + with open(filePair[1], "r") as fp: textTab = fp.read() parserTab = html_parser.HtmlParser(textTab, filePair[1]) try: data = parser.guessDataFromHtmlTitle() except: - self.l.error('Cannot parse HTML file %s to check if it is a valid result. Check manually.', filePair[0]) + self.l.error( + "Cannot parse HTML file %s to check if it is a valid result. Check manually.", + filePair[0], + ) continue try: - guessedClass = classParser.parseClass(data['class_']) + guessedClass = classParser.parseClass(data["class_"]) except: - self.l.error('Issue parsing class of file %s. Check manually.', filePair[0]) + self.l.error( + "Issue parsing class of file %s. Check manually.", filePair[0] + ) continue - self.l.debug('Fetched result data: %s, guessed class %s', data, guessedClass) + self.l.debug( + "Fetched result data: %s, guessed class %s", data, guessedClass + ) ret[filePair] = (parser, parserTab) return ret def _extractPlace(self, placeStr: str): - s = placeStr.replace('.', '') + s = placeStr.replace(".", "") matches = self.rePlaceSingle.fullmatch(s) if matches is not None: return (int(matches.group(1)), None) - + matches = self.rePlaceDouble.fullmatch(s) if matches is not None: return (int(matches.group(1)), int(matches.group(2))) self.l.error('Could not parse place string "%s"', placeStr) - raise Exception('Place cannot be parsed') + raise Exception("Place cannot be parsed") - def _analyzeSingleParser(self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults): + def _analyzeSingleParser( + self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults + ): data = parser.guessDataFromHtmlTitle() - competitionClass = data['class_'] - competitionGroup = data['group'] - dance = data['dance'] + competitionClass = data["class_"] + competitionGroup = data["group"] + dance = data["dance"] result = parser.parseResult() - self.l.log(5, 'Raw data extracted: %s', result) + self.l.log(5, "Raw data extracted: %s", result) for person in result.results.keys(): placeStr = result.results[person] place, placeTo = self._extractPlace(placeStr) - competitionResult = types.HtmlSingleCompetitionResult(person.name, place, placeTo, person.finalist) - results.add(competitionGroup, competitionClass, dance, person.id, competitionResult) - # + competitionResult = types.HtmlSingleCompetitionResult( + person.name, place, placeTo, person.finalist + ) + results.add( + competitionGroup, competitionClass, dance, person.id, competitionResult + ) + # - def _analyzeIndividualResults(self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults): + def _analyzeIndividualResults( + self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults + ): data = parser.guessDataFromHtmlTitle() - competitionClass = data['class_'] - competitionGroup = data['group'] - dance = data['dance'] + competitionClass = data["class_"] + competitionGroup = data["group"] + dance = data["dance"] result = parser.parseIndividualResult(competitionGroup, competitionClass, dance) - self.l.log(5, 'Found individual results: %s', result.participants) + self.l.log(5, "Found individual results: %s", result.participants) results.tabges.update(result.participants) - def extractAllData(self, parsers: ParserList_t) -> types.HtmlCompetitionTotalResults: + def extractAllData( + self, parsers: ParserList_t + ) -> types.HtmlCompetitionTotalResults: ret = types.HtmlCompetitionTotalResults() for fileNameTuple in parsers: fileName = fileNameTuple[0] - self.l.debug('Extracting data from file %s', fileName) + self.l.debug("Extracting data from file %s", fileName) self._analyzeSingleParser(parsers[fileNameTuple][0], ret) if parsers[fileNameTuple][1] is None: - self.l.info('Skipping extraction of individual result as class is not yet finished.') + self.l.info( + "Skipping extraction of individual result as class is not yet finished." + ) else: - self.l.debug('Fetching individual result of combined competitions in %s', fileName) + self.l.debug( + "Fetching individual result of combined competitions in %s", + fileName, + ) self._analyzeIndividualResults(parsers[fileNameTuple][1], ret) return ret + class DataWorker: def __init__(self): - self.l = logging.getLogger('solo_turnier.worker.DataWorker') - - def combineRowsByPerson(self, rows: list[ResultRow]) -> dict[ResultPerson, list[CompetitionResult]]: + self.l = logging.getLogger("solo_turnier.worker.DataWorker") + + def combineRowsByPerson( + self, rows: list[ResultRow] + ) -> dict[ResultPerson, list[CompetitionResult]]: ret = {} for row in rows: result = CompetitionResult.extractFromResultRow(row) - if result.place == '-' or result.placeTo == '-': + if result.place == "-" or result.placeTo == "-": continue - + person = ResultPerson.extractFromResultRow(row) if person not in ret: ret[person] = [] @@ -297,69 +341,94 @@ class DataWorker: The second one is True if there was the need to override a group but it was possible to extract from other data The second one can be seen as a warning """ - def consolidateGroups(self, data:dict[ResultPerson, list[CompetitionResult]]) -> tuple[bool, bool]: + + def consolidateGroups( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> tuple[bool, bool]: ambiguous = False warnChange = False - unambiguousGroups = set(['Kin.', 'Jun.', 'Jug.']) - combinations = set(['Kin./Jun.', 'Jun./Jug.']) + unambiguousGroups = set(["Kin.", "Jun.", "Jug."]) + combinations = set(["Kin./Jun.", "Jun./Jug."]) for person in data: groupsRaw = set([c.group for c in data[person]]) - + unknown = groupsRaw.difference(unambiguousGroups).difference(combinations) if len(unknown) > 0: - raise Exception(f'There were unknown groups found for {person}: {unknown}') - + raise Exception( + f"There were unknown groups found for {person}: {unknown}" + ) + numUnambiguousGroups = len(groupsRaw.intersection(unambiguousGroups)) if numUnambiguousGroups == 0: if len(groupsRaw) == 2: warnChange = True - person.group = 'Jun.' + person.group = "Jun." else: ambiguous = True if len(groupsRaw) == 1: person.group = list(groupsRaw)[0] - + elif numUnambiguousGroups == 1: if len(groupsRaw.intersection(combinations)) > 0: warnChange = True - + person.group = list(groupsRaw.intersection(unambiguousGroups))[0] - + else: - raise Exception(f'{person} cannot have different groups.') - + raise Exception(f"{person} cannot have different groups.") + return (not ambiguous, warnChange) def _createHtmlLUT(self, htmlImports: list[html_parser.HtmlImport]): ret = {} - parser = html_parser.HtmlParser('') + parser = html_parser.HtmlParser("") for imp in htmlImports: parsed = parser.guessDataFromHtmlTitle(imp.title) - key = (parsed['group'], parsed['class_'], parsed['dance']) + key = (parsed["group"], parsed["class_"], parsed["dance"]) ret[key] = imp - self.l.debug('LUT[%s] = %s', key, imp) - self.l.debug('LUT completed') + self.l.debug("LUT[%s] = %s", key, imp) + self.l.debug("LUT completed") return ret - def mergeHtmlData(self, data:dict[ResultPerson, list[CompetitionResult]], htmlImports: list[html_parser.HtmlImport]): + def mergeHtmlData( + self, + data: dict[ResultPerson, list[CompetitionResult]], + htmlImports: list[html_parser.HtmlImport], + ): lut = self._createHtmlLUT(htmlImports) - + for person in data: for competition in data[person]: - key = (competition.competitionGroup, competition.competitionClass, competition.dance) + key = ( + competition.competitionGroup, + competition.competitionClass, + competition.dance, + ) htmlImport = lut[key] participant = htmlImport.participants[str(competition.id)] if participant.name != person.name: - self.l.error(f'Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully.') + self.l.error( + f"Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully." + ) competition.finalist = participant.finalist - - def getAllDancesInCompetitions(self, data:dict[ResultPerson, list[CompetitionResult]]) -> list[str]: + + def getAllDancesInCompetitions( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> list[str]: allDances = [ - 'Samba', 'Cha Cha', 'Rumba', 'Paso Doble', 'Jive', - 'Langs. Walzer', 'Tango', 'Wiener Walzer', 'Slowfox', 'Quickstep' + "Samba", + "Cha Cha", + "Rumba", + "Paso Doble", + "Jive", + "Langs. Walzer", + "Tango", + "Wiener Walzer", + "Slowfox", + "Quickstep", ] dancesPresent = {d: False for d in allDances} @@ -369,21 +438,24 @@ class DataWorker: return [d for d in allDances if dancesPresent[d]] - def collectPersonsInGroups(self, data:dict[ResultPerson, list[CompetitionResult]]) -> list[tuple[str, list[ResultPerson]]]: + def collectPersonsInGroups( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> list[tuple[str, list[ResultPerson]]]: groups = { - 'Kin.': [p for p in data.keys() if p.group == 'Kin.'], - 'Jun.': [p for p in data.keys() if p.group == 'Jun.'], - 'Jug.': [p for p in data.keys() if p.group == 'Jug.'], + "Kin.": [p for p in data.keys() if p.group == "Kin."], + "Jun.": [p for p in data.keys() if p.group == "Jun."], + "Jug.": [p for p in data.keys() if p.group == "Jug."], } - found = groups['Kin.'] + groups['Jun.'] + groups['Jug.'] - groups['Sonst'] = [p for p in data.keys() if p not in found] + found = groups["Kin."] + groups["Jun."] + groups["Jug."] + groups["Sonst"] = [p for p in data.keys() if p not in found] return groups def sortPersonsInGroup(self, persons: list[ResultPerson]) -> list[ResultPerson]: ids = [p.id for p in persons] def decorateByName(p: ResultPerson): - return (f'{p.name} ({p.club})', p) + return (f"{p.name} ({p.club})", p) + def decorateById(p: ResultPerson): return (p.id, p) @@ -394,96 +466,106 @@ class DataWorker: else: decorated = [decorateById(p) for p in persons] showIds = True - + decorated.sort() return ([d[1] for d in decorated], showIds) - - def mapPersonResultsToDanceList(self, results: list[CompetitionResult], dances: list[str]) -> list[CompetitionResult|None]: + + def mapPersonResultsToDanceList( + self, results: list[CompetitionResult], dances: list[str] + ) -> list[CompetitionResult | None]: ret = [] for dance in dances: competitions = [c for c in results if c.dance == dance] if len(competitions) == 0: ret.append(None) elif len(competitions) > 1: - raise Exception(f'Multiple competitions with the same dance "{dance}" found.') + raise Exception( + f'Multiple competitions with the same dance "{dance}" found.' + ) else: ret.append(competitions[0]) - + return ret + class Worker: def __init__(self): - self.l = logging.getLogger('solo_turnier.worker.Worker') - self._allDances = ( - ['Samba', 'Cha Cha', 'Rumba', 'Paso Doble', 'Jive'] + - ['Langs. Walzer', 'Tango', 'Wiener Walzer', 'Slowfox', 'Quickstep'] - ) - - def collectAllData( - self, - htmlCandidatesPreview: list[str], - htmlResultsFileNames: list[str] - ) -> types.State3: - - previewWorker = PreviewWorker() - self.l.info('Filtering for pure preview rounds.') - parsers = previewWorker.filterFilesPreview(htmlCandidatesPreview) - self.l.debug('Remaining files: %s', list(parsers.keys())) + self.l = logging.getLogger("solo_turnier.worker.Worker") + self._allDances = ["Samba", "Cha Cha", "Rumba", "Paso Doble", "Jive"] + [ + "Langs. Walzer", + "Tango", + "Wiener Walzer", + "Slowfox", + "Quickstep", + ] - self.l.info('Extracting person data from the preview rounds.') + def collectAllData( + self, htmlCandidatesPreview: list[str], htmlResultsFileNames: list[str] + ) -> types.State3: + previewWorker = PreviewWorker() + self.l.info("Filtering for pure preview rounds.") + parsers = previewWorker.filterFilesPreview(htmlCandidatesPreview) + self.l.debug("Remaining files: %s", list(parsers.keys())) + + self.l.info("Extracting person data from the preview rounds.") previewImport = previewWorker.importAllData(parsers) - self.l.debug('Total preview imported participants: %s', pformat(previewImport.participants)) - self.l.log(5, 'Total preview results: %s', pformat(previewImport.results)) + self.l.debug( + "Total preview imported participants: %s", + pformat(previewImport.participants), + ) + self.l.log(5, "Total preview results: %s", pformat(previewImport.results)) resultExtractor = ResultExtractor() resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) htmlResults = resultExtractor.extractAllData(resultParsers) - self.l.info('Overall result data extracted: %s', pformat(htmlResults.results)) - + self.l.info("Overall result data extracted: %s", pformat(htmlResults.results)) + return types.State3(previewImport, htmlResults) def combineData(self, importedData: types.State3): - self.l.info('Starting to build data sets.') + self.l.info("Starting to build data sets.") groups = self._extractGroups(importedData) - self.l.debug('Found groups in the dataset: %s', groups) + self.l.debug("Found groups in the dataset: %s", groups) totalResult = {} for group in groups: - self.l.debug('Collecting data for total result of group %s', group) + self.l.debug("Collecting data for total result of group %s", group) dances = self._extractDancesPerGroup(importedData, group) - self.l.log(5, 'Found dances in group %s: %s', group, dances) + self.l.log(5, "Found dances in group %s: %s", group, dances) participants = self._extractParticipantsPerGroup(importedData, group) - self.l.log(5, 'Related participants %s', participants) + self.l.log(5, "Related participants %s", participants) results = {} for participant in participants: - self.l.log(5, 'Collecting data for %s', participant) + self.l.log(5, "Collecting data for %s", participant) resultsOfParticipant = self._getResultOfSingleParticipant( - participant, group, importedData.previewImport, - importedData.htmlResults, dances - ) - self.l.log(5, 'Obtained result %s', resultsOfParticipant) + participant, + group, + importedData.previewImport, + importedData.htmlResults, + dances, + ) + self.l.log(5, "Obtained result %s", resultsOfParticipant) results[participant] = resultsOfParticipant - - self.l.log(5, 'Result before native fixing: %s', pformat(results)) + + self.l.log(5, "Result before native fixing: %s", pformat(results)) # self._fixNativePlaces(dances, results) self._fixNativeDataFromTable(dances, results, importedData.htmlResults) - self.l.log(5, 'Result after native fixing: %s', pformat(results)) + self.l.log(5, "Result after native fixing: %s", pformat(results)) # self.l.log(5,'Fixed data %s', results) totalResult[group] = types.TotalGroupResult(dances, results) - - self.l.log(5, 'Total result of all groups: %s', pformat(totalResult)) + + self.l.log(5, "Total result of all groups: %s", pformat(totalResult)) ret = types.State4(totalResult) return ret - def _extractGroups(self, data: types.State3): groupParser = solo_turnier.group.GroupParser() @@ -497,12 +579,14 @@ class Worker: # groupSet.add(gr) groupSet.update(gr.getContainedGroups()) # self.l.log(5, 'Group type %s', type(gr)) - - self.l.log(5, 'Set of active groups: %s', groupSet) + + self.l.log(5, "Set of active groups: %s", groupSet) groups = groupParser.getGroupsAsSortedList(groupSet) return groups - - def _extractDancesPerGroup(self, data: types.State3, group: solo_turnier.group.Group): + + def _extractDancesPerGroup( + self, data: types.State3, group: solo_turnier.group.Group + ): groupParser = solo_turnier.group.GroupParser() dances = set() @@ -513,24 +597,27 @@ class Worker: if group not in currentGroup.getContainedGroups(): continue foundDances.add(tup[2]) - + dances.update(foundDances.intersection(self._allDances)) additionalDances.update(foundDances.difference(self._allDances)) if len(additionalDances) > 0: - self.l.error('There were dances found, that are not registered. A bug? The dances were: %s', additionalDances) - + self.l.error( + "There were dances found, that are not registered. A bug? The dances were: %s", + additionalDances, + ) + dancesList = [x for x in self._allDances if x in dances] additionalDancesList = list(additionalDances) additionalDancesList.sort() return dancesList + additionalDancesList - + def _extractParticipantsPerGroup( self, importedData: types.State3, # previewData: types.HtmlPreviewImport, - group: solo_turnier.group.Group - ) -> list[types.HtmlPreviewParticipant]: + group: solo_turnier.group.Group, + ) -> list[types.HtmlPreviewParticipant]: groupParser = types.group.GroupParser() ret = [] @@ -546,14 +633,14 @@ class Worker: fixture = importedData.htmlResults.tabges[tup] if fixture[2] is not None and fixture[2] != group: - self.l.log(5, 'Skipping id %s in group %s as not part', tup[3], group) + self.l.log(5, "Skipping id %s in group %s as not part", tup[3], group) continue part = importedData.htmlResults.results[tup][0] part.id = int(tup[3]) ret.append(part) - self.l.log(5, 'ret %s', ret) + self.l.log(5, "ret %s", ret) # raise Exception('Test') # for id in previewData.participants: @@ -562,30 +649,32 @@ class Worker: # if participant.group == group: # ret.append(participant) return ret - + def _getResultOfSingleParticipant( self, participant: types.HtmlParticipant, nominalGroup: solo_turnier.group.Group, previewResults: types.HtmlPreviewImport, totalResults: types.HtmlCompetitionTotalResults, - allDances: list[str] - ) -> list[types.SingleParticipantResult|None]: + allDances: list[str], + ) -> list[types.SingleParticipantResult | None]: rawResults = totalResults.getById(participant.id) - self.l.log(5, 'Found result data for id %i (raw): %s', participant.id, rawResults) + self.l.log( + 5, "Found result data for id %i (raw): %s", participant.id, rawResults + ) results = [None for x in allDances] for danceIdx, dance in enumerate(allDances): # self.l.log(5, '%s %s', dance, danceIdx) - def getResult() -> types.SingleParticipantResult|None: + def getResult() -> types.SingleParticipantResult | None: for key in rawResults: if key[0] != dance: continue rawResult = rawResults[key] if len(rawResult) != 1: - raise Exception('Multiple results found with same key') + raise Exception("Multiple results found with same key") rawResult = rawResult[0] nativeClass = key[2] @@ -594,33 +683,37 @@ class Worker: # self.l.log(5, 'Result %s => %s', key, rawResult) ret = types.SingleParticipantResult( - key[2], nativeClass, dance, rawResult.finalist, - rawResult.place, rawResult.placeTo + key[2], + nativeClass, + dance, + rawResult.finalist, + rawResult.place, + rawResult.placeTo, ) return ret return None - + results[danceIdx] = getResult() - + return results def _fixNativeDataFromTable( self, dances: list[str], data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]], - importedData: types.HtmlCompetitionTotalResults - ): - rePlace = re.compile('([0-9]+)(?:-([0-9]+))?') + importedData: types.HtmlCompetitionTotalResults, + ): + rePlace = re.compile("([0-9]+)(?:-([0-9]+))?") classParser = competition_class.CompetitionClassParser() for participant in data.keys(): - self.l.log(5, 'fixing participant %s', participant) + self.l.log(5, "fixing participant %s", participant) results = data[participant] for result in results: if result is None: continue - self.l.log(5, 'Looking at result set %s', result) + self.l.log(5, "Looking at result set %s", result) def selectEntry(k): return k[2] == result.dance and int(k[3]) == participant.id @@ -633,13 +726,19 @@ class Worker: continue raw = importedData.tabges[keys[selectedIndex]] - self.l.log(5,'Raw %s', raw) + self.l.log(5, "Raw %s", raw) nativePlaceRaw = raw[0] matcher = rePlace.fullmatch(nativePlaceRaw) if matcher is None: - self.l.error('Cannot parse place string %s for participant %u (%s) in dance %s', nativePlaceRaw, participant.id, participant, result.dance) + self.l.error( + "Cannot parse place string %s for participant %u (%s) in dance %s", + nativePlaceRaw, + participant.id, + participant, + result.dance, + ) continue - self.l.log(5, 'Found strings by regex: %s', matcher.groups()) + self.l.log(5, "Found strings by regex: %s", matcher.groups()) result.placeNative = matcher.group(1) result.placeNativeTo = matcher.group(2) @@ -651,32 +750,36 @@ class Worker: def _fixNativePlaces( self, dances: list[str], - data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]] - ): + data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]], + ): classParser = solo_turnier.competition_class.CompetitionClassParser() allClasses = classParser.getAllClasses() allClasses.reverse() for class_ in allClasses: for danceIdx, dance in enumerate(dances): - self.l.log(5, 'Fixing native places for class %s in dance %s', class_, dance) + self.l.log( + 5, "Fixing native places for class %s in dance %s", class_, dance + ) remainingParticipants = [] for participant in data.keys(): results = data[participant] danceResult = results[danceIdx] - + if danceResult is None: continue # self.l.log(5, 'Result of dance: %s', danceResult) - + if classParser.isABetterThanB(danceResult.nativeClass, class_): # self.l.log(5, 'Skipping %s as the native class is higher', participant) continue - - remainingParticipants.append((danceResult.place, participant.id, participant)) + + remainingParticipants.append( + (danceResult.place, participant.id, participant) + ) remainingParticipants.sort() # self.l.log(5, 'Remaining participants %s', remainingParticipants) @@ -684,17 +787,20 @@ class Worker: def getAllParticipantsWithSamePlace(): first = remainingParticipants.pop(0) ret = [first] - while len(remainingParticipants) > 0 and remainingParticipants[0][0] == first[0]: + while ( + len(remainingParticipants) > 0 + and remainingParticipants[0][0] == first[0] + ): ret.append(remainingParticipants.pop(0)) return ret - + def updateNativePlaces(samePlaced, placeStart): nextPlace = placeStart + len(samePlaced) if len(samePlaced) == 1: placeTo = None else: placeTo = nextPlace - 1 - + for p in samePlaced: data[p[2]][danceIdx].placeNative = placeStart data[p[2]][danceIdx].placeNativeTo = placeTo @@ -706,33 +812,38 @@ class Worker: while len(remainingParticipants) > 0: samePlaced = getAllParticipantsWithSamePlace() place = updateNativePlaces(samePlaced, place) - + # self.l.log(5, '(Partially) fixed places: %s', (data)) def filterOutFinalists(self, data: types.State4, filterOut: bool): for group in data.results: - self.l.debug('Cleaning up group %s', group.name) + self.l.debug("Cleaning up group %s", group.name) participants = data.results[group].results.keys() droppedParticipants = [] for participant in participants: - self.l.debug('Checking %s', participant) + self.l.debug("Checking %s", participant) - def isFinalistInDance(x: types.HtmlSingleCompetitionResult|None): + def isFinalistInDance(x: types.HtmlSingleCompetitionResult | None): if x is None: return False return x.finalist - mapped = list(map(isFinalistInDance, data.results[group].results[participant])) + + mapped = list( + map(isFinalistInDance, data.results[group].results[participant]) + ) finalist = True in mapped - self.l.log(5,'Check for finalist (in dances %s): %s', mapped, finalist) + self.l.log(5, "Check for finalist (in dances %s): %s", mapped, finalist) if finalist: participant.finalist = True else: participant.finalist = False - self.l.info('Dropping %s from the output as no finalist', participant) + self.l.info( + "Dropping %s from the output as no finalist", participant + ) droppedParticipants.append(participant) - + if filterOut: for droppedParticipant in droppedParticipants: data.results[group].results.pop(droppedParticipant) From d1133d6d368067530b6aa3bc11c98aa0cd042cdb Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 17:12:26 +0100 Subject: [PATCH 10/31] Added mor elinter tools to toolchain --- requiremnts.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requiremnts.txt b/requiremnts.txt index 0a75558..ce97304 100644 --- a/requiremnts.txt +++ b/requiremnts.txt @@ -23,6 +23,8 @@ packaging==23.2 pathspec==0.11.2 platformdirs==4.0.0 pluggy==1.0.0 +pydocstyle==6.3.0 +pyflakes==3.1.0 pynsist==2.8 pyparsing==3.0.9 pytest==7.2.0 @@ -30,6 +32,7 @@ pytest-cov==4.0.0 pytest-mock==3.10.0 requests==2.31.0 requests_download==0.1.2 +snowballstemmer==2.2.0 soupsieve==2.3.2.post1 tabulate==0.9.0 tomli==2.0.1 From 01290517567b9a6da11600c56616c38931553b33 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 17:40:57 +0100 Subject: [PATCH 11/31] Register black with VS code --- auswertung.code-workspace | 5 ++++- pyproject.toml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml diff --git a/auswertung.code-workspace b/auswertung.code-workspace index f7266e4..9daad0d 100644 --- a/auswertung.code-workspace +++ b/auswertung.code-workspace @@ -13,6 +13,9 @@ "python.testing.pytestEnabled": false, "python.autoComplete.extraPaths": [ "${workspaceFolder:code}/venv/lib" - ] + ], + "editor.formatOnSave": true, + "editor.renderWhitespace": "all", + "python.formatting.provider": "black", } } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3000714 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[too.black] + +line-length = 120 From c06c5ed791f84ecbe3d40635e91196086b3e37f6 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 17:52:19 +0100 Subject: [PATCH 12/31] Adc config for flake linter --- .flake8 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..aaf780b --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +per-file-ignores = __init__.py:F401 +extend-exclude = build +extend-ignore = E501 From f5132ce8e8a13f6e825935e715b30aa986f459af Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 18:07:49 +0100 Subject: [PATCH 13/31] Removed preview worker to simplify code base --- src/solo_turnier/batch.py | 10 +- src/solo_turnier/cli.py | 3 - src/solo_turnier/html_locator.py | 4 - src/solo_turnier/worker.py | 206 +------------------------------ 4 files changed, 4 insertions(+), 219 deletions(-) diff --git a/src/solo_turnier/batch.py b/src/solo_turnier/batch.py index ad489e3..c600d30 100644 --- a/src/solo_turnier/batch.py +++ b/src/solo_turnier/batch.py @@ -16,15 +16,9 @@ class BatchWorker: locator = solo_turnier.html_locator.HtmlLocator() self.l.info( - 'Checking for feasible preview HTML export files in "%s"', + 'Checking for feasible HTML export files in "%s"', self.config.importHtmlPath(), ) - htmlCandidatesPreview = locator.findPreviewRoundCandidates( - self.config.importHtmlPath() - ) - self.l.debug( - "Found HTML file candidates for preview rounds: %s", htmlCandidatesPreview - ) htmlResultFiles = locator.findCandidates(self.config.importHtmlPath()) self.l.debug( @@ -32,7 +26,7 @@ class BatchWorker: ) worker = solo_turnier.worker.Worker() - importedData = worker.collectAllData(htmlCandidatesPreview, htmlResultFiles) + importedData = worker.collectAllData(htmlResultFiles) combinedData = worker.combineData(importedData) worker.filterOutFinalists(combinedData, removeFilteredParicipants) diff --git a/src/solo_turnier/cli.py b/src/solo_turnier/cli.py index 2d240c5..d4c256f 100644 --- a/src/solo_turnier/cli.py +++ b/src/solo_turnier/cli.py @@ -75,9 +75,6 @@ class Cli: def importHtmlPath(self): return self.__args.html[0] - def importCSVPath(self): - return self.__args.import_from[0] - def output(self): return self.__args.output[0] diff --git a/src/solo_turnier/html_locator.py b/src/solo_turnier/html_locator.py index 429ab6b..95bd20b 100644 --- a/src/solo_turnier/html_locator.py +++ b/src/solo_turnier/html_locator.py @@ -31,7 +31,3 @@ class HtmlLocator: candidatesErg = self.__findRecursivelyCandidates(path, "erg.htm") candidates = [self.__fingMatchingTabs(x) for x in candidatesErg] return candidates - - def findPreviewRoundCandidates(self, path: str): - candidates = self.__findRecursivelyCandidates(path, "tabges.htm") - return candidates diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index c4faa1c..7cda1c1 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -66,126 +66,9 @@ class ResultPerson: return text.__hash__() -class ImportNotParsableException(Exception): - pass - - ParserList_t = dict[str, html_parser.HtmlParser] -class PreviewWorker: - def __init__(self): - self.l = logging.getLogger("solo_turnier.worker.PreviewWorker") - self.participants = {} - self.previewResults = {} - - def filterFilesPreview(self, files: list[str]) -> ParserList_t: - self.l.debug( - "Filtering the list of parsers by removing all non preview entries." - ) - ret = {} - for file in files: - with open(file, "r") as fp: - text = fp.read() - - parser = html_parser.HtmlParser(text, file) - - try: - data = parser.guessDataFromHtmlTitle() - except: - self.l.error( - f"Unable to parse html file in {file}. Please check manually." - ) - continue - - if data["class_"] == "Sichtung": - self.l.debug(f"Found candidate in {file}. Adding to the list.") - ret[file] = parser - else: - self.l.debug( - f'Rejecting file {file} as the name {data["class_"]} did not match.' - ) - - return ret - - def __extractPersonsFromSinglePreview(self, parser: html_parser.HtmlParser): - imported = parser.parsePreparationRound() - parser.cleanPreparationRoundImport(imported) - data = imported["data"] - - headerData = parser.guessDataFromHtmlTitle() - dance = headerData["dance"] - classParser = solo_turnier.competition_class.CompetitionClassParser() - - def getRowIndexOfClass(): - return data["titles"].index("Platz von\nPlatz bis") - - self.l.log(5, data) - - if data["titles"][0] != "Wertungsrichter": - self.l.fatal("Cannot parse the parsed content of the preview file.") - raise ImportNotParsableException("Incompatible export file") - - if data["titles"][-1] == "Startgruppe": - self.l.debug( - "Combined competition found. Extracting group from table required." - ) - extractGroup = True - else: - self.l.debug("Using group from the title.") - group = parser.guessDataFromHtmlTitle(imported["title"])["group"] - extractGroup = False - - classRowIndex = getRowIndexOfClass() - - for index, e in enumerate(data["table"][0]): - if e["text"] == "": - # Skip empty columns - continue - - # Extract data from column - name = e["meta"] - id = int(e["text"]) - if extractGroup: - group = data["table"][-1][index]["text"] - - # dance = - class_ = classParser.parseClass(data["table"][classRowIndex][index]["text"]) - - participant = types.HtmlPreviewParticipant(name, id, group) - - l = self.participants.get(id, []) - self.l.log( - 5, - "Checking for existence of %s in %s: %s", - participant, - l, - participant in l, - ) - if participant not in l: - l.append(participant) - self.participants[id] = l - - results = self.previewResults.get(participant, {}) - results[dance] = class_ - self.previewResults[participant] = results - - def importAllData(self, parsers: ParserList_t) -> types.HtmlPreviewImport: - self.participants = {} - - for file in parsers: - parser = parsers[file] - try: - self.__extractPersonsFromSinglePreview(parser) - except: - self.l.error( - "Failed to parse preview round in file %s. Skipping this file's content.", - parser.fileName, - ) - - return types.HtmlPreviewImport(self.participants, self.previewResults) - - class ResultExtractor: def __init__(self): self.l = logging.getLogger("solo_turnier.worker.ResultExtractor") @@ -500,28 +383,13 @@ class Worker: "Quickstep", ] - def collectAllData( - self, htmlCandidatesPreview: list[str], htmlResultsFileNames: list[str] - ) -> types.State3: - previewWorker = PreviewWorker() - self.l.info("Filtering for pure preview rounds.") - parsers = previewWorker.filterFilesPreview(htmlCandidatesPreview) - self.l.debug("Remaining files: %s", list(parsers.keys())) - - self.l.info("Extracting person data from the preview rounds.") - previewImport = previewWorker.importAllData(parsers) - self.l.debug( - "Total preview imported participants: %s", - pformat(previewImport.participants), - ) - self.l.log(5, "Total preview results: %s", pformat(previewImport.results)) - + def collectAllData(self, htmlResultsFileNames: list[str]) -> types.State3: resultExtractor = ResultExtractor() resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) htmlResults = resultExtractor.extractAllData(resultParsers) self.l.info("Overall result data extracted: %s", pformat(htmlResults.results)) - return types.State3(previewImport, htmlResults) + return types.State3(None, htmlResults) def combineData(self, importedData: types.State3): self.l.info("Starting to build data sets.") @@ -546,7 +414,6 @@ class Worker: resultsOfParticipant = self._getResultOfSingleParticipant( participant, group, - importedData.previewImport, importedData.htmlResults, dances, ) @@ -654,7 +521,6 @@ class Worker: self, participant: types.HtmlParticipant, nominalGroup: solo_turnier.group.Group, - previewResults: types.HtmlPreviewImport, totalResults: types.HtmlCompetitionTotalResults, allDances: list[str], ) -> list[types.SingleParticipantResult | None]: @@ -747,74 +613,6 @@ class Worker: pass - def _fixNativePlaces( - self, - dances: list[str], - data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]], - ): - classParser = solo_turnier.competition_class.CompetitionClassParser() - allClasses = classParser.getAllClasses() - allClasses.reverse() - - for class_ in allClasses: - for danceIdx, dance in enumerate(dances): - self.l.log( - 5, "Fixing native places for class %s in dance %s", class_, dance - ) - - remainingParticipants = [] - - for participant in data.keys(): - results = data[participant] - danceResult = results[danceIdx] - - if danceResult is None: - continue - - # self.l.log(5, 'Result of dance: %s', danceResult) - - if classParser.isABetterThanB(danceResult.nativeClass, class_): - # self.l.log(5, 'Skipping %s as the native class is higher', participant) - continue - - remainingParticipants.append( - (danceResult.place, participant.id, participant) - ) - - remainingParticipants.sort() - # self.l.log(5, 'Remaining participants %s', remainingParticipants) - - def getAllParticipantsWithSamePlace(): - first = remainingParticipants.pop(0) - ret = [first] - while ( - len(remainingParticipants) > 0 - and remainingParticipants[0][0] == first[0] - ): - ret.append(remainingParticipants.pop(0)) - return ret - - def updateNativePlaces(samePlaced, placeStart): - nextPlace = placeStart + len(samePlaced) - if len(samePlaced) == 1: - placeTo = None - else: - placeTo = nextPlace - 1 - - for p in samePlaced: - data[p[2]][danceIdx].placeNative = placeStart - data[p[2]][danceIdx].placeNativeTo = placeTo - - return nextPlace - - places = list(map(lambda x: x[0], remainingParticipants)) - place = 1 - while len(remainingParticipants) > 0: - samePlaced = getAllParticipantsWithSamePlace() - place = updateNativePlaces(samePlaced, place) - - # self.l.log(5, '(Partially) fixed places: %s', (data)) - def filterOutFinalists(self, data: types.State4, filterOut: bool): for group in data.results: self.l.debug("Cleaning up group %s", group.name) From 6c6b8485fc1916cdc78857b9b109f7b116f26db9 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 18:26:35 +0100 Subject: [PATCH 14/31] Drop preview result from data classes --- src/solo_turnier/types.py | 5 +---- src/solo_turnier/worker.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/solo_turnier/types.py b/src/solo_turnier/types.py index bfd2404..039f73e 100644 --- a/src/solo_turnier/types.py +++ b/src/solo_turnier/types.py @@ -307,10 +307,7 @@ class State4: class State3: - def __init__( - self, previewImport: HtmlPreviewImport, htmlResults: HtmlCompetitionTotalResults - ): - self.previewImport = previewImport + def __init__(self, htmlResults: HtmlCompetitionTotalResults): self.htmlResults = htmlResults diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index 7cda1c1..ceb25a5 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -387,9 +387,9 @@ class Worker: resultExtractor = ResultExtractor() resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) htmlResults = resultExtractor.extractAllData(resultParsers) - self.l.info("Overall result data extracted: %s", pformat(htmlResults.results)) + self.l.debug("Overall result data extracted: %s", pformat(htmlResults.results)) - return types.State3(None, htmlResults) + return types.State3(htmlResults) def combineData(self, importedData: types.State3): self.l.info("Starting to build data sets.") From f182f4fbcc263431b4271a0497a3545f446c18aa Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sun, 19 Nov 2023 18:27:15 +0100 Subject: [PATCH 15/31] Make table matching between files more robust --- src/solo_turnier/worker.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index ceb25a5..99e8a34 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -498,10 +498,15 @@ class Worker: if group not in activeGroups: continue - fixture = importedData.htmlResults.tabges[tup] - if fixture[2] is not None and fixture[2] != group: - self.l.log(5, "Skipping id %s in group %s as not part", tup[3], group) - continue + fixture = importedData.htmlResults.tabges.get(tup, None) + if fixture is None: + self.l.error("A fixture for the tuple %s could not be read.", tup) + else: + if fixture[2] is not None and fixture[2] != group: + self.l.log( + 5, "Skipping id %s in group %s as not part", tup[3], group + ) + continue part = importedData.htmlResults.results[tup][0] part.id = int(tup[3]) From 90acb45a0390e6c3229b3d482d3f3edf276f2a0c Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 10:21:27 +0100 Subject: [PATCH 16/31] Fix types in group comparision --- src/solo_turnier/worker.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index 99e8a34..bee9d46 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -502,9 +502,15 @@ class Worker: if fixture is None: self.l.error("A fixture for the tuple %s could not be read.", tup) else: - if fixture[2] is not None and fixture[2] != group: + if ( + fixture[2] is not None + and groupParser.parseClass(fixture[2]) != group + ): self.l.log( - 5, "Skipping id %s in group %s as not part", tup[3], group + 5, + "Skipping id %s in group %s as in other group.", + tup[3], + group, ) continue From a0c52a56fcc5d160142359b93f6dc8c3cdf7bfdf Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 10:22:07 +0100 Subject: [PATCH 17/31] Started to work towards new group management --- src/solo_turnier/worker.py | 115 ++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index bee9d46..fc94abe 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -382,6 +382,7 @@ class Worker: "Slowfox", "Quickstep", ] + self._groupParser = solo_turnier.group.GroupParser() def collectAllData(self, htmlResultsFileNames: list[str]) -> types.State3: resultExtractor = ResultExtractor() @@ -393,9 +394,18 @@ class Worker: def combineData(self, importedData: types.State3): self.l.info("Starting to build data sets.") - groups = self._extractGroups(importedData) + + self.l.debug("Getting per participant groups") + groupMapping = self._getGroupMapping(importedData) + self.l.log(5, "ID-to-group mapping of the parsed data: %s", str(groupMapping)) + + # groups = self._extractGroups(importedData) + groups = self._extractGroupsFromGroupMapping(groupMapping) self.l.debug("Found groups in the dataset: %s", groups) + invertedGroupMapping = self._invertGroupMapping(groupMapping, groups) + self.l.log(5, "Inverted group maping: %s", invertedGroupMapping) + totalResult = {} for group in groups: @@ -451,6 +461,109 @@ class Worker: groups = groupParser.getGroupsAsSortedList(groupSet) return groups + def _getGroupMapping( + self, importedData: types.State3 + ) -> dict[int, solo_turnier.group.Group | None]: + groupParser = solo_turnier.group.GroupParser() + + def _getBestGroupGuess(groups, id): + counts = {} + grNones = 0 + for gr in set(groups): + length = len(list(filter(lambda x: x == gr, groups))) + if isinstance(gr, tuple) or gr is None: + grNones = grNones + length + else: + counts[gr] = length + counts[None] = grNones + candidates = list(counts.keys()) + + def ccomp(i1): + return counts[i1] + + candidates.sort(key=ccomp, reverse=True) + + if len(candidates) == 1: + self.l.warning("Unrequired group guessing started.") + return candidates[0] + if len(candidates) == 0: + self.l.error("Problem during the group guessing triggered.") + return None + + if counts[candidates[0]] > counts[candidates[1]]: + if candidates[0] is None: + self.l.error( + "Majority of guessed groups is ambigous. Guessing failed for id %d. Falling back to second best guess.", + id, + ) + return candidates[1] + + self.l.info("Using best fit %s for guessed group.", candidates[0]) + return candidates[0] + + self.l.warning("Group guessing failed.") + return None + + groupsPerId = {} + for tup in importedData.htmlResults.results: + competitionGroup = groupParser.parseClass(tup[0]) + fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) + id = int(tup[3]) + if fixture[2] is not None: + group = groupParser.parseClass(fixture[2]) + else: + containedGroups = competitionGroup.getContainedGroups() + if len(containedGroups) > 1: + self.l.error( + "The group for participant %d is ambiguous in (%s %s %s).", + id, + tup[0], + tup[1], + tup[2], + ) + group = containedGroups + else: + group = competitionGroup + + knownGroups = groupsPerId.get(id, []) + if group is not None: + knownGroups.append(group) + groupsPerId[id] = knownGroups + + ret = {} + for id in groupsPerId.keys(): + groupCandidates = groupsPerId[id] + groupSet = set(groupCandidates) + + if len(groupSet) == 1: + ret[id] = groupSet.pop() + elif len(groupSet) > 1: + self.l.warning( + "Multiple groups for id %d found: %s", id, groupsPerId[id] + ) + ret[id] = _getBestGroupGuess(groupCandidates, id) + else: + self.l.warning("No group for id %d could be found.", id) + ret[id] = None + return ret + + def _extractGroupsFromGroupMapping(self, mapping): + foundGroups = set() + for id in mapping: + foundGroups.add(mapping[id]) + sortedGroup = self._groupParser.getGroupsAsSortedList(foundGroups) + missingGroups = foundGroups.difference(sortedGroup) + sortedGroup = sortedGroup + list(missingGroups) + return sortedGroup + + def _invertGroupMapping(self, mapping, groups): + ret = {} + for group in groups: + ret[group] = [] + for id in mapping: + ret[mapping[id]].append(id) + return ret + def _extractDancesPerGroup( self, data: types.State3, group: solo_turnier.group.Group ): From 16d78e18f327f697d3629fb618d1baf502fa495f Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 10:44:16 +0100 Subject: [PATCH 18/31] Refactored worker classes to be in individual modules --- src/solo_turnier/__init__.py | 2 + src/solo_turnier/batch.py | 2 +- src/solo_turnier/worker.py | 717 +------------------- src/solo_turnier/workers/DataWorker.py | 189 ++++++ src/solo_turnier/workers/ResultExtractor.py | 125 ++++ src/solo_turnier/workers/Worker.py | 406 +++++++++++ src/solo_turnier/workers/__init__.py | 3 + 7 files changed, 728 insertions(+), 716 deletions(-) create mode 100644 src/solo_turnier/workers/DataWorker.py create mode 100644 src/solo_turnier/workers/ResultExtractor.py create mode 100644 src/solo_turnier/workers/Worker.py create mode 100644 src/solo_turnier/workers/__init__.py diff --git a/src/solo_turnier/__init__.py b/src/solo_turnier/__init__.py index a321b6d..16e588f 100644 --- a/src/solo_turnier/__init__.py +++ b/src/solo_turnier/__init__.py @@ -13,3 +13,5 @@ from . import output from . import batch from . import flask + +from . import workers diff --git a/src/solo_turnier/batch.py b/src/solo_turnier/batch.py index c600d30..f942763 100644 --- a/src/solo_turnier/batch.py +++ b/src/solo_turnier/batch.py @@ -25,7 +25,7 @@ class BatchWorker: "Using HTML result files for result extraction: %s", htmlResultFiles ) - worker = solo_turnier.worker.Worker() + worker = solo_turnier.workers.Worker.Worker() importedData = worker.collectAllData(htmlResultFiles) combinedData = worker.combineData(importedData) diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index fc94abe..e87a079 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -1,14 +1,6 @@ -import logging -from pprint import pformat - -import re - -import solo_turnier -from solo_turnier import html_parser from .reader import ResultRow -from .types import HtmlCompetitionResultRow as CompetitionResult -from . import types -from . import competition_class + + class HtmlPerson: @@ -64,708 +56,3 @@ class ResultPerson: def __hash__(self): text = str(self) return text.__hash__() - - -ParserList_t = dict[str, html_parser.HtmlParser] - - -class ResultExtractor: - def __init__(self): - self.l = logging.getLogger("solo_turnier.worker.ResultExtractor") - self.rePlaceSingle = re.compile(" *([0-9]+) *") - self.rePlaceDouble = re.compile(" *([0-9]+) *- *([0-9]+) *") - - def getAllParsers(self, files: list[tuple[str, str]]) -> ParserList_t: - ret = {} - classParser = competition_class.CompetitionClassParser() - - for filePair in files: - with open(filePair[0], "r") as fp: - text = fp.read() - parser = html_parser.HtmlParser(text, filePair[0]) - - if filePair[1] is None: - parserTab = None - else: - with open(filePair[1], "r") as fp: - textTab = fp.read() - parserTab = html_parser.HtmlParser(textTab, filePair[1]) - - try: - data = parser.guessDataFromHtmlTitle() - except: - self.l.error( - "Cannot parse HTML file %s to check if it is a valid result. Check manually.", - filePair[0], - ) - continue - - try: - guessedClass = classParser.parseClass(data["class_"]) - except: - self.l.error( - "Issue parsing class of file %s. Check manually.", filePair[0] - ) - continue - - self.l.debug( - "Fetched result data: %s, guessed class %s", data, guessedClass - ) - ret[filePair] = (parser, parserTab) - - return ret - - def _extractPlace(self, placeStr: str): - s = placeStr.replace(".", "") - - matches = self.rePlaceSingle.fullmatch(s) - if matches is not None: - return (int(matches.group(1)), None) - - matches = self.rePlaceDouble.fullmatch(s) - if matches is not None: - return (int(matches.group(1)), int(matches.group(2))) - - self.l.error('Could not parse place string "%s"', placeStr) - raise Exception("Place cannot be parsed") - - def _analyzeSingleParser( - self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults - ): - data = parser.guessDataFromHtmlTitle() - competitionClass = data["class_"] - competitionGroup = data["group"] - dance = data["dance"] - - result = parser.parseResult() - self.l.log(5, "Raw data extracted: %s", result) - - for person in result.results.keys(): - placeStr = result.results[person] - place, placeTo = self._extractPlace(placeStr) - competitionResult = types.HtmlSingleCompetitionResult( - person.name, place, placeTo, person.finalist - ) - results.add( - competitionGroup, competitionClass, dance, person.id, competitionResult - ) - # - - def _analyzeIndividualResults( - self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults - ): - data = parser.guessDataFromHtmlTitle() - competitionClass = data["class_"] - competitionGroup = data["group"] - dance = data["dance"] - - result = parser.parseIndividualResult(competitionGroup, competitionClass, dance) - self.l.log(5, "Found individual results: %s", result.participants) - results.tabges.update(result.participants) - - def extractAllData( - self, parsers: ParserList_t - ) -> types.HtmlCompetitionTotalResults: - ret = types.HtmlCompetitionTotalResults() - - for fileNameTuple in parsers: - fileName = fileNameTuple[0] - self.l.debug("Extracting data from file %s", fileName) - self._analyzeSingleParser(parsers[fileNameTuple][0], ret) - - if parsers[fileNameTuple][1] is None: - self.l.info( - "Skipping extraction of individual result as class is not yet finished." - ) - else: - self.l.debug( - "Fetching individual result of combined competitions in %s", - fileName, - ) - self._analyzeIndividualResults(parsers[fileNameTuple][1], ret) - - return ret - - -class DataWorker: - def __init__(self): - self.l = logging.getLogger("solo_turnier.worker.DataWorker") - - def combineRowsByPerson( - self, rows: list[ResultRow] - ) -> dict[ResultPerson, list[CompetitionResult]]: - ret = {} - for row in rows: - result = CompetitionResult.extractFromResultRow(row) - - if result.place == "-" or result.placeTo == "-": - continue - - person = ResultPerson.extractFromResultRow(row) - if person not in ret: - ret[person] = [] - ret[person].append(result) - return ret - - def checkUniqueIds(self, data: dict[ResultPerson, list[CompetitionResult]]) -> bool: - unique = True - for person in data: - ids = set([c.id for c in data[person]]) - if len(ids) == 1: - person.id = list(ids)[0] - else: - unique = False - - return unique - - """ - Return a tuple - The first one is True, if all persons could be unambiguously identified a group - The second one is True if there was the need to override a group but it was possible to extract from other data - The second one can be seen as a warning - """ - - def consolidateGroups( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> tuple[bool, bool]: - ambiguous = False - warnChange = False - - unambiguousGroups = set(["Kin.", "Jun.", "Jug."]) - combinations = set(["Kin./Jun.", "Jun./Jug."]) - - for person in data: - groupsRaw = set([c.group for c in data[person]]) - - unknown = groupsRaw.difference(unambiguousGroups).difference(combinations) - if len(unknown) > 0: - raise Exception( - f"There were unknown groups found for {person}: {unknown}" - ) - - numUnambiguousGroups = len(groupsRaw.intersection(unambiguousGroups)) - - if numUnambiguousGroups == 0: - if len(groupsRaw) == 2: - warnChange = True - person.group = "Jun." - else: - ambiguous = True - if len(groupsRaw) == 1: - person.group = list(groupsRaw)[0] - - elif numUnambiguousGroups == 1: - if len(groupsRaw.intersection(combinations)) > 0: - warnChange = True - - person.group = list(groupsRaw.intersection(unambiguousGroups))[0] - - else: - raise Exception(f"{person} cannot have different groups.") - - return (not ambiguous, warnChange) - - def _createHtmlLUT(self, htmlImports: list[html_parser.HtmlImport]): - ret = {} - parser = html_parser.HtmlParser("") - for imp in htmlImports: - parsed = parser.guessDataFromHtmlTitle(imp.title) - key = (parsed["group"], parsed["class_"], parsed["dance"]) - ret[key] = imp - self.l.debug("LUT[%s] = %s", key, imp) - self.l.debug("LUT completed") - return ret - - def mergeHtmlData( - self, - data: dict[ResultPerson, list[CompetitionResult]], - htmlImports: list[html_parser.HtmlImport], - ): - lut = self._createHtmlLUT(htmlImports) - - for person in data: - for competition in data[person]: - key = ( - competition.competitionGroup, - competition.competitionClass, - competition.dance, - ) - htmlImport = lut[key] - participant = htmlImport.participants[str(competition.id)] - if participant.name != person.name: - self.l.error( - f"Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully." - ) - competition.finalist = participant.finalist - - def getAllDancesInCompetitions( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> list[str]: - allDances = [ - "Samba", - "Cha Cha", - "Rumba", - "Paso Doble", - "Jive", - "Langs. Walzer", - "Tango", - "Wiener Walzer", - "Slowfox", - "Quickstep", - ] - dancesPresent = {d: False for d in allDances} - - for person in data: - for competition in data[person]: - dancesPresent[competition.dance] = True - - return [d for d in allDances if dancesPresent[d]] - - def collectPersonsInGroups( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> list[tuple[str, list[ResultPerson]]]: - groups = { - "Kin.": [p for p in data.keys() if p.group == "Kin."], - "Jun.": [p for p in data.keys() if p.group == "Jun."], - "Jug.": [p for p in data.keys() if p.group == "Jug."], - } - found = groups["Kin."] + groups["Jun."] + groups["Jug."] - groups["Sonst"] = [p for p in data.keys() if p not in found] - return groups - - def sortPersonsInGroup(self, persons: list[ResultPerson]) -> list[ResultPerson]: - ids = [p.id for p in persons] - - def decorateByName(p: ResultPerson): - return (f"{p.name} ({p.club})", p) - - def decorateById(p: ResultPerson): - return (p.id, p) - - if any([id == None for id in ids]): - # We need to sort by name - decorated = [decorateByName(p) for p in persons] - showIds = False - else: - decorated = [decorateById(p) for p in persons] - showIds = True - - decorated.sort() - - return ([d[1] for d in decorated], showIds) - - def mapPersonResultsToDanceList( - self, results: list[CompetitionResult], dances: list[str] - ) -> list[CompetitionResult | None]: - ret = [] - for dance in dances: - competitions = [c for c in results if c.dance == dance] - if len(competitions) == 0: - ret.append(None) - elif len(competitions) > 1: - raise Exception( - f'Multiple competitions with the same dance "{dance}" found.' - ) - else: - ret.append(competitions[0]) - - return ret - - -class Worker: - def __init__(self): - self.l = logging.getLogger("solo_turnier.worker.Worker") - self._allDances = ["Samba", "Cha Cha", "Rumba", "Paso Doble", "Jive"] + [ - "Langs. Walzer", - "Tango", - "Wiener Walzer", - "Slowfox", - "Quickstep", - ] - self._groupParser = solo_turnier.group.GroupParser() - - def collectAllData(self, htmlResultsFileNames: list[str]) -> types.State3: - resultExtractor = ResultExtractor() - resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) - htmlResults = resultExtractor.extractAllData(resultParsers) - self.l.debug("Overall result data extracted: %s", pformat(htmlResults.results)) - - return types.State3(htmlResults) - - def combineData(self, importedData: types.State3): - self.l.info("Starting to build data sets.") - - self.l.debug("Getting per participant groups") - groupMapping = self._getGroupMapping(importedData) - self.l.log(5, "ID-to-group mapping of the parsed data: %s", str(groupMapping)) - - # groups = self._extractGroups(importedData) - groups = self._extractGroupsFromGroupMapping(groupMapping) - self.l.debug("Found groups in the dataset: %s", groups) - - invertedGroupMapping = self._invertGroupMapping(groupMapping, groups) - self.l.log(5, "Inverted group maping: %s", invertedGroupMapping) - - totalResult = {} - - for group in groups: - self.l.debug("Collecting data for total result of group %s", group) - - dances = self._extractDancesPerGroup(importedData, group) - self.l.log(5, "Found dances in group %s: %s", group, dances) - - participants = self._extractParticipantsPerGroup(importedData, group) - self.l.log(5, "Related participants %s", participants) - - results = {} - - for participant in participants: - self.l.log(5, "Collecting data for %s", participant) - resultsOfParticipant = self._getResultOfSingleParticipant( - participant, - group, - importedData.htmlResults, - dances, - ) - self.l.log(5, "Obtained result %s", resultsOfParticipant) - results[participant] = resultsOfParticipant - - self.l.log(5, "Result before native fixing: %s", pformat(results)) - # self._fixNativePlaces(dances, results) - self._fixNativeDataFromTable(dances, results, importedData.htmlResults) - self.l.log(5, "Result after native fixing: %s", pformat(results)) - # self.l.log(5,'Fixed data %s', results) - - totalResult[group] = types.TotalGroupResult(dances, results) - - self.l.log(5, "Total result of all groups: %s", pformat(totalResult)) - - ret = types.State4(totalResult) - return ret - - def _extractGroups(self, data: types.State3): - groupParser = solo_turnier.group.GroupParser() - - groupSet = set([]) - # for id in data.previewImport.participants: - # participants = data.previewImport.participants[id] - # for participant in participants: - # groupSet.add(participant.group) - for tup in data.htmlResults.results.keys(): - gr = groupParser.parseClass(tup[0]) - # groupSet.add(gr) - groupSet.update(gr.getContainedGroups()) - # self.l.log(5, 'Group type %s', type(gr)) - - self.l.log(5, "Set of active groups: %s", groupSet) - groups = groupParser.getGroupsAsSortedList(groupSet) - return groups - - def _getGroupMapping( - self, importedData: types.State3 - ) -> dict[int, solo_turnier.group.Group | None]: - groupParser = solo_turnier.group.GroupParser() - - def _getBestGroupGuess(groups, id): - counts = {} - grNones = 0 - for gr in set(groups): - length = len(list(filter(lambda x: x == gr, groups))) - if isinstance(gr, tuple) or gr is None: - grNones = grNones + length - else: - counts[gr] = length - counts[None] = grNones - candidates = list(counts.keys()) - - def ccomp(i1): - return counts[i1] - - candidates.sort(key=ccomp, reverse=True) - - if len(candidates) == 1: - self.l.warning("Unrequired group guessing started.") - return candidates[0] - if len(candidates) == 0: - self.l.error("Problem during the group guessing triggered.") - return None - - if counts[candidates[0]] > counts[candidates[1]]: - if candidates[0] is None: - self.l.error( - "Majority of guessed groups is ambigous. Guessing failed for id %d. Falling back to second best guess.", - id, - ) - return candidates[1] - - self.l.info("Using best fit %s for guessed group.", candidates[0]) - return candidates[0] - - self.l.warning("Group guessing failed.") - return None - - groupsPerId = {} - for tup in importedData.htmlResults.results: - competitionGroup = groupParser.parseClass(tup[0]) - fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) - id = int(tup[3]) - if fixture[2] is not None: - group = groupParser.parseClass(fixture[2]) - else: - containedGroups = competitionGroup.getContainedGroups() - if len(containedGroups) > 1: - self.l.error( - "The group for participant %d is ambiguous in (%s %s %s).", - id, - tup[0], - tup[1], - tup[2], - ) - group = containedGroups - else: - group = competitionGroup - - knownGroups = groupsPerId.get(id, []) - if group is not None: - knownGroups.append(group) - groupsPerId[id] = knownGroups - - ret = {} - for id in groupsPerId.keys(): - groupCandidates = groupsPerId[id] - groupSet = set(groupCandidates) - - if len(groupSet) == 1: - ret[id] = groupSet.pop() - elif len(groupSet) > 1: - self.l.warning( - "Multiple groups for id %d found: %s", id, groupsPerId[id] - ) - ret[id] = _getBestGroupGuess(groupCandidates, id) - else: - self.l.warning("No group for id %d could be found.", id) - ret[id] = None - return ret - - def _extractGroupsFromGroupMapping(self, mapping): - foundGroups = set() - for id in mapping: - foundGroups.add(mapping[id]) - sortedGroup = self._groupParser.getGroupsAsSortedList(foundGroups) - missingGroups = foundGroups.difference(sortedGroup) - sortedGroup = sortedGroup + list(missingGroups) - return sortedGroup - - def _invertGroupMapping(self, mapping, groups): - ret = {} - for group in groups: - ret[group] = [] - for id in mapping: - ret[mapping[id]].append(id) - return ret - - def _extractDancesPerGroup( - self, data: types.State3, group: solo_turnier.group.Group - ): - groupParser = solo_turnier.group.GroupParser() - - dances = set() - additionalDances = set() - foundDances = set() - for tup in data.htmlResults.results.keys(): - currentGroup = groupParser.parseClass(tup[0]) - if group not in currentGroup.getContainedGroups(): - continue - foundDances.add(tup[2]) - - dances.update(foundDances.intersection(self._allDances)) - additionalDances.update(foundDances.difference(self._allDances)) - - if len(additionalDances) > 0: - self.l.error( - "There were dances found, that are not registered. A bug? The dances were: %s", - additionalDances, - ) - - dancesList = [x for x in self._allDances if x in dances] - additionalDancesList = list(additionalDances) - additionalDancesList.sort() - return dancesList + additionalDancesList - - def _extractParticipantsPerGroup( - self, - importedData: types.State3, - # previewData: types.HtmlPreviewImport, - group: solo_turnier.group.Group, - ) -> list[types.HtmlPreviewParticipant]: - groupParser = types.group.GroupParser() - - ret = [] - - # self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) - # self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) - - for tup in importedData.htmlResults.results.keys(): - currentGroup = groupParser.parseClass(tup[0]) - activeGroups = currentGroup.getContainedGroups() - if group not in activeGroups: - continue - - fixture = importedData.htmlResults.tabges.get(tup, None) - if fixture is None: - self.l.error("A fixture for the tuple %s could not be read.", tup) - else: - if ( - fixture[2] is not None - and groupParser.parseClass(fixture[2]) != group - ): - self.l.log( - 5, - "Skipping id %s in group %s as in other group.", - tup[3], - group, - ) - continue - - part = importedData.htmlResults.results[tup][0] - part.id = int(tup[3]) - ret.append(part) - - self.l.log(5, "ret %s", ret) - # raise Exception('Test') - - # for id in previewData.participants: - # participantList = previewData.participants[id] - # for participant in participantList: - # if participant.group == group: - # ret.append(participant) - return ret - - def _getResultOfSingleParticipant( - self, - participant: types.HtmlParticipant, - nominalGroup: solo_turnier.group.Group, - totalResults: types.HtmlCompetitionTotalResults, - allDances: list[str], - ) -> list[types.SingleParticipantResult | None]: - rawResults = totalResults.getById(participant.id) - self.l.log( - 5, "Found result data for id %i (raw): %s", participant.id, rawResults - ) - - results = [None for x in allDances] - - for danceIdx, dance in enumerate(allDances): - # self.l.log(5, '%s %s', dance, danceIdx) - def getResult() -> types.SingleParticipantResult | None: - for key in rawResults: - if key[0] != dance: - continue - rawResult = rawResults[key] - - if len(rawResult) != 1: - raise Exception("Multiple results found with same key") - rawResult = rawResult[0] - - nativeClass = key[2] - # nativeClass = previewResults.results[participant][dance] - # nativeClass = key[2] - - # self.l.log(5, 'Result %s => %s', key, rawResult) - ret = types.SingleParticipantResult( - key[2], - nativeClass, - dance, - rawResult.finalist, - rawResult.place, - rawResult.placeTo, - ) - - return ret - return None - - results[danceIdx] = getResult() - - return results - - def _fixNativeDataFromTable( - self, - dances: list[str], - data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]], - importedData: types.HtmlCompetitionTotalResults, - ): - rePlace = re.compile("([0-9]+)(?:-([0-9]+))?") - classParser = competition_class.CompetitionClassParser() - - for participant in data.keys(): - self.l.log(5, "fixing participant %s", participant) - results = data[participant] - for result in results: - if result is None: - continue - self.l.log(5, "Looking at result set %s", result) - - def selectEntry(k): - return k[2] == result.dance and int(k[3]) == participant.id - - keys = list(importedData.tabges.keys()) - selected = list(map(selectEntry, keys)) - try: - selectedIndex = selected.index(True) - except: - continue - - raw = importedData.tabges[keys[selectedIndex]] - self.l.log(5, "Raw %s", raw) - nativePlaceRaw = raw[0] - matcher = rePlace.fullmatch(nativePlaceRaw) - if matcher is None: - self.l.error( - "Cannot parse place string %s for participant %u (%s) in dance %s", - nativePlaceRaw, - participant.id, - participant, - result.dance, - ) - continue - self.l.log(5, "Found strings by regex: %s", matcher.groups()) - result.placeNative = matcher.group(1) - result.placeNativeTo = matcher.group(2) - - if raw[1] is not None: - result.nativeClass = classParser.parseAbbreviatedClass(raw[1]) - - pass - - def filterOutFinalists(self, data: types.State4, filterOut: bool): - for group in data.results: - self.l.debug("Cleaning up group %s", group.name) - participants = data.results[group].results.keys() - droppedParticipants = [] - - for participant in participants: - self.l.debug("Checking %s", participant) - - def isFinalistInDance(x: types.HtmlSingleCompetitionResult | None): - if x is None: - return False - return x.finalist - - mapped = list( - map(isFinalistInDance, data.results[group].results[participant]) - ) - finalist = True in mapped - self.l.log(5, "Check for finalist (in dances %s): %s", mapped, finalist) - - if finalist: - participant.finalist = True - else: - participant.finalist = False - self.l.info( - "Dropping %s from the output as no finalist", participant - ) - droppedParticipants.append(participant) - - if filterOut: - for droppedParticipant in droppedParticipants: - data.results[group].results.pop(droppedParticipant) diff --git a/src/solo_turnier/workers/DataWorker.py b/src/solo_turnier/workers/DataWorker.py new file mode 100644 index 0000000..49f3e73 --- /dev/null +++ b/src/solo_turnier/workers/DataWorker.py @@ -0,0 +1,189 @@ +from ..reader import ResultRow +from ..worker import ResultPerson +from ..types import HtmlCompetitionResultRow as CompetitionResult +from solo_turnier import html_parser + + +class DataWorker: + def __init__(self): + self.l = logging.getLogger("solo_turnier.worker.DataWorker") + + def combineRowsByPerson( + self, rows: list[ResultRow] + ) -> dict[ResultPerson, list[CompetitionResult]]: + ret = {} + for row in rows: + result = CompetitionResult.extractFromResultRow(row) + + if result.place == "-" or result.placeTo == "-": + continue + + person = ResultPerson.extractFromResultRow(row) + if person not in ret: + ret[person] = [] + ret[person].append(result) + return ret + + def checkUniqueIds(self, data: dict[ResultPerson, list[CompetitionResult]]) -> bool: + unique = True + for person in data: + ids = set([c.id for c in data[person]]) + if len(ids) == 1: + person.id = list(ids)[0] + else: + unique = False + + return unique + + """ + Return a tuple + The first one is True, if all persons could be unambiguously identified a group + The second one is True if there was the need to override a group but it was possible to extract from other data + The second one can be seen as a warning + """ + + def consolidateGroups( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> tuple[bool, bool]: + ambiguous = False + warnChange = False + + unambiguousGroups = set(["Kin.", "Jun.", "Jug."]) + combinations = set(["Kin./Jun.", "Jun./Jug."]) + + for person in data: + groupsRaw = set([c.group for c in data[person]]) + + unknown = groupsRaw.difference(unambiguousGroups).difference(combinations) + if len(unknown) > 0: + raise Exception( + f"There were unknown groups found for {person}: {unknown}" + ) + + numUnambiguousGroups = len(groupsRaw.intersection(unambiguousGroups)) + + if numUnambiguousGroups == 0: + if len(groupsRaw) == 2: + warnChange = True + person.group = "Jun." + else: + ambiguous = True + if len(groupsRaw) == 1: + person.group = list(groupsRaw)[0] + + elif numUnambiguousGroups == 1: + if len(groupsRaw.intersection(combinations)) > 0: + warnChange = True + + person.group = list(groupsRaw.intersection(unambiguousGroups))[0] + + else: + raise Exception(f"{person} cannot have different groups.") + + return (not ambiguous, warnChange) + + def _createHtmlLUT(self, htmlImports: list[html_parser.HtmlImport]): + ret = {} + parser = html_parser.HtmlParser("") + for imp in htmlImports: + parsed = parser.guessDataFromHtmlTitle(imp.title) + key = (parsed["group"], parsed["class_"], parsed["dance"]) + ret[key] = imp + self.l.debug("LUT[%s] = %s", key, imp) + self.l.debug("LUT completed") + return ret + + def mergeHtmlData( + self, + data: dict[ResultPerson, list[CompetitionResult]], + htmlImports: list[html_parser.HtmlImport], + ): + lut = self._createHtmlLUT(htmlImports) + + for person in data: + for competition in data[person]: + key = ( + competition.competitionGroup, + competition.competitionClass, + competition.dance, + ) + htmlImport = lut[key] + participant = htmlImport.participants[str(competition.id)] + if participant.name != person.name: + self.l.error( + f"Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully." + ) + competition.finalist = participant.finalist + + def getAllDancesInCompetitions( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> list[str]: + allDances = [ + "Samba", + "Cha Cha", + "Rumba", + "Paso Doble", + "Jive", + "Langs. Walzer", + "Tango", + "Wiener Walzer", + "Slowfox", + "Quickstep", + ] + dancesPresent = {d: False for d in allDances} + + for person in data: + for competition in data[person]: + dancesPresent[competition.dance] = True + + return [d for d in allDances if dancesPresent[d]] + + def collectPersonsInGroups( + self, data: dict[ResultPerson, list[CompetitionResult]] + ) -> list[tuple[str, list[ResultPerson]]]: + groups = { + "Kin.": [p for p in data.keys() if p.group == "Kin."], + "Jun.": [p for p in data.keys() if p.group == "Jun."], + "Jug.": [p for p in data.keys() if p.group == "Jug."], + } + found = groups["Kin."] + groups["Jun."] + groups["Jug."] + groups["Sonst"] = [p for p in data.keys() if p not in found] + return groups + + def sortPersonsInGroup(self, persons: list[ResultPerson]) -> list[ResultPerson]: + ids = [p.id for p in persons] + + def decorateByName(p: ResultPerson): + return (f"{p.name} ({p.club})", p) + + def decorateById(p: ResultPerson): + return (p.id, p) + + if any([id == None for id in ids]): + # We need to sort by name + decorated = [decorateByName(p) for p in persons] + showIds = False + else: + decorated = [decorateById(p) for p in persons] + showIds = True + + decorated.sort() + + return ([d[1] for d in decorated], showIds) + + def mapPersonResultsToDanceList( + self, results: list[CompetitionResult], dances: list[str] + ) -> list[CompetitionResult | None]: + ret = [] + for dance in dances: + competitions = [c for c in results if c.dance == dance] + if len(competitions) == 0: + ret.append(None) + elif len(competitions) > 1: + raise Exception( + f'Multiple competitions with the same dance "{dance}" found.' + ) + else: + ret.append(competitions[0]) + + return ret diff --git a/src/solo_turnier/workers/ResultExtractor.py b/src/solo_turnier/workers/ResultExtractor.py new file mode 100644 index 0000000..bafaaa1 --- /dev/null +++ b/src/solo_turnier/workers/ResultExtractor.py @@ -0,0 +1,125 @@ +from solo_turnier import html_parser +from .. import types +import logging +import re +from .. import competition_class + +ParserList_t = dict[str, html_parser.HtmlParser] + + +class ResultExtractor: + def __init__(self): + self.l = logging.getLogger("solo_turnier.worker.ResultExtractor") + self.rePlaceSingle = re.compile(" *([0-9]+) *") + self.rePlaceDouble = re.compile(" *([0-9]+) *- *([0-9]+) *") + + def getAllParsers(self, files: list[tuple[str, str]]) -> ParserList_t: + ret = {} + classParser = competition_class.CompetitionClassParser() + + for filePair in files: + with open(filePair[0], "r") as fp: + text = fp.read() + parser = html_parser.HtmlParser(text, filePair[0]) + + if filePair[1] is None: + parserTab = None + else: + with open(filePair[1], "r") as fp: + textTab = fp.read() + parserTab = html_parser.HtmlParser(textTab, filePair[1]) + + try: + data = parser.guessDataFromHtmlTitle() + except: + self.l.error( + "Cannot parse HTML file %s to check if it is a valid result. Check manually.", + filePair[0], + ) + continue + + try: + guessedClass = classParser.parseClass(data["class_"]) + except: + self.l.error( + "Issue parsing class of file %s. Check manually.", filePair[0] + ) + continue + + self.l.debug( + "Fetched result data: %s, guessed class %s", data, guessedClass + ) + ret[filePair] = (parser, parserTab) + + return ret + + def _extractPlace(self, placeStr: str): + s = placeStr.replace(".", "") + + matches = self.rePlaceSingle.fullmatch(s) + if matches is not None: + return (int(matches.group(1)), None) + + matches = self.rePlaceDouble.fullmatch(s) + if matches is not None: + return (int(matches.group(1)), int(matches.group(2))) + + self.l.error('Could not parse place string "%s"', placeStr) + raise Exception("Place cannot be parsed") + + def _analyzeSingleParser( + self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults + ): + data = parser.guessDataFromHtmlTitle() + competitionClass = data["class_"] + competitionGroup = data["group"] + dance = data["dance"] + + result = parser.parseResult() + self.l.log(5, "Raw data extracted: %s", result) + + for person in result.results.keys(): + placeStr = result.results[person] + place, placeTo = self._extractPlace(placeStr) + competitionResult = types.HtmlSingleCompetitionResult( + person.name, place, placeTo, person.finalist + ) + results.add( + competitionGroup, competitionClass, dance, person.id, competitionResult + ) + # + + def _analyzeIndividualResults( + self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults + ): + data = parser.guessDataFromHtmlTitle() + competitionClass = data["class_"] + competitionGroup = data["group"] + dance = data["dance"] + + result = parser.parseIndividualResult(competitionGroup, competitionClass, dance) + self.l.log(5, "Found individual results: %s", result.participants) + results.tabges.update(result.participants) + + def extractAllData( + self, parsers: ParserList_t + ) -> types.HtmlCompetitionTotalResults: + ret = types.HtmlCompetitionTotalResults() + + for fileNameTuple in parsers: + fileName = fileNameTuple[0] + self.l.debug("Extracting data from file %s", fileName) + self._analyzeSingleParser(parsers[fileNameTuple][0], ret) + + if parsers[fileNameTuple][1] is None: + self.l.info( + "Skipping extraction of individual result as class is not yet finished." + ) + else: + self.l.debug( + "Fetching individual result of combined competitions in %s", + fileName, + ) + self._analyzeIndividualResults(parsers[fileNameTuple][1], ret) + + return ret diff --git a/src/solo_turnier/workers/Worker.py b/src/solo_turnier/workers/Worker.py new file mode 100644 index 0000000..6e6e525 --- /dev/null +++ b/src/solo_turnier/workers/Worker.py @@ -0,0 +1,406 @@ +import logging +import solo_turnier +from .. import types +from .ResultExtractor import ResultExtractor +from pprint import pformat +import re +from .. import competition_class + + +class Worker: + def __init__(self): + self.l = logging.getLogger("solo_turnier.worker.Worker") + self._allDances = ["Samba", "Cha Cha", "Rumba", "Paso Doble", "Jive"] + [ + "Langs. Walzer", + "Tango", + "Wiener Walzer", + "Slowfox", + "Quickstep", + ] + self._groupParser = solo_turnier.group.GroupParser() + + def collectAllData(self, htmlResultsFileNames: list[str]) -> types.State3: + resultExtractor = ResultExtractor() + resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) + htmlResults = resultExtractor.extractAllData(resultParsers) + self.l.debug("Overall result data extracted: %s", pformat(htmlResults.results)) + + return types.State3(htmlResults) + + def combineData(self, importedData: types.State3): + self.l.info("Starting to build data sets.") + + self.l.debug("Getting per participant groups") + groupMapping = self._getGroupMapping(importedData) + self.l.log(5, "ID-to-group mapping of the parsed data: %s", str(groupMapping)) + + # groups = self._extractGroups(importedData) + groups = self._extractGroupsFromGroupMapping(groupMapping) + self.l.debug("Found groups in the dataset: %s", groups) + + invertedGroupMapping = self._invertGroupMapping(groupMapping, groups) + self.l.log(5, "Inverted group maping: %s", invertedGroupMapping) + + totalResult = {} + + for group in groups: + self.l.debug("Collecting data for total result of group %s", group) + + dances = self._extractDancesPerGroup(importedData, group) + self.l.log(5, "Found dances in group %s: %s", group, dances) + + participants = self._extractParticipantsPerGroup(importedData, group) + self.l.log(5, "Related participants %s", participants) + + results = {} + + for participant in participants: + self.l.log(5, "Collecting data for %s", participant) + resultsOfParticipant = self._getResultOfSingleParticipant( + participant, + group, + importedData.htmlResults, + dances, + ) + self.l.log(5, "Obtained result %s", resultsOfParticipant) + results[participant] = resultsOfParticipant + + self.l.log(5, "Result before native fixing: %s", pformat(results)) + # self._fixNativePlaces(dances, results) + self._fixNativeDataFromTable(dances, results, importedData.htmlResults) + self.l.log(5, "Result after native fixing: %s", pformat(results)) + # self.l.log(5,'Fixed data %s', results) + + totalResult[group] = types.TotalGroupResult(dances, results) + + self.l.log(5, "Total result of all groups: %s", pformat(totalResult)) + + ret = types.State4(totalResult) + return ret + + def _extractGroups(self, data: types.State3): + groupParser = solo_turnier.group.GroupParser() + + groupSet = set([]) + # for id in data.previewImport.participants: + # participants = data.previewImport.participants[id] + # for participant in participants: + # groupSet.add(participant.group) + for tup in data.htmlResults.results.keys(): + gr = groupParser.parseClass(tup[0]) + # groupSet.add(gr) + groupSet.update(gr.getContainedGroups()) + # self.l.log(5, 'Group type %s', type(gr)) + + self.l.log(5, "Set of active groups: %s", groupSet) + groups = groupParser.getGroupsAsSortedList(groupSet) + return groups + + def _getGroupMapping( + self, importedData: types.State3 + ) -> dict[int, solo_turnier.group.Group | None]: + groupParser = solo_turnier.group.GroupParser() + + def _getBestGroupGuess(groups, id): + counts = {} + grNones = 0 + for gr in set(groups): + length = len(list(filter(lambda x: x == gr, groups))) + if isinstance(gr, tuple) or gr is None: + grNones = grNones + length + else: + counts[gr] = length + counts[None] = grNones + candidates = list(counts.keys()) + + def ccomp(i1): + return counts[i1] + + candidates.sort(key=ccomp, reverse=True) + + if len(candidates) == 1: + self.l.warning("Unrequired group guessing started.") + return candidates[0] + if len(candidates) == 0: + self.l.error("Problem during the group guessing triggered.") + return None + + if counts[candidates[0]] > counts[candidates[1]]: + if candidates[0] is None: + self.l.error( + "Majority of guessed groups is ambigous. Guessing failed for id %d. Falling back to second best guess.", + id, + ) + return candidates[1] + + self.l.info("Using best fit %s for guessed group.", candidates[0]) + return candidates[0] + + self.l.warning("Group guessing failed.") + return None + + groupsPerId = {} + for tup in importedData.htmlResults.results: + competitionGroup = groupParser.parseClass(tup[0]) + fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) + id = int(tup[3]) + if fixture[2] is not None: + group = groupParser.parseClass(fixture[2]) + else: + containedGroups = competitionGroup.getContainedGroups() + if len(containedGroups) > 1: + self.l.error( + "The group for participant %d is ambiguous in (%s %s %s).", + id, + tup[0], + tup[1], + tup[2], + ) + group = containedGroups + else: + group = competitionGroup + + knownGroups = groupsPerId.get(id, []) + if group is not None: + knownGroups.append(group) + groupsPerId[id] = knownGroups + + ret = {} + for id in groupsPerId.keys(): + groupCandidates = groupsPerId[id] + groupSet = set(groupCandidates) + + if len(groupSet) == 1: + ret[id] = groupSet.pop() + elif len(groupSet) > 1: + self.l.warning( + "Multiple groups for id %d found: %s", id, groupsPerId[id] + ) + ret[id] = _getBestGroupGuess(groupCandidates, id) + else: + self.l.warning("No group for id %d could be found.", id) + ret[id] = None + return ret + + def _extractGroupsFromGroupMapping(self, mapping): + foundGroups = set() + for id in mapping: + foundGroups.add(mapping[id]) + sortedGroup = self._groupParser.getGroupsAsSortedList(foundGroups) + missingGroups = foundGroups.difference(sortedGroup) + sortedGroup = sortedGroup + list(missingGroups) + return sortedGroup + + def _invertGroupMapping(self, mapping, groups): + ret = {} + for group in groups: + ret[group] = [] + for id in mapping: + ret[mapping[id]].append(id) + return ret + + def _extractDancesPerGroup( + self, data: types.State3, group: solo_turnier.group.Group + ): + groupParser = solo_turnier.group.GroupParser() + + dances = set() + additionalDances = set() + foundDances = set() + for tup in data.htmlResults.results.keys(): + currentGroup = groupParser.parseClass(tup[0]) + if group not in currentGroup.getContainedGroups(): + continue + foundDances.add(tup[2]) + + dances.update(foundDances.intersection(self._allDances)) + additionalDances.update(foundDances.difference(self._allDances)) + + if len(additionalDances) > 0: + self.l.error( + "There were dances found, that are not registered. A bug? The dances were: %s", + additionalDances, + ) + + dancesList = [x for x in self._allDances if x in dances] + additionalDancesList = list(additionalDances) + additionalDancesList.sort() + return dancesList + additionalDancesList + + def _extractParticipantsPerGroup( + self, + importedData: types.State3, + # previewData: types.HtmlPreviewImport, + group: solo_turnier.group.Group, + ) -> list[types.HtmlPreviewParticipant]: + groupParser = types.group.GroupParser() + + ret = [] + + # self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) + # self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) + + for tup in importedData.htmlResults.results.keys(): + currentGroup = groupParser.parseClass(tup[0]) + activeGroups = currentGroup.getContainedGroups() + if group not in activeGroups: + continue + + fixture = importedData.htmlResults.tabges.get(tup, None) + if fixture is None: + self.l.error("A fixture for the tuple %s could not be read.", tup) + else: + if ( + fixture[2] is not None + and groupParser.parseClass(fixture[2]) != group + ): + self.l.log( + 5, + "Skipping id %s in group %s as in other group.", + tup[3], + group, + ) + continue + + part = importedData.htmlResults.results[tup][0] + part.id = int(tup[3]) + ret.append(part) + + self.l.log(5, "ret %s", ret) + # raise Exception('Test') + + # for id in previewData.participants: + # participantList = previewData.participants[id] + # for participant in participantList: + # if participant.group == group: + # ret.append(participant) + return ret + + def _getResultOfSingleParticipant( + self, + participant: types.HtmlParticipant, + nominalGroup: solo_turnier.group.Group, + totalResults: types.HtmlCompetitionTotalResults, + allDances: list[str], + ) -> list[types.SingleParticipantResult | None]: + rawResults = totalResults.getById(participant.id) + self.l.log( + 5, "Found result data for id %i (raw): %s", participant.id, rawResults + ) + + results = [None for x in allDances] + + for danceIdx, dance in enumerate(allDances): + # self.l.log(5, '%s %s', dance, danceIdx) + def getResult() -> types.SingleParticipantResult | None: + for key in rawResults: + if key[0] != dance: + continue + rawResult = rawResults[key] + + if len(rawResult) != 1: + raise Exception("Multiple results found with same key") + rawResult = rawResult[0] + + nativeClass = key[2] + # nativeClass = previewResults.results[participant][dance] + # nativeClass = key[2] + + # self.l.log(5, 'Result %s => %s', key, rawResult) + ret = types.SingleParticipantResult( + key[2], + nativeClass, + dance, + rawResult.finalist, + rawResult.place, + rawResult.placeTo, + ) + + return ret + return None + + results[danceIdx] = getResult() + + return results + + def _fixNativeDataFromTable( + self, + dances: list[str], + data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]], + importedData: types.HtmlCompetitionTotalResults, + ): + rePlace = re.compile("([0-9]+)(?:-([0-9]+))?") + classParser = competition_class.CompetitionClassParser() + + for participant in data.keys(): + self.l.log(5, "fixing participant %s", participant) + results = data[participant] + for result in results: + if result is None: + continue + self.l.log(5, "Looking at result set %s", result) + + def selectEntry(k): + return k[2] == result.dance and int(k[3]) == participant.id + + keys = list(importedData.tabges.keys()) + selected = list(map(selectEntry, keys)) + try: + selectedIndex = selected.index(True) + except: + continue + + raw = importedData.tabges[keys[selectedIndex]] + self.l.log(5, "Raw %s", raw) + nativePlaceRaw = raw[0] + matcher = rePlace.fullmatch(nativePlaceRaw) + if matcher is None: + self.l.error( + "Cannot parse place string %s for participant %u (%s) in dance %s", + nativePlaceRaw, + participant.id, + participant, + result.dance, + ) + continue + self.l.log(5, "Found strings by regex: %s", matcher.groups()) + result.placeNative = matcher.group(1) + result.placeNativeTo = matcher.group(2) + + if raw[1] is not None: + result.nativeClass = classParser.parseAbbreviatedClass(raw[1]) + + pass + + def filterOutFinalists(self, data: types.State4, filterOut: bool): + for group in data.results: + self.l.debug("Cleaning up group %s", group.name) + participants = data.results[group].results.keys() + droppedParticipants = [] + + for participant in participants: + self.l.debug("Checking %s", participant) + + def isFinalistInDance(x: types.HtmlSingleCompetitionResult | None): + if x is None: + return False + return x.finalist + + mapped = list( + map(isFinalistInDance, data.results[group].results[participant]) + ) + finalist = True in mapped + self.l.log(5, "Check for finalist (in dances %s): %s", mapped, finalist) + + if finalist: + participant.finalist = True + else: + participant.finalist = False + self.l.info( + "Dropping %s from the output as no finalist", participant + ) + droppedParticipants.append(participant) + + if filterOut: + for droppedParticipant in droppedParticipants: + data.results[group].results.pop(droppedParticipant) diff --git a/src/solo_turnier/workers/__init__.py b/src/solo_turnier/workers/__init__.py new file mode 100644 index 0000000..2ca2255 --- /dev/null +++ b/src/solo_turnier/workers/__init__.py @@ -0,0 +1,3 @@ +from . import ResultExtractor +from . import DataWorker +from . import Worker From 69e20dd4c80d8b81be03a7175e824306d13c5ae5 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 11:12:50 +0100 Subject: [PATCH 19/31] Enhance data structure to reuse common class objects --- src/solo_turnier/output.py | 2 +- src/solo_turnier/worker.py | 9 +++++++++ src/solo_turnier/workers/DataWorker.py | 1 + src/solo_turnier/workers/Worker.py | 24 ++++++++---------------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/solo_turnier/output.py b/src/solo_turnier/output.py index 3ee874a..8d21f46 100644 --- a/src/solo_turnier/output.py +++ b/src/solo_turnier/output.py @@ -16,7 +16,7 @@ sectionMap = { class AbstractOutputter: def __init__(self): - self.worker = solo_turnier.worker.DataWorker() + self.worker = solo_turnier.workers.DataWorker.DataWorker() self.groups = [] self.dances = [] self.showIds = False diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index e87a079..b099fbc 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -1,6 +1,15 @@ from .reader import ResultRow +# import logging +# from pprint import pformat +# import re + +# import solo_turnier + +# from .types import HtmlCompetitionResultRow as CompetitionResult +# from . import types +# from . import competition_class class HtmlPerson: diff --git a/src/solo_turnier/workers/DataWorker.py b/src/solo_turnier/workers/DataWorker.py index 49f3e73..5bf21f8 100644 --- a/src/solo_turnier/workers/DataWorker.py +++ b/src/solo_turnier/workers/DataWorker.py @@ -2,6 +2,7 @@ from ..reader import ResultRow from ..worker import ResultPerson from ..types import HtmlCompetitionResultRow as CompetitionResult from solo_turnier import html_parser +import logging class DataWorker: diff --git a/src/solo_turnier/workers/Worker.py b/src/solo_turnier/workers/Worker.py index 6e6e525..40bfdf8 100644 --- a/src/solo_turnier/workers/Worker.py +++ b/src/solo_turnier/workers/Worker.py @@ -42,6 +42,7 @@ class Worker: self.l.log(5, "Inverted group maping: %s", invertedGroupMapping) totalResult = {} + ret = types.State4(totalResult) for group in groups: self.l.debug("Collecting data for total result of group %s", group) @@ -75,32 +76,27 @@ class Worker: self.l.log(5, "Total result of all groups: %s", pformat(totalResult)) - ret = types.State4(totalResult) return ret def _extractGroups(self, data: types.State3): - groupParser = solo_turnier.group.GroupParser() - groupSet = set([]) # for id in data.previewImport.participants: # participants = data.previewImport.participants[id] # for participant in participants: # groupSet.add(participant.group) for tup in data.htmlResults.results.keys(): - gr = groupParser.parseClass(tup[0]) + gr = self._groupParser.parseClass(tup[0]) # groupSet.add(gr) groupSet.update(gr.getContainedGroups()) # self.l.log(5, 'Group type %s', type(gr)) self.l.log(5, "Set of active groups: %s", groupSet) - groups = groupParser.getGroupsAsSortedList(groupSet) + groups = self._groupParser.getGroupsAsSortedList(groupSet) return groups def _getGroupMapping( self, importedData: types.State3 ) -> dict[int, solo_turnier.group.Group | None]: - groupParser = solo_turnier.group.GroupParser() - def _getBestGroupGuess(groups, id): counts = {} grNones = 0 @@ -141,11 +137,11 @@ class Worker: groupsPerId = {} for tup in importedData.htmlResults.results: - competitionGroup = groupParser.parseClass(tup[0]) + competitionGroup = self._groupParser.parseClass(tup[0]) fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) id = int(tup[3]) if fixture[2] is not None: - group = groupParser.parseClass(fixture[2]) + group = self._groupParser.parseClass(fixture[2]) else: containedGroups = competitionGroup.getContainedGroups() if len(containedGroups) > 1: @@ -202,13 +198,11 @@ class Worker: def _extractDancesPerGroup( self, data: types.State3, group: solo_turnier.group.Group ): - groupParser = solo_turnier.group.GroupParser() - dances = set() additionalDances = set() foundDances = set() for tup in data.htmlResults.results.keys(): - currentGroup = groupParser.parseClass(tup[0]) + currentGroup = self._groupParser.parseClass(tup[0]) if group not in currentGroup.getContainedGroups(): continue foundDances.add(tup[2]) @@ -233,15 +227,13 @@ class Worker: # previewData: types.HtmlPreviewImport, group: solo_turnier.group.Group, ) -> list[types.HtmlPreviewParticipant]: - groupParser = types.group.GroupParser() - ret = [] # self.l.log(5, 'Table %s', pformat(importedData.htmlResults.tabges)) # self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) for tup in importedData.htmlResults.results.keys(): - currentGroup = groupParser.parseClass(tup[0]) + currentGroup = self._groupParser.parseClass(tup[0]) activeGroups = currentGroup.getContainedGroups() if group not in activeGroups: continue @@ -252,7 +244,7 @@ class Worker: else: if ( fixture[2] is not None - and groupParser.parseClass(fixture[2]) != group + and self._groupParser.parseClass(fixture[2]) != group ): self.l.log( 5, From b4ec4f896c9699f2b685312cc02f71e0f4329ed3 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 11:53:58 +0100 Subject: [PATCH 20/31] Refactor name of group parser method --- src/solo_turnier/group.py | 8 ++++---- src/solo_turnier/html_parser.py | 2 +- src/solo_turnier/reader.py | 4 ++-- src/solo_turnier/types.py | 2 +- src/solo_turnier/workers/Worker.py | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index bd6f813..19c5153 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -71,7 +71,7 @@ class GroupParser: "Masters V": self.MAS5, } - def parseClass(self, cls: str) -> Group_t: + def parseGroup(self, cls: str) -> Group_t: match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls) if match is not None: grpA = self.mapNames[match.group(1)] @@ -80,9 +80,9 @@ class GroupParser: else: return self.mapNames[cls] - def isPureClass(self, cls: str) -> bool: - parsedClass = self.parseClass(cls) - return isinstance(parsedClass, Group) + def isPureGroup(self, cls: str) -> bool: + parsedGroup = self.parseGroup(cls) + return isinstance(parsedGroup, Group) def getGroups(self) -> list[Group]: return [ diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index f74f795..3ff09b6 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -46,7 +46,7 @@ class HtmlParser: return { "dance": dance.strip(), "class_": str(self.classParser.parseClass(rawClass, True)), - "group": str(self.groupParser.parseClass(rawGroup)), + "group": str(self.groupParser.parseGroup(rawGroup)), } def parseResult(self): diff --git a/src/solo_turnier/reader.py b/src/solo_turnier/reader.py index 28501ab..c162858 100644 --- a/src/solo_turnier/reader.py +++ b/src/solo_turnier/reader.py @@ -37,7 +37,7 @@ class CSVResultReader: def __processRow(row): result = ResultRow( - competitionGroup=groupParser.parseClass(row[2]), + competitionGroup=groupParser.parseGroup(row[2]), competitionClass=classParser.parseClass(row[3]), dance=row[4], id=row[5], @@ -46,7 +46,7 @@ class CSVResultReader: club=row[10], place=row[12], placeTo=row[13], - group=groupParser.parseClass(row[15]), + group=groupParser.parseGroup(row[15]), class_=classParser.parseClass(row[16]), ) self.l.log(5, "Found row in CSV: %s", result) diff --git a/src/solo_turnier/types.py b/src/solo_turnier/types.py index 039f73e..6742682 100644 --- a/src/solo_turnier/types.py +++ b/src/solo_turnier/types.py @@ -39,7 +39,7 @@ class HtmlPreviewParticipant: self.name = name self.id = id groupParser = group.GroupParser() - self.group = groupParser.parseClass(group_) + self.group = groupParser.parseGroup(group_) self.finalist = None def __eq__(self, o): diff --git a/src/solo_turnier/workers/Worker.py b/src/solo_turnier/workers/Worker.py index 40bfdf8..11e8bfe 100644 --- a/src/solo_turnier/workers/Worker.py +++ b/src/solo_turnier/workers/Worker.py @@ -85,7 +85,7 @@ class Worker: # for participant in participants: # groupSet.add(participant.group) for tup in data.htmlResults.results.keys(): - gr = self._groupParser.parseClass(tup[0]) + gr = self._groupParser.parseGroup(tup[0]) # groupSet.add(gr) groupSet.update(gr.getContainedGroups()) # self.l.log(5, 'Group type %s', type(gr)) @@ -137,11 +137,11 @@ class Worker: groupsPerId = {} for tup in importedData.htmlResults.results: - competitionGroup = self._groupParser.parseClass(tup[0]) + competitionGroup = self._groupParser.parseGroup(tup[0]) fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) id = int(tup[3]) if fixture[2] is not None: - group = self._groupParser.parseClass(fixture[2]) + group = self._groupParser.parseGroup(fixture[2]) else: containedGroups = competitionGroup.getContainedGroups() if len(containedGroups) > 1: @@ -202,7 +202,7 @@ class Worker: additionalDances = set() foundDances = set() for tup in data.htmlResults.results.keys(): - currentGroup = self._groupParser.parseClass(tup[0]) + currentGroup = self._groupParser.parseGroup(tup[0]) if group not in currentGroup.getContainedGroups(): continue foundDances.add(tup[2]) @@ -233,7 +233,7 @@ class Worker: # self.l.log(5, 'Results %s', pformat(importedData.htmlResults.results)) for tup in importedData.htmlResults.results.keys(): - currentGroup = self._groupParser.parseClass(tup[0]) + currentGroup = self._groupParser.parseGroup(tup[0]) activeGroups = currentGroup.getContainedGroups() if group not in activeGroups: continue @@ -244,7 +244,7 @@ class Worker: else: if ( fixture[2] is not None - and self._groupParser.parseClass(fixture[2]) != group + and self._groupParser.parseGroup(fixture[2]) != group ): self.l.log( 5, From 82d7717fde5cb1a4669677b20abdd0578ff20a67 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 12:09:42 +0100 Subject: [PATCH 21/31] Removed preview extractor methods --- src/solo_turnier/html_parser.py | 80 --------------------------------- 1 file changed, 80 deletions(-) diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index 3ff09b6..2f1c02d 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -105,89 +105,9 @@ class HtmlParser: # title = self.soup.find('div', class_='eventhead').table.tr.td.contents[0] - # ret = HtmlImport(title, participants) ret = HtmlResultImport(participants) return ret - def parsePreparationRound(self): - title = self.soup.find("div", class_="eventhead").table.tr.td.contents[0] - tableData = [] - rowTitles = [] - - def __mapBr(td): - for br in td.find_all("br"): - br.replace_with("\n") - td.smooth() - return td - - def __extractTitles(table): - for row in table.find_all("tr")[1:]: - rowTitles.append(__mapBr(row.td).string) - - def __extractColumns(table): - content = [] - - def __extractContent(td): - for br in td.find_all("br"): - br.replace_with("\n") - - span = td.span - if span is not None: - span = span.extract() - meta = span.string - else: - meta = None - - td.smooth() - - return {"text": td.string.replace("\xa0", " ").strip(), "meta": meta} - - def __extractRow(row): - entries = [] - for entry in row.find_all("td")[1:]: - entries.append(__extractContent(entry)) - return entries - - for row in table.find_all("tr")[1:]: - content.append(__extractRow(row)) - - return content - - def __mergeColumns(columns1, columns2): - return list(map(lambda x, y: x + y, columns1, columns2)) - - extract = self.soup.find("div", class_="extract") - tables = extract.find_all("table", class_="tab1") - - __extractTitles(tables[0]) - tableData = __extractColumns(tables[0]) - - for table in tables[1:]: - tableData = __mergeColumns(tableData, __extractColumns(table)) - - data = {"titles": rowTitles, "table": tableData} - - return {"title": title, "data": data} - - def cleanPreparationRoundImport(self, data): - def __cleanTable(table): - def __cleanText(s: str): - # print("cleaning string ", s) - return s.strip(" \n\xa0") - - def __cleanEntry(entry): - entry["text"] = __cleanText(entry["text"]) - if entry["meta"] is not None: - entry["meta"] = __cleanText(entry["meta"]) - - for row in table: - for entry in row: - # print(entry) - __cleanEntry(entry) - - data["title"] = data["title"].strip() - __cleanTable(data["data"]["table"]) - def parseIndividualResult(self, competitionGroup, competitionClass, dance): participants = {} From b78b964a55a1bbcff5d7a823ea87ff4764e337c5 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 12:35:19 +0100 Subject: [PATCH 22/31] Moved types into package --- .../{types.py => types/__init__.py} | 43 +++---------------- src/solo_turnier/types/csvResultRow.py | 30 +++++++++++++ src/solo_turnier/types/place.py | 10 +++++ src/solo_turnier/workers/ResultExtractor.py | 8 ++-- 4 files changed, 50 insertions(+), 41 deletions(-) rename src/solo_turnier/{types.py => types/__init__.py} (90%) create mode 100644 src/solo_turnier/types/csvResultRow.py create mode 100644 src/solo_turnier/types/place.py diff --git a/src/solo_turnier/types.py b/src/solo_turnier/types/__init__.py similarity index 90% rename from src/solo_turnier/types.py rename to src/solo_turnier/types/__init__.py index 6742682..5356a57 100644 --- a/src/solo_turnier/types.py +++ b/src/solo_turnier/types/__init__.py @@ -1,37 +1,10 @@ -from . import group -from . import competition_class +from .place import Place + +from .. import group +from .. import competition_class -class CSVResultRow: - def __init__( - self, - firstName, - lastName, - club, - id, - group, - class_, - dance, - place, - placeTo, - competitionGroup, - competitionClass, - ): - self.firstName = firstName - self.lastName = lastName - self.name = f"{firstName} {lastName}" - self.club = club - self.id = id - self.group = group - self.class_ = class_ - self.dance = dance - self.place = place - self.placeTo = placeTo - self.competitionGroup = competitionGroup - self.competitionClass = competitionClass - - def __repr__(self): - return f"{self.name} ({self.id}, {self.club}) is in {self.group} {self.class_} and danced the {self.dance} in {self.competitionGroup} {self.competitionClass} getting place {self.place}-{self.placeTo}" +from .csvResultRow import CSVResultRow class HtmlPreviewParticipant: @@ -92,12 +65,6 @@ class HtmlParticipant: return self.id >= other.id -# class PreviewParticipationData: -# def __init__(self, dance: str, class_: competition_class.CompetitionClass): -# self.class_ = class_ -# self.dance = dance - - class HtmlPreviewImport: def __init__( self, diff --git a/src/solo_turnier/types/csvResultRow.py b/src/solo_turnier/types/csvResultRow.py new file mode 100644 index 0000000..faeb8bd --- /dev/null +++ b/src/solo_turnier/types/csvResultRow.py @@ -0,0 +1,30 @@ +class CSVResultRow: + def __init__( + self, + firstName, + lastName, + club, + id, + group, + class_, + dance, + place, + placeTo, + competitionGroup, + competitionClass, + ): + self.firstName = firstName + self.lastName = lastName + self.name = f"{firstName} {lastName}" + self.club = club + self.id = id + self.group = group + self.class_ = class_ + self.dance = dance + self.place = place + self.placeTo = placeTo + self.competitionGroup = competitionGroup + self.competitionClass = competitionClass + + def __repr__(self): + return f"{self.name} ({self.id}, {self.club}) is in {self.group} {self.class_} and danced the {self.dance} in {self.competitionGroup} {self.competitionClass} getting place {self.place}-{self.placeTo}" diff --git a/src/solo_turnier/types/place.py b/src/solo_turnier/types/place.py new file mode 100644 index 0000000..8480d99 --- /dev/null +++ b/src/solo_turnier/types/place.py @@ -0,0 +1,10 @@ +class Place: + def __init__(self, place: int, placeTo: int | None = None): + self.place = place + self.placeTo = placeTo + + def __str__(self): + if self.placeTo is None: + return f"{self.place}." + + return f"{self.place}.-{self.placeTo}" diff --git a/src/solo_turnier/workers/ResultExtractor.py b/src/solo_turnier/workers/ResultExtractor.py index bafaaa1..839621d 100644 --- a/src/solo_turnier/workers/ResultExtractor.py +++ b/src/solo_turnier/workers/ResultExtractor.py @@ -53,16 +53,18 @@ class ResultExtractor: return ret - def _extractPlace(self, placeStr: str): + def _extractPlace(self, placeStr: str) -> types.Place: s = placeStr.replace(".", "") matches = self.rePlaceSingle.fullmatch(s) if matches is not None: - return (int(matches.group(1)), None) + return types.Place(int(matches.group(1))) + # return (int(matches.group(1)), None) matches = self.rePlaceDouble.fullmatch(s) if matches is not None: - return (int(matches.group(1)), int(matches.group(2))) + return types.Place(int(matches.group(1)), int(matches.group(2))) + # return (int(matches.group(1)), int(matches.group(2))) self.l.error('Could not parse place string "%s"', placeStr) raise Exception("Place cannot be parsed") From 345d060afa7d80f40512311fadf55e0fd094b242 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 13:39:23 +0100 Subject: [PATCH 23/31] Fixed imports in types package --- src/solo_turnier/types/__init__.py | 408 +----------------- .../types/htmlCompetitionResultRow.py | 35 ++ .../types/htmlCompetitionTotalResults.py | 50 +++ src/solo_turnier/types/htmlParticipant.py | 26 ++ src/solo_turnier/types/htmlPreviewImport.py | 18 + .../types/htmlPreviewParticipant.py | 28 ++ src/solo_turnier/types/htmlResultImport.py | 9 + .../types/htmlResultTotalTable.py | 6 + .../types/htmlSingleCompetitionResult.py | 26 ++ src/solo_turnier/types/outputTable.py | 7 + src/solo_turnier/types/participant.py | 17 + src/solo_turnier/types/participantResult.py | 23 + .../types/singleParticipantResult.py | 45 ++ src/solo_turnier/types/stages.py | 34 ++ .../types/tableCompetitionEntry.py | 44 ++ src/solo_turnier/types/tableEntry.py | 9 + src/solo_turnier/types/tableRow.py | 16 + src/solo_turnier/types/totalGroupResult.py | 15 + 18 files changed, 426 insertions(+), 390 deletions(-) create mode 100644 src/solo_turnier/types/htmlCompetitionResultRow.py create mode 100644 src/solo_turnier/types/htmlCompetitionTotalResults.py create mode 100644 src/solo_turnier/types/htmlParticipant.py create mode 100644 src/solo_turnier/types/htmlPreviewImport.py create mode 100644 src/solo_turnier/types/htmlPreviewParticipant.py create mode 100644 src/solo_turnier/types/htmlResultImport.py create mode 100644 src/solo_turnier/types/htmlResultTotalTable.py create mode 100644 src/solo_turnier/types/htmlSingleCompetitionResult.py create mode 100644 src/solo_turnier/types/outputTable.py create mode 100644 src/solo_turnier/types/participant.py create mode 100644 src/solo_turnier/types/participantResult.py create mode 100644 src/solo_turnier/types/singleParticipantResult.py create mode 100644 src/solo_turnier/types/stages.py create mode 100644 src/solo_turnier/types/tableCompetitionEntry.py create mode 100644 src/solo_turnier/types/tableEntry.py create mode 100644 src/solo_turnier/types/tableRow.py create mode 100644 src/solo_turnier/types/totalGroupResult.py diff --git a/src/solo_turnier/types/__init__.py b/src/solo_turnier/types/__init__.py index 5356a57..6f7bab0 100644 --- a/src/solo_turnier/types/__init__.py +++ b/src/solo_turnier/types/__init__.py @@ -5,393 +5,21 @@ from .. import competition_class from .csvResultRow import CSVResultRow - - -class HtmlPreviewParticipant: - def __init__(self, name, id, group_): - self.name = name - self.id = id - groupParser = group.GroupParser() - self.group = groupParser.parseGroup(group_) - self.finalist = None - - def __eq__(self, o): - if type(o) != HtmlPreviewParticipant: - return False - - return all( - map( - lambda x, y: x == y, - (self.name, self.id, self.group), - (o.name, o.id, o.group), - ) - ) - - def __repr__(self): - return f"{self.id} ({self.name}, {self.group})" - - def __hash__(self): - return hash((self.id, self.name, self.group)) - - def __gt__(self, other): - return self.id >= other.id - - -class HtmlParticipant: - def __init__(self, name, id): - self.name = name - self.id = id - self.finalist = None - - def __eq__(self, o): - if type(o) != HtmlPreviewParticipant: - return False - - return all( - map( - lambda x, y: x == y, - (self.name, self.id, self.group), - (o.name, o.id, o.group), - ) - ) - - def __repr__(self): - return f"{self.id}: {self.name}" - - def __hash__(self): - return hash((self.id, self.name)) - - def __gt__(self, other): - return self.id >= other.id - - -class HtmlPreviewImport: - def __init__( - self, - participants: dict[int, list[HtmlPreviewParticipant]], - results: dict[ - HtmlPreviewParticipant, dict[str, competition_class.CompetitionClass] - ], - ): - self.participants = participants - self.results = results - - def __repr__(self): - return (str(self.participants), str(self.results)) - - -class HtmlResultImport: - def __init__(self, results: dict[HtmlParticipant, str]): - self.results = results - - def __repr__(self): - return str(self.results) - - -class HtmlResultTotalTable: - def __init__(self, participants): - self.participants = participants - - def __repr__(self): - return str(self.participants) - - -class HtmlCompetitionResultRow: - def __init__(self, name, id, dance, group, class_, place, placeTo, finalist): - self.dance = dance - self.group = group - self.class_ = class_ - self.place = place - self.placeTo = placeTo - self.id = int(id) - self.name = name - self.finalist = finalist - - def __repr__(self): - if self.place == self.placeTo: - result = f"{self.place}." - else: - result = f"{self.place}.-{self.placeTo}." - - if self.finalist == True: - finalist = "[F]" - else: - finalist = "" - return f"Result[{self.id}]({self.group} {self.class_} {self.dance} as {result}{finalist})" - - def __eq__(self, o): - if not isinstance(o, CompetitionResult): - return False - - return ( - self.dance == o.dance - and self.competitionClass == o.competitionClass - and self.competitionGroup == o.competitionGroup - and self.place == o.place - and self.placeTo == o.placeTo - and self.id == o.id - ) - - -class HtmlSingleCompetitionResult: - def __init__(self, name, place, placeTo, finalist): - self.name = name - self.place = place - self.placeTo = placeTo - self.finalist = finalist - - def __repr__(self): - if self.placeTo is None: - place = self.place - else: - place = f"{self.place}-{self.placeTo}" - - if self.finalist: - return f"Res({self.name} [F], placed {place})" - else: - return f"Res({self.name}, placed {place})" - - def __gt__(self, other): - return self.id > other.id - - def __eq__(self, other): - return self.id == other.id - - def __hash__(self): - return hash(self.id) - - -class HtmlCompetitionTotalResults: - def __init__(self): - self.results = {} - self.tabges = {} - - def __getTuple( - self, - group: group.Group_t, - class_: competition_class.Class_t, - dance: str, - id: int, - ): - return (group, class_, dance, id) - - def get( - self, - group: group.Group_t, - class_: competition_class.Class_t, - dance: str, - id: int, - ) -> list[HtmlSingleCompetitionResult]: - return self.results[self.__getTuple(group, class_, dance, id)] - - def getById( - self, id: int - ) -> dict[ - tuple[str, group.Group_t, competition_class.Class_t], - HtmlSingleCompetitionResult, - ]: - ret = {} - - for k in self.results: - if int(k[3]) != id: - continue - # ret = ret + self.results[k] - # Dance, Group, Class - key = (k[2], k[0], k[1]) - ret[key] = self.results[k] - - return ret - - def add(self, group, class_, dance, id, result: HtmlSingleCompetitionResult): - tup = self.__getTuple(group, class_, dance, id) - l = self.results.get(tup, []) - l.append(result) - self.results[tup] = l - - -class SingleParticipantResult: - def __init__( - self, - competitionClass: competition_class.Class_t, - nativeClass: competition_class.CompetitionClass, - dance: str, - finalist: bool, - place: int, - placeTo: int | None, - ): - self.competitionClass = competitionClass - self.nativeClass = nativeClass - self.dance = dance - self.finalist = finalist - self.place = place - self.placeTo = placeTo - - if placeTo == place: - self.placeTo = None - - self.placeNative = None - self.placeNativeTo = None - - def __repr__(self): - asFinalist = " as finalist" if self.finalist else "" - - if self.placeTo is None: - return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" - - return f"SR[{self.place}-{self.placeTo} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" - - def getPlace(self): - if self.placeTo is None: - return f"{self.place}." - else: - return f"{self.place}.-{self.placeTo}." - - def getNativePlace(self): - if self.placeNativeTo is None: - return f"{self.placeNative}." - else: - return f"{self.placeNative}.-{self.placeNativeTo}." - - -class TotalGroupResult: - def __init__( - self, - dances: list[str], - results: dict[HtmlPreviewParticipant, list[SingleParticipantResult]], - ): - self.dances = dances - self.results = results - - def __repr__(self): - return f"TotalGroupResult({self.dances}, {self.results})" - - -class State4: - def __init__(self, resultPerGroup: dict[group.Group, TotalGroupResult]): - parser = group.GroupParser() - self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) - self.results = resultPerGroup - - -class State3: - def __init__(self, htmlResults: HtmlCompetitionTotalResults): - self.htmlResults = htmlResults - - -class Participant: - def __init__( - self, - firstName: str, - lastName: str, - club: str, - group: group.Group, - class_: competition_class.CompetitionClass, - ): - self.firstName = firstName - self.lastName = lastName - self.club = club - self.group = group - self.class_ = class_ - - -class ParticipantResult: - def __init__( - self, - id: int, - finalist: bool, - cancelled: bool, - group: group.Group_t, - class_: competition_class.Class_t, - dance: str, - place, - placeTo, - ): - self.id = id - self.finalist = finalist - self.cancelled = cancelled - self.group = group - self.class_ = class_ - self.dance = dance - self.place = place - self.placeTo = placeTo - - -class Stage2: - def __init__(self, results: dict[Participant, list[ParticipantResult]]): - self.results = results - - -class TableCompetitionEntry: - def __init__( - self, - cancelled: bool, - finalist: bool, - class_: competition_class.Class_t, - place: int = -1, - placeTo: int = -1, - group: group.Group_t = None, - id: int = None, - ): - self.finalist = finalist - self.cancelled = cancelled - self.group = group - self.class_ = class_ - self.place = place - self.placeTo = placeTo - - def __repr__(self): - def paramMerging(l): - return ", ".join(filter(lambda x: x is not None, l)) - - if self.cancelled: - params = paramMerging([self.group, self.class_, self.id]) - if len(params) > 0: - return f"- ({params})" - else: - return "-" - elif not self.finalist: - params = paramMerging([self.group, self.class_, self.id]) - if len(params) > 0: - return f"x ({params})" - else: - return "x" - else: - if self.place == self.placeTo: - place = f"{self.place}." - else: - place = f"{self.place}.-{self.placeTo}." - params = paramMerging([self.group, self.class_, self.id]) - return f"{place} ({params})" - - -class TableEntry: - def __init__(self, competitions: list[TableCompetitionEntry]): - self.competitions = competitions - - def __repr__(self): - return ", ".join(self.competitions) - - -class TableRow: - def __init__(self, participant: Participant, id: int, entries: list[TableEntry]): - self.participant = participant - self.id = id - self.entries = entries - - def getRowList(self): - if self.id is not None: - first = f"{self.id}. {self.participant.firstName} {self.participant.lastName} ({self.participant.club})" - else: - first = f"{self.participant.firstName} {self.participant.lastName} ({self.participant.club})" - return [first] + map(str, self.entries) - - -class OutputTable: - def __init__(self, dances: list[str], rows: list[TableRow]): - self.dances = dances - self.rows = rows - - -class Stage1: - def __init__(self, tables: dict[group.Group, OutputTable]): - self.tables = tables +from .htmlPreviewParticipant import HtmlPreviewParticipant +from .htmlParticipant import HtmlParticipant +from .htmlPreviewImport import HtmlPreviewImport +from .htmlResultImport import HtmlResultImport +from .htmlResultTotalTable import HtmlResultTotalTable +from .htmlCompetitionResultRow import HtmlCompetitionResultRow +from .htmlSingleCompetitionResult import HtmlSingleCompetitionResult +from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults +from .singleParticipantResult import SingleParticipantResult +from .totalGroupResult import TotalGroupResult +from .participant import Participant +from .participantResult import ParticipantResult +from .tableCompetitionEntry import TableCompetitionEntry +from .tableEntry import TableEntry +from .tableRow import TableRow +from .outputTable import OutputTable + +from .stages import * diff --git a/src/solo_turnier/types/htmlCompetitionResultRow.py b/src/solo_turnier/types/htmlCompetitionResultRow.py new file mode 100644 index 0000000..ca89be2 --- /dev/null +++ b/src/solo_turnier/types/htmlCompetitionResultRow.py @@ -0,0 +1,35 @@ +class HtmlCompetitionResultRow: + def __init__(self, name, id, dance, group, class_, place, placeTo, finalist): + self.dance = dance + self.group = group + self.class_ = class_ + self.place = place + self.placeTo = placeTo + self.id = int(id) + self.name = name + self.finalist = finalist + + def __repr__(self): + if self.place == self.placeTo: + result = f"{self.place}." + else: + result = f"{self.place}.-{self.placeTo}." + + if self.finalist == True: + finalist = "[F]" + else: + finalist = "" + return f"Result[{self.id}]({self.group} {self.class_} {self.dance} as {result}{finalist})" + + def __eq__(self, o): + if not isinstance(o, CompetitionResult): + return False + + return ( + self.dance == o.dance + and self.competitionClass == o.competitionClass + and self.competitionGroup == o.competitionGroup + and self.place == o.place + and self.placeTo == o.placeTo + and self.id == o.id + ) diff --git a/src/solo_turnier/types/htmlCompetitionTotalResults.py b/src/solo_turnier/types/htmlCompetitionTotalResults.py new file mode 100644 index 0000000..e6d5ea1 --- /dev/null +++ b/src/solo_turnier/types/htmlCompetitionTotalResults.py @@ -0,0 +1,50 @@ +import solo_turnier +from .htmlSingleCompetitionResult import HtmlSingleCompetitionResult + + +class HtmlCompetitionTotalResults: + def __init__(self): + self.results = {} + self.tabges = {} + + def __getTuple( + self, + group: solo_turnier.group.Group_t, + class_: solo_turnier.competition_class.Class_t, + dance: str, + id: int, + ): + return (group, class_, dance, id) + + def get( + self, + group: solo_turnier.group.Group_t, + class_: solo_turnier.competition_class.Class_t, + dance: str, + id: int, + ) -> list[HtmlSingleCompetitionResult]: + return self.results[self.__getTuple(group, class_, dance, id)] + + def getById( + self, id: int + ) -> dict[ + tuple[str, solo_turnier.group.Group_t, solo_turnier.competition_class.Class_t], + HtmlSingleCompetitionResult, + ]: + ret = {} + + for k in self.results: + if int(k[3]) != id: + continue + # ret = ret + self.results[k] + # Dance, Group, Class + key = (k[2], k[0], k[1]) + ret[key] = self.results[k] + + return ret + + def add(self, group, class_, dance, id, result: HtmlSingleCompetitionResult): + tup = self.__getTuple(group, class_, dance, id) + l = self.results.get(tup, []) + l.append(result) + self.results[tup] = l diff --git a/src/solo_turnier/types/htmlParticipant.py b/src/solo_turnier/types/htmlParticipant.py new file mode 100644 index 0000000..a0be30c --- /dev/null +++ b/src/solo_turnier/types/htmlParticipant.py @@ -0,0 +1,26 @@ +class HtmlParticipant: + def __init__(self, name, id): + self.name = name + self.id = id + self.finalist = None + + def __eq__(self, o): + if type(o) != HtmlPreviewParticipant: + return False + + return all( + map( + lambda x, y: x == y, + (self.name, self.id, self.group), + (o.name, o.id, o.group), + ) + ) + + def __repr__(self): + return f"{self.id}: {self.name}" + + def __hash__(self): + return hash((self.id, self.name)) + + def __gt__(self, other): + return self.id >= other.id diff --git a/src/solo_turnier/types/htmlPreviewImport.py b/src/solo_turnier/types/htmlPreviewImport.py new file mode 100644 index 0000000..36405ed --- /dev/null +++ b/src/solo_turnier/types/htmlPreviewImport.py @@ -0,0 +1,18 @@ +from .htmlPreviewParticipant import HtmlPreviewParticipant +import solo_turnier + + +class HtmlPreviewImport: + def __init__( + self, + participants: dict[int, list[HtmlPreviewParticipant]], + results: dict[ + HtmlPreviewParticipant, + dict[str, solo_turnier.competition_class.CompetitionClass], + ], + ): + self.participants = participants + self.results = results + + def __repr__(self): + return (str(self.participants), str(self.results)) diff --git a/src/solo_turnier/types/htmlPreviewParticipant.py b/src/solo_turnier/types/htmlPreviewParticipant.py new file mode 100644 index 0000000..b072e67 --- /dev/null +++ b/src/solo_turnier/types/htmlPreviewParticipant.py @@ -0,0 +1,28 @@ +class HtmlPreviewParticipant: + def __init__(self, name, id, group_): + self.name = name + self.id = id + groupParser = group.GroupParser() + self.group = groupParser.parseGroup(group_) + self.finalist = None + + def __eq__(self, o): + if type(o) != HtmlPreviewParticipant: + return False + + return all( + map( + lambda x, y: x == y, + (self.name, self.id, self.group), + (o.name, o.id, o.group), + ) + ) + + def __repr__(self): + return f"{self.id} ({self.name}, {self.group})" + + def __hash__(self): + return hash((self.id, self.name, self.group)) + + def __gt__(self, other): + return self.id >= other.id diff --git a/src/solo_turnier/types/htmlResultImport.py b/src/solo_turnier/types/htmlResultImport.py new file mode 100644 index 0000000..de9a49c --- /dev/null +++ b/src/solo_turnier/types/htmlResultImport.py @@ -0,0 +1,9 @@ +from .htmlParticipant import HtmlParticipant + + +class HtmlResultImport: + def __init__(self, results: dict[HtmlParticipant, str]): + self.results = results + + def __repr__(self): + return str(self.results) diff --git a/src/solo_turnier/types/htmlResultTotalTable.py b/src/solo_turnier/types/htmlResultTotalTable.py new file mode 100644 index 0000000..c5952f7 --- /dev/null +++ b/src/solo_turnier/types/htmlResultTotalTable.py @@ -0,0 +1,6 @@ +class HtmlResultTotalTable: + def __init__(self, participants): + self.participants = participants + + def __repr__(self): + return str(self.participants) diff --git a/src/solo_turnier/types/htmlSingleCompetitionResult.py b/src/solo_turnier/types/htmlSingleCompetitionResult.py new file mode 100644 index 0000000..916748b --- /dev/null +++ b/src/solo_turnier/types/htmlSingleCompetitionResult.py @@ -0,0 +1,26 @@ +class HtmlSingleCompetitionResult: + def __init__(self, name, place, placeTo, finalist): + self.name = name + self.place = place + self.placeTo = placeTo + self.finalist = finalist + + def __repr__(self): + if self.placeTo is None: + place = self.place + else: + place = f"{self.place}-{self.placeTo}" + + if self.finalist: + return f"Res({self.name} [F], placed {place})" + else: + return f"Res({self.name}, placed {place})" + + def __gt__(self, other): + return self.id > other.id + + def __eq__(self, other): + return self.id == other.id + + def __hash__(self): + return hash(self.id) diff --git a/src/solo_turnier/types/outputTable.py b/src/solo_turnier/types/outputTable.py new file mode 100644 index 0000000..8eebe01 --- /dev/null +++ b/src/solo_turnier/types/outputTable.py @@ -0,0 +1,7 @@ +from .tableRow import TableRow + + +class OutputTable: + def __init__(self, dances: list[str], rows: list[TableRow]): + self.dances = dances + self.rows = rows diff --git a/src/solo_turnier/types/participant.py b/src/solo_turnier/types/participant.py new file mode 100644 index 0000000..f0cfd65 --- /dev/null +++ b/src/solo_turnier/types/participant.py @@ -0,0 +1,17 @@ +import solo_turnier + + +class Participant: + def __init__( + self, + firstName: str, + lastName: str, + club: str, + group: solo_turnier.group.Group, + class_: solo_turnier.competition_class.CompetitionClass, + ): + self.firstName = firstName + self.lastName = lastName + self.club = club + self.group = group + self.class_ = class_ diff --git a/src/solo_turnier/types/participantResult.py b/src/solo_turnier/types/participantResult.py new file mode 100644 index 0000000..aeb684e --- /dev/null +++ b/src/solo_turnier/types/participantResult.py @@ -0,0 +1,23 @@ +import solo_turnier + + +class ParticipantResult: + def __init__( + self, + id: int, + finalist: bool, + cancelled: bool, + group: solo_turnier.group.Group_t, + class_: solo_turnier.competition_class.Class_t, + dance: str, + place, + placeTo, + ): + self.id = id + self.finalist = finalist + self.cancelled = cancelled + self.group = group + self.class_ = class_ + self.dance = dance + self.place = place + self.placeTo = placeTo diff --git a/src/solo_turnier/types/singleParticipantResult.py b/src/solo_turnier/types/singleParticipantResult.py new file mode 100644 index 0000000..b280c83 --- /dev/null +++ b/src/solo_turnier/types/singleParticipantResult.py @@ -0,0 +1,45 @@ +import solo_turnier + + +class SingleParticipantResult: + def __init__( + self, + competitionClass: solo_turnier.competition_class.Class_t, + nativeClass: solo_turnier.competition_class.CompetitionClass, + dance: str, + finalist: bool, + place: int, + placeTo: int | None, + ): + self.competitionClass = competitionClass + self.nativeClass = nativeClass + self.dance = dance + self.finalist = finalist + self.place = place + self.placeTo = placeTo + + if placeTo == place: + self.placeTo = None + + self.placeNative = None + self.placeNativeTo = None + + def __repr__(self): + asFinalist = " as finalist" if self.finalist else "" + + if self.placeTo is None: + return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" + + return f"SR[{self.place}-{self.placeTo} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" + + def getPlace(self): + if self.placeTo is None: + return f"{self.place}." + else: + return f"{self.place}.-{self.placeTo}." + + def getNativePlace(self): + if self.placeNativeTo is None: + return f"{self.placeNative}." + else: + return f"{self.placeNative}.-{self.placeNativeTo}." diff --git a/src/solo_turnier/types/stages.py b/src/solo_turnier/types/stages.py new file mode 100644 index 0000000..7105414 --- /dev/null +++ b/src/solo_turnier/types/stages.py @@ -0,0 +1,34 @@ +import solo_turnier + +from .totalGroupResult import TotalGroupResult +from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults +from .participant import Participant +from .participantResult import ParticipantResult +from .outputTable import OutputTable + + +class State4: + def __init__( + self, resultPerGroup: dict[solo_turnier.group.Group, TotalGroupResult] + ): + parser = group.GroupParser() + self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) + self.results = resultPerGroup + + +class State3: + def __init__(self, htmlResults: HtmlCompetitionTotalResults): + self.htmlResults = htmlResults + + +class Stage2: + def __init__(self, results: dict[Participant, list[ParticipantResult]]): + self.results = results + + +class Stage1: + def __init__( + self, + tables: dict[solo_turnier.group.Group, OutputTable], + ): + self.tables = tables diff --git a/src/solo_turnier/types/tableCompetitionEntry.py b/src/solo_turnier/types/tableCompetitionEntry.py new file mode 100644 index 0000000..f7e037d --- /dev/null +++ b/src/solo_turnier/types/tableCompetitionEntry.py @@ -0,0 +1,44 @@ +import solo_turnier + + +class TableCompetitionEntry: + def __init__( + self, + cancelled: bool, + finalist: bool, + class_: solo_turnier.competition_class.Class_t, + place: int = -1, + placeTo: int = -1, + group: solo_turnier.group.Group_t = None, + id: int = None, + ): + self.finalist = finalist + self.cancelled = cancelled + self.group = group + self.class_ = class_ + self.place = place + self.placeTo = placeTo + + def __repr__(self): + def paramMerging(l): + return ", ".join(filter(lambda x: x is not None, l)) + + if self.cancelled: + params = paramMerging([self.group, self.class_, self.id]) + if len(params) > 0: + return f"- ({params})" + else: + return "-" + elif not self.finalist: + params = paramMerging([self.group, self.class_, self.id]) + if len(params) > 0: + return f"x ({params})" + else: + return "x" + else: + if self.place == self.placeTo: + place = f"{self.place}." + else: + place = f"{self.place}.-{self.placeTo}." + params = paramMerging([self.group, self.class_, self.id]) + return f"{place} ({params})" diff --git a/src/solo_turnier/types/tableEntry.py b/src/solo_turnier/types/tableEntry.py new file mode 100644 index 0000000..1a9334f --- /dev/null +++ b/src/solo_turnier/types/tableEntry.py @@ -0,0 +1,9 @@ +from .tableCompetitionEntry import TableCompetitionEntry + + +class TableEntry: + def __init__(self, competitions: list[TableCompetitionEntry]): + self.competitions = competitions + + def __repr__(self): + return ", ".join(self.competitions) diff --git a/src/solo_turnier/types/tableRow.py b/src/solo_turnier/types/tableRow.py new file mode 100644 index 0000000..427bf49 --- /dev/null +++ b/src/solo_turnier/types/tableRow.py @@ -0,0 +1,16 @@ +from .participant import Participant +from .tableEntry import TableEntry + + +class TableRow: + def __init__(self, participant: Participant, id: int, entries: list[TableEntry]): + self.participant = participant + self.id = id + self.entries = entries + + def getRowList(self): + if self.id is not None: + first = f"{self.id}. {self.participant.firstName} {self.participant.lastName} ({self.participant.club})" + else: + first = f"{self.participant.firstName} {self.participant.lastName} ({self.participant.club})" + return [first] + map(str, self.entries) diff --git a/src/solo_turnier/types/totalGroupResult.py b/src/solo_turnier/types/totalGroupResult.py new file mode 100644 index 0000000..58f811a --- /dev/null +++ b/src/solo_turnier/types/totalGroupResult.py @@ -0,0 +1,15 @@ +from .htmlPreviewParticipant import HtmlPreviewParticipant +from .singleParticipantResult import SingleParticipantResult + + +class TotalGroupResult: + def __init__( + self, + dances: list[str], + results: dict[HtmlPreviewParticipant, list[SingleParticipantResult]], + ): + self.dances = dances + self.results = results + + def __repr__(self): + return f"TotalGroupResult({self.dances}, {self.results})" From e081769b6c8cd78e5f042e03920733780f58eabf Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 13:41:23 +0100 Subject: [PATCH 24/31] Fix minor bug to make things work for now --- src/solo_turnier/types/stages.py | 2 +- src/solo_turnier/workers/ResultExtractor.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/solo_turnier/types/stages.py b/src/solo_turnier/types/stages.py index 7105414..d834050 100644 --- a/src/solo_turnier/types/stages.py +++ b/src/solo_turnier/types/stages.py @@ -11,7 +11,7 @@ class State4: def __init__( self, resultPerGroup: dict[solo_turnier.group.Group, TotalGroupResult] ): - parser = group.GroupParser() + parser = solo_turnier.group.GroupParser() self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) self.results = resultPerGroup diff --git a/src/solo_turnier/workers/ResultExtractor.py b/src/solo_turnier/workers/ResultExtractor.py index 839621d..54dc0e9 100644 --- a/src/solo_turnier/workers/ResultExtractor.py +++ b/src/solo_turnier/workers/ResultExtractor.py @@ -82,7 +82,9 @@ class ResultExtractor: for person in result.results.keys(): placeStr = result.results[person] - place, placeTo = self._extractPlace(placeStr) + placeObj = self._extractPlace(placeStr) + place = placeObj.place + placeTo = placeObj.placeTo competitionResult = types.HtmlSingleCompetitionResult( person.name, place, placeTo, person.finalist ) From 04b43def9d0d56f71875381c2bf845bbcc6d50de Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 13:50:13 +0100 Subject: [PATCH 25/31] Dropping CSV parsing methods --- src/solo_turnier/__init__.py | 1 - src/solo_turnier/reader.py | 96 ----------------- src/solo_turnier/types/__init__.py | 5 - src/solo_turnier/types/csvResultRow.py | 30 ------ src/solo_turnier/worker.py | 20 ---- src/solo_turnier/workers/DataWorker.py | 143 ------------------------- 6 files changed, 295 deletions(-) delete mode 100644 src/solo_turnier/reader.py delete mode 100644 src/solo_turnier/types/csvResultRow.py diff --git a/src/solo_turnier/__init__.py b/src/solo_turnier/__init__.py index 16e588f..1446c3e 100644 --- a/src/solo_turnier/__init__.py +++ b/src/solo_turnier/__init__.py @@ -3,7 +3,6 @@ from . import group from . import types from . import cli -from . import reader from . import participant from . import html_locator diff --git a/src/solo_turnier/reader.py b/src/solo_turnier/reader.py deleted file mode 100644 index c162858..0000000 --- a/src/solo_turnier/reader.py +++ /dev/null @@ -1,96 +0,0 @@ -import solo_turnier -import csv -import os -import logging -import re -from pprint import pformat -from .types import CSVResultRow as ResultRow - - -class CSVResultReader: - def __init__(self, fileName: str): - self.fileName = fileName - self.l = logging.getLogger("solo_turnier.reader.CSVResultReader") - - def readFile(self): - with open(self.fileName, "r") as fp: - dialect = csv.Sniffer().sniff(fp.read(1024)) - fp.seek(0) - - csvReader = csv.reader(fp, dialect) - - rows = [] - for row in csvReader: - rows.append(row) - - ret = {"header": rows[0], "data": rows[1:]} - - self.l.log(5, "Imported results from allresults.csv file: %s", (ret)) - return ret - - def extractResult(self, entries=None) -> list[ResultRow]: - if entries is None: - entries = self.readFile() - - groupParser = solo_turnier.group.GroupParser() - classParser = solo_turnier.competition_class.CompetitionClassParser() - - def __processRow(row): - result = ResultRow( - competitionGroup=groupParser.parseGroup(row[2]), - competitionClass=classParser.parseClass(row[3]), - dance=row[4], - id=row[5], - firstName=row[6], - lastName=row[7], - club=row[10], - place=row[12], - placeTo=row[13], - group=groupParser.parseGroup(row[15]), - class_=classParser.parseClass(row[16]), - ) - self.l.log(5, "Found row in CSV: %s", result) - return result - - ret = list(map(__processRow, entries["data"])) - - self.l.log(5, "Extracted rows from CSV data: %s", ret) - return ret - - -class CSVExtractor: - def __init__(self): - self.l = logging.getLogger("solo_turnier.worker") - self.__groupMaps = {"Kinder": "Kin.", "Junioren": "Jun.", "Jugend": "Jug."} - self.__classMaps = {"Newcomer": "Newc.", "Beginner": "Beg.", "Advanced": "Adv."} - - def __mapGroup(self, group): - return self.__groupMaps.get(group, group) - - def __mapClass(self, class_): - return self.__classMaps.get(class_, class_) - - def mapCSVImport(self, imported) -> list[ResultRow]: - ret = [] - - def __processRow(row): - result = ResultRow( - competitionGroup=self.__mapGroup(row[2]), - competitionClass=self.__mapClass(row[3]), - dance=row[4], - id=row[5], - firstName=row[6], - lastName=row[7], - club=row[10], - place=row[12], - placeTo=row[13], - group=self.__mapGroup(row[15]), - class_=self.__mapClass(row[16]), - ) - ret.append(result) - self.l.log(5, "Found row in CSV: %s", result) - - for row in imported["data"]: - __processRow(row) - - return ret diff --git a/src/solo_turnier/types/__init__.py b/src/solo_turnier/types/__init__.py index 6f7bab0..31e08e4 100644 --- a/src/solo_turnier/types/__init__.py +++ b/src/solo_turnier/types/__init__.py @@ -1,10 +1,5 @@ from .place import Place -from .. import group -from .. import competition_class - - -from .csvResultRow import CSVResultRow from .htmlPreviewParticipant import HtmlPreviewParticipant from .htmlParticipant import HtmlParticipant from .htmlPreviewImport import HtmlPreviewImport diff --git a/src/solo_turnier/types/csvResultRow.py b/src/solo_turnier/types/csvResultRow.py deleted file mode 100644 index faeb8bd..0000000 --- a/src/solo_turnier/types/csvResultRow.py +++ /dev/null @@ -1,30 +0,0 @@ -class CSVResultRow: - def __init__( - self, - firstName, - lastName, - club, - id, - group, - class_, - dance, - place, - placeTo, - competitionGroup, - competitionClass, - ): - self.firstName = firstName - self.lastName = lastName - self.name = f"{firstName} {lastName}" - self.club = club - self.id = id - self.group = group - self.class_ = class_ - self.dance = dance - self.place = place - self.placeTo = placeTo - self.competitionGroup = competitionGroup - self.competitionClass = competitionClass - - def __repr__(self): - return f"{self.name} ({self.id}, {self.club}) is in {self.group} {self.class_} and danced the {self.dance} in {self.competitionGroup} {self.competitionClass} getting place {self.place}-{self.placeTo}" diff --git a/src/solo_turnier/worker.py b/src/solo_turnier/worker.py index b099fbc..127c681 100644 --- a/src/solo_turnier/worker.py +++ b/src/solo_turnier/worker.py @@ -1,17 +1,3 @@ -from .reader import ResultRow - -# import logging -# from pprint import pformat - -# import re - -# import solo_turnier - -# from .types import HtmlCompetitionResultRow as CompetitionResult -# from . import types -# from . import competition_class - - class HtmlPerson: def __init__(self, name, id, group): self.name = name @@ -39,12 +25,6 @@ class ResultPerson: self.id = id self.group = group - @staticmethod - def extractFromResultRow(row: ResultRow): - return ResultPerson( - firstName=row.firstName, lastName=row.lastName, club=row.club - ) - def __eq__(self, o): if not isinstance(o, ResultPerson): return False diff --git a/src/solo_turnier/workers/DataWorker.py b/src/solo_turnier/workers/DataWorker.py index 5bf21f8..8c77d30 100644 --- a/src/solo_turnier/workers/DataWorker.py +++ b/src/solo_turnier/workers/DataWorker.py @@ -1,4 +1,3 @@ -from ..reader import ResultRow from ..worker import ResultPerson from ..types import HtmlCompetitionResultRow as CompetitionResult from solo_turnier import html_parser @@ -9,148 +8,6 @@ class DataWorker: def __init__(self): self.l = logging.getLogger("solo_turnier.worker.DataWorker") - def combineRowsByPerson( - self, rows: list[ResultRow] - ) -> dict[ResultPerson, list[CompetitionResult]]: - ret = {} - for row in rows: - result = CompetitionResult.extractFromResultRow(row) - - if result.place == "-" or result.placeTo == "-": - continue - - person = ResultPerson.extractFromResultRow(row) - if person not in ret: - ret[person] = [] - ret[person].append(result) - return ret - - def checkUniqueIds(self, data: dict[ResultPerson, list[CompetitionResult]]) -> bool: - unique = True - for person in data: - ids = set([c.id for c in data[person]]) - if len(ids) == 1: - person.id = list(ids)[0] - else: - unique = False - - return unique - - """ - Return a tuple - The first one is True, if all persons could be unambiguously identified a group - The second one is True if there was the need to override a group but it was possible to extract from other data - The second one can be seen as a warning - """ - - def consolidateGroups( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> tuple[bool, bool]: - ambiguous = False - warnChange = False - - unambiguousGroups = set(["Kin.", "Jun.", "Jug."]) - combinations = set(["Kin./Jun.", "Jun./Jug."]) - - for person in data: - groupsRaw = set([c.group for c in data[person]]) - - unknown = groupsRaw.difference(unambiguousGroups).difference(combinations) - if len(unknown) > 0: - raise Exception( - f"There were unknown groups found for {person}: {unknown}" - ) - - numUnambiguousGroups = len(groupsRaw.intersection(unambiguousGroups)) - - if numUnambiguousGroups == 0: - if len(groupsRaw) == 2: - warnChange = True - person.group = "Jun." - else: - ambiguous = True - if len(groupsRaw) == 1: - person.group = list(groupsRaw)[0] - - elif numUnambiguousGroups == 1: - if len(groupsRaw.intersection(combinations)) > 0: - warnChange = True - - person.group = list(groupsRaw.intersection(unambiguousGroups))[0] - - else: - raise Exception(f"{person} cannot have different groups.") - - return (not ambiguous, warnChange) - - def _createHtmlLUT(self, htmlImports: list[html_parser.HtmlImport]): - ret = {} - parser = html_parser.HtmlParser("") - for imp in htmlImports: - parsed = parser.guessDataFromHtmlTitle(imp.title) - key = (parsed["group"], parsed["class_"], parsed["dance"]) - ret[key] = imp - self.l.debug("LUT[%s] = %s", key, imp) - self.l.debug("LUT completed") - return ret - - def mergeHtmlData( - self, - data: dict[ResultPerson, list[CompetitionResult]], - htmlImports: list[html_parser.HtmlImport], - ): - lut = self._createHtmlLUT(htmlImports) - - for person in data: - for competition in data[person]: - key = ( - competition.competitionGroup, - competition.competitionClass, - competition.dance, - ) - htmlImport = lut[key] - participant = htmlImport.participants[str(competition.id)] - if participant.name != person.name: - self.l.error( - f"Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully." - ) - competition.finalist = participant.finalist - - def getAllDancesInCompetitions( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> list[str]: - allDances = [ - "Samba", - "Cha Cha", - "Rumba", - "Paso Doble", - "Jive", - "Langs. Walzer", - "Tango", - "Wiener Walzer", - "Slowfox", - "Quickstep", - ] - dancesPresent = {d: False for d in allDances} - - for person in data: - for competition in data[person]: - dancesPresent[competition.dance] = True - - return [d for d in allDances if dancesPresent[d]] - - def collectPersonsInGroups( - self, data: dict[ResultPerson, list[CompetitionResult]] - ) -> list[tuple[str, list[ResultPerson]]]: - groups = { - "Kin.": [p for p in data.keys() if p.group == "Kin."], - "Jun.": [p for p in data.keys() if p.group == "Jun."], - "Jug.": [p for p in data.keys() if p.group == "Jug."], - } - found = groups["Kin."] + groups["Jun."] + groups["Jug."] - groups["Sonst"] = [p for p in data.keys() if p not in found] - return groups - def sortPersonsInGroup(self, persons: list[ResultPerson]) -> list[ResultPerson]: ids = [p.id for p in persons] From 3ea5b745572db0979ef5a8cb3b3fea76d54677fb Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Mon, 20 Nov 2023 21:03:49 +0100 Subject: [PATCH 26/31] Make basic blocks obvious in logs --- src/solo_turnier/competition_class.py | 2 +- src/solo_turnier/group.py | 2 +- src/solo_turnier/types/place.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/solo_turnier/competition_class.py b/src/solo_turnier/competition_class.py index d24f0e1..52f68ec 100644 --- a/src/solo_turnier/competition_class.py +++ b/src/solo_turnier/competition_class.py @@ -6,7 +6,7 @@ class CompetitionClass: self.name = text def __repr__(self): - return self.name + return f"C({self.name})" class CombinedCompetitionClass: diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index 19c5153..ffd5b7b 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -6,7 +6,7 @@ class Group: self.name = text def __repr__(self): - return self.name + return f"G({self.name})" def getContainedGroups(self): return (self,) diff --git a/src/solo_turnier/types/place.py b/src/solo_turnier/types/place.py index 8480d99..63ffb33 100644 --- a/src/solo_turnier/types/place.py +++ b/src/solo_turnier/types/place.py @@ -3,8 +3,8 @@ class Place: self.place = place self.placeTo = placeTo - def __str__(self): + def __repr__(self): if self.placeTo is None: - return f"{self.place}." + return f"P({self.place}.)" - return f"{self.place}.-{self.placeTo}" + return f"P({self.place}.-{self.placeTo}.)" From 43180c6e054bf48f6c3db272af8399bf2509f89d Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Tue, 21 Nov 2023 20:14:12 +0100 Subject: [PATCH 27/31] First running version Not yet comparable with previous code base due to different outputs. --- src/solo_turnier/competition_class.py | 6 + src/solo_turnier/group.py | 6 + src/solo_turnier/html_parser.py | 37 ++- src/solo_turnier/output.py | 6 +- src/solo_turnier/types/__init__.py | 9 +- src/solo_turnier/types/competitionTuple.py | 27 ++ .../types/htmlCompetitionTotalResults.py | 11 +- src/solo_turnier/types/htmlParticipant.py | 2 +- src/solo_turnier/types/htmlPreviewImport.py | 18 -- .../types/htmlSingleCompetitionFixture.py | 17 ++ .../types/htmlSingleCompetitionResult.py | 11 +- src/solo_turnier/types/outputTable.py | 7 - src/solo_turnier/types/participant.py | 25 +- src/solo_turnier/types/participantResult.py | 23 -- src/solo_turnier/types/person.py | 3 + .../types/singleParticipantResult.py | 31 +-- src/solo_turnier/types/stages.py | 18 +- .../types/tableCompetitionEntry.py | 44 ---- src/solo_turnier/types/tableEntry.py | 9 - src/solo_turnier/types/tableRow.py | 16 -- src/solo_turnier/types/totalGroupResult.py | 6 +- src/solo_turnier/workers/ResultExtractor.py | 32 ++- src/solo_turnier/workers/Worker.py | 238 ++++++++++++++---- 23 files changed, 339 insertions(+), 263 deletions(-) create mode 100644 src/solo_turnier/types/competitionTuple.py delete mode 100644 src/solo_turnier/types/htmlPreviewImport.py create mode 100644 src/solo_turnier/types/htmlSingleCompetitionFixture.py delete mode 100644 src/solo_turnier/types/outputTable.py delete mode 100644 src/solo_turnier/types/participantResult.py create mode 100644 src/solo_turnier/types/person.py delete mode 100644 src/solo_turnier/types/tableCompetitionEntry.py delete mode 100644 src/solo_turnier/types/tableEntry.py delete mode 100644 src/solo_turnier/types/tableRow.py diff --git a/src/solo_turnier/competition_class.py b/src/solo_turnier/competition_class.py index 52f68ec..7411845 100644 --- a/src/solo_turnier/competition_class.py +++ b/src/solo_turnier/competition_class.py @@ -26,6 +26,12 @@ class CombinedCompetitionClass: else: return f"{self.clsA}/{self.clsB}/{self.clsC}" + def __eq__(self, other): + return type(self) == type(other) and self.__dict__ == other.__dict__ + + def __hash__(self): + return hash(("combinedClass", self.clsA, self.clsB, self.clsC)) + Class_t = CompetitionClass | CombinedCompetitionClass diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index ffd5b7b..4efc369 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -20,6 +20,12 @@ class CombinedGroup: def __repr__(self): return f"{self.clsA}/{self.clsB}" + def __hash__(self): + return hash(("combinedGroup", self.clsA, self.clsB)) + + def __eq__(self, other): + return type(self) == type(other) and self.__hash__() == other.__hash__() + def getContainedGroups(self): return (self.clsA, self.clsB) diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index 2f1c02d..81b04bf 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -3,10 +3,11 @@ from bs4 import BeautifulSoup import logging import re -from .types import HtmlPreviewParticipant, HtmlParticipant, HtmlResultTotalTable -from .types import HtmlPreviewImport as HtmlImport, HtmlResultImport +from .types import HtmlParticipant, HtmlResultTotalTable +from .types import HtmlResultImport from .group import GroupParser from .competition_class import CompetitionClassParser +import solo_turnier class IncompleteRoundException(Exception): @@ -45,11 +46,11 @@ class HtmlParser: return { "dance": dance.strip(), - "class_": str(self.classParser.parseClass(rawClass, True)), - "group": str(self.groupParser.parseGroup(rawGroup)), + "class_": self.classParser.parseClass(rawClass, True), + "group": self.groupParser.parseGroup(rawGroup), } - def parseResult(self): + def parseResult(self) -> HtmlResultImport: participants = {} def __parseRows(rows, finalist: bool): @@ -111,6 +112,8 @@ class HtmlParser: def parseIndividualResult(self, competitionGroup, competitionClass, dance): participants = {} + rePlaceParser = re.compile("([0-9]+)(?:-([0-9]+))?") + def __parseTable(table): rows = table.find_all("tr") @@ -148,7 +151,20 @@ class HtmlParser: rawStr = tag.contents[0].strip() if rawStr.endswith("-"): rawStr = rawStr[:-1] - return rawStr + + matcher = rePlaceParser.fullmatch(rawStr) + if matcher is None: + self.l.error( + "Could not parse place string '%s' to get fixture.", rawStr + ) + return None + + place = int(matcher.group(1)) + placeTo = matcher.group(2) + if placeTo is not None: + placeTo = int(placeTo) + + return solo_turnier.types.Place(place, placeTo) places = list(map(getSinglePlaceStr, placeTags)) return places @@ -180,8 +196,13 @@ class HtmlParser: cls = classes[idx] if classes is not None else None grp = groups[idx] if groups is not None else None - tup = (competitionGroup, competitionClass, dance, id) - participants[tup] = (places[idx], cls, grp) + tup = solo_turnier.types.CompetitionTuple( + competitionGroup, competitionClass, dance, int(id) + ) + fixture = solo_turnier.types.HtmlSingleCompetitionFixture( + place=places[idx], class_=cls, group=grp + ) + participants[tup] = fixture tables = self.soup.find("div", class_="extract").find_all("table") for table in tables: diff --git a/src/solo_turnier/output.py b/src/solo_turnier/output.py index 8d21f46..69816ce 100644 --- a/src/solo_turnier/output.py +++ b/src/solo_turnier/output.py @@ -87,8 +87,8 @@ class ConsoleOutputter(AbstractOutputter): if result is None: return "" - placeNative = getPlace(result.placeNative, result.placeNativeTo) - place = getPlace(result.place, result.placeTo) + placeNative = str(result.nativePlace) + place = str(result.place) lineOne = f"{placeNative} ({result.nativeClass})" lineTwo = f"[{place} in {result.competitionClass}]" @@ -106,7 +106,7 @@ class ConsoleOutputter(AbstractOutputter): print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) def output(self, data: types.State4): - for idx, group in enumerate(data.groups): + for idx, group in enumerate(data.results): if idx > 0: print() diff --git a/src/solo_turnier/types/__init__.py b/src/solo_turnier/types/__init__.py index 31e08e4..09f5ff1 100644 --- a/src/solo_turnier/types/__init__.py +++ b/src/solo_turnier/types/__init__.py @@ -1,20 +1,17 @@ from .place import Place +from .person import Person from .htmlPreviewParticipant import HtmlPreviewParticipant from .htmlParticipant import HtmlParticipant -from .htmlPreviewImport import HtmlPreviewImport from .htmlResultImport import HtmlResultImport from .htmlResultTotalTable import HtmlResultTotalTable from .htmlCompetitionResultRow import HtmlCompetitionResultRow +from .competitionTuple import CompetitionTuple from .htmlSingleCompetitionResult import HtmlSingleCompetitionResult +from .htmlSingleCompetitionFixture import HtmlSingleCompetitionFixture from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults from .singleParticipantResult import SingleParticipantResult from .totalGroupResult import TotalGroupResult from .participant import Participant -from .participantResult import ParticipantResult -from .tableCompetitionEntry import TableCompetitionEntry -from .tableEntry import TableEntry -from .tableRow import TableRow -from .outputTable import OutputTable from .stages import * diff --git a/src/solo_turnier/types/competitionTuple.py b/src/solo_turnier/types/competitionTuple.py new file mode 100644 index 0000000..947355f --- /dev/null +++ b/src/solo_turnier/types/competitionTuple.py @@ -0,0 +1,27 @@ +import solo_turnier + + +class CompetitionTuple: + def __init__( + self, + group: solo_turnier.group.Group_t, + class_: solo_turnier.competition_class.Class_t, + dance: str, + id: int, + ): + self.group = group + self.class_ = class_ + self.dance = dance + self.id = id + + def __hash__(self): + return hash(("Tuple", self.group, self.class_, self.dance, self.id)) + + def __repr__(self): + return f"T({self.group},{self.class_},{self.dance},{self.id})" + + def __eq__(self, other): + if type(other) != type(self): + return False + + return self.__hash__() == other.__hash__() diff --git a/src/solo_turnier/types/htmlCompetitionTotalResults.py b/src/solo_turnier/types/htmlCompetitionTotalResults.py index e6d5ea1..fb46b0e 100644 --- a/src/solo_turnier/types/htmlCompetitionTotalResults.py +++ b/src/solo_turnier/types/htmlCompetitionTotalResults.py @@ -1,11 +1,12 @@ import solo_turnier from .htmlSingleCompetitionResult import HtmlSingleCompetitionResult +from .competitionTuple import CompetitionTuple class HtmlCompetitionTotalResults: def __init__(self): self.results = {} - self.tabges = {} + self.fixups = {} def __getTuple( self, @@ -14,7 +15,7 @@ class HtmlCompetitionTotalResults: dance: str, id: int, ): - return (group, class_, dance, id) + return CompetitionTuple(group, class_, dance, id) def get( self, @@ -34,16 +35,16 @@ class HtmlCompetitionTotalResults: ret = {} for k in self.results: - if int(k[3]) != id: + if int(k.id) != id: continue # ret = ret + self.results[k] # Dance, Group, Class - key = (k[2], k[0], k[1]) + key = (k.dance, k.group, k.class_) ret[key] = self.results[k] return ret - def add(self, group, class_, dance, id, result: HtmlSingleCompetitionResult): + def add(self, group, class_, dance, id: int, result: HtmlSingleCompetitionResult): tup = self.__getTuple(group, class_, dance, id) l = self.results.get(tup, []) l.append(result) diff --git a/src/solo_turnier/types/htmlParticipant.py b/src/solo_turnier/types/htmlParticipant.py index a0be30c..8502d6e 100644 --- a/src/solo_turnier/types/htmlParticipant.py +++ b/src/solo_turnier/types/htmlParticipant.py @@ -5,7 +5,7 @@ class HtmlParticipant: self.finalist = None def __eq__(self, o): - if type(o) != HtmlPreviewParticipant: + if type(o) != HtmlParticipant: return False return all( diff --git a/src/solo_turnier/types/htmlPreviewImport.py b/src/solo_turnier/types/htmlPreviewImport.py deleted file mode 100644 index 36405ed..0000000 --- a/src/solo_turnier/types/htmlPreviewImport.py +++ /dev/null @@ -1,18 +0,0 @@ -from .htmlPreviewParticipant import HtmlPreviewParticipant -import solo_turnier - - -class HtmlPreviewImport: - def __init__( - self, - participants: dict[int, list[HtmlPreviewParticipant]], - results: dict[ - HtmlPreviewParticipant, - dict[str, solo_turnier.competition_class.CompetitionClass], - ], - ): - self.participants = participants - self.results = results - - def __repr__(self): - return (str(self.participants), str(self.results)) diff --git a/src/solo_turnier/types/htmlSingleCompetitionFixture.py b/src/solo_turnier/types/htmlSingleCompetitionFixture.py new file mode 100644 index 0000000..fbd2334 --- /dev/null +++ b/src/solo_turnier/types/htmlSingleCompetitionFixture.py @@ -0,0 +1,17 @@ +from .place import Place +import solo_turnier + + +class HtmlSingleCompetitionFixture: + def __init__( + self, + place: Place, + group: solo_turnier.group.Group, + class_: solo_turnier.competition_class.CompetitionClass, + ): + self.place = place + self.group = group + self.class_ = class_ + + def __repr__(self): + return f"Fix({self.place},{self.group},{self.class_})" diff --git a/src/solo_turnier/types/htmlSingleCompetitionResult.py b/src/solo_turnier/types/htmlSingleCompetitionResult.py index 916748b..175aca0 100644 --- a/src/solo_turnier/types/htmlSingleCompetitionResult.py +++ b/src/solo_turnier/types/htmlSingleCompetitionResult.py @@ -1,15 +1,14 @@ +from .place import Place + + class HtmlSingleCompetitionResult: - def __init__(self, name, place, placeTo, finalist): + def __init__(self, name: str, place: Place, finalist: bool): self.name = name self.place = place - self.placeTo = placeTo self.finalist = finalist def __repr__(self): - if self.placeTo is None: - place = self.place - else: - place = f"{self.place}-{self.placeTo}" + place = self.place if self.finalist: return f"Res({self.name} [F], placed {place})" diff --git a/src/solo_turnier/types/outputTable.py b/src/solo_turnier/types/outputTable.py deleted file mode 100644 index 8eebe01..0000000 --- a/src/solo_turnier/types/outputTable.py +++ /dev/null @@ -1,7 +0,0 @@ -from .tableRow import TableRow - - -class OutputTable: - def __init__(self, dances: list[str], rows: list[TableRow]): - self.dances = dances - self.rows = rows diff --git a/src/solo_turnier/types/participant.py b/src/solo_turnier/types/participant.py index f0cfd65..c69fece 100644 --- a/src/solo_turnier/types/participant.py +++ b/src/solo_turnier/types/participant.py @@ -1,17 +1,20 @@ import solo_turnier +from .person import Person -class Participant: + +class Participant(Person): def __init__( self, - firstName: str, - lastName: str, - club: str, - group: solo_turnier.group.Group, - class_: solo_turnier.competition_class.CompetitionClass, + name: str, + id: int, + finalist: bool = None, ): - self.firstName = firstName - self.lastName = lastName - self.club = club - self.group = group - self.class_ = class_ + super().__init__(name) + self.id = id + self.finalist = finalist + + def __repr__(self): + if self.finalist == True: + return f"Part({self.id} {self.name},F)" + return f"Part({self.id} {self.name})" diff --git a/src/solo_turnier/types/participantResult.py b/src/solo_turnier/types/participantResult.py deleted file mode 100644 index aeb684e..0000000 --- a/src/solo_turnier/types/participantResult.py +++ /dev/null @@ -1,23 +0,0 @@ -import solo_turnier - - -class ParticipantResult: - def __init__( - self, - id: int, - finalist: bool, - cancelled: bool, - group: solo_turnier.group.Group_t, - class_: solo_turnier.competition_class.Class_t, - dance: str, - place, - placeTo, - ): - self.id = id - self.finalist = finalist - self.cancelled = cancelled - self.group = group - self.class_ = class_ - self.dance = dance - self.place = place - self.placeTo = placeTo diff --git a/src/solo_turnier/types/person.py b/src/solo_turnier/types/person.py new file mode 100644 index 0000000..5c7b20e --- /dev/null +++ b/src/solo_turnier/types/person.py @@ -0,0 +1,3 @@ +class Person: + def __init__(self, name: str): + self.name = name diff --git a/src/solo_turnier/types/singleParticipantResult.py b/src/solo_turnier/types/singleParticipantResult.py index b280c83..bf548cf 100644 --- a/src/solo_turnier/types/singleParticipantResult.py +++ b/src/solo_turnier/types/singleParticipantResult.py @@ -1,5 +1,7 @@ import solo_turnier +from .place import Place + class SingleParticipantResult: def __init__( @@ -8,38 +10,17 @@ class SingleParticipantResult: nativeClass: solo_turnier.competition_class.CompetitionClass, dance: str, finalist: bool, - place: int, - placeTo: int | None, + place: Place, + nativePlace: Place = None, ): self.competitionClass = competitionClass self.nativeClass = nativeClass self.dance = dance self.finalist = finalist self.place = place - self.placeTo = placeTo - - if placeTo == place: - self.placeTo = None - - self.placeNative = None - self.placeNativeTo = None + self.nativePlace = nativePlace def __repr__(self): asFinalist = " as finalist" if self.finalist else "" - if self.placeTo is None: - return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" - - return f"SR[{self.place}-{self.placeTo} in {self.dance} {self.competitionClass} ({self.placeNative}-{self.placeNativeTo}, {self.nativeClass}){asFinalist}]" - - def getPlace(self): - if self.placeTo is None: - return f"{self.place}." - else: - return f"{self.place}.-{self.placeTo}." - - def getNativePlace(self): - if self.placeNativeTo is None: - return f"{self.placeNative}." - else: - return f"{self.placeNative}.-{self.placeNativeTo}." + return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.nativePlace} {self.nativeClass}){asFinalist}]" diff --git a/src/solo_turnier/types/stages.py b/src/solo_turnier/types/stages.py index d834050..301c11d 100644 --- a/src/solo_turnier/types/stages.py +++ b/src/solo_turnier/types/stages.py @@ -2,14 +2,11 @@ import solo_turnier from .totalGroupResult import TotalGroupResult from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults -from .participant import Participant -from .participantResult import ParticipantResult -from .outputTable import OutputTable class State4: def __init__( - self, resultPerGroup: dict[solo_turnier.group.Group, TotalGroupResult] + self, resultPerGroup: dict[solo_turnier.group.Group | None, TotalGroupResult] ): parser = solo_turnier.group.GroupParser() self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) @@ -19,16 +16,3 @@ class State4: class State3: def __init__(self, htmlResults: HtmlCompetitionTotalResults): self.htmlResults = htmlResults - - -class Stage2: - def __init__(self, results: dict[Participant, list[ParticipantResult]]): - self.results = results - - -class Stage1: - def __init__( - self, - tables: dict[solo_turnier.group.Group, OutputTable], - ): - self.tables = tables diff --git a/src/solo_turnier/types/tableCompetitionEntry.py b/src/solo_turnier/types/tableCompetitionEntry.py deleted file mode 100644 index f7e037d..0000000 --- a/src/solo_turnier/types/tableCompetitionEntry.py +++ /dev/null @@ -1,44 +0,0 @@ -import solo_turnier - - -class TableCompetitionEntry: - def __init__( - self, - cancelled: bool, - finalist: bool, - class_: solo_turnier.competition_class.Class_t, - place: int = -1, - placeTo: int = -1, - group: solo_turnier.group.Group_t = None, - id: int = None, - ): - self.finalist = finalist - self.cancelled = cancelled - self.group = group - self.class_ = class_ - self.place = place - self.placeTo = placeTo - - def __repr__(self): - def paramMerging(l): - return ", ".join(filter(lambda x: x is not None, l)) - - if self.cancelled: - params = paramMerging([self.group, self.class_, self.id]) - if len(params) > 0: - return f"- ({params})" - else: - return "-" - elif not self.finalist: - params = paramMerging([self.group, self.class_, self.id]) - if len(params) > 0: - return f"x ({params})" - else: - return "x" - else: - if self.place == self.placeTo: - place = f"{self.place}." - else: - place = f"{self.place}.-{self.placeTo}." - params = paramMerging([self.group, self.class_, self.id]) - return f"{place} ({params})" diff --git a/src/solo_turnier/types/tableEntry.py b/src/solo_turnier/types/tableEntry.py deleted file mode 100644 index 1a9334f..0000000 --- a/src/solo_turnier/types/tableEntry.py +++ /dev/null @@ -1,9 +0,0 @@ -from .tableCompetitionEntry import TableCompetitionEntry - - -class TableEntry: - def __init__(self, competitions: list[TableCompetitionEntry]): - self.competitions = competitions - - def __repr__(self): - return ", ".join(self.competitions) diff --git a/src/solo_turnier/types/tableRow.py b/src/solo_turnier/types/tableRow.py deleted file mode 100644 index 427bf49..0000000 --- a/src/solo_turnier/types/tableRow.py +++ /dev/null @@ -1,16 +0,0 @@ -from .participant import Participant -from .tableEntry import TableEntry - - -class TableRow: - def __init__(self, participant: Participant, id: int, entries: list[TableEntry]): - self.participant = participant - self.id = id - self.entries = entries - - def getRowList(self): - if self.id is not None: - first = f"{self.id}. {self.participant.firstName} {self.participant.lastName} ({self.participant.club})" - else: - first = f"{self.participant.firstName} {self.participant.lastName} ({self.participant.club})" - return [first] + map(str, self.entries) diff --git a/src/solo_turnier/types/totalGroupResult.py b/src/solo_turnier/types/totalGroupResult.py index 58f811a..ac1e6c7 100644 --- a/src/solo_turnier/types/totalGroupResult.py +++ b/src/solo_turnier/types/totalGroupResult.py @@ -1,15 +1,15 @@ -from .htmlPreviewParticipant import HtmlPreviewParticipant from .singleParticipantResult import SingleParticipantResult +from .participant import Participant class TotalGroupResult: def __init__( self, dances: list[str], - results: dict[HtmlPreviewParticipant, list[SingleParticipantResult]], + results: dict[Participant, list[SingleParticipantResult]], ): self.dances = dances self.results = results def __repr__(self): - return f"TotalGroupResult({self.dances}, {self.results})" + return f"TotalGrR({self.dances}, {self.results})" diff --git a/src/solo_turnier/workers/ResultExtractor.py b/src/solo_turnier/workers/ResultExtractor.py index 54dc0e9..507794d 100644 --- a/src/solo_turnier/workers/ResultExtractor.py +++ b/src/solo_turnier/workers/ResultExtractor.py @@ -38,13 +38,7 @@ class ResultExtractor: ) continue - try: - guessedClass = classParser.parseClass(data["class_"]) - except: - self.l.error( - "Issue parsing class of file %s. Check manually.", filePair[0] - ) - continue + guessedClass = data["class_"] self.l.debug( "Fetched result data: %s, guessed class %s", data, guessedClass @@ -82,18 +76,20 @@ class ResultExtractor: for person in result.results.keys(): placeStr = result.results[person] - placeObj = self._extractPlace(placeStr) - place = placeObj.place - placeTo = placeObj.placeTo + place = self._extractPlace(placeStr) competitionResult = types.HtmlSingleCompetitionResult( - person.name, place, placeTo, person.finalist + person.name, place, person.finalist ) results.add( - competitionGroup, competitionClass, dance, person.id, competitionResult + competitionGroup, + competitionClass, + dance, + int(person.id), + competitionResult, ) # - def _analyzeIndividualResults( + def _analyzeResultFixups( self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults ): data = parser.guessDataFromHtmlTitle() @@ -101,9 +97,11 @@ class ResultExtractor: competitionGroup = data["group"] dance = data["dance"] - result = parser.parseIndividualResult(competitionGroup, competitionClass, dance) - self.l.log(5, "Found individual results: %s", result.participants) - results.tabges.update(result.participants) + resultFixups = parser.parseIndividualResult( + competitionGroup, competitionClass, dance + ) + self.l.log(5, "Found additional result fixups: %s", resultFixups.participants) + results.fixups.update(resultFixups.participants) def extractAllData( self, parsers: ParserList_t @@ -124,6 +122,6 @@ class ResultExtractor: "Fetching individual result of combined competitions in %s", fileName, ) - self._analyzeIndividualResults(parsers[fileNameTuple][1], ret) + self._analyzeResultFixups(parsers[fileNameTuple][1], ret) return ret diff --git a/src/solo_turnier/workers/Worker.py b/src/solo_turnier/workers/Worker.py index 11e8bfe..cb7b5e3 100644 --- a/src/solo_turnier/workers/Worker.py +++ b/src/solo_turnier/workers/Worker.py @@ -18,12 +18,16 @@ class Worker: "Quickstep", ] self._groupParser = solo_turnier.group.GroupParser() + self._classParser = solo_turnier.competition_class.CompetitionClassParser() def collectAllData(self, htmlResultsFileNames: list[str]) -> types.State3: resultExtractor = ResultExtractor() resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames) htmlResults = resultExtractor.extractAllData(resultParsers) - self.l.debug("Overall result data extracted: %s", pformat(htmlResults.results)) + self.l.log(5, "Overall result data extracted: %s", pformat(htmlResults.results)) + self.l.log( + 5, "Overall result fixups extracted: %s", pformat(htmlResults.fixups) + ) return types.State3(htmlResults) @@ -34,15 +38,92 @@ class Worker: groupMapping = self._getGroupMapping(importedData) self.l.log(5, "ID-to-group mapping of the parsed data: %s", str(groupMapping)) - # groups = self._extractGroups(importedData) groups = self._extractGroupsFromGroupMapping(groupMapping) self.l.debug("Found groups in the dataset: %s", groups) invertedGroupMapping = self._invertGroupMapping(groupMapping, groups) self.l.log(5, "Inverted group maping: %s", invertedGroupMapping) + idToParticipantMapping = self._invertIdMapping(importedData.htmlResults) + self.l.log(5, "Id to participant mappting: %s", idToParticipantMapping) + totalResult = {} + + for group in invertedGroupMapping: + self.l.debug("Collecting data for group %s", group) + + participants = invertedGroupMapping[group] + self.l.log(5, "Participants in group: %s", participants) + + tuplesInCurrentGroup = [] + + for participantId in participants: + tuplesInCurrentGroup.extend( + self._filterResultKeys(importedData.htmlResults, id=participantId) + ) + self.l.log( + 5, + "Tuples of filtered in group %s: %s", + group, + list(tuplesInCurrentGroup), + ) + + dancesInGroup = self._extractAllDancesFromTuples(tuplesInCurrentGroup) + self.l.debug("Found dances in group %s: %s", group, dancesInGroup) + + resultsInCurrentGroup = {} + + for participantId in participants: + self.l.log(5, "Handling participant with ID %d", participantId) + # tuples = self._filterResultKeys(im) + participant = idToParticipantMapping[participantId] + self.l.log(5, "Participant in question: %s", participant) + + participant.finalist = False + + resultsInCurrentGroup[participant] = [] + + for tup in self._filterResultKeys( + importedData.htmlResults, id=participantId + ): + singleHtmlResultList = importedData.htmlResults.results[tup] + if len(singleHtmlResultList) > 1: + self.l.warning( + "More than one result per tuple (%s) found.", tup + ) + + singleHtmlResult = singleHtmlResultList[0] + singleResult = solo_turnier.types.SingleParticipantResult( + competitionClass=tup.class_, + nativeClass=tup.class_, + dance=tup.dance, + finalist=singleHtmlResult.finalist, + place=singleHtmlResult.place, + nativePlace=singleHtmlResult.place, + ) + + if tup in importedData.htmlResults.fixups: + fixup = importedData.htmlResults.fixups[tup] + self.l.log( + 5, "Fixture found for %s, %s: %s", participant, tup, fixup + ) + self._applyFixture(singleResult, fixup) + + resultsInCurrentGroup[participant].append(singleResult) + + if singleHtmlResult.finalist: + participant.finalist = True + + ########################################################################################################################### + + totalGroupResult = types.TotalGroupResult( + dancesInGroup, resultsInCurrentGroup + ) + self.l.log(5, "Total group result of group %s: %s", group, totalGroupResult) + totalResult[group] = totalGroupResult + ret = types.State4(totalResult) + return ret for group in groups: self.l.debug("Collecting data for total result of group %s", group) @@ -80,15 +161,9 @@ class Worker: def _extractGroups(self, data: types.State3): groupSet = set([]) - # for id in data.previewImport.participants: - # participants = data.previewImport.participants[id] - # for participant in participants: - # groupSet.add(participant.group) for tup in data.htmlResults.results.keys(): gr = self._groupParser.parseGroup(tup[0]) - # groupSet.add(gr) groupSet.update(gr.getContainedGroups()) - # self.l.log(5, 'Group type %s', type(gr)) self.l.log(5, "Set of active groups: %s", groupSet) groups = self._groupParser.getGroupsAsSortedList(groupSet) @@ -124,8 +199,9 @@ class Worker: if counts[candidates[0]] > counts[candidates[1]]: if candidates[0] is None: self.l.error( - "Majority of guessed groups is ambigous. Guessing failed for id %d. Falling back to second best guess.", + "Majority of guessed groups is ambiguous. Guessing failed for id %d. Falling back to second best guess %s.", id, + candidates[1], ) return candidates[1] @@ -137,20 +213,22 @@ class Worker: groupsPerId = {} for tup in importedData.htmlResults.results: - competitionGroup = self._groupParser.parseGroup(tup[0]) - fixture = importedData.htmlResults.tabges.get(tup, (None, None, None)) - id = int(tup[3]) - if fixture[2] is not None: - group = self._groupParser.parseGroup(fixture[2]) + competitionGroup = tup.group + fixture = importedData.htmlResults.fixups.get( + tup, solo_turnier.types.HtmlSingleCompetitionFixture(None, None, None) + ) + id = tup.id + if fixture.group is not None: + group = fixture.group else: containedGroups = competitionGroup.getContainedGroups() if len(containedGroups) > 1: self.l.error( "The group for participant %d is ambiguous in (%s %s %s).", id, - tup[0], - tup[1], - tup[2], + tup.group, + tup.class_, + tup.dance, ) group = containedGroups else: @@ -193,8 +271,95 @@ class Worker: ret[group] = [] for id in mapping: ret[mapping[id]].append(id) + for key in ret: + ret[key].sort() return ret + def _filterResultKeys( + self, + results: solo_turnier.types.HtmlCompetitionTotalResults, + group: solo_turnier.group.Group_t | None = None, + class_: solo_turnier.competition_class.Class_t | None = None, + dance: str | None = None, + id: int | None = None, + ): + def checker(x: solo_turnier.types.CompetitionTuple) -> bool: + if group is not None and group != x.group: + return False + if class_ is not None and class_ != x.class_: + return False + if dance is not None and dance != x.dance: + return False + if id is not None and id != x.id: + return False + return True + + return filter(checker, results.results.keys()) + + def _extractAllDancesFromTuples( + self, tuples: list[solo_turnier.types.CompetitionTuple] + ) -> list[str]: + danceSet = set() + danceSet.update(map(lambda x: x.dance, tuples)) + + # Check for unknown dances here + setDiff = danceSet.difference(self._allDances) + if len(setDiff) > 0: + self.l.warning( + "There are dances in the data set that are not known in the program. A bug?" + ) + return [x for x in self._allDances if x in danceSet] + list(setDiff) + + def _invertIdMapping( + self, htmlData: solo_turnier.types.HtmlCompetitionTotalResults + ): + mapping = {} + for tup in htmlData.results: + id = tup.id + results = htmlData.results[tup] + if len(results) > 1: + self.l.error( + "Non-unique results for tuple %s were found. Most probably this is a bug. The results are %s.", + tup, + results, + ) + elif len(results) == 0: + self.l.error("No results for tuple %s found.", tup) + continue + + if id not in mapping: + mapping[id] = solo_turnier.types.Participant( + name=results[0].name, id=id + ) + else: + if mapping[id].name != results[0].name or mapping[id].id != id: + self.l.error( + "Invalid id to participant mapping found. The name of id has changed. Tuple was %s (values %s), mapping was %s", + tup, + results, + mapping[id], + ) + return mapping + + def _filterResultsById( + self, data: solo_turnier.types.HtmlCompetitionTotalResults, ids: list[int] + ): + ret = {} + + return ret + + def _applyFixture( + self, + singleResult: solo_turnier.types.SingleParticipantResult, + fixture: solo_turnier.types.HtmlSingleCompetitionFixture, + ): + singleResult.nativePlace = fixture.place + + if fixture.class_ is not None: + singleResult.nativeClass = self._classParser.parseAbbreviatedClass( + fixture.class_ + ) + def _extractDancesPerGroup( self, data: types.State3, group: solo_turnier.group.Group ): @@ -365,34 +530,19 @@ class Worker: pass def filterOutFinalists(self, data: types.State4, filterOut: bool): - for group in data.results: - self.l.debug("Cleaning up group %s", group.name) - participants = data.results[group].results.keys() - droppedParticipants = [] + if filterOut: + for group in data.results: + groupName = "unknown" if group is None else group.name + self.l.debug("Cleaning up group %s", groupName) + participants = data.results[group].results.keys() + droppedParticipants = [] - for participant in participants: - self.l.debug("Checking %s", participant) + for participant in participants: + if participant.finalist == False: + self.l.info( + "Dropping %s from the output as no finalist", participant + ) + droppedParticipants.append(participant) - def isFinalistInDance(x: types.HtmlSingleCompetitionResult | None): - if x is None: - return False - return x.finalist - - mapped = list( - map(isFinalistInDance, data.results[group].results[participant]) - ) - finalist = True in mapped - self.l.log(5, "Check for finalist (in dances %s): %s", mapped, finalist) - - if finalist: - participant.finalist = True - else: - participant.finalist = False - self.l.info( - "Dropping %s from the output as no finalist", participant - ) - droppedParticipants.append(participant) - - if filterOut: for droppedParticipant in droppedParticipants: data.results[group].results.pop(droppedParticipant) From abf4974d405119c2e85b2be7ab3d214b582e7495 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Wed, 22 Nov 2023 15:49:45 +0100 Subject: [PATCH 28/31] Reduce the changes to make the tests not break due to optical changes --- src/solo_turnier/competition_class.py | 2 +- src/solo_turnier/group.py | 2 +- src/solo_turnier/types/place.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solo_turnier/competition_class.py b/src/solo_turnier/competition_class.py index 7411845..019baae 100644 --- a/src/solo_turnier/competition_class.py +++ b/src/solo_turnier/competition_class.py @@ -6,7 +6,7 @@ class CompetitionClass: self.name = text def __repr__(self): - return f"C({self.name})" + return f"{self.name}" class CombinedCompetitionClass: diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index 4efc369..830b3ba 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -6,7 +6,7 @@ class Group: self.name = text def __repr__(self): - return f"G({self.name})" + return f"{self.name}" def getContainedGroups(self): return (self,) diff --git a/src/solo_turnier/types/place.py b/src/solo_turnier/types/place.py index 63ffb33..60621ef 100644 --- a/src/solo_turnier/types/place.py +++ b/src/solo_turnier/types/place.py @@ -5,6 +5,6 @@ class Place: def __repr__(self): if self.placeTo is None: - return f"P({self.place}.)" + return f"{self.place}." - return f"P({self.place}.-{self.placeTo}.)" + return f"{self.place}.-{self.placeTo}." From c690ce755ab8e9905916820492a0d973960d6537 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Wed, 22 Nov 2023 16:33:02 +0100 Subject: [PATCH 29/31] Fixed bug with pure string group --- src/solo_turnier/html_parser.py | 16 +++++++++++++--- src/solo_turnier/output.py | 23 +++++++++++++++++++++++ src/solo_turnier/types/participant.py | 3 +++ src/solo_turnier/workers/Worker.py | 4 +--- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/solo_turnier/html_parser.py b/src/solo_turnier/html_parser.py index 81b04bf..53077a7 100644 --- a/src/solo_turnier/html_parser.py +++ b/src/solo_turnier/html_parser.py @@ -114,6 +114,9 @@ class HtmlParser: rePlaceParser = re.compile("([0-9]+)(?:-([0-9]+))?") + groupParser = solo_turnier.group.GroupParser() + classParser = solo_turnier.competition_class.CompetitionClassParser() + def __parseTable(table): rows = table.find_all("tr") @@ -176,7 +179,12 @@ class HtmlParser: classRow = findRowIndex("Startklasse") if classRow is not None: classTags = rows[classRow]("td")[1 : (numIds + 1)] - return list(map(lambda x: x.contents[0], classTags)) + return list( + map( + lambda x: classParser.parseAbbreviatedClass(x.contents[0]), + classTags, + ) + ) return None classes = getClass() @@ -185,8 +193,10 @@ class HtmlParser: def getGroups(): groupRow = findRowIndex("Startgruppe") if groupRow is not None: - classTags = rows[groupRow]("td")[1 : (numIds + 1)] - return list(map(lambda x: x.contents[0], classTags)) + groupTags = rows[groupRow]("td")[1 : (numIds + 1)] + return list( + map(lambda x: groupParser.parseGroup(x.contents[0]), groupTags) + ) return None groups = getGroups() diff --git a/src/solo_turnier/output.py b/src/solo_turnier/output.py index 69816ce..02daaa3 100644 --- a/src/solo_turnier/output.py +++ b/src/solo_turnier/output.py @@ -65,6 +65,26 @@ class ConsoleOutputter(AbstractOutputter): print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) print() + def _reshapeRow( + self, + results: list[solo_turnier.types.SingleParticipantResult], + dances: list[str], + ) -> list[solo_turnier.types.SingleParticipantResult]: + ret = [None for x in dances] + + for result in results: + if result.dance not in dances: + self.l.error( + "Result in unknown dance found in table. This is a bug. (%s)", + result, + ) + continue + + idx = dances.index(result.dance) + ret[idx] = result + + return ret + def _outputGroup( self, group: solo_turnier.group.Group, groupResults: types.TotalGroupResult ): @@ -76,6 +96,9 @@ class ConsoleOutputter(AbstractOutputter): for participant in participants: results = groupResults.results[participant] + results = self._reshapeRow(results, groupResults.dances) + + self.l.log(5, "Results of %s: %s", participant, results) def mapResultColumn(result: types.SingleParticipantResult): def getPlace(place, placeTo): diff --git a/src/solo_turnier/types/participant.py b/src/solo_turnier/types/participant.py index c69fece..eacbbce 100644 --- a/src/solo_turnier/types/participant.py +++ b/src/solo_turnier/types/participant.py @@ -18,3 +18,6 @@ class Participant(Person): if self.finalist == True: return f"Part({self.id} {self.name},F)" return f"Part({self.id} {self.name})" + + def __ge__(self, other): + return self.id >= other.id diff --git a/src/solo_turnier/workers/Worker.py b/src/solo_turnier/workers/Worker.py index cb7b5e3..8cf04c9 100644 --- a/src/solo_turnier/workers/Worker.py +++ b/src/solo_turnier/workers/Worker.py @@ -356,9 +356,7 @@ class Worker: singleResult.nativePlace = fixture.place if fixture.class_ is not None: - singleResult.nativeClass = self._classParser.parseAbbreviatedClass( - fixture.class_ - ) + singleResult.nativeClass = fixture.class_ def _extractDancesPerGroup( self, data: types.State3, group: solo_turnier.group.Group From f944aabc38f25d558e8af2299272fe4d8ad982f6 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Wed, 22 Nov 2023 18:52:25 +0100 Subject: [PATCH 30/31] Outsource the table shaping for use with flask --- src/solo_turnier/batch.py | 5 +- src/solo_turnier/output.py | 20 ++++--- src/solo_turnier/templates/index.html | 9 ++- src/solo_turnier/types/__init__.py | 1 + src/solo_turnier/types/participant.py | 4 +- .../types/singleParticipantResult.py | 3 + src/solo_turnier/types/stages.py | 17 +++++- src/solo_turnier/types/tableData.py | 11 ++++ src/solo_turnier/workers/OutputShaper.py | 58 +++++++++++++++++++ src/solo_turnier/workers/__init__.py | 1 + 10 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 src/solo_turnier/types/tableData.py create mode 100644 src/solo_turnier/workers/OutputShaper.py diff --git a/src/solo_turnier/batch.py b/src/solo_turnier/batch.py index f942763..428bc73 100644 --- a/src/solo_turnier/batch.py +++ b/src/solo_turnier/batch.py @@ -31,4 +31,7 @@ class BatchWorker: worker.filterOutFinalists(combinedData, removeFilteredParicipants) - return combinedData + outputShaper = solo_turnier.workers.OutputShaper.OutputShaper() + shapedData = outputShaper.shapeResults(combinedData) + + return shapedData diff --git a/src/solo_turnier/output.py b/src/solo_turnier/output.py index 02daaa3..8be659c 100644 --- a/src/solo_turnier/output.py +++ b/src/solo_turnier/output.py @@ -86,17 +86,21 @@ class ConsoleOutputter(AbstractOutputter): return ret def _outputGroup( - self, group: solo_turnier.group.Group, groupResults: types.TotalGroupResult + self, + group: solo_turnier.group.Group, + groupResults: solo_turnier.types.GroupTableData, ): - print(f"Einzeltanzwettbewerb der Gruppe {group}") + if group is not None: + print(f"Einzeltanzwettbewerb der Gruppe {group}") + else: + print("Einzeltanzwettbewerbe ohne eindeutige Gruppenzuordnung") tableData = [["Tanz"] + groupResults.dances] - participants = list(groupResults.results.keys()) + participants = list(groupResults.resultsInGroup.keys()) participants.sort(key=lambda x: (x.id, x.name)) for participant in participants: - results = groupResults.results[participant] - results = self._reshapeRow(results, groupResults.dances) + results = groupResults.resultsInGroup[participant] self.l.log(5, "Results of %s: %s", participant, results) @@ -128,14 +132,14 @@ class ConsoleOutputter(AbstractOutputter): self.l.log(5, "table data: %s", pprint.pformat(tableData)) print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) - def output(self, data: types.State4): - for idx, group in enumerate(data.results): + def output(self, data: types.Stage5): + for idx, group in enumerate(data.resultsPerGroup): if idx > 0: print() self.l.debug("Output for group %s", group) - self._outputGroup(group, data.results[group]) + self._outputGroup(group, data.resultsPerGroup[group]) # self.groups = self.worker.collectPersonsInGroups(data) # self.dances = self.worker.getAllDancesInCompetitions(data) diff --git a/src/solo_turnier/templates/index.html b/src/solo_turnier/templates/index.html index 67f77bb..a62df9a 100644 --- a/src/solo_turnier/templates/index.html +++ b/src/solo_turnier/templates/index.html @@ -13,7 +13,11 @@ {% for group in data.groups %} {% block groupBlk scoped %}
+ {% if group is none %} +

Auswertung ohne eindeutige Gruppe

+ {% else %}

Auswertung Gruppe {{ group.name }}

+ {% endif %} @@ -39,9 +43,10 @@ {% if not participant.finalist %} Kein/e Finalist/in {% endif %} - {{ res.getNativePlace() }} ({{ res.nativeClass }})
+ {{ res.getNativePlace() }} + ({{ res.nativeClass }})
- {{ res.getPlace() }} in {{ res.competitionClass }} + {{ res.place }} in {{ res.competitionClass }} {% endif %} diff --git a/src/solo_turnier/types/__init__.py b/src/solo_turnier/types/__init__.py index 09f5ff1..b1aa540 100644 --- a/src/solo_turnier/types/__init__.py +++ b/src/solo_turnier/types/__init__.py @@ -13,5 +13,6 @@ from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults from .singleParticipantResult import SingleParticipantResult from .totalGroupResult import TotalGroupResult from .participant import Participant +from .tableData import * from .stages import * diff --git a/src/solo_turnier/types/participant.py b/src/solo_turnier/types/participant.py index eacbbce..8c6efd8 100644 --- a/src/solo_turnier/types/participant.py +++ b/src/solo_turnier/types/participant.py @@ -19,5 +19,5 @@ class Participant(Person): return f"Part({self.id} {self.name},F)" return f"Part({self.id} {self.name})" - def __ge__(self, other): - return self.id >= other.id + def __gt__(self, other): + return self.id > other.id diff --git a/src/solo_turnier/types/singleParticipantResult.py b/src/solo_turnier/types/singleParticipantResult.py index bf548cf..969fe88 100644 --- a/src/solo_turnier/types/singleParticipantResult.py +++ b/src/solo_turnier/types/singleParticipantResult.py @@ -24,3 +24,6 @@ class SingleParticipantResult: asFinalist = " as finalist" if self.finalist else "" return f"SR[{self.place} in {self.dance} {self.competitionClass} ({self.nativePlace} {self.nativeClass}){asFinalist}]" + + def getNativePlace(self) -> str: + return str(self.nativePlace) diff --git a/src/solo_turnier/types/stages.py b/src/solo_turnier/types/stages.py index 301c11d..dbad5d2 100644 --- a/src/solo_turnier/types/stages.py +++ b/src/solo_turnier/types/stages.py @@ -2,12 +2,23 @@ import solo_turnier from .totalGroupResult import TotalGroupResult from .htmlCompetitionTotalResults import HtmlCompetitionTotalResults +from .singleParticipantResult import SingleParticipantResult +from .participant import Participant +from .tableData import GroupTableData + +NullableGroup = solo_turnier.group.Group | None + + +class Stage5: + def __init__( + self, + resultsPerGroup: dict[NullableGroup, GroupTableData], + ): + self.resultsPerGroup = resultsPerGroup class State4: - def __init__( - self, resultPerGroup: dict[solo_turnier.group.Group | None, TotalGroupResult] - ): + def __init__(self, resultPerGroup: dict[NullableGroup, TotalGroupResult]): parser = solo_turnier.group.GroupParser() self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys()) self.results = resultPerGroup diff --git a/src/solo_turnier/types/tableData.py b/src/solo_turnier/types/tableData.py new file mode 100644 index 0000000..c4478eb --- /dev/null +++ b/src/solo_turnier/types/tableData.py @@ -0,0 +1,11 @@ +import solo_turnier +from .participant import Participant +from .singleParticipantResult import SingleParticipantResult + +SortedResultList = dict[Participant, list[SingleParticipantResult | None]] + + +class GroupTableData: + def __init__(self, dances: list[str], results: SortedResultList): + self.dances = dances + self.resultsInGroup = results diff --git a/src/solo_turnier/workers/OutputShaper.py b/src/solo_turnier/workers/OutputShaper.py new file mode 100644 index 0000000..c06c2c5 --- /dev/null +++ b/src/solo_turnier/workers/OutputShaper.py @@ -0,0 +1,58 @@ +import solo_turnier +import logging + + +class OutputShaper: + def __init__(self): + self.l = logging.getLogger("solo_turnier.worker.OutputShaper") + + def shapeResults( + self, results: solo_turnier.types.State4 + ) -> solo_turnier.types.Stage5: + ret = {} + for group in results.results: + ret[group] = self._handleGroup(results.results[group]) + + return solo_turnier.types.Stage5(ret) + + def _handleGroup( + self, + totalGroupResult: solo_turnier.types.TotalGroupResult, + ) -> solo_turnier.types.GroupTableData: + sortedResultList = {} + for participant in totalGroupResult.results: + sortedResultList[participant] = [None for x in totalGroupResult.dances] + + for result in totalGroupResult.results[participant]: + if result.dance not in totalGroupResult.dances: + self.l.error( + "Result in unknown dance found in table. This is a bug. (%s)", + result, + ) + continue + + idx = totalGroupResult.dances.index(result.dance) + sortedResultList[participant][idx] = result + return solo_turnier.types.GroupTableData( + totalGroupResult.dances, sortedResultList + ) + + def _reshapeRow( + self, + results: list[solo_turnier.types.SingleParticipantResult], + dances: list[str], + ) -> list[solo_turnier.types.SingleParticipantResult]: + ret = [None for x in dances] + + for result in results: + if result.dance not in dances: + self.l.error( + "Result in unknown dance found in table. This is a bug. (%s)", + result, + ) + continue + + idx = dances.index(result.dance) + ret[idx] = result + + return ret diff --git a/src/solo_turnier/workers/__init__.py b/src/solo_turnier/workers/__init__.py index 2ca2255..b5deb20 100644 --- a/src/solo_turnier/workers/__init__.py +++ b/src/solo_turnier/workers/__init__.py @@ -1,3 +1,4 @@ from . import ResultExtractor from . import DataWorker from . import Worker +from . import OutputShaper From b79a7dcd4936adc6d77aa1076e72d2b8e9546baf Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Wed, 22 Nov 2023 19:03:48 +0100 Subject: [PATCH 31/31] Make HTML output work again --- src/solo_turnier/batch.py | 2 +- src/solo_turnier/group.py | 4 +++- src/solo_turnier/static/style.css | 6 ++++++ src/solo_turnier/templates/index.html | 15 ++++++++------- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/solo_turnier/batch.py b/src/solo_turnier/batch.py index 428bc73..a2142d2 100644 --- a/src/solo_turnier/batch.py +++ b/src/solo_turnier/batch.py @@ -11,7 +11,7 @@ class BatchWorker: self.l = logging.getLogger("solo_turnier.batch") self.config = config - def run(self, removeFilteredParicipants=True): + def run(self, removeFilteredParicipants=True) -> solo_turnier.types.Stage5: self.l.debug(self.config.__dict__) locator = solo_turnier.html_locator.HtmlLocator() diff --git a/src/solo_turnier/group.py b/src/solo_turnier/group.py index 830b3ba..165b1a3 100644 --- a/src/solo_turnier/group.py +++ b/src/solo_turnier/group.py @@ -104,4 +104,6 @@ class GroupParser: ] def getGroupsAsSortedList(self, groups) -> list[Group]: - return [x for x in self.getGroups() if x in groups] + mainGroups = [x for x in self.getGroups() if x in groups] + additionalGroups = set(groups).difference(mainGroups) + return mainGroups + list(additionalGroups) diff --git a/src/solo_turnier/static/style.css b/src/solo_turnier/static/style.css index 5158548..1320070 100644 --- a/src/solo_turnier/static/style.css +++ b/src/solo_turnier/static/style.css @@ -22,6 +22,12 @@ color: gray; } +.tab-summary .no-finalist-dance { + color: gray; + text-decoration-style: solid; + text-decoration-line: line-through; +} + @media print { @page { size: landscape; diff --git a/src/solo_turnier/templates/index.html b/src/solo_turnier/templates/index.html index a62df9a..6927f0c 100644 --- a/src/solo_turnier/templates/index.html +++ b/src/solo_turnier/templates/index.html @@ -10,7 +10,7 @@ {#

Finalauswertung Solo-Turniere

#} - {% for group in data.groups %} + {% for group in data.resultsPerGroup %} {% block groupBlk scoped %}
{% if group is none %} @@ -21,11 +21,11 @@
Teilnehmer
- {% for dance in data.results[group].dances %} + {% for dance in data.resultsPerGroup[group].dances %} {% endfor %} - {% set activeGroup = data.results[group].results %} + {% set activeGroup = data.resultsPerGroup[group].resultsInGroup %} {% for participant, results in activeGroup|dictsort() %} {% block participantGrp scoped %} {% set rowCls = "" %} @@ -35,16 +35,17 @@ {% if participant.finalist or not onlyFinalists %} - {% for dance in data.results[group].dances %} + {% for dance in data.resultsPerGroup[group].dances %} {% block danceResult scoped %} {% set res = activeGroup[participant][loop.index0] %}
Teilnehmer{{ dance }}
{{ participant.name }} ({{ participant.id }}) {% if res is not none %} {% if not participant.finalist %} - Kein/e Finalist/in + Kein/e Finalist/in
{% endif %} - {{ res.getNativePlace() }} - ({{ res.nativeClass }})
+ + {{ res.getNativePlace() }} ({{ res.nativeClass }})
+
{{ res.place }} in {{ res.competitionClass }}