Compare commits

..

72 Commits
v0.9.2 ... main

Author SHA1 Message Date
0198ecf752 Use all dependencies in non-dev mode 2024-05-08 13:44:00 +02:00
801c7f2bbd Update version 2024-05-08 13:26:21 +02:00
16ed0280cd Code styling fixed 2024-05-08 13:25:46 +02:00
5712c3da63 Only im port debugpy if you want to debug stuff 2024-05-08 13:25:01 +02:00
3b98cef590 Update version 2024-03-14 19:08:18 +01:00
4bc626b163 Fixed some code styings 2024-03-14 19:07:32 +01:00
533f3ef237 Allow for old title 2024-03-14 19:06:43 +01:00
61a607e516 Correctly mark non-finalists 2024-03-14 19:06:34 +01:00
cd8ab716b8 Fix visuel representation 2024-03-14 19:06:19 +01:00
a7f8bad0ec Add optional output for combined groups 2024-03-14 18:52:31 +01:00
7ec359d2f2 Add club to output 2024-03-14 13:08:27 +01:00
5c4b0106fc Make the parsing compatible with the new structure of HTML exports 2024-03-14 10:56:00 +01:00
08a9f7bd0e Migrate to pipenv based dependencz management 2024-03-06 20:36:22 +01:00
79925de8cd Create Makefile target for formatting of code 2024-03-06 18:47:53 +01:00
d1b7c604b5 Fix code formatting issues 2024-03-06 18:46:54 +01:00
d35d56cb81 Update requirements 2024-03-06 18:44:58 +01:00
831cf85d15 Update version to 2.0.0 2024-03-06 18:27:39 +01:00
e4aa47cfe6 Simplify output to not show native class and place
No combinations of competitions are possible currently
2024-03-06 18:26:32 +01:00
70e7008f9c Reduce log level of non-solo competitions 2024-03-06 18:20:50 +01:00
fda97a811c Fix Regex about competition names (now OT) 2024-03-06 18:20:25 +01:00
9c2b812387 Fix logger names 2024-03-06 18:19:49 +01:00
b36d5a81ce Fix names of classes and allow for combined competitions with other classes 2024-03-06 18:19:29 +01:00
8d227af0e4 Update to new version 2023-11-22 19:07:11 +01:00
f440affd99 Merge branch 'tmp/live-changes' 2023-11-22 19:05:24 +01:00
b79a7dcd49 Make HTML output work again 2023-11-22 19:03:48 +01:00
f944aabc38 Outsource the table shaping for use with flask 2023-11-22 18:52:25 +01:00
c690ce755a Fixed bug with pure string group 2023-11-22 16:33:02 +01:00
abf4974d40 Reduce the changes to make the tests not break due to optical changes 2023-11-22 15:49:45 +01:00
43180c6e05 First running version
Not yet comparable with previous code base due to different outputs.
2023-11-21 20:14:12 +01:00
3ea5b74557 Make basic blocks obvious in logs 2023-11-20 21:03:49 +01:00
04b43def9d Dropping CSV parsing methods 2023-11-20 13:50:13 +01:00
e081769b6c Fix minor bug to make things work for now 2023-11-20 13:41:23 +01:00
345d060afa Fixed imports in types package 2023-11-20 13:39:23 +01:00
b78b964a55 Moved types into package 2023-11-20 12:35:19 +01:00
82d7717fde Removed preview extractor methods 2023-11-20 12:09:42 +01:00
b4ec4f896c Refactor name of group parser method 2023-11-20 11:53:58 +01:00
7382df03a8 Merge branch 'fix/split-modules' into tmp/live-changes 2023-11-20 11:40:04 +01:00
69e20dd4c8 Enhance data structure to reuse common class objects 2023-11-20 11:12:50 +01:00
16d78e18f3 Refactored worker classes to be in individual modules 2023-11-20 10:44:16 +01:00
a0c52a56fc Started to work towards new group management 2023-11-20 10:22:07 +01:00
90acb45a03 Fix types in group comparision 2023-11-20 10:21:27 +01:00
f182f4fbcc Make table matching between files more robust 2023-11-19 18:27:15 +01:00
6c6b8485fc Drop preview result from data classes 2023-11-19 18:26:35 +01:00
f5132ce8e8 Removed preview worker to simplify code base 2023-11-19 18:15:45 +01:00
c06c5ed791 Adc config for flake linter 2023-11-19 17:52:19 +01:00
0129051756 Register black with VS code 2023-11-19 17:40:57 +01:00
d1133d6d36 Added mor elinter tools to toolchain 2023-11-19 17:12:26 +01:00
83a0003024 Make style code conformant with black 2023-11-19 17:07:20 +01:00
ca9164552c Install black as dev dependency 2023-11-19 17:04:35 +01:00
ee3789aab9 Increase consistency of chceks 2023-11-19 16:55:42 +01:00
30e4a43a9f Extract groups for pure combined competitions 2023-11-19 14:20:12 +01:00
727ce0ca3a Automation of test to compare with previous runs 2023-11-19 14:03:48 +01:00
c4b2d0a23b Enhanced matching of HTML title w.r.t custom alias in TT 2023-11-19 13:19:32 +01:00
f075e1e167 Reducing log level of information about dropped non-finalists 2023-11-19 13:18:12 +01:00
638b2a3002 Reenable checking of additional dance to make bugs more prominent 2023-11-19 13:04:01 +01:00
8298cd85a8 Added file changes made live during competition to make things run smoothly 2023-11-13 09:31:43 +01:00
82fabe69a7 Update to new version 2023-11-08 21:33:59 +01:00
88bffcad8a Merge branch 'fix/incomplete-tables' 2023-11-08 21:33:06 +01:00
c7212316ee Fix native class from final result table 2023-11-08 20:46:05 +01:00
ee63871e1c Augment logging a bit 2023-11-08 20:45:44 +01:00
bb10d5fa06 Ignore failed preview round scans 2023-11-08 20:45:19 +01:00
36fa6a6dca Augment the logging for debugging 2023-11-08 20:42:11 +01:00
aaa311ee03 Avoid issue with rounds that contain all previous participants 2023-11-08 20:00:01 +01:00
366f496ba3 Enable external debugger 2023-10-06 18:39:55 +02:00
66d687d9f0 Update to new version 2023-10-06 18:18:50 +02:00
1d69d9fe8a Fix filtering w.r.t finalists in web styling
Mutiple rows were colored similarly of some are hidden
2023-10-06 18:17:37 +02:00
6f11554c18 Prepare next version 2023-10-06 17:53:15 +02:00
03fed1e1e4 Handle incomplete competitions as well 2023-10-06 17:52:52 +02:00
be5ac238bc Revert "Add UI dialog for unknown path"
This reverts commit 90f82e677e.
2023-10-06 16:20:39 +02:00
5015a4e4e1 Prepare next version 2023-10-06 13:04:49 +02:00
90f82e677e Add UI dialog for unknown path 2023-10-06 13:02:45 +02:00
fac3fe1b34 Fix bug related to showing all participants 2023-09-29 17:28:00 +02:00
55 changed files with 3120 additions and 1824 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

@ -3,3 +3,6 @@ all:
installer:
pynsist src/installer.cfg
@PHONY: black
black:
black src

25
Pipfile Normal file
View File

@ -0,0 +1,25 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
beautifulsoup4 = "*"
coloredlogs = "*"
flask = "*"
tabulate = "*"
colorama = "*"
[dev-packages]
black = "*"
debugpy = "*"
flake8 = "*"
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
pydocstyle = "*"
pylint = "*"
pynsist = "*"
[requires]
python_version = "3.11"

715
Pipfile.lock generated Normal file
View File

@ -0,0 +1,715 @@
{
"_meta": {
"hash": {
"sha256": "7c711e876affdb7a45715a84641460c07f98a71fd83bb8eb6cbf4a4b13e9ab9a"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.11"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"astroid": {
"hashes": [
"sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
"sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
"version": "==3.1.0"
},
"attrs": {
"hashes": [
"sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
"sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==23.2.0"
},
"beautifulsoup4": {
"hashes": [
"sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051",
"sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"
],
"index": "pypi",
"markers": "python_full_version >= '3.6.0'",
"version": "==4.12.3"
},
"black": {
"hashes": [
"sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8",
"sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8",
"sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd",
"sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9",
"sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31",
"sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92",
"sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f",
"sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29",
"sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4",
"sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693",
"sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218",
"sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a",
"sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23",
"sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0",
"sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982",
"sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894",
"sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540",
"sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430",
"sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b",
"sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2",
"sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6",
"sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==24.2.0"
},
"blinker": {
"hashes": [
"sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
"sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.7.0"
},
"certifi": {
"hashes": [
"sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
"sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==2024.2.2"
},
"charset-normalizer": {
"hashes": [
"sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
"sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
"sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
"sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
"sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
"sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
"sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
"sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
"sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
"sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
"sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
"sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
"sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
"sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
"sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
"sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
"sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
"sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
"sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
"sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
"sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
"sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
"sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
"sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
"sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
"sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
"sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
"sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
"sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
"sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
"sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
"sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
"sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
"sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
"sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
"sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
"sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
"sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
"sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
"sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
"sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
"sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
"sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
"sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
"sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
"sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
"sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
"sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
"sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
"sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
"sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
"sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
"sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
"sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
"sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
"sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
"sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
"sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
"sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
"sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
"sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
"sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
"sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
"sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
"sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
"sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
"sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
"sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
"sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
"sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
"sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
"sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
"sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
"sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
"sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
"sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
"sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
"sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
"sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
"sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
"sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
"sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
"sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
"sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
"sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
"sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
"sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
"sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
"sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
"sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
],
"index": "pypi",
"markers": "python_full_version >= '3.7.0'",
"version": "==3.3.2"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
"colorama": {
"hashes": [
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
"version": "==0.4.6"
},
"coloredlogs": {
"hashes": [
"sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
"sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==15.0.1"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa",
"sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003",
"sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f",
"sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c",
"sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e",
"sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0",
"sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9",
"sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52",
"sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
"sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
"sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0",
"sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079",
"sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352",
"sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
"sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
"sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
"sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
"sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765",
"sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc",
"sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
"sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501",
"sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7",
"sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
"sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f",
"sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
"sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
"sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
"sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51",
"sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840",
"sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
"sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
"sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e",
"sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45",
"sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba",
"sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d",
"sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
"sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
"sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
"sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
"sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9",
"sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a",
"sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
"sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1",
"sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3",
"sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914",
"sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
"sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
"sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d",
"sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0",
"sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94",
"sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc",
"sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==7.4.3"
},
"debugpy": {
"hashes": [
"sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb",
"sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146",
"sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8",
"sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242",
"sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0",
"sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741",
"sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539",
"sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23",
"sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3",
"sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39",
"sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd",
"sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9",
"sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace",
"sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42",
"sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0",
"sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7",
"sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e",
"sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234",
"sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98",
"sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703",
"sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42",
"sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.8.1"
},
"dill": {
"hashes": [
"sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
"sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==0.3.8"
},
"distlib": {
"hashes": [
"sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
"sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
],
"index": "pypi",
"version": "==0.3.8"
},
"exceptiongroup": {
"hashes": [
"sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
"sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.2.0"
},
"flake8": {
"hashes": [
"sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132",
"sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.1'",
"version": "==7.0.0"
},
"flask": {
"hashes": [
"sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e",
"sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==3.0.2"
},
"humanfriendly": {
"hashes": [
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==10.0"
},
"idna": {
"hashes": [
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
],
"index": "pypi",
"markers": "python_version >= '3.5'",
"version": "==3.6"
},
"iniconfig": {
"hashes": [
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"isort": {
"hashes": [
"sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
"sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
"version": "==5.13.2"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
"sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.1.3"
},
"markupsafe": {
"hashes": [
"sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
"sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
"sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
"sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
"sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
"sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
"sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
"sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
"sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
"sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
"sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
"sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
"sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
"sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
"sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
"sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
"sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
"sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
"sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
"sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
"sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
"sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
"sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
"sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
"sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
"sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
"sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
"sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
"sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
"sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
"sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
"sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
"sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
"sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
"sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
"sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
"sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
"sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
"sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
"sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
"sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
"sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
"sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
"sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
"sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
"sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
"sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
"sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
"sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
"sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
"sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
"sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
"sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
"sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
"sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
"sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
"sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
"sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
"sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
"sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.1.5"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy-extensions": {
"hashes": [
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
],
"index": "pypi",
"markers": "python_version >= '3.5'",
"version": "==1.0.0"
},
"packaging": {
"hashes": [
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==23.2"
},
"pathspec": {
"hashes": [
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==0.12.1"
},
"platformdirs": {
"hashes": [
"sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
"sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.2.0"
},
"pluggy": {
"hashes": [
"sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
"sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.4.0"
},
"pycodestyle": {
"hashes": [
"sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f",
"sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.11.1"
},
"pydocstyle": {
"hashes": [
"sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019",
"sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==6.3.0"
},
"pyflakes": {
"hashes": [
"sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f",
"sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==3.2.0"
},
"pylint": {
"hashes": [
"sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
"sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
"version": "==3.1.0"
},
"pynsist": {
"hashes": [
"sha256:465c4596cba5cc3698d4719ddc7afea31ce9dc8936a7b4d3feffec6e8adc0b5d",
"sha256:7d3e8343c10cdbfb262ab63201a62d38ed86f4f2d0cffc2677c9917793d800a6"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==2.8"
},
"pyparsing": {
"hashes": [
"sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad",
"sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"
],
"index": "pypi",
"markers": "python_full_version >= '3.6.8'",
"version": "==3.1.2"
},
"pytest": {
"hashes": [
"sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd",
"sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==8.0.2"
},
"pytest-cov": {
"hashes": [
"sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
"sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==4.1.0"
},
"pytest-mock": {
"hashes": [
"sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f",
"sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==3.12.0"
},
"requests": {
"hashes": [
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.31.0"
},
"requests-download": {
"hashes": [
"sha256:92d895a6ca51ea51aa42bab864bddaee31b5601c7e7e1ade4c27b0eb6695d846",
"sha256:994d9d332befae6616f562769bab163f08d6404dc7e28fb7bfed4a0a43a754ad"
],
"index": "pypi",
"version": "==0.1.2"
},
"snowballstemmer": {
"hashes": [
"sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
"sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
],
"index": "pypi",
"version": "==2.2.0"
},
"soupsieve": {
"hashes": [
"sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690",
"sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.5"
},
"tabulate": {
"hashes": [
"sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c",
"sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==0.9.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"index": "pypi",
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"tomlkit": {
"hashes": [
"sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
"sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==0.12.4"
},
"urllib3": {
"hashes": [
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.2.1"
},
"vulture": {
"hashes": [
"sha256:12d745f7710ffbf6aeb8279ba9068a24d4e52e8ed333b8b044035c9d6b823aba",
"sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.11"
},
"werkzeug": {
"hashes": [
"sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc",
"sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==3.0.1"
},
"yarg": {
"hashes": [
"sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492",
"sha256:55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"
],
"index": "pypi",
"version": "==0.1.9"
}
},
"develop": {}
}

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,34 +1,51 @@
attrs==22.1.0
beautifulsoup4==4.11.1
blinker==1.6.2
certifi==2023.7.22
charset-normalizer==3.2.0
astroid==3.1.0
attrs==23.2.0
beautifulsoup4==4.12.3
black==24.2.0
blinker==1.7.0
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
coloredlogs==15.0.1
coverage==6.5.0
debugpy==1.6.7
distlib==0.3.7
exceptiongroup==1.0.1
Flask==2.3.3
coverage==7.4.3
debugpy==1.8.1
dill==0.3.8
distlib==0.3.8
exceptiongroup==1.2.0
flake8==7.0.0
Flask==3.0.2
humanfriendly==10.0
idna==3.4
iniconfig==1.1.1
idna==3.6
iniconfig==2.0.0
isort==5.13.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
packaging==21.3
pluggy==1.0.0
Jinja2==3.1.3
MarkupSafe==2.1.5
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==23.2
pathspec==0.12.1
platformdirs==4.2.0
pluggy==1.4.0
pycodestyle==2.11.1
pydocstyle==6.3.0
pyflakes==3.2.0
pylint==3.1.0
pynsist==2.8
pyparsing==3.0.9
pytest==7.2.0
pytest-cov==4.0.0
pytest-mock==3.10.0
pyparsing==3.1.2
pytest==8.0.2
pytest-cov==4.1.0
pytest-mock==3.12.0
requests==2.31.0
requests_download==0.1.2
soupsieve==2.3.2.post1
snowballstemmer==2.2.0
soupsieve==2.5
tabulate==0.9.0
toml==0.10.2
tomli==2.0.1
urllib3==2.0.5
Werkzeug==2.3.7
tomlkit==0.12.4
urllib3==2.2.1
vulture==2.11
Werkzeug==3.0.1
yarg==0.1.9

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

@ -1,6 +1,6 @@
[Application]
name=Solo Auswertung
version=0.9.2
version=2.1.1
# How to launch the app - this calls the 'main' function from the 'myapp' package:
entry_point=main:main
# icon=myapp.ico
@ -16,40 +16,19 @@ console=true
[Include]
# Packages from PyPI that your application requires, one per line
# These must have wheels on PyPI:
pypi_wheels = attrs==22.1.0
beautifulsoup4==4.11.1
blinker==1.6.2
certifi==2023.7.22
charset-normalizer==3.2.0
pypi_wheels = beautifulsoup4==4.12.3
blinker==1.8.2
click==8.1.7
colorama==0.4.6
coloredlogs==15.0.1
coverage==6.5.0
debugpy==1.6.7
distlib==0.3.7
exceptiongroup==1.0.1
Flask==2.3.3
flask==3.0.3
humanfriendly==10.0
idna==3.4
iniconfig==1.1.1
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
packaging==21.3
pluggy==1.0.0
pynsist==2.8
pyparsing==3.0.9
pytest==7.2.0
pytest-cov==4.0.0
pytest-mock==3.10.0
requests==2.31.0
requests_download==0.1.2
soupsieve==2.3.2.post1
itsdangerous==2.2.0
jinja2==3.1.4
markupsafe==2.1.5
soupsieve==2.5
tabulate==0.9.0
tomli==2.0.1
urllib3==2.0.5
Werkzeug==2.3.7
yarg==0.1.9
werkzeug==3.0.3
packages = solo_turnier

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,18 +20,23 @@ 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()
port=cli.getPort(),
showOnlyFinalists=not cli.showAllParticipants(),
externalDebugger=cli.externalDebugger,
)
else:
combinedData = batchWorker.run()
combinedData = batchWorker.run(
removeFilteredParicipants=not cli.showAllParticipants()
)
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,30 +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)
if not self.config.showAllParticipants():
worker.filterOutFinalists(combinedData, removeFilteredParicipants)
worker.filterOutFinalists(combinedData, removeFilteredParicipants)
return combinedData
outputShaper = solo_turnier.workers.OutputShaper.OutputShaper()
shapedData = outputShaper.shapeResults(combinedData)
return shapedData

View File

@ -1,27 +1,60 @@
import argparse
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:
import debugpy
debugpy.listen(5678)
debugpy.wait_for_client()
self.externalDebugger = self.__args.debug
map = {
0: logging.ERROR,
1: logging.WARN,
@ -42,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,61 +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 NoEClassException(Exception):
def __init__(self, *args):
super(NoEClassException, self).__init__(*args)
class CompetitionClassParser:
E = CompetitionClass("E")
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,
"E": self.E,
}
self.namesPreview = [
'Sichtung'
]
self.namesPreview = ["Sichtung"]
self.mapShortNames = self.mapNames
def parseClass(self, cls: str, allowPreview: bool = False) -> Class_t:
if allowPreview and cls in self.namesPreview:
return self.PREVIEW
# match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls)
# if match is not None:
# clsA = self.mapNames[match.group(1)]
# clsB = self.mapNames[match.group(2)]
# return CombinedCompetitionClass(clsA, clsB)
# else:
# return self.mapNames[cls]
match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls)
if match is not None:
clsA = self.mapNames[match.group(1)]
clsB = self.mapNames[match.group(2)]
return CombinedCompetitionClass(clsA, clsB)
else:
if cls in self.mapNames:
return self.mapNames[cls]
else:
raise NoEClassException(f'The class "{cls}" is not parsable.')
def parseAbbreviatedClass(self, cls: str) -> Class_t:
return self.mapShortNames[cls]
def isPureClass(self, cls: str, allowPreview: bool = False) -> bool:
parsedClass = self.parseClass(cls, allowPreview)

View File

@ -1,17 +1,32 @@
import flask
import solo_turnier
import logging
_l = logging.getLogger(__name__)
def startFlask(
batchWorker: solo_turnier.batch.BatchWorker,
debug: bool = False,
port: int = 8082
):
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)
return flask.render_template('index.html', data=combinedData)
return flask.render_template(
"index.html", data=combinedData, onlyFinalists=showOnlyFinalists
)
app.run(host='0.0.0.0', port=port, debug=debug)
@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)

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,204 +3,252 @@ 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 CannotParseRowException(Exception):
def __init__(self, *args):
super(CannotParseRowException, self).__init__(*args)
class HtmlParser:
def __init__(self, text: str, fileName: str = None):
self.l = logging.getLogger('solo_turnier.html_parser')
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('.*?OT, Solos (.*?)(?: ".*")?').fullmatch(title)
if match is None:
raise Exception(f'Cannot parse title "{title}"')
self.l.debug(
'Parsing HTML page title "%s" as OT failed. Falling back to legacy ETW.',
title,
)
match = re.compile('.*?ETW, Solos (.*?)(?: ".*")?').fullmatch(title)
if match is None:
self.l.info(
'Cannot parse html title "%s". Is it a solo competition? Possible bug.',
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')
nameRegex = re.compile("(.*) \\(([0-9]+)\\)")
if len(tds) != 2:
return
def __parseNameAndId(string: str, tds) -> tuple[str, str]:
match = nameRegex.fullmatch(string)
if match is None:
self.l.error("Could not match %s to regex search pattern", str(tds))
raise CannotParseRowException(
f"Could not match {tds} to regex search pattern for 'name (id)'"
)
name = match.group(1)
number = match.group(2)
return name, number
regex = re.compile('(.*) \\(([0-9]+)\\)')
def __parseRows(rows, parsers):
def parseRow(row):
for parser in parsers:
try:
parser(row("td"))
return
except CannotParseRowException:
pass
place = tds[0].contents[0]
match = regex.fullmatch(tds[1].contents[0])
if match is None:
raise Exception(f'Could not match {tds} to regex search pattern')
name = match.group(1)
number = match.group(2)
participant = HtmlParticipant(name, number)
participant.finalist = finalist
participants[participant] = place
# No parser was found if we get here.
self.l.error("Cannot parse row in table.")
for row in rows:
__parseRow(row)
parseRow(row)
def __ensureLength(tds, length):
if len(tds) != length:
raise CannotParseRowException(
"The row has %d entries but %d are expected." % (len(tds), length)
)
def __parseFormationRowGeneric(tds, finalist):
__ensureLength(tds, 2)
place = tds[0].contents[0]
name, number = __parseNameAndId(tds[1].contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = finalist
participant.club = ""
participants[participant] = place
def __parseFirstTable(table):
roundName = table.tr.td.contents[0]
if roundName != 'Endrunde':
raise Exception('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)
def __parseFormationRow(tds):
__parseFormationRowGeneric(tds, True)
def __parsePairRow(tds):
__ensureLength(tds, 4)
place = tds[0].contents[0]
tdNameClub = tds[1]
tdClub = tdNameClub.i.extract()
name, number = __parseNameAndId(tdNameClub.contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = True
participant.club = tdClub.contents[0]
participants[participant] = place
__parseRows(
table.find_all("tr")[2:],
[
__parsePairRow,
__parseFormationRow,
],
)
def __parseRemainingTables(tables):
def __parseFormationRow(tds):
__parseFormationRowGeneric(tds, False)
def __parsePairRow(tds):
__ensureLength(tds, 3)
place = tds[0].contents[0]
name, number = __parseNameAndId(tds[1].contents[0], tds)
participant = HtmlParticipant(name, number)
participant.finalist = False
participant.club = tds[2].contents[0]
participants[participant] = place
def __parseSeparatorRow(tds):
__ensureLength(tds, 1)
if len(list(tds[0].stripped_strings)) == 0:
return
raise CannotParseRowException("No empty string")
regexZwischenRunde = re.compile("[1-9]\. Zwischenrunde")
def __parseRoundHeading(tds):
__ensureLength(tds, 1)
s = "".join(tds[0].stripped_strings)
if s.startswith("Vorrunde"):
return
if regexZwischenRunde.match(s) is not None:
return
raise CannotParseRowException("Kein Header einer Runde gefunden.")
def __parseAllSolosQualifiedFormation(tds):
__ensureLength(tds, 2)
if tds[1].contents[0].startswith("Alle Starter weiter genommen."):
return
raise CannotParseRowException(
'Not found the text "Alle Starter weiter genommen"'
)
def __parseAllSolosQualifiedPair(tds):
__ensureLength(tds, 3)
if tds[1].contents[0].startswith("Alle Mannschaften weiter genommen."):
return
raise CannotParseRowException(
'Not found the text "Alle Mannschaften weiter genommen"'
)
for table in tables:
__parseRows(table.find_all('tr'), False)
__parseRows(
table.find_all("tr"),
[
__parseAllSolosQualifiedFormation,
__parseAllSolosQualifiedPair,
__parsePairRow,
__parseFormationRow,
__parseSeparatorRow,
__parseRoundHeading,
],
)
tables = self.soup.find('div', class_='extract').find_all('table')
if len(tables) > 0:
__parseFirstTable(tables[0])
tables = self.soup.find("div", class_="extract").find_all("table")
__parseRemainingTables(tables[1:])
try:
if len(tables) > 0:
__parseFirstTable(tables[0])
__parseRemainingTables(tables[1:])
except IncompleteRoundException:
pass
# 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:
@ -208,48 +256,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,105 @@ 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}"
lines = [lineOne]
groupCompetition = result.competitionGroup
if isinstance(groupCompetition, solo_turnier.group.CombinedGroup):
lineTwo = f"[{place} in {groupCompetition}]"
lines.append(lineTwo)
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)
participantName = f"{participant.name} ({participant.id})"
if participant.club is not None:
participantName = f"{participantName}, {participant.club}"
tableRow = [f"{participantName}"] + list(mappedResults)
tableData.append(tableRow)
self.l.log(5, 'table data: %s', pprint.pformat(tableData))
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

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

View File

@ -0,0 +1,5 @@
{% if onlyFinalists %}
.no-finalist {
display: none;
}
{% endif %}

View File

@ -6,46 +6,61 @@
{# <meta name="description" content="Webpage for xxxx"> #}
<!-- http://meyerweb.com/eric/tools/css/reset/ -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="/custom.css">
</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 = "" %}
{% if not participant.finalist %}
{% set rowCls = "no-finalist" %}
{% endif %}
{% if participant.finalist or not onlyFinalists %}
<tr class="{{ rowCls }}">
<td>{{ participant.name }} ({{ participant.id }})</td>
{% for dance in data.results[group].dances %}
<td>
{{ participant.name }} ({{ participant.id }})
{% if participant.club is not none %}
, {{ participant.club}}
{% endif %}
</td>
{% for dance in data.resultsPerGroup[group].dances %}
{% block danceResult scoped %}
{% set res = activeGroup[participant][loop.index0] %}
<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="competition-place">
{{ res.getPlace() }} in {{ res.competitionClass }}
<span class="{% if not res.finalist %}no-finalist-dance{% endif %}">
{{ res.getNativePlace() }}
{% if res.isCombinedGroup() %}
<br />
({{ res.place }} {{ res.competitionGroup }})
{% endif %}
</span>
{% endif %}
</td>
{% endblock %}
{% endfor %}
</tr>
{% endif %}
{% endblock %}
{% endfor %}
</table>

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,49 @@
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,27 @@
class HtmlParticipant:
def __init__(self, name, id):
self.name = name
self.id = id
self.finalist = None
self.club = 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,26 @@
from .place import Place
class HtmlSingleCompetitionResult:
def __init__(self, name: str, place: Place, finalist: bool, club: str):
self.name = name
self.place = place
self.finalist = finalist
self.club = club
def __repr__(self):
place = self.place
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,25 @@
import solo_turnier
from .person import Person
class Participant(Person):
def __init__(
self,
name: str,
id: int,
finalist: bool = None,
club: str = None,
):
super().__init__(name)
self.id = id
self.finalist = finalist
self.club = club
def __repr__(self):
if self.finalist == True:
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,34 @@
import solo_turnier
from .place import Place
class SingleParticipantResult:
def __init__(
self,
competitionClass: solo_turnier.competition_class.Class_t,
competitionGroup: solo_turnier.group.Group_t,
nativeClass: solo_turnier.competition_class.CompetitionClass,
dance: str,
finalist: bool,
place: Place,
nativePlace: Place = None,
):
self.competitionClass = competitionClass
self.competitionGroup = competitionGroup
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)
def isCombinedGroup(self) -> bool:
return isinstance(self.competitionGroup, solo_turnier.group.CombinedGroup)

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,654 +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]
self.__extractPersonsFromSinglePreview(parser)
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()
with open(filePair[1], 'r') as fp:
textTab = fp.read()
parser = html_parser.HtmlParser(text, filePair[0])
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)
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', (results))
# self._fixNativePlaces(dances, results)
self._fixNativePlacesFromTable(dances, results, importedData.htmlResults)
# self.l.log(5, 'Result after native fixing: %s', pformat(results))
self.l.log(5,'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 = previewResults.results[participant][dance]
# 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 _fixNativePlacesFromTable(
self,
dances: list[str],
data: dict[types.HtmlPreviewParticipant, list[types.SingleParticipantResult]],
importedData: types.HtmlCompetitionTotalResults
):
rePlace = re.compile('([0-9]+)(?:-([0-9]+))?')
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)
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,134 @@
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(__name__)
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 competition_class.NoEClassException as ex:
self.l.info(
"The HTML file %s does not represent a solo E class. Skipping it. (%s)",
filePair[0],
ex,
)
continue
except:
self.l.warning(
"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, person.club
)
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,547 @@
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(__name__)
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 mapping: %s", invertedGroupMapping)
idToParticipantMapping = self._invertIdMapping(importedData.htmlResults)
self.l.log(5, "Id to participant mapping: %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_,
competitionGroup=tup.group,
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, club=results[0].club
)
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