import logging from pprint import pformat from solo_turnier import html_parser from .reader import ResultRow 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})' def __eq__(self, o): if not isinstance(o, HtmlPerson): return False return str(self) == str(o) def __hash__(self): return str(self).__hash__() class ResultPerson: def __init__(self, firstName, lastName, club, id = None, group = None): self.firstName = firstName self.lastName = 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 ) 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 ) def __repr__(self): if self.id is None: return f'{self.name} ({self.club})' else: return f'{self.name} ({self.club}) [{self.id}]' def __hash__(self): text = str(self) return text.__hash__() class CompetitionResult: def __init__(self, dance, group, class_, place, placeTo, id, competitionGroup, competitionClass): self.dance = dance self.group = group self.class_ = class_ self.place = place self.placeTo = placeTo self.id = int(id) self.competitionGroup = competitionGroup self.competitionClass = competitionClass self.finalist = None @staticmethod def extractFromResultRow(row: ResultRow): return CompetitionResult( dance=row.dance, group=row.group, class_=row.class_, place=row.place, placeTo=row.placeTo, id=row.id, competitionGroup=row.competitionGroup, competitionClass=row.competitionClass ) 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 ImportNotParsableException(Exception): pass class PreviewWorker: def __init__(self): self.l = logging.getLogger('solo_turnier.worker.PreviewWorker') self.persons = None self.parsers = None def filterFilesPreview(self, files: list[str]) -> dict[str, html_parser.HtmlParser]: 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'] 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') ids = [] names = [] indices = [] for index, e in enumerate(data['table'][0]): if e['text'] == '': continue indices.append(index) ids.append(e['text']) names.append(e['meta']) groups = [] if data['titles'][-1] == 'Startgruppe': self.l.debug('Combined competition found. Extracting group from table') groups = [data['table'][-1][idx]['text'] for idx in indices] else: self.l.debug('Using group from the title.') group = parser.guessDataFromHtmlTitle(imported['title'])['group'] groups = [group for i in indices] def __mappingFcn(id, name, group): return HtmlPerson(name, id, group) currentPersons = list(map(__mappingFcn, ids, names, groups)) self.l.log(5, 'Extracted persons in preview round: %s', currentPersons) for p in currentPersons: current = self.parsers.get(p, []) current.append(parser) self.parsers[p] = current def __resetStructures(self): self.persons = None self.parsers = {} def extractPersonsFromPreview(self, parsers): self.__resetStructures() for file in parsers: self.l.debug('Extracting person data from %s', file) self.__extractPersonsFromSinglePreview(parsers[file]) self.persons = self.parsers.keys() self.l.log(5, 'Extracted person data: %s', pformat(self.parsers)) 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