Merge branch 'tmp/live-changes'

This commit is contained in:
Christian Wolf 2023-11-22 19:05:24 +01:00
commit f440affd99
50 changed files with 2114 additions and 1749 deletions

4
.flake8 Normal file
View File

@ -0,0 +1,4 @@
[flake8]
per-file-ignores = __init__.py:F401
extend-exclude = build
extend-ignore = E501

View File

@ -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",
}
}

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[too.black]
line-length = 120

View File

@ -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,8 +18,13 @@ 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
pydocstyle==6.3.0
pyflakes==3.1.0
pynsist==2.8
pyparsing==3.0.9
pytest==7.2.0
@ -26,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

34
run-all-examples.sh Executable file
View File

@ -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

View File

@ -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,
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()

View File

@ -1,10 +1,8 @@
from . import competition_class
from . import group
from . import types
from . import cli
from . import reader
from . import participant
from . import html_locator
@ -14,3 +12,5 @@ from . import output
from . import batch
from . import flask
from . import workers

View File

@ -1,4 +1,3 @@
import solo_turnier
import logging
import os
@ -6,29 +5,33 @@ 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):
def run(self, removeFilteredParicipants=True) -> solo_turnier.types.Stage5:
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 HTML export files in "%s"',
self.config.importHtmlPath(),
)
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)
worker = solo_turnier.workers.Worker.Worker()
importedData = worker.collectAllData(htmlResultFiles)
combinedData = worker.combineData(importedData)
worker.filterOutFinalists(combinedData, removeFilteredParicipants)
return combinedData
outputShaper = solo_turnier.workers.OutputShaper.OutputShaper()
shapedData = outputShaper.shapeResults(combinedData)
return shapedData

View File

@ -3,19 +3,50 @@ 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:
@ -44,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]

View File

@ -1,60 +1,72 @@
import re
class CompetitionClass:
def __init__(self, text: str):
self.name = text
def __repr__(self):
return self.name
return f"{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}"
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
class CompetitionClassParser:
NEWC = CompetitionClass('Newc.')
BEG = CompetitionClass('Beg.')
ADV = CompetitionClass('Adv.')
PREVIEW = CompetitionClass('Sichtung')
class CompetitionClassParser:
NEWC = CompetitionClass("Newc.")
BEG = CompetitionClass("Beg.")
ADV = CompetitionClass("Adv.")
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
return self.PREVIEW
match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls)
match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls)
if match is not None:
clsA = self.mapNames[match.group(1)]
clsB = self.mapNames[match.group(2)]

View File

@ -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)

View File

@ -1,11 +1,16 @@
import re
class Group:
def __init__(self, text: str):
self.name = text
def __repr__(self):
return self.name
return f"{self.name}"
def getContainedGroups(self):
return (self,)
class CombinedGroup:
def __init__(self, grpA: Group, grpB: Group):
@ -13,61 +18,67 @@ class CombinedGroup:
self.clsB = grpB
def __repr__(self):
return f'{self.clsA}/{self.clsB}'
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)
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)
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)]
grpB = self.mapNames[match.group(2)]
@ -75,12 +86,12 @@ 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[
return [
GroupParser.KIN,
GroupParser.JUN,
GroupParser.JUG,
@ -89,8 +100,10 @@ class GroupParser:
GroupParser.MAS2,
GroupParser.MAS3,
GroupParser.MAS4,
GroupParser.MAS5
GroupParser.MAS5,
]
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)

View File

@ -1,9 +1,10 @@
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 = []
@ -21,16 +22,12 @@ class HtmlLocator:
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')
return candidates

View File

@ -3,72 +3,75 @@ 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):
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})'
return f"HtmlParser({self.fileName})"
def getEventTitle(self):
return self.soup.find('div', class_='eventhead').table.tr.td.contents[0]
return self.soup.find("div", class_="eventhead").table.tr.td.contents[0]
def guessDataFromHtmlTitle(self, title = None):
def guessDataFromHtmlTitle(self, title=None):
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)
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_": self.classParser.parseClass(rawClass, True),
"group": self.groupParser.parseGroup(rawGroup),
}
def parseResult(self):
def parseResult(self) -> HtmlResultImport:
participants = {}
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)
@ -81,17 +84,17 @@ class HtmlParser:
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')
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)
__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:
@ -103,118 +106,37 @@ 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]
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 = {}
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')
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:
@ -222,48 +144,78 @@ class HtmlParser:
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
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
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)]
return list(map(lambda x: x.contents[0], classTags))
classTags = rows[classRow]("td")[1 : (numIds + 1)]
return list(
map(
lambda x: classParser.parseAbbreviatedClass(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)]
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()
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
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')
tables = self.soup.find("div", class_="extract").find_all("table")
for table in tables:
__parseTable(table)
return HtmlResultTotalTable( participants)
return HtmlResultTotalTable(participants)

View File

@ -1,4 +1,3 @@
import logging
from tabulate import tabulate
import pprint
@ -6,17 +5,18 @@ 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()
self.worker = solo_turnier.workers.DataWorker.DataWorker()
self.groups = []
self.dances = []
self.showIds = False
@ -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,65 +51,95 @@ 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):
print(f"Einzeltanzwettbewerb der Gruppe {group}")
def _reshapeRow(
self,
results: list[solo_turnier.types.SingleParticipantResult],
dances: list[str],
) -> list[solo_turnier.types.SingleParticipantResult]:
ret = [None for x in dances]
tableData = [['Tanz'] + groupResults.dances]
participants = list(groupResults.results.keys())
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: solo_turnier.types.GroupTableData,
):
if group is not None:
print(f"Einzeltanzwettbewerb der Gruppe {group}")
else:
print("Einzeltanzwettbewerbe ohne eindeutige Gruppenzuordnung")
tableData = [["Tanz"] + groupResults.dances]
participants = list(groupResults.resultsInGroup.keys())
participants.sort(key=lambda x: (x.id, x.name))
for participant in participants:
results = groupResults.results[participant]
results = groupResults.resultsInGroup[participant]
self.l.log(5, "Results of %s: %s", participant, results)
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}]'
placeNative = str(result.nativePlace)
place = str(result.place)
lineOne = f"{placeNative} ({result.nativeClass})"
lineTwo = f"[{place} in {result.competitionClass}]"
lines = [lineOne, lineTwo]
if not result.finalist:
lines = ['kein/e Finalist/in'] + lines
lines = ["kein/e Finalist/in"] + lines
return '\n'.join(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):
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.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)

View File

@ -1,12 +1,5 @@
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
@ -17,18 +10,19 @@ class 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})'
return f"{self.firstName} {self.lastName} ({self.club}, {self.group})"
def __hash__(self):
return self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__()
return (
self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__()
)

View File

@ -1,101 +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.parseClass(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.parseClass(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

View File

@ -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;

View File

@ -10,18 +10,22 @@
</head>
<body>
{# <h1>Finalauswertung Solo-Turniere</h1> #}
{% for group in data.groups %}
{% for group in data.resultsPerGroup %}
{% block groupBlk scoped %}
<div class="section">
{% if group is none %}
<h1>Auswertung ohne eindeutige Gruppe</h1>
{% else %}
<h1>Auswertung Gruppe {{ group.name }}</h1>
{% endif %}
<table class="tab-summary">
<tr>
<th>Teilnehmer</th>
{% for dance in data.results[group].dances %}
{% for dance in data.resultsPerGroup[group].dances %}
<th>{{ dance }}</th>
{% endfor %}
</tr>
{% set activeGroup = data.results[group].results %}
{% set activeGroup = data.resultsPerGroup[group].resultsInGroup %}
{% for participant, results in activeGroup|dictsort() %}
{% block participantGrp scoped %}
{% set rowCls = "" %}
@ -31,17 +35,19 @@
{% if participant.finalist or not onlyFinalists %}
<tr class="{{ rowCls }}">
<td>{{ participant.name }} ({{ participant.id }})</td>
{% for dance in data.results[group].dances %}
{% for dance in data.resultsPerGroup[group].dances %}
{% block danceResult scoped %}
{% set res = activeGroup[participant][loop.index0] %}
<td>
{% if res is not none %}
{% if not participant.finalist %}
Kein/e Finalist/in
Kein/e Finalist/in <br />
{% endif %}
{{ res.getNativePlace() }} ({{ res.nativeClass }}) <br />
<span class="{% if not res.finalist %}no-finalist-dance{% endif %}">
{{ res.getNativePlace() }} ({{ res.nativeClass }}) <br />
</span>
<span class="competition-place">
{{ res.getPlace() }} in {{ res.competitionClass }}
{{ res.place }} in {{ res.competitionClass }}
</span>
{% endif %}
</td>

View File

@ -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]

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -4,20 +4,22 @@ import json
import solo_turnier.html_parser
@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:
@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:
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')
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:
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')
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:
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

View File

@ -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

View File

@ -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)
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)
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)
htmlParticipant3Dance1 = html_parser.HtmlParticipant("Max 3 Mustermann", "1.", True)
htmlParticipant4Dance1 = html_parser.HtmlParticipant('Max 4 Mustermann', '3.', False)
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]]
@ -311,28 +470,54 @@ def test_mergeHtmlData(mocker):
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]]

View File

@ -1,349 +0,0 @@
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}'
class HtmlPreviewParticipant:
def __init__(self, name, id, group_):
self.name = name
self.id = id
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)))
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))
# 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]]
):
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})'
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,
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):
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

View File

@ -0,0 +1,18 @@
from .place import Place
from .person import Person
from .htmlPreviewParticipant import HtmlPreviewParticipant
from .htmlParticipant import HtmlParticipant
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 .tableData import *
from .stages import *

View File

@ -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__()

View File

@ -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
)

View File

@ -0,0 +1,51 @@
import solo_turnier
from .htmlSingleCompetitionResult import HtmlSingleCompetitionResult
from .competitionTuple import CompetitionTuple
class HtmlCompetitionTotalResults:
def __init__(self):
self.results = {}
self.fixups = {}
def __getTuple(
self,
group: solo_turnier.group.Group_t,
class_: solo_turnier.competition_class.Class_t,
dance: str,
id: int,
):
return CompetitionTuple(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.id) != id:
continue
# ret = ret + self.results[k]
# Dance, Group, Class
key = (k.dance, k.group, k.class_)
ret[key] = self.results[k]
return ret
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)
self.results[tup] = l

View File

@ -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) != HtmlParticipant:
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

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,6 @@
class HtmlResultTotalTable:
def __init__(self, participants):
self.participants = participants
def __repr__(self):
return str(self.participants)

View File

@ -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_})"

View File

@ -0,0 +1,25 @@
from .place import Place
class HtmlSingleCompetitionResult:
def __init__(self, name: str, place: Place, finalist: bool):
self.name = name
self.place = place
self.finalist = finalist
def __repr__(self):
place = self.place
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)

View File

@ -0,0 +1,23 @@
import solo_turnier
from .person import Person
class Participant(Person):
def __init__(
self,
name: str,
id: int,
finalist: bool = None,
):
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})"
def __gt__(self, other):
return self.id > other.id

View File

@ -0,0 +1,3 @@
class Person:
def __init__(self, name: str):
self.name = name

View File

@ -0,0 +1,10 @@
class Place:
def __init__(self, place: int, placeTo: int | None = None):
self.place = place
self.placeTo = placeTo
def __repr__(self):
if self.placeTo is None:
return f"{self.place}."
return f"{self.place}.-{self.placeTo}."

View File

@ -0,0 +1,29 @@
import solo_turnier
from .place import Place
class SingleParticipantResult:
def __init__(
self,
competitionClass: solo_turnier.competition_class.Class_t,
nativeClass: solo_turnier.competition_class.CompetitionClass,
dance: str,
finalist: bool,
place: Place,
nativePlace: Place = None,
):
self.competitionClass = competitionClass
self.nativeClass = nativeClass
self.dance = dance
self.finalist = finalist
self.place = place
self.nativePlace = nativePlace
def __repr__(self):
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)

View File

@ -0,0 +1,29 @@
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[NullableGroup, TotalGroupResult]):
parser = solo_turnier.group.GroupParser()
self.groups = parser.getGroupsAsSortedList(resultPerGroup.keys())
self.results = resultPerGroup
class State3:
def __init__(self, htmlResults: HtmlCompetitionTotalResults):
self.htmlResults = htmlResults

View File

@ -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

View File

@ -0,0 +1,15 @@
from .singleParticipantResult import SingleParticipantResult
from .participant import Participant
class TotalGroupResult:
def __init__(
self,
dances: list[str],
results: dict[Participant, list[SingleParticipantResult]],
):
self.dances = dances
self.results = results
def __repr__(self):
return f"TotalGrR({self.dances}, {self.results})"

View File

@ -1,15 +1,3 @@
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:
def __init__(self, name, id, group):
self.name = name
@ -17,7 +5,7 @@ class HtmlPerson:
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):
@ -27,671 +15,33 @@ 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
)
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__()
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')
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']
)
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))
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)
def combineData(self, importedData: types.State3):
self.l.info('Starting to build data sets.')
groups = self._extractGroups(importedData)
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)
dances = self._extractDancesPerGroup(importedData, group)
self.l.log(5, 'Found dances in group %s: %s', group, dances)
participants = self._extractParticipantsPerGroup(importedData.previewImport, 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.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._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):
groupSet = set([])
for id in data.previewImport.participants:
participants = data.previewImport.participants[id]
for participant in participants:
groupSet.add(participant.group)
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):
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))
if len(additionalDances) > 0:
self.l.warning('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,
previewData: types.HtmlPreviewImport,
group: solo_turnier.group.Group
) -> list[types.HtmlPreviewParticipant]:
ret = []
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,
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)
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))
selectedIndex = selected.index(True)
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 _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)
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.warning('Dropping %s from the output as no finalist', participant)
droppedParticipants.append(participant)
if filterOut:
for droppedParticipant in droppedParticipants:
data.results[group].results.pop(droppedParticipant)

View File

@ -0,0 +1,47 @@
from ..worker import ResultPerson
from ..types import HtmlCompetitionResultRow as CompetitionResult
from solo_turnier import html_parser
import logging
class DataWorker:
def __init__(self):
self.l = logging.getLogger("solo_turnier.worker.DataWorker")
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

View File

@ -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

View File

@ -0,0 +1,127 @@
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
guessedClass = data["class_"]
self.l.debug(
"Fetched result data: %s, guessed class %s", data, guessedClass
)
ret[filePair] = (parser, parserTab)
return ret
def _extractPlace(self, placeStr: str) -> types.Place:
s = placeStr.replace(".", "")
matches = self.rePlaceSingle.fullmatch(s)
if matches is not 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 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")
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 = self._extractPlace(placeStr)
competitionResult = types.HtmlSingleCompetitionResult(
person.name, place, person.finalist
)
results.add(
competitionGroup,
competitionClass,
dance,
int(person.id),
competitionResult,
)
#
def _analyzeResultFixups(
self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults
):
data = parser.guessDataFromHtmlTitle()
competitionClass = data["class_"]
competitionGroup = data["group"]
dance = data["dance"]
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
) -> 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._analyzeResultFixups(parsers[fileNameTuple][1], ret)
return ret

View File

@ -0,0 +1,546 @@
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()
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.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)
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._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)
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))
return ret
def _extractGroups(self, data: types.State3):
groupSet = set([])
for tup in data.htmlResults.results.keys():
gr = self._groupParser.parseGroup(tup[0])
groupSet.update(gr.getContainedGroups())
self.l.log(5, "Set of active groups: %s", groupSet)
groups = self._groupParser.getGroupsAsSortedList(groupSet)
return groups
def _getGroupMapping(
self, importedData: types.State3
) -> dict[int, solo_turnier.group.Group | None]:
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 ambiguous. Guessing failed for id %d. Falling back to second best guess %s.",
id,
candidates[1],
)
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 = 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.group,
tup.class_,
tup.dance,
)
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)
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 = fixture.class_
def _extractDancesPerGroup(
self, data: types.State3, group: solo_turnier.group.Group
):
dances = set()
additionalDances = set()
foundDances = set()
for tup in data.htmlResults.results.keys():
currentGroup = self._groupParser.parseGroup(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]:
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 = self._groupParser.parseGroup(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 self._groupParser.parseGroup(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):
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:
if participant.finalist == False:
self.l.info(
"Dropping %s from the output as no finalist", participant
)
droppedParticipants.append(participant)
for droppedParticipant in droppedParticipants:
data.results[group].results.pop(droppedParticipant)

View File

@ -0,0 +1,4 @@
from . import ResultExtractor
from . import DataWorker
from . import Worker
from . import OutputShaper