Compare commits

..

No commits in common. "main" and "v0.9.5" have entirely different histories.
main ... v0.9.5

54 changed files with 1829 additions and 3093 deletions

View File

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

View File

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

25
Pipfile
View File

@ -1,25 +0,0 @@
[[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
View File

@ -1,715 +0,0 @@
{
"_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,9 +13,6 @@
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.autoComplete.extraPaths": [ "python.autoComplete.extraPaths": [
"${workspaceFolder:code}/venv/lib" "${workspaceFolder:code}/venv/lib"
], ]
"editor.formatOnSave": true,
"editor.renderWhitespace": "all",
"python.formatting.provider": "black",
} }
} }

View File

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

View File

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

View File

@ -1,34 +0,0 @@
#!/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] [Application]
name=Solo Auswertung name=Solo Auswertung
version=2.1.1 version=0.9.5
# How to launch the app - this calls the 'main' function from the 'myapp' package: # How to launch the app - this calls the 'main' function from the 'myapp' package:
entry_point=main:main entry_point=main:main
# icon=myapp.ico # icon=myapp.ico
@ -16,19 +16,40 @@ console=true
[Include] [Include]
# Packages from PyPI that your application requires, one per line # Packages from PyPI that your application requires, one per line
# These must have wheels on PyPI: # These must have wheels on PyPI:
pypi_wheels = beautifulsoup4==4.12.3 pypi_wheels = attrs==22.1.0
blinker==1.8.2 beautifulsoup4==4.11.1
blinker==1.6.2
certifi==2023.7.22
charset-normalizer==3.2.0
click==8.1.7 click==8.1.7
colorama==0.4.6 colorama==0.4.6
coloredlogs==15.0.1 coloredlogs==15.0.1
flask==3.0.3 coverage==6.5.0
debugpy==1.6.7
distlib==0.3.7
exceptiongroup==1.0.1
Flask==2.3.3
humanfriendly==10.0 humanfriendly==10.0
itsdangerous==2.2.0 idna==3.4
jinja2==3.1.4 iniconfig==1.1.1
markupsafe==2.1.5 itsdangerous==2.1.2
soupsieve==2.5 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
tabulate==0.9.0 tabulate==0.9.0
werkzeug==3.0.3 tomli==2.0.1
urllib3==2.0.5
Werkzeug==2.3.7
yarg==0.1.9
packages = solo_turnier packages = solo_turnier

View File

@ -2,17 +2,15 @@ import solo_turnier
import logging import logging
import coloredlogs import coloredlogs
def __initLogging(): def __initLogging():
logging.basicConfig() logging.basicConfig()
logging.root.setLevel(logging.NOTSET) logging.root.setLevel(logging.NOTSET)
logger = logging.getLogger("solo_turnier") logger = logging.getLogger('solo_turnier')
coloredlogs.install(level=5, logger=logger) coloredlogs.install(level=5, logger=logger)
return logger return logger
def main(): def main():
l = __initLogging() l = __initLogging()
cli = solo_turnier.cli.Cli(l) cli = solo_turnier.cli.Cli(l)
@ -20,14 +18,12 @@ def main():
batchWorker = solo_turnier.batch.BatchWorker(cli) batchWorker = solo_turnier.batch.BatchWorker(cli)
if cli.showGUI(): if cli.showGUI():
raise Exception("Not yet implemented") raise Exception('Not yet implemented')
elif cli.startFlaskServer(): elif cli.startFlaskServer():
solo_turnier.flask.startFlask( solo_turnier.flask.startFlask(
batchWorker, batchWorker,
debug=cli.getLogLevel() > 0, debug=cli.getLogLevel() > 0,
port=cli.getPort(), port=cli.getPort()
showOnlyFinalists=not cli.showAllParticipants(),
externalDebugger=cli.externalDebugger,
) )
else: else:
combinedData = batchWorker.run( combinedData = batchWorker.run(
@ -37,6 +33,5 @@ def main():
consoleOutputtter = solo_turnier.output.ConsoleOutputter() consoleOutputtter = solo_turnier.output.ConsoleOutputter()
consoleOutputtter.output(combinedData) consoleOutputtter.output(combinedData)
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View File

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

View File

@ -1,3 +1,4 @@
import solo_turnier import solo_turnier
import logging import logging
import os import os
@ -5,33 +6,29 @@ import pprint
import tabulate import tabulate
class BatchWorker: class BatchWorker:
def __init__(self, config: solo_turnier.cli.Cli): def __init__(
self.l = logging.getLogger("solo_turnier.batch") self,
config: solo_turnier.cli.Cli
):
self.l = logging.getLogger('solo_turnier.batch')
self.config = config self.config = config
def run(self, removeFilteredParicipants=True) -> solo_turnier.types.Stage5: def run(self, removeFilteredParicipants=True):
self.l.debug(self.config.__dict__) self.l.debug(self.config.__dict__)
locator = solo_turnier.html_locator.HtmlLocator() locator = solo_turnier.html_locator.HtmlLocator()
self.l.info( self.l.info('Checking for feasible preview HTML export files in "%s"', self.config.importHtmlPath())
'Checking for feasible HTML export files in "%s"', htmlCandidatesPreview = locator.findPreviewRoundCandidates(self.config.importHtmlPath())
self.config.importHtmlPath(), self.l.debug('Found HTML file candidates for preview rounds: %s', htmlCandidatesPreview)
)
htmlResultFiles = locator.findCandidates(self.config.importHtmlPath()) htmlResultFiles = locator.findCandidates(self.config.importHtmlPath())
self.l.debug( self.l.debug('Using HTML result files for result extraction: %s', htmlResultFiles)
"Using HTML result files for result extraction: %s", htmlResultFiles
)
worker = solo_turnier.workers.Worker.Worker() worker = solo_turnier.worker.Worker()
importedData = worker.collectAllData(htmlResultFiles) importedData = worker.collectAllData(htmlCandidatesPreview, htmlResultFiles)
combinedData = worker.combineData(importedData) combinedData = worker.combineData(importedData)
worker.filterOutFinalists(combinedData, removeFilteredParicipants) worker.filterOutFinalists(combinedData, removeFilteredParicipants)
outputShaper = solo_turnier.workers.OutputShaper.OutputShaper() return combinedData
shapedData = outputShaper.shapeResults(combinedData)
return shapedData

View File

@ -1,60 +1,27 @@
import argparse import argparse
import logging import logging
import debugpy
class Cli: class Cli:
def __init__(self, l: logging.Logger): def __init__(self, l: logging.Logger):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
# parser.add_argument('--gui', help='Show the GUI', action='store_true') # parser.add_argument('--gui', help='Show the GUI', action='store_true')
parser.add_argument( parser.add_argument('--no-flask', action='store_false', dest='flask', help='Disable the internal flask web server')
"--no-flask", parser.add_argument('--port', help='The port to listen for incoming requests', default='8082')
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( parser.add_argument('html', help='The path from where to look for HTML export files', nargs=1, default=['.'])
"html", parser.add_argument('-o', '--output', help='Set the output path of the script', nargs=1, default=[None])
help="The path from where to look for HTML export files", parser.add_argument('--all-participants', '-a', action='store_true', help='Show all participants not only finalists')
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( parser.add_argument('-v', '--verbose', help='Increase verbosity', action='count', default=0)
"-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(
"-d",
"--debug",
action="store_true",
help="Activate debugging during startup",
)
self.__args = parser.parse_args() self.__args = parser.parse_args()
if self.__args.debug: if self.__args.debug:
import debugpy
debugpy.listen(5678) debugpy.listen(5678)
debugpy.wait_for_client() debugpy.wait_for_client()
self.externalDebugger = self.__args.debug
map = { map = {
0: logging.ERROR, 0: logging.ERROR,
1: logging.WARN, 1: logging.WARN,
@ -64,25 +31,28 @@ class Cli:
} }
logLevel = map.get(self.__args.verbose, logging.DEBUG) logLevel = map.get(self.__args.verbose, logging.DEBUG)
l.setLevel(logLevel) l.setLevel(logLevel)
def showGUI(self): def showGUI(self):
# return self.__args.gui # return self.__args.gui
return False return False
def startFlaskServer(self): def startFlaskServer(self):
return self.__args.flask return self.__args.flask
def importHtmlPath(self): def importHtmlPath(self):
return self.__args.html[0] return self.__args.html[0]
def importCSVPath(self):
return self.__args.import_from[0]
def output(self): def output(self):
return self.__args.output[0] return self.__args.output[0]
def getLogLevel(self): def getLogLevel(self):
return self.__args.verbose return self.__args.verbose
def showAllParticipants(self): def showAllParticipants(self):
return self.__args.all_participants return self.__args.all_participants
def getPort(self): def getPort(self):
return int(self.__args.port) return int(self.__args.port)

View File

@ -1,77 +1,66 @@
import re import re
class CompetitionClass: class CompetitionClass:
def __init__(self, text: str): def __init__(self, text: str):
self.name = text self.name = text
def __repr__(self): def __repr__(self):
return f"{self.name}" return self.name
class CombinedCompetitionClass: class CombinedCompetitionClass:
def __init__( def __init__(self, clsA: CompetitionClass, clsB: CompetitionClass, clsC: CompetitionClass = None):
self,
clsA: CompetitionClass,
clsB: CompetitionClass,
clsC: CompetitionClass = None,
):
self.clsA = clsA self.clsA = clsA
self.clsB = clsB self.clsB = clsB
self.clsC = clsC self.clsC = clsC
def __repr__(self): def __repr__(self):
if self.clsC is None: if self.clsC is None:
return f"{self.clsA}/{self.clsB}" return f'{self.clsA}/{self.clsB}'
else: 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_t = CompetitionClass | CombinedCompetitionClass
class NoEClassException(Exception):
def __init__(self, *args):
super(NoEClassException, self).__init__(*args)
class CompetitionClassParser: class CompetitionClassParser:
E = CompetitionClass("E") NEWC = CompetitionClass('Newc.')
BEG = CompetitionClass('Beg.')
ADV = CompetitionClass('Adv.')
PREVIEW = CompetitionClass('Sichtung')
def __init__(self): def __init__(self):
self.mapNames = { self.mapNames = {
"E": self.E, 'Newc': self.NEWC,
'Newc.': self.NEWC,
'Newcomer': self.NEWC,
'Beg': self.BEG,
'Beg.': self.BEG,
'Beginner': self.BEG,
'Adv': self.ADV,
'Adv.': self.ADV,
'Advanced': self.ADV,
} }
self.namesPreview = ["Sichtung"] self.namesPreview = [
self.mapShortNames = self.mapNames 'Sichtung'
]
def parseClass(self, cls: str, allowPreview: bool = False) -> Class_t: def parseClass(self, cls: str, allowPreview: bool = False) -> Class_t:
# match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls) if allowPreview and cls in self.namesPreview:
# if match is not None: return self.PREVIEW
# clsA = self.mapNames[match.group(1)]
# clsB = self.mapNames[match.group(2)] match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls)
# return CombinedCompetitionClass(clsA, clsB) if match is not None:
# else: clsA = self.mapNames[match.group(1)]
# return self.mapNames[cls] clsB = self.mapNames[match.group(2)]
return CombinedCompetitionClass(clsA, clsB)
if cls in self.mapNames:
return self.mapNames[cls]
else: else:
raise NoEClassException(f'The class "{cls}" is not parsable.') return self.mapNames[cls]
def parseAbbreviatedClass(self, cls: str) -> Class_t:
return self.mapShortNames[cls]
def isPureClass(self, cls: str, allowPreview: bool = False) -> bool: def isPureClass(self, cls: str, allowPreview: bool = False) -> bool:
parsedClass = self.parseClass(cls, allowPreview) parsedClass = self.parseClass(cls, allowPreview)
return isinstance(parsedClass, CompetitionClass) return isinstance(parsedClass, CompetitionClass)
def getAllClasses(self) -> list[CompetitionClass]: def getAllClasses(self) -> list[CompetitionClass]:
return [self.NEWC, self.BEG, self.ADV] return [self.NEWC, self.BEG, self.ADV]

View File

@ -1,32 +1,26 @@
import flask import flask
import solo_turnier import solo_turnier
import logging
_l = logging.getLogger(__name__)
def startFlask( def startFlask(
batchWorker: solo_turnier.batch.BatchWorker, batchWorker: solo_turnier.batch.BatchWorker,
debug: bool = False, debug: bool = False,
port: int = 8082, port: int = 8082,
showOnlyFinalists: bool = True, showOnlyFinalists: bool = True
externalDebugger: bool = False, ):
):
app = flask.Flask(__name__) app = flask.Flask(__name__)
@app.route("/") @app.route('/')
def index(): def index():
combinedData = batchWorker.run(False) combinedData = batchWorker.run(False)
_l.debug("Show only finalists %s", showOnlyFinalists)
return flask.render_template( return flask.render_template('index.html', data=combinedData)
"index.html", data=combinedData, onlyFinalists=showOnlyFinalists
)
@app.get("/custom.css") @app.get('/custom.css')
def css(): def css():
ret = flask.render_template("custom.css", onlyFinalists=showOnlyFinalists) ret = flask.render_template(
return flask.Response(ret, mimetype="text/css") '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)
app.run(host="0.0.0.0", port=port, debug=debug, use_reloader=useReloader)

View File

@ -1,109 +1,96 @@
import re import re
class Group: class Group:
def __init__(self, text: str): def __init__(self, text: str):
self.name = text self.name = text
def __repr__(self): def __repr__(self):
return f"{self.name}" return self.name
def getContainedGroups(self):
return (self,)
class CombinedGroup: class CombinedGroup:
def __init__(self, grpA: Group, grpB: Group): def __init__(self, grpA: Group, grpB: Group):
self.clsA = grpA self.clsA = grpA
self.clsB = grpB self.clsB = grpB
def __repr__(self): 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 Group_t = Group | CombinedGroup
class GroupParser: class GroupParser:
KIN = Group("Kin.") KIN = Group('Kin.')
JUN = Group("Jun.") JUN = Group('Jun.')
JUG = Group("Jug.") JUG = Group('Jug.')
HGR = Group("Hgr.") HGR = Group('Hgr.')
MAS1 = Group("Mas. I") MAS1 = Group('Mas. I')
MAS2 = Group("Mas. II") MAS2 = Group('Mas. II')
MAS3 = Group("Mas. III") MAS3 = Group('Mas. III')
MAS4 = Group("Mas. IV") MAS4 = Group('Mas. IV')
MAS5 = Group("Mas. V") MAS5 = Group('Mas. V')
def __init__(self): def __init__(self):
self.mapNames = { self.mapNames = {
"Kin": self.KIN, 'Kin': self.KIN,
"Kin.": self.KIN, 'Kin.': self.KIN,
"Kinder": self.KIN, 'Kinder': self.KIN,
"Jun": self.JUN,
"Jun.": self.JUN, 'Jun': self.JUN,
"Junioren": self.JUN, 'Jun.': self.JUN,
"Jug": self.JUG, 'Junioren': self.JUN,
"Jug.": self.JUG,
"Jugend": self.JUG, 'Jug': self.JUG,
"Hgr": self.HGR, 'Jug.': self.JUG,
"HGr": self.HGR, 'Jugend': self.JUG,
"Hgr.": self.HGR,
"HGr.": self.HGR, 'Hgr': self.HGR,
"Hauptgruppe": self.HGR, 'HGr': self.HGR,
"Mas. I": self.MAS1, 'Hgr.': self.HGR,
"Mas. II": self.MAS2, 'HGr.': self.HGR,
"Mas. III": self.MAS3, 'Hauptgruppe': self.HGR,
"Mas. IV": self.MAS4,
"Mas. V": self.MAS5, 'Mas. I': self.MAS1,
"Mas I": self.MAS1, 'Mas. II': self.MAS2,
"Mas II": self.MAS2, 'Mas. III': self.MAS3,
"Mas III": self.MAS3, 'Mas. IV': self.MAS4,
"Mas IV": self.MAS4, 'Mas. V': self.MAS5,
"Mas V": self.MAS5, 'Mas I': self.MAS1,
"Masters I": self.MAS1, 'Mas II': self.MAS2,
"Masters II": self.MAS2, 'Mas III': self.MAS3,
"Masters III": self.MAS3, 'Mas IV': self.MAS4,
"Masters IV": self.MAS4, 'Mas V': self.MAS5,
"Masters V": self.MAS5, 'Masters I': self.MAS1,
'Masters II': self.MAS2,
'Masters III': self.MAS3,
'Masters IV': self.MAS4,
'Masters V': self.MAS5,
} }
def parseGroup(self, cls: str) -> Group_t: def parseClass(self, cls: str) -> Group_t:
match = re.compile("^(\\w+\\.?)/(\\w+\\.?)$").match(cls) match = re.compile('^(\\w+\\.?)/(\\w+\\.?)$').match(cls)
if match is not None: if match is not None:
grpA = self.mapNames[match.group(1)] grpA = self.mapNames[match.group(1)]
grpB = self.mapNames[match.group(2)] grpB = self.mapNames[match.group(2)]
return CombinedGroup(grpA, grpB) return CombinedGroup(grpA, grpB)
else: else:
return self.mapNames[cls] return self.mapNames[cls]
def isPureGroup(self, cls: str) -> bool: def isPureClass(self, cls: str) -> bool:
parsedGroup = self.parseGroup(cls) parsedClass = self.parseClass(cls)
return isinstance(parsedGroup, Group) return isinstance(parsedClass, Group)
def getGroups(self) -> list[Group]: def getGroups(self) -> list[Group]:
return [ return[
GroupParser.KIN, GroupParser.KIN,
GroupParser.JUN, GroupParser.JUN,
GroupParser.JUG, GroupParser.JUG,
GroupParser.HGR, GroupParser.HGR,
GroupParser.MAS1, GroupParser.MAS1,
GroupParser.MAS2, GroupParser.MAS2,
GroupParser.MAS3, GroupParser.MAS3,
GroupParser.MAS4, GroupParser.MAS4,
GroupParser.MAS5, GroupParser.MAS5
] ]
def getGroupsAsSortedList(self, groups) -> list[Group]: def getGroupsAsSortedList(self, groups) -> list[Group]:
mainGroups = [x for x in self.getGroups() if x in groups] return [x for x in self.getGroups() if x in groups]
additionalGroups = set(groups).difference(mainGroups)
return mainGroups + list(additionalGroups)

View File

@ -1,33 +1,36 @@
import os import os
import logging import logging
class HtmlLocator: class HtmlLocator:
def __init__(self): 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): def __findRecursivelyCandidates(self, path: str, fileName: str):
ret = [] ret = []
ls = os.listdir(path) ls = os.listdir(path)
if fileName in ls and os.path.isfile(os.path.join(path, fileName)): if fileName in ls and os.path.isfile(os.path.join(path, fileName)):
ret.append(os.path.join(path, fileName)) ret.append(os.path.join(path, fileName))
for p in ls: for p in ls:
subPath = os.path.join(path, p) subPath = os.path.join(path, p)
if os.path.isdir(subPath): if os.path.isdir(subPath):
ret = ret + self.__findRecursivelyCandidates(subPath, fileName) ret = ret + self.__findRecursivelyCandidates(subPath, fileName)
return ret return ret
def __fingMatchingTabs(self, ergCandidate): def __fingMatchingTabs(self, ergCandidate):
path = os.path.dirname(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): if not os.path.exists(tabPath):
tabPath = None tabPath = None
return (ergCandidate, tabPath) return (ergCandidate, tabPath)
def findCandidates(self, path: str): 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] candidates = [self.__fingMatchingTabs(x) for x in candidatesErg]
return candidates return candidates
def findPreviewRoundCandidates(self, path: str):
candidates = self.__findRecursivelyCandidates(path, 'tabges.htm')
return candidates

View File

@ -3,211 +3,91 @@ from bs4 import BeautifulSoup
import logging import logging
import re import re
from .types import HtmlParticipant, HtmlResultTotalTable from .types import HtmlPreviewParticipant, HtmlParticipant, HtmlResultTotalTable
from .types import HtmlResultImport from .types import HtmlPreviewImport as HtmlImport, HtmlResultImport
from .group import GroupParser from .group import GroupParser
from .competition_class import CompetitionClassParser from .competition_class import CompetitionClassParser
import solo_turnier
class IncompleteRoundException(Exception): class IncompleteRoundException(Exception):
def __init__(self, *args): def __init__(self, *args):
super(IncompleteRoundException, self).__init__(*args) super(IncompleteRoundException, self).__init__(*args)
class CannotParseRowException(Exception):
def __init__(self, *args):
super(CannotParseRowException, self).__init__(*args)
class HtmlParser: class HtmlParser:
def __init__(self, text: str, fileName: str = None): def __init__(self, text: str, fileName: str = None):
self.l = logging.getLogger("solo_turnier.html_parser") self.l = logging.getLogger('solo_turnier.html_parser')
self.soup = BeautifulSoup(text, "html.parser") self.soup = BeautifulSoup(text, 'html.parser')
self.fileName = fileName self.fileName = fileName
self.groupParser = GroupParser() self.groupParser = GroupParser()
self.classParser = CompetitionClassParser() self.classParser = CompetitionClassParser()
def __repr__(self): def __repr__(self):
if self.fileName is None: if self.fileName is None:
return "HtmlParser(direct text)" return 'HtmlParser(direct text)'
else: else:
return f"HtmlParser({self.fileName})" return f'HtmlParser({self.fileName})'
def getEventTitle(self): 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: if title is None:
title = self.getEventTitle() title = self.getEventTitle()
match = re.compile('.*?OT, Solos (.*?)(?: ".*")?').fullmatch(title) match = re.compile('.*?ETW, Solos (.*)').match(title)
if match is None: if match is None:
self.l.debug( raise Exception(f'Cannot parse title "{title}"')
'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) rest = match.group(1)
rawGroup, rawClass, dance = rest.split(" ", 2) rawGroup, rawClass, dance = rest.split(' ', 2)
return { return {
"dance": dance.strip(), 'dance': dance.strip(),
"class_": self.classParser.parseClass(rawClass, True), 'class_': str(self.classParser.parseClass(rawClass, True)),
"group": self.groupParser.parseGroup(rawGroup), 'group': str(self.groupParser.parseClass(rawGroup))
} }
def parseResult(self) -> HtmlResultImport: def parseResult(self):
participants = {} participants = {}
nameRegex = re.compile("(.*) \\(([0-9]+)\\)") def __parseRows(rows, finalist: bool):
def __parseRow(row):
tds = row.find_all('td')
def __parseNameAndId(string: str, tds) -> tuple[str, str]: if len(tds) != 2:
match = nameRegex.fullmatch(string) return
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
def __parseRows(rows, parsers): regex = re.compile('(.*) \\(([0-9]+)\\)')
def parseRow(row):
for parser in parsers: place = tds[0].contents[0]
try:
parser(row("td")) match = regex.fullmatch(tds[1].contents[0])
return if match is None:
except CannotParseRowException: raise Exception(f'Could not match {tds} to regex search pattern')
pass name = match.group(1)
number = match.group(2)
# No parser was found if we get here.
self.l.error("Cannot parse row in table.")
participant = HtmlParticipant(name, number)
participant.finalist = finalist
participants[participant] = place
for row in rows: 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): def __parseFirstTable(table):
roundName = table.tr.td.contents[0] roundName = table.tr.td.contents[0]
if roundName != "Endrunde": if roundName != 'Endrunde':
self.l.warning("Found table with round name %s.", roundName) self.l.warning('Found table with round name %s.', roundName)
raise IncompleteRoundException("Could not parse HTML file") raise IncompleteRoundException('Could not parse HTML file')
def __parseFormationRow(tds): __parseRows(table.find_all('tr')[2:], True)
__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 __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: for table in tables:
__parseRows( __parseRows(table.find_all('tr'), False)
table.find_all("tr"),
[
__parseAllSolosQualifiedFormation,
__parseAllSolosQualifiedPair,
__parsePairRow,
__parseFormationRow,
__parseSeparatorRow,
__parseRoundHeading,
],
)
tables = self.soup.find("div", class_="extract").find_all("table")
tables = self.soup.find('div', class_='extract').find_all('table')
try: try:
if len(tables) > 0: if len(tables) > 0:
__parseFirstTable(tables[0]) __parseFirstTable(tables[0])
@ -218,116 +98,167 @@ class HtmlParser:
# title = self.soup.find('div', class_='eventhead').table.tr.td.contents[0] # title = self.soup.find('div', class_='eventhead').table.tr.td.contents[0]
ret = HtmlResultImport(participants) # ret = HtmlImport(title, participants)
ret = HtmlResultImport(participants)
return ret 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): def parseIndividualResult(self, competitionGroup, competitionClass, dance):
participants = {} participants = {}
rePlaceParser = re.compile("([0-9]+)(?:-([0-9]+))?")
groupParser = solo_turnier.group.GroupParser()
classParser = solo_turnier.competition_class.CompetitionClassParser()
def __parseTable(table): def __parseTable(table):
rows = table.find_all("tr") rows = table.find_all('tr')
def __getIds(): def __getIds():
row = rows[1] row = rows[1]
entries = row("td") entries = row('td')
entries = entries[1:] entries = entries[1:]
entries = [x for x in entries if len(x.contents[0].strip()) > 0] entries = [x for x in entries if len(x.contents[0].strip()) > 0]
return [x.contents[0].strip() for x in entries] return [x.contents[0].strip() for x in entries]
ids = __getIds() ids = __getIds()
numIds = len(ids) 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 findRowIndex(prefixStr):
def isRowMatchingCriteria(row): def isRowMatchingCriteria(row):
if row.td.contents[0].startswith(prefixStr): if row.td.contents[0].startswith(prefixStr):
return True return True
return False return False
l = list(map(isRowMatchingCriteria, rows)) l = list(map(isRowMatchingCriteria, rows))
if True not in l: if True not in l:
return None return None
return l.index(True) return l.index(True)
def getPlaces(): def getPlaces():
placeRowIdx = findRowIndex("Platz von") placeRowIdx = findRowIndex('Platz von')
placeTags = rows[placeRowIdx]("td")[1 : (numIds + 1)] placeTags = rows[placeRowIdx]('td')[1:(numIds+1)]
def getSinglePlaceStr(tag): def getSinglePlaceStr(tag):
for br in tag("br"): for br in tag('br'):
br.replace_with("-") br.replace_with('-')
tag.smooth() tag.smooth()
rawStr = tag.contents[0].strip() rawStr = tag.contents[0].strip()
if rawStr.endswith("-"): if rawStr.endswith('-'):
rawStr = rawStr[:-1] 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)) places = list(map(getSinglePlaceStr, placeTags))
return places return places
places = getPlaces() places = getPlaces()
self.l.log(5, "Found places: %s", places) self.l.log(5, 'Found places: %s', places)
def getClass(): def getClass():
classRow = findRowIndex("Startklasse") classRow = findRowIndex('Startklasse')
if classRow is not None: if classRow is not None:
classTags = rows[classRow]("td")[1 : (numIds + 1)] classTags = rows[classRow]('td')[1:(numIds+1)]
return list( return list(map(lambda x: x.contents[0], classTags))
map(
lambda x: classParser.parseAbbreviatedClass(x.contents[0]),
classTags,
)
)
return None return None
classes = getClass() classes = getClass()
self.l.log(5, "Classes: %s", classes) self.l.log(5, 'Classes: %s', classes)
def getGroups(): def getGroups():
groupRow = findRowIndex("Startgruppe") groupRow = findRowIndex('Startgruppe')
if groupRow is not None: if groupRow is not None:
groupTags = rows[groupRow]("td")[1 : (numIds + 1)] classTags = rows[groupRow]('td')[1:(numIds+1)]
return list( return list(map(lambda x: x.contents[0], classTags))
map(lambda x: groupParser.parseGroup(x.contents[0]), groupTags)
)
return None return None
groups = getGroups() groups = getGroups()
self.l.log(5, "Groups: %s", groups) self.l.log(5, 'Groups: %s', groups)
for idx, id in enumerate(ids): for idx, id in enumerate(ids):
cls = classes[idx] if classes is not None else None cls = classes[idx] if classes is not None else None
grp = groups[idx] if groups is not None else None grp = groups[idx] if groups is not None else None
tup = solo_turnier.types.CompetitionTuple( tup = (competitionGroup, competitionClass, dance, id)
competitionGroup, competitionClass, dance, int(id) participants[tup] = (places[idx], cls, grp)
)
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: for table in tables:
__parseTable(table) __parseTable(table)
return HtmlResultTotalTable(participants) return HtmlResultTotalTable( participants)

View File

@ -1,3 +1,4 @@
import logging import logging
from tabulate import tabulate from tabulate import tabulate
import pprint import pprint
@ -5,18 +6,17 @@ import pprint
import solo_turnier import solo_turnier
from solo_turnier import types from solo_turnier import types
sections = ("Kin.", "Jun.", "Jug.", "Sonst") sections = ('Kin.', 'Jun.', 'Jug.', 'Sonst')
sectionMap = { sectionMap = {
"Kin.": "Kinder", 'Kin.': 'Kinder',
"Jun.": "Junioren", 'Jun.': 'Junioren',
"Jug.": "Jugend", 'Jug.': 'Jugend',
"Sonst": "Undefiniert", 'Sonst': 'Undefiniert'
} }
class AbstractOutputter: class AbstractOutputter:
def __init__(self): def __init__(self):
self.worker = solo_turnier.workers.DataWorker.DataWorker() self.worker = solo_turnier.worker.DataWorker()
self.groups = [] self.groups = []
self.dances = [] self.dances = []
self.showIds = False self.showIds = False
@ -24,26 +24,24 @@ class AbstractOutputter:
def getRowData(self, person: solo_turnier.worker.ResultPerson, results): def getRowData(self, person: solo_turnier.worker.ResultPerson, results):
mappedResults = self.worker.mapPersonResultsToDanceList(results, self.dances) mappedResults = self.worker.mapPersonResultsToDanceList(results, self.dances)
if self.showIds: if self.showIds:
name = f"{person.name} ({person.id})" name = f'{person.name} ({person.id})'
else: else:
name = person.name name = person.name
ret = [name] ret = [name]
for result in mappedResults: for result in mappedResults:
if result is None: if result is None:
ret.append("") ret.append('')
elif result.finalist == False: elif result.finalist == False:
ret.append("x") ret.append('x')
elif result.place == result.placeTo: elif result.place == result.placeTo:
ret.append(f"{result.place}. ({result.class_})") ret.append(f'{result.place}. ({result.class_})')
else: else:
ret.append(f"{result.place}.-{result.placeTo}. ({result.class_})") ret.append(f'{result.place}.-{result.placeTo}. ({result.class_})')
return ret return ret
def getTabularData(self, data, section): def getTabularData(self, data, section):
sortedPersons, self.showIds = self.worker.sortPersonsInGroup( sortedPersons, self.showIds = self.worker.sortPersonsInGroup(self.groups[section])
self.groups[section]
)
tableData = [] tableData = []
for person in sortedPersons: for person in sortedPersons:
@ -51,105 +49,65 @@ class AbstractOutputter:
return tableData return tableData
class ConsoleOutputter(AbstractOutputter): class ConsoleOutputter(AbstractOutputter):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.l = logging.getLogger("solo_turnier.output.console") self.l = logging.getLogger('solo_turnier.output.console')
def __outputSection(self, data, section): def __outputSection(self, data, section):
tableData = self.getTabularData(data, section) tableData = self.getTabularData(data, section)
tableData = [["Name"] + self.dances] + tableData tableData = [['Name'] + self.dances] + tableData
print(f"Einzeltanzwettbewerb der {sectionMap[section]}") print(f"Einzeltanzwettbewerb der {sectionMap[section]}")
print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) print(tabulate(tableData, headers='firstrow', tablefmt='fancy_grid'))
print() print()
def _reshapeRow( def _outputGroup(self, group: solo_turnier.group.Group, groupResults: types.TotalGroupResult):
self, print(f"Einzeltanzwettbewerb der Gruppe {group}")
results: list[solo_turnier.types.SingleParticipantResult],
dances: list[str], tableData = [['Tanz'] + groupResults.dances]
) -> list[solo_turnier.types.SingleParticipantResult]: participants = list(groupResults.results.keys())
ret = [None for x in dances]
for result in results:
if result.dance not in dances:
self.l.error(
"Result in unknown dance found in table. This is a bug. (%s)",
result,
)
continue
idx = dances.index(result.dance)
ret[idx] = result
return ret
def _outputGroup(
self,
group: solo_turnier.group.Group,
groupResults: 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)) participants.sort(key=lambda x: (x.id, x.name))
for participant in participants: for participant in participants:
results = groupResults.resultsInGroup[participant] results = groupResults.results[participant]
self.l.log(5, "Results of %s: %s", participant, results)
def mapResultColumn(result: types.SingleParticipantResult): def mapResultColumn(result: types.SingleParticipantResult):
def getPlace(place, placeTo): def getPlace(place, placeTo):
if placeTo is None: if placeTo is None:
return f"{place}." return f'{place}.'
else: else:
return f"{place}.-{placeTo}." return f'{place}.-{placeTo}.'
if result is None: if result is None:
return "" return ''
placeNative = str(result.nativePlace) placeNative = getPlace(result.placeNative, result.placeNativeTo)
place = str(result.place) place = getPlace(result.place, result.placeTo)
lineOne = f"{placeNative}" lineOne = f'{placeNative} ({result.nativeClass})'
lineTwo = f'[{place} in {result.competitionClass}]'
lines = [lineOne]
groupCompetition = result.competitionGroup
if isinstance(groupCompetition, solo_turnier.group.CombinedGroup):
lineTwo = f"[{place} in {groupCompetition}]"
lines.append(lineTwo)
lines = [lineOne, lineTwo]
if not result.finalist: 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) 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) 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)) def output(self, data: types.State4):
print(tabulate(tableData, headers="firstrow", tablefmt="fancy_grid")) for idx, group in enumerate(data.groups):
def output(self, data: types.Stage5):
for idx, group in enumerate(data.resultsPerGroup):
if idx > 0: if idx > 0:
print() print()
self.l.debug("Output for group %s", group) self.l.debug('Output for group %s', group)
self._outputGroup(group, data.resultsPerGroup[group]) self._outputGroup(group, data.results[group])
# self.groups = self.worker.collectPersonsInGroups(data) # self.groups = self.worker.collectPersonsInGroups(data)
# self.dances = self.worker.getAllDancesInCompetitions(data) # self.dances = self.worker.getAllDancesInCompetitions(data)

View File

@ -1,28 +1,34 @@
class Person: 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.firstName = firstName
self.lastName = lastName self.lastName = lastName
self.club = club self.club = club
self.group = group self.group = group
def __eq__(self, o): def __eq__(self, o):
if not isinstance(o, Person): if not isinstance(o, Person):
False False
return ( return (
self.firstName == o.firstName self.firstName == o.firstName and
and self.lastName == o.lastName self.lastName == o.lastName and
and self.club == o.club self.club == o.club and
and self.group == o.group self.group == o.group
) )
def getTuple(self): def getTuple(self):
return (self.firstName, self.lastName, self.club) return (self.firstName, self.lastName, self.club)
def __repr__(self): 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): def __hash__(self):
return ( return self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__()
self.firstName.__hash__() + self.lastName.__hash__() + self.club.__hash__()
)

101
src/solo_turnier/reader.py Normal file
View File

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

View File

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

View File

@ -1,23 +1,21 @@
import solo_turnier.competition_class import solo_turnier.competition_class
import pytest import pytest
@pytest.fixture(params=range(9)) @pytest.fixture(params=range(9))
def fix_pureClass(request): def fix_pureClass(request):
cases = ( cases = (
("Newc", "Newc."), ('Newc', 'Newc.'),
("Newc.", "Newc."), ('Newc.', 'Newc.'),
("Newcomer", "Newc."), ('Newcomer', 'Newc.'),
("Beg", "Beg."), ('Beg', 'Beg.'),
("Beg.", "Beg."), ('Beg.', 'Beg.'),
("Beginner", "Beg."), ('Beginner', 'Beg.'),
("Adv", "Adv."), ('Adv', 'Adv.'),
("Adv.", "Adv."), ('Adv.', 'Adv.'),
("Advanced", "Adv."), ('Advanced', 'Adv.'),
) )
return cases[request.param] return cases[request.param]
def test_pureClassParsing(fix_pureClass): def test_pureClassParsing(fix_pureClass):
className = fix_pureClass[0] className = fix_pureClass[0]
expected = fix_pureClass[1] expected = fix_pureClass[1]
@ -30,43 +28,39 @@ def test_pureClassParsing(fix_pureClass):
assert parser.isPureClass(className) assert parser.isPureClass(className)
def test_classParsingWithPreview(): def test_classParsingWithPreview():
parser = solo_turnier.competition_class.CompetitionClassParser() 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 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(): def test_classParsingInvalidPreview():
parser = solo_turnier.competition_class.CompetitionClassParser() parser = solo_turnier.competition_class.CompetitionClassParser()
try: try:
parser.parseClass("Sichtung") parser.parseClass('Sichtung')
assert False assert False
except: except:
assert True assert True
try: try:
parser.isPureClass("Sichtung") parser.isPureClass('Sichtung')
assert False assert False
except: except:
assert True assert True
@pytest.fixture(params=range(4)) @pytest.fixture(params=range(4))
def fix_combinedClass(request): def fix_combinedClass(request):
cases = ( cases = (
("Newc/Beg", "Newc./Beg."), ('Newc/Beg', 'Newc./Beg.'),
("Newc./Beg", "Newc./Beg."), ('Newc./Beg', 'Newc./Beg.'),
("Beginner/Adv", "Beg./Adv."), ('Beginner/Adv', 'Beg./Adv.'),
("Beg/Adv", "Beg./Adv."), ('Beg/Adv', 'Beg./Adv.'),
) )
return cases[request.param] return cases[request.param]
def test_combinedClassParsing(fix_combinedClass): def test_combinedClassParsing(fix_combinedClass):
className = fix_combinedClass[0] className = fix_combinedClass[0]
expected = fix_combinedClass[1] expected = fix_combinedClass[1]

View File

@ -2,13 +2,12 @@ import solo_turnier.reader
import os import os
import json import json
def test_import(): 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) reader = solo_turnier.reader.CSVResultReader(fileName)
ret = reader.readFile() 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) expected = json.load(fp)
assert ret == expected assert ret == expected

View File

@ -1,23 +1,21 @@
import solo_turnier.group import solo_turnier.group
import pytest import pytest
@pytest.fixture(params=range(9)) @pytest.fixture(params=range(9))
def fix_pureClass(request): def fix_pureClass(request):
cases = ( cases = (
("Kin", "Kin."), ('Kin', 'Kin.'),
("Kin.", "Kin."), ('Kin.', 'Kin.'),
("Kinder", "Kin."), ('Kinder', 'Kin.'),
("Jun", "Jun."), ('Jun', 'Jun.'),
("Jun.", "Jun."), ('Jun.', 'Jun.'),
("Junioren", "Jun."), ('Junioren', 'Jun.'),
("Jug", "Jug."), ('Jug', 'Jug.'),
("Jug.", "Jug."), ('Jug.', 'Jug.'),
("Jugend", "Jug."), ('Jugend', 'Jug.'),
) )
return cases[request.param] return cases[request.param]
def test_pureClassParsing(fix_pureClass): def test_pureClassParsing(fix_pureClass):
className = fix_pureClass[0] className = fix_pureClass[0]
expected = fix_pureClass[1] expected = fix_pureClass[1]
@ -30,18 +28,16 @@ def test_pureClassParsing(fix_pureClass):
assert parser.isPureClass(className) assert parser.isPureClass(className)
@pytest.fixture(params=range(4)) @pytest.fixture(params=range(4))
def fix_combinedClass(request): def fix_combinedClass(request):
cases = ( cases = (
("Kin/Jun", "Kin./Jun."), ('Kin/Jun', 'Kin./Jun.'),
("Kin./Jun", "Kin./Jun."), ('Kin./Jun', 'Kin./Jun.'),
("Junioren/Jug", "Jun./Jug."), ('Junioren/Jug', 'Jun./Jug.'),
("Jun/Jug", "Jun./Jug."), ('Jun/Jug', 'Jun./Jug.'),
) )
return cases[request.param] return cases[request.param]
def test_combinedClassParsing(fix_combinedClass): def test_combinedClassParsing(fix_combinedClass):
className = fix_combinedClass[0] className = fix_combinedClass[0]
expected = fix_combinedClass[1] expected = fix_combinedClass[1]

View File

@ -1,17 +1,17 @@
import os import os
import solo_turnier.html_locator import solo_turnier.html_locator
def test_fetchLocationCandidates(): 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) relFolder = os.path.relpath(folder)
locator = solo_turnier.html_locator.HtmlLocator() locator = solo_turnier.html_locator.HtmlLocator()
candidates = locator.findCandidates(relFolder) candidates = locator.findCandidates(relFolder)
expected = [ expected = [
"solo_turnier/tests/html_locator/export/2-bar/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/erg.htm',
"solo_turnier/tests/html_locator/export/3-baz/subfolder/4-baz/erg.htm", 'solo_turnier/tests/html_locator/export/3-baz/subfolder/4-baz/erg.htm'
] ]
assert set(candidates) == set(expected) assert set(candidates) == set(expected)

View File

@ -4,22 +4,20 @@ import json
import solo_turnier.html_parser import solo_turnier.html_parser
@pytest.fixture(scope='module', params=range(2))
@pytest.fixture(scope="module", params=range(2))
def dataProviderHtmlParser(request): def dataProviderHtmlParser(request):
variant = str(request.param + 1) variant = str(request.param+1)
dir = os.path.join(os.path.dirname(__file__), "html_parser", "erg", variant) dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'erg', variant)
htmlFile = os.path.join(dir, "erg.htm") htmlFile = os.path.join(dir, 'erg.htm')
jsonFile = os.path.join(dir, "expected.json") jsonFile = os.path.join(dir, 'expected.json')
with open(htmlFile, "r") as fp: with open(htmlFile, 'r') as fp:
html = fp.read() html = fp.read()
with open(jsonFile, "r") as fp: with open(jsonFile, 'r') as fp:
jsonContent = json.load(fp) jsonContent = json.load(fp)
return (html, jsonContent) return (html, jsonContent)
def test_extractDataFromHtml(dataProviderHtmlParser): def test_extractDataFromHtml(dataProviderHtmlParser):
htmlString = dataProviderHtmlParser[0] htmlString = dataProviderHtmlParser[0]
expected = dataProviderHtmlParser[1] expected = dataProviderHtmlParser[1]
@ -31,71 +29,67 @@ def test_extractDataFromHtml(dataProviderHtmlParser):
for i in actualResult.participants: for i in actualResult.participants:
participants[i] = actualResult.participants[i].__dict__ participants[i] = actualResult.participants[i].__dict__
assert actualResult.title == expected["title"] assert actualResult.title == expected['title']
assert participants == expected["participants"] assert participants == expected['participants']
@pytest.fixture(params=range(6)) @pytest.fixture(params=range(6))
def fixture_guessDataFromTitle(request): def fixture_guessDataFromTitle(request):
cases = { cases = {
"09.07.2022 - ETW, Solos Jun. Beginner Jive": { '09.07.2022 - ETW, Solos Jun. Beginner Jive': {
"class_": "Beg.", 'class_': 'Beg.',
"dance": "Jive", 'dance': 'Jive',
"group": "Jun.", 'group': 'Jun.'
}, },
"09.07.2022 - ETW, Solos Jun. Newc./Beg. Rumba": { '09.07.2022 - ETW, Solos Jun. Newc./Beg. Rumba': {
"class_": "Newc./Beg.", 'class_': 'Newc./Beg.',
"dance": "Rumba", 'dance': 'Rumba',
"group": "Jun.", 'group': 'Jun.'
}, },
"09.07.2022 - ETW, Solos Kin./Jun. Beginner Cha Cha": { '09.07.2022 - ETW, Solos Kin./Jun. Beginner Cha Cha': {
"class_": "Beg.", 'class_': 'Beg.',
"dance": "Cha Cha", 'dance': 'Cha Cha',
"group": "Kin./Jun.", 'group': 'Kin./Jun.'
}, },
"09.07.2022 - ETW, Solos Kin. Newcomer Samba": { '09.07.2022 - ETW, Solos Kin. Newcomer Samba': {
"class_": "Newc.", 'class_': 'Newc.',
"dance": "Samba", 'dance': 'Samba',
"group": "Kin.", 'group': 'Kin.'
}, },
"09.07.2022 - ETW, Solos Jugend Beg./Adv. Wiener Walzer": { '09.07.2022 - ETW, Solos Jugend Beg./Adv. Wiener Walzer': {
"class_": "Beg./Adv.", 'class_': 'Beg./Adv.',
"dance": "Wiener Walzer", 'dance': 'Wiener Walzer',
"group": "Jug.", 'group': 'Jug.'
}, },
"09.07.2022 - ETW, Solos Jugend Sichtung Wiener Walzer": { '09.07.2022 - ETW, Solos Jugend Sichtung Wiener Walzer': {
"class_": "Sichtung", 'class_': 'Sichtung',
"dance": "Wiener Walzer", 'dance': 'Wiener Walzer',
"group": "Jug.", 'group': 'Jug.'
}, },
} }
keys = list(cases.keys()) keys = list(cases.keys())
key = keys[request.param] key = keys[request.param]
return (key, cases[key]) return (key, cases[key])
def test_guessDataFromTitle(fixture_guessDataFromTitle): def test_guessDataFromTitle(fixture_guessDataFromTitle):
parser = solo_turnier.html_parser.HtmlParser("") parser = solo_turnier.html_parser.HtmlParser('')
ret = parser.guessDataFromHtmlTitle(fixture_guessDataFromTitle[0]) ret = parser.guessDataFromHtmlTitle(fixture_guessDataFromTitle[0])
assert ret == fixture_guessDataFromTitle[1] assert ret == fixture_guessDataFromTitle[1]
@pytest.fixture(params=range(1)) @pytest.fixture(params=range(1))
def fixture_parsePreparationResult(request): def fixture_parsePreparationResult(request):
variant = str(request.param + 1) variant = str(request.param+1)
dir = os.path.join(os.path.dirname(__file__), "html_parser", "tabges", variant) dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'tabges', variant)
htmlFile = os.path.join(dir, "tabges.htm") htmlFile = os.path.join(dir, 'tabges.htm')
jsonFile = os.path.join(dir, "expected.json") jsonFile = os.path.join(dir, 'expected.json')
with open(htmlFile, "r") as fp: with open(htmlFile, 'r') as fp:
html = fp.read() html = fp.read()
with open(jsonFile, "r") as fp: with open(jsonFile, 'r') as fp:
jsonContent = json.load(fp) jsonContent = json.load(fp)
return (html, jsonContent) return (html, jsonContent)
def test_parsePreparationResult(fixture_parsePreparationResult): def test_parsePreparationResult(fixture_parsePreparationResult):
html = fixture_parsePreparationResult[0] html = fixture_parsePreparationResult[0]
jsonContent = fixture_parsePreparationResult[1] jsonContent = fixture_parsePreparationResult[1]
@ -105,27 +99,25 @@ def test_parsePreparationResult(fixture_parsePreparationResult):
assert ret == jsonContent assert ret == jsonContent
@pytest.fixture(params=range(1)) @pytest.fixture(params=range(1))
def fixture_cleanPreparationImport(request): def fixture_cleanPreparationImport(request):
variant = str(request.param + 1) variant = str(request.param+1)
dir = os.path.join(os.path.dirname(__file__), "html_parser", "tabges", variant) dir = os.path.join(os.path.dirname(__file__), 'html_parser', 'tabges', variant)
srcFile = os.path.join(dir, "expected.json") srcFile = os.path.join(dir, 'expected.json')
expectedFile = os.path.join(dir, "cleaned.json") expectedFile = os.path.join(dir, 'cleaned.json')
with open(srcFile, "r") as fp: with open(srcFile, 'r') as fp:
source = json.load(fp) source = json.load(fp)
with open(expectedFile, "r") as fp: with open(expectedFile, 'r') as fp:
expected = json.load(fp) expected = json.load(fp)
return (source, expected) return (source, expected)
def test_cleanPreparationImport(fixture_cleanPreparationImport): def test_cleanPreparationImport(fixture_cleanPreparationImport):
src = fixture_cleanPreparationImport[0] src = fixture_cleanPreparationImport[0]
expected = fixture_cleanPreparationImport[1] expected = fixture_cleanPreparationImport[1]
parser = solo_turnier.html_parser.HtmlParser("") parser = solo_turnier.html_parser.HtmlParser('')
parser.cleanPreparationRoundImport(src) parser.cleanPreparationRoundImport(src)
assert src == expected assert src == expected

View File

@ -1,11 +1,10 @@
import pytest import pytest
import solo_turnier.types as types import solo_turnier.types as types
def test_HtmlPreviewParticipant_eq(): def test_HtmlPreviewParticipant_eq():
name = "Max Mustermann" name = 'Max Mustermann'
id = 123 id = 123
group = "Kin" group = 'Kin'
participant = types.HtmlPreviewParticipant(name, id, group) participant = types.HtmlPreviewParticipant(name, id, group)
l = [] l = []
@ -14,6 +13,6 @@ def test_HtmlPreviewParticipant_eq():
assert participant in l assert participant in l
assert types.HtmlPreviewParticipant(name, id, group) 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, 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,298 +4,178 @@ import json
import pytest import pytest
import pytest_mock import pytest_mock
def __importJSONData(name): def __importJSONData(name):
path = os.path.join(os.path.dirname(__file__), "worker", name) path = os.path.join(os.path.dirname(__file__), 'worker', name)
with open(path, "r") as fp: with open(path, 'r') as fp:
return json.load(fp) return json.load(fp)
@pytest.fixture @pytest.fixture
def fixture_csvExtractor(): def fixture_csvExtractor():
data = __importJSONData("csvImport.json") data = __importJSONData('csvImport.json')
expected = __importJSONData("csvImportResult.json") expected = __importJSONData('csvImportResult.json')
return (data, expected) return (data, expected)
def test_csvExtractor(fixture_csvExtractor): def test_csvExtractor(fixture_csvExtractor):
extractor = worker.CSVExtractor() extractor = worker.CSVExtractor()
mapped = extractor.mapCSVImport(fixture_csvExtractor[0]) mapped = extractor.mapCSVImport(fixture_csvExtractor[0])
assert len(mapped) == len(fixture_csvExtractor[1]) 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 assert mapped[i].__dict__ == elem
def test_extractPersonFromRow(): def test_extractPersonFromRow():
row = worker.ResultRow( row = worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Beg./Adv.')
"Max",
"Mustermann",
"TSC Entenhausen",
"2",
"Kin",
"Adv.",
"Rumba",
"2",
"2",
"Kin.",
"Beg./Adv.",
)
person = worker.ResultPerson.extractFromResultRow(row) person = worker.ResultPerson.extractFromResultRow(row)
expected = { expected = {
"firstName": "Max", 'firstName': 'Max',
"lastName": "Mustermann", 'lastName': 'Mustermann',
"name": "Max Mustermann", 'name': 'Max Mustermann',
"club": "TSC Entenhausen", 'club': 'TSC Entenhausen',
"id": None, 'id': None,
"group": None, 'group': None
} }
assert person.__dict__ == expected assert person.__dict__ == expected
def test_extractCompetitionFromRow(): def test_extractCompetitionFromRow():
row = worker.ResultRow( row = worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Beg./Adv.')
"Max",
"Mustermann",
"TSC Entenhausen",
"2",
"Kin",
"Adv.",
"Rumba",
"2",
"2",
"Kin.",
"Beg./Adv.",
)
competition = worker.CompetitionResult.extractFromResultRow(row) competition = worker.CompetitionResult.extractFromResultRow(row)
expected = { expected = {
"dance": "Rumba", 'dance': 'Rumba',
"class_": "Adv.", 'class_': 'Adv.',
"group": "Kin", 'group': 'Kin',
"place": "2", 'place': '2',
"placeTo": "2", 'placeTo': '2',
"id": 2, 'id': 2,
"finalist": None, 'finalist': None,
"competitionGroup": "Kin.", 'competitionGroup': 'Kin.',
"competitionClass": "Beg./Adv.", 'competitionClass': 'Beg./Adv.'
} }
assert competition.__dict__ == expected assert competition.__dict__ == expected
def test_combineRowsByPerson(): def test_combineRowsByPerson():
rows = [ rows = [
worker.ResultRow( worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Cha Cha', '-', '-', 'Kin.', 'Adv.'),
"Max", worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Adv.', 'Rumba', '2', '2', 'Kin.', 'Adv.'),
"Mustermann", worker.ResultRow('Max', 'Mustermann', 'TSC Entenhausen', '2', 'Kin', 'Beg.', 'Jive', '1', '1', 'Kin.', 'Beg.'),
"TSC Entenhausen", worker.ResultRow('Maxime', 'Musterfrau', '1. SC Entenhausen', '1', 'Kin', 'Adv.', 'Rumba', '1', '1', 'Kin.', 'Adv.')
"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() dataWorker = worker.DataWorker()
result = dataWorker.combineRowsByPerson(rows) result = dataWorker.combineRowsByPerson(rows)
expected = { expected = {
worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen"): [ worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen'): [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', '2', 'Kin.', 'Adv.'),
"Rumba", "Kin", "Adv.", "2", "2", "2", "Kin.", "Adv." worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', '2', 'Kin.', 'Beg.')
),
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 assert result == expected
def test_checkUniqueIds_True(): def test_checkUniqueIds_True():
person1 = worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen") person1 = worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen')
person2 = worker.ResultPerson("Maxime", "Musterfrau", "1. SC Entenhausen") person2 = worker.ResultPerson('Maxime', 'Musterfrau', '1. SC Entenhausen')
data = { data = {
person1: [ person1: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', 2, 'Kin.', 'Beg.')
),
worker.CompetitionResult(
"Jive", "Kin", "Beg.", "1", "1", 2, "Kin.", "Beg."
),
], ],
person2: [ person2: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '1', '1', 1, 'Kin.', 'Adv.')
"Rumba", "Kin", "Adv.", "1", "1", 1, "Kin.", "Adv." ]
)
],
} }
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
assert dataWorker.checkUniqueIds(data) == True assert dataWorker.checkUniqueIds(data) == True
assert person1.id == 2 assert person1.id == 2
assert person2.id == 1 assert person2.id == 1
def test_checkUniqueIds_False(): def test_checkUniqueIds_False():
person1 = worker.ResultPerson("Max", "Mustermann", "TSC Entenhausen") person1 = worker.ResultPerson('Max', 'Mustermann', 'TSC Entenhausen')
person2 = worker.ResultPerson("Maxime", "Musterfrau", "1. SC Entenhausen") person2 = worker.ResultPerson('Maxime', 'Musterfrau', '1. SC Entenhausen')
data = { data = {
person1: [ person1: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Kin', 'Beg.', '1', '1', 3, 'Kin.', 'Beg.')
),
worker.CompetitionResult(
"Jive", "Kin", "Beg.", "1", "1", 3, "Kin.", "Beg."
),
], ],
person2: [ person2: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin', 'Adv.', '1', '1', 1, 'Kin.', 'Adv.')
"Rumba", "Kin", "Adv.", "1", "1", 1, "Kin.", "Adv." ]
)
],
} }
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
assert dataWorker.checkUniqueIds(data) == False assert dataWorker.checkUniqueIds(data) == False
assert person1.id == None assert person1.id == None
assert person2.id == 1 assert person2.id == 1
@pytest.fixture(params=range(5)) @pytest.fixture(params=range(5))
def fixture_consolidateGroups(request): def fixture_consolidateGroups(request):
person1 = worker.ResultPerson("Max 1", "Mustermann", "TSC Entenhausen") person1 = worker.ResultPerson('Max 1', 'Mustermann', 'TSC Entenhausen')
person2 = worker.ResultPerson("Max 2", "Mustermann", "TSC Entenhausen") person2 = worker.ResultPerson('Max 2', 'Mustermann', 'TSC Entenhausen')
person3 = worker.ResultPerson("Max 3", "Mustermann", "TSC Entenhausen") person3 = worker.ResultPerson('Max 3', 'Mustermann', 'TSC Entenhausen')
person4 = worker.ResultPerson("Max 4", "Mustermann", "TSC Entenhausen") person4 = worker.ResultPerson('Max 4', 'Mustermann', 'TSC Entenhausen')
# persons = (person1, person2, person3, person4) # persons = (person1, person2, person3, person4)
dict1 = { dict1 = {
person1: [ person1: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '1', '1', 3, 'Kin.', 'Beg.')
),
worker.CompetitionResult(
"Jive", "Kin.", "Beg.", "1", "1", 3, "Kin.", "Beg."
),
] ]
} }
dict2 = { dict2 = {
person2: [ person2: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Kin./Jun.', 'Beg.', '1', '1', 3, 'Kin./Jun.', 'Beg.')
),
worker.CompetitionResult(
"Jive", "Kin./Jun.", "Beg.", "1", "1", 3, "Kin./Jun.", "Beg."
),
] ]
} }
dict3 = { dict3 = {
person3: [ person3: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.')
"Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv."
)
] ]
} }
dict4 = { dict4 = {
person4: [ person4: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin./Jun.', 'Adv.', '2', '2', 2, 'Kin./Jun.', 'Adv.')
"Rumba", "Kin./Jun.", "Adv.", "2", "2", 2, "Kin./Jun.", "Adv."
)
] ]
} }
dict5 = { dict5 = {
person4: [ person4: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin./Jun.', 'Adv.', '2', '2', 2, 'Kin./Jun.', 'Adv.'),
"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(
"Cha Cha", "Jun./Jug.", "Beg.", "3", "4", 2, "Jun./Jug.", "Beg."
),
] ]
} }
cases = ( cases = (
(dict1 | dict3, (True, False), {}), (dict1|dict3, (True, False), {}),
(dict1 | dict2 | dict3, (True, True), {}), (dict1|dict2|dict3, (True, True), {}),
(dict4, (False, False), {person4: "Kin./Jun."}), (dict4, (False, False), {person4: 'Kin./Jun.'}),
(dict1 | dict2 | dict3 | dict4, (False, True), {person4: "Kin./Jun."}), (dict1|dict2|dict3|dict4, (False, True), {person4: 'Kin./Jun.'}),
(dict5, (True, True), {person4: "Jun."}), (dict5, (True, True), {person4: 'Jun.'}),
) )
return cases[request.param] return cases[request.param]
@pytest.fixture(params=range(2)) @pytest.fixture(params=range(2))
def fixture_consolidateGroups_fail(request, fixture_consolidateGroups): def fixture_consolidateGroups_fail(request, fixture_consolidateGroups):
person = worker.ResultPerson("Max 5", "Mustermann", "TSC Entenhausen") person = worker.ResultPerson('Max 5', 'Mustermann', 'TSC Entenhausen')
dict1 = { dict1 = {
person: [ person: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Jun.', 'Beg.', '1', '1', 3, 'Jun.', 'Adv.')
),
worker.CompetitionResult(
"Jive", "Jun.", "Beg.", "1", "1", 3, "Jun.", "Adv."
),
] ]
} }
dict2 = { dict2 = {
person: [ person: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Adv.', '2', '2', 2, 'Kin.', 'Adv.'),
"Rumba", "Kin.", "Adv.", "2", "2", 2, "Kin.", "Adv." worker.CompetitionResult('Jive', 'Hgr', 'Beg.', '1', '1', 3, 'Hgr', '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] return cases[request.param]
def test_consolidateGroups(fixture_consolidateGroups): def test_consolidateGroups(fixture_consolidateGroups):
data = fixture_consolidateGroups[0] data = fixture_consolidateGroups[0]
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
@ -303,8 +183,7 @@ def test_consolidateGroups(fixture_consolidateGroups):
assert dataWorker.consolidateGroups(data) == fixture_consolidateGroups[1] assert dataWorker.consolidateGroups(data) == fixture_consolidateGroups[1]
for person in data: 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): def test_consolidateGroups_failing(fixture_consolidateGroups_fail):
data = fixture_consolidateGroups_fail data = fixture_consolidateGroups_fail
@ -313,143 +192,105 @@ def test_consolidateGroups_failing(fixture_consolidateGroups_fail):
with pytest.raises(Exception): with pytest.raises(Exception):
dataWorker.consolidateGroups(data) dataWorker.consolidateGroups(data)
def test_createHtmlLUT(mocker): def test_createHtmlLUT(mocker):
mock = mocker.patch("solo_turnier.html_parser.HtmlParser.guessDataFromHtmlTitle") mock = mocker.patch('solo_turnier.html_parser.HtmlParser.guessDataFromHtmlTitle')
mock.side_effect = [ mock.side_effect= [
{"group": "group1", "class_": "class1", "dance": "dance1"}, {'group': 'group1', 'class_': 'class1', 'dance': 'dance1'},
{"group": "group2", "class_": "class2", "dance": "dance2"}, {'group': 'group2', 'class_': 'class2', 'dance': 'dance2'},
{"group": "group3", "class_": "class3", "dance": "dance3"}, {'group': 'group3', 'class_': 'class3', 'dance': 'dance3'},
] ]
importMock1 = mocker.patch("solo_turnier.html_parser.HtmlImport") importMock1 = mocker.patch('solo_turnier.html_parser.HtmlImport')
importMock2 = mocker.patch("solo_turnier.html_parser.HtmlImport") importMock2 = mocker.patch('solo_turnier.html_parser.HtmlImport')
importMock3 = mocker.patch("solo_turnier.html_parser.HtmlImport") importMock3 = mocker.patch('solo_turnier.html_parser.HtmlImport')
importMock1.title = "Fake title 1" importMock1.title = 'Fake title 1'
importMock2.title = "Fake title 2" importMock2.title = 'Fake title 2'
importMock3.title = "Fake title 3" importMock3.title = 'Fake title 3'
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
structure = dataWorker._createHtmlLUT([importMock1, importMock2, importMock3]) structure = dataWorker._createHtmlLUT([importMock1, importMock2, importMock3])
expected = { expected = {
("group1", "class1", "dance1"): importMock1, ('group1', 'class1', 'dance1'): importMock1,
("group2", "class2", "dance2"): importMock2, ('group2', 'class2', 'dance2'): importMock2,
("group3", "class3", "dance3"): importMock3, ('group3', 'class3', 'dance3'): importMock3,
} }
assert expected == structure assert expected == structure
def test_mergeHtmlData(mocker): def test_mergeHtmlData(mocker):
person1 = worker.ResultPerson("Max 1", "Mustermann", "TSC Entenhausen") person1 = worker.ResultPerson('Max 1', 'Mustermann', 'TSC Entenhausen')
person2 = worker.ResultPerson("Max 2", "Mustermann", "TSC Entenhausen") person2 = worker.ResultPerson('Max 2', 'Mustermann', 'TSC Entenhausen')
person3 = worker.ResultPerson("Max 3", "Mustermann", "TSC Entenhausen") person3 = worker.ResultPerson('Max 3', 'Mustermann', 'TSC Entenhausen')
person4 = worker.ResultPerson("Max 4", "Mustermann", "TSC Entenhausen") person4 = worker.ResultPerson('Max 4', 'Mustermann', 'TSC Entenhausen')
data = { data = {
person1: [ person1: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '1', '1', 1, 'Kin./Jun.', 'Beg.'),
"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( worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Beg.', '1', '1', 1, 'Kin.', 'Newc./Beg.'),
"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: [ person2: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '2', '2', 2, 'Kin./Jun.', 'Beg.'),
"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( worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Newc.', '1', '1', 2, 'Kin.', 'Newc./Beg.'),
"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: [ person3: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Jun.', 'Beg.', '1', '1', 3, 'Kin./Jun.', 'Beg.'),
"Rumba", "Jun.", "Beg.", "1", "1", 3, "Kin./Jun.", "Beg."
),
# worker.CompetitionResult('Cha Cha', 'Jun.', 'Adv.', '1', '1', 3, 'Kin.', 'Adv.'), # worker.CompetitionResult('Cha Cha', 'Jun.', 'Adv.', '1', '1', 3, 'Kin.', 'Adv.'),
# worker.CompetitionResult('Jive', 'Jun.', 'Beg.', '2', '2', 3, 'Kin.', 'Beg.'), # worker.CompetitionResult('Jive', 'Jun.', 'Beg.', '2', '2', 3, 'Kin.', 'Beg.'),
# worker.CompetitionResult('Langs. Walzer', 'Jun.', 'Newc./Beg.', '1', '1', 3, 'Kin.', 'Beg.'), # worker.CompetitionResult('Langs. Walzer', 'Jun.', 'Newc./Beg.', '1', '1', 3, 'Kin.', 'Beg.'),
], ],
person4: [ person4: [
worker.CompetitionResult( worker.CompetitionResult('Rumba', 'Kin.', 'Beg.', '3', '3', 4, 'Kin./Jun.', 'Beg.'),
"Rumba", "Kin.", "Beg.", "3", "3", 4, "Kin./Jun.", "Beg."
),
# worker.CompetitionResult('Cha Cha', 'Kin.', 'Adv.', '1', '1', 4, 'Kin.', 'Adv.'), # worker.CompetitionResult('Cha Cha', 'Kin.', 'Adv.', '1', '1', 4, 'Kin.', 'Adv.'),
# worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '2', '2', 4, 'Kin.', 'Beg.'), # worker.CompetitionResult('Jive', 'Kin.', 'Beg.', '2', '2', 4, 'Kin.', 'Beg.'),
# worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Newc./Beg.', '1', '1', 4, 'Kin.', 'Beg.'), # worker.CompetitionResult('Langs. Walzer', 'Kin.', 'Newc./Beg.', '1', '1', 4, 'Kin.', 'Beg.'),
], ],
} }
htmlParticipant1Dance1 = 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) htmlParticipant1Dance2 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.', True)
htmlParticipant1Dance3 = html_parser.HtmlParticipant( htmlParticipant1Dance3 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.-2.', True)
"Max 1 Mustermann", "1.-2.", True htmlParticipant1Dance4 = html_parser.HtmlParticipant('Max 1 Mustermann', '1.', True)
)
htmlParticipant1Dance4 = html_parser.HtmlParticipant("Max 1 Mustermann", "1.", True) htmlParticipant2Dance1 = html_parser.HtmlParticipant('Max 2 Mustermann', '2.', True)
htmlParticipant2Dance2 = html_parser.HtmlParticipant('Max 2 Mustermann', '2.', True)
htmlParticipant2Dance3 = html_parser.HtmlParticipant('Max 2 Mustermann', '1.-2.', True)
htmlParticipant2Dance4 = html_parser.HtmlParticipant('Max 2 Mustermann', '1.', True)
htmlParticipant3Dance1 = html_parser.HtmlParticipant('Max 3 Mustermann', '1.', True)
htmlParticipant2Dance1 = html_parser.HtmlParticipant("Max 2 Mustermann", "2.", True) htmlParticipant4Dance1 = html_parser.HtmlParticipant('Max 4 Mustermann', '3.', False)
htmlParticipant2Dance2 = html_parser.HtmlParticipant("Max 2 Mustermann", "2.", True)
htmlParticipant2Dance3 = html_parser.HtmlParticipant(
"Max 2 Mustermann", "1.-2.", True
)
htmlParticipant2Dance4 = html_parser.HtmlParticipant("Max 2 Mustermann", "1.", True)
htmlParticipant3Dance1 = html_parser.HtmlParticipant("Max 3 Mustermann", "1.", True)
htmlParticipant4Dance1 = html_parser.HtmlParticipant(
"Max 4 Mustermann", "3.", False
)
htmlParticipantsDance1 = { htmlParticipantsDance1 = {
"1": htmlParticipant1Dance1, '1': htmlParticipant1Dance1,
"2": htmlParticipant2Dance1, '2': htmlParticipant2Dance1,
"3": htmlParticipant3Dance1, '3': htmlParticipant3Dance1,
"4": htmlParticipant4Dance1, '4': htmlParticipant4Dance1
} }
htmlParticipantsDance2 = { htmlParticipantsDance2 = {
"1": htmlParticipant1Dance2, '1': htmlParticipant1Dance2,
"2": htmlParticipant2Dance2, '2': htmlParticipant2Dance2,
} }
htmlParticipantsDance3 = { htmlParticipantsDance3 = {
"1": htmlParticipant1Dance3, '1': htmlParticipant1Dance3,
"2": htmlParticipant2Dance3, '2': htmlParticipant2Dance3,
} }
htmlParticipantsDance4 = { htmlParticipantsDance4 = {
"1": htmlParticipant1Dance4, '1': htmlParticipant1Dance4,
"2": htmlParticipant2Dance4, '2': htmlParticipant2Dance4,
} }
htmlCompetition1 = html_parser.HtmlImport( htmlCompetition1 = html_parser.HtmlImport('ETW, Solos Kin./Jun. Beginner Rumba', htmlParticipantsDance1)
"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)
htmlCompetition2 = html_parser.HtmlImport( htmlCompetition4 = html_parser.HtmlImport('ETW, Solos Kin. Newc./Beg. Langs. Walzer', htmlParticipantsDance4)
"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 = worker.DataWorker()
dataWorker.mergeHtmlData( dataWorker.mergeHtmlData(data, [htmlCompetition1, htmlCompetition2, htmlCompetition3, htmlCompetition4])
data, [htmlCompetition1, htmlCompetition2, htmlCompetition3, htmlCompetition4]
)
person1Finalist = [c.finalist for c in data[person1]] person1Finalist = [c.finalist for c in data[person1]]
person2Finalist = [c.finalist for c in data[person2]] person2Finalist = [c.finalist for c in data[person2]]
@ -465,59 +306,33 @@ def test_mergeHtmlData(mocker):
person1: [True, True, True, True], person1: [True, True, True, True],
person2: [True, True, True, True], person2: [True, True, True, True],
person3: [True], person3: [True],
person4: [False], person4: [False],
} }
assert finalists == expectedFinalists assert finalists == expectedFinalists
@pytest.fixture(params=range(4)) @pytest.fixture(params=range(4))
def fixture_getAllDancesInCompetition(request, mocker): def fixture_getAllDancesInCompetition(request, mocker):
def mockCompetition(comp): def mockCompetition(comp):
def mockUser(): def mockUser():
return mocker.patch("solo_turnier.worker.ResultPerson") return mocker.patch('solo_turnier.worker.ResultPerson')
def mockDances(dances): def mockDances(dances):
def mockDance(name): def mockDance(name):
mock = mocker.patch("solo_turnier.worker.CompetitionResult") mock = mocker.patch('solo_turnier.worker.CompetitionResult')
mock.dance = name mock.dance = name
return mock return mock
return [mockDance(d) for d in dances] return [mockDance(d) for d in dances]
return {mockUser(): mockDances(dances) for dances in comp} return {mockUser(): mockDances(dances) for dances in comp}
cases = ( cases = (
([["Samba"]], ["Samba"]), ([['Samba']], ['Samba']),
([["Samba", "Rumba"], ["Cha Cha"]], ["Samba", "Cha Cha", "Rumba"]), ([['Samba', 'Rumba'], ['Cha Cha']], ['Samba', 'Cha Cha', 'Rumba']),
( ([['Samba', 'Rumba'], ['Cha Cha', 'Tango', 'Langs. Walzer']], ['Samba', 'Cha Cha', 'Rumba', 'Langs. Walzer', 'Tango']),
[["Samba", "Rumba"], ["Cha Cha", "Tango", "Langs. Walzer"]], ([['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", "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] case = cases[request.param]
return (mockCompetition(case[0]), case[1]) return (mockCompetition(case[0]), case[1])
def test_getAllDancesInCompetitions(fixture_getAllDancesInCompetition): def test_getAllDancesInCompetitions(fixture_getAllDancesInCompetition):
print(fixture_getAllDancesInCompetition) print(fixture_getAllDancesInCompetition)
data = fixture_getAllDancesInCompetition[0] data = fixture_getAllDancesInCompetition[0]
@ -525,76 +340,57 @@ def test_getAllDancesInCompetitions(fixture_getAllDancesInCompetition):
ret = dataWorker.getAllDancesInCompetitions(data) ret = dataWorker.getAllDancesInCompetitions(data)
assert ret == fixture_getAllDancesInCompetition[1] assert ret == fixture_getAllDancesInCompetition[1]
def test_collectPersonsInGroups(mocker): def test_collectPersonsInGroups(mocker):
def mockPerson(group): def mockPerson(group):
mock = mocker.patch("solo_turnier.worker.ResultPerson") mock = mocker.patch('solo_turnier.worker.ResultPerson')
mock.group = group mock.group = group
return mock return mock
persons = ( persons = (
mockPerson("Kin."), mockPerson('Kin.'), mockPerson('Kin.'), mockPerson('Jun.'),
mockPerson("Kin."), mockPerson('Kin.'), mockPerson(None), mockPerson('Jug.'),
mockPerson("Jun."), mockPerson(None), mockPerson('Kin./Jun.'), mockPerson('Jun.')
mockPerson("Kin."),
mockPerson(None),
mockPerson("Jug."),
mockPerson(None),
mockPerson("Kin./Jun."),
mockPerson("Jun."),
) )
data = {p: [] for p in persons} data = {p: [] for p in persons}
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
groups = dataWorker.collectPersonsInGroups(data) groups = dataWorker.collectPersonsInGroups(data)
assert groups["Kin."] == [persons[0], persons[1], persons[3]] assert groups['Kin.'] == [persons[0], persons[1], persons[3]]
assert groups["Jun."] == [persons[2], persons[8]] assert groups['Jun.'] == [persons[2], persons[8]]
assert groups["Jug."] == [persons[5]] assert groups['Jug.'] == [persons[5]]
assert groups["Sonst"] == [persons[4], persons[6], persons[7]] assert groups['Sonst'] == [persons[4], persons[6], persons[7]]
def test_sortPersons_withId(mocker): def test_sortPersons_withId(mocker):
def mockPerson(id): def mockPerson(id):
mock = mocker.patch("solo_turnier.worker.ResultPerson") mock = mocker.patch('solo_turnier.worker.ResultPerson')
mock.id = id mock.id = id
return mock return mock
persons = [mockPerson(2), mockPerson(1), mockPerson(5), mockPerson(3)] persons = [mockPerson(2), mockPerson(1), mockPerson(5), mockPerson(3)]
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
sorted, showIds = dataWorker.sortPersonsInGroup(persons) sorted, showIds = dataWorker.sortPersonsInGroup(persons)
assert sorted == [persons[1], persons[0], persons[3], persons[2]] assert sorted == [persons[1], persons[0], persons[3], persons[2]]
assert showIds == True assert showIds == True
def test_sortPersons_withoutId(mocker): def test_sortPersons_withoutId(mocker):
def mockPerson(name): def mockPerson(name):
mock = mocker.patch("solo_turnier.worker.ResultPerson") mock = mocker.patch('solo_turnier.worker.ResultPerson')
mock.id = 3 mock.id = 3
mock.name = name mock.name = name
mock.club = "TSC Entenhausen" mock.club = 'TSC Entenhausen'
return mock return mock
persons = [mockPerson('Max'), mockPerson('Isabel'), mockPerson('Reimund'), mockPerson('Anna')]
persons = [
mockPerson("Max"),
mockPerson("Isabel"),
mockPerson("Reimund"),
mockPerson("Anna"),
]
persons[2].id = None persons[2].id = None
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
sorted, showIds = dataWorker.sortPersonsInGroup(persons) sorted, showIds = dataWorker.sortPersonsInGroup(persons)
assert sorted == [persons[3], persons[1], persons[0], persons[2]] assert sorted == [persons[3], persons[1], persons[0], persons[2]]
assert showIds == False assert showIds == False
def test_mapPersonResultsToDanceList(mocker): def test_mapPersonResultsToDanceList(mocker):
def mockResult(dance): def mockResult(dance):
mock = mocker.patch("solo_turnier.worker.CompetitionResult") mock = mocker.patch('solo_turnier.worker.CompetitionResult')
mock.dance = dance mock.dance = dance
return mock return mock
dances = ['Cha Cha', 'Rumba', 'Langs. Walzer', 'Quickstep']
dances = ["Cha Cha", "Rumba", "Langs. Walzer", "Quickstep"] results = [mockResult('Rumba'), mockResult('Quickstep'), mockResult('Cha Cha')]
results = [mockResult("Rumba"), mockResult("Quickstep"), mockResult("Cha Cha")]
dataWorker = worker.DataWorker() dataWorker = worker.DataWorker()
mappedResults = dataWorker.mapPersonResultsToDanceList(results, dances) mappedResults = dataWorker.mapPersonResultsToDanceList(results, dances)
assert mappedResults == [results[2], results[0], None, results[1]] assert mappedResults == [results[2], results[0], None, results[1]]

349
src/solo_turnier/types.py Normal file
View File

@ -0,0 +1,349 @@
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

@ -1,18 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,35 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,9 +0,0 @@
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

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

View File

@ -1,17 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,25 +0,0 @@
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

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

View File

@ -1,10 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,15 +0,0 @@
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,12 +1,24 @@
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: class HtmlPerson:
def __init__(self, name, id, group): def __init__(self, name, id, group):
self.name = name self.name = name
self.id = id self.id = id
self.group = group self.group = group
def __repr__(self): def __repr__(self):
return f"{self.name} ({self.id}, {self.group})" return f'{self.name} ({self.id}, {self.group})'
def __eq__(self, o): def __eq__(self, o):
if not isinstance(o, HtmlPerson): if not isinstance(o, HtmlPerson):
return False return False
@ -15,33 +27,661 @@ class HtmlPerson:
def __hash__(self): def __hash__(self):
return str(self).__hash__() return str(self).__hash__()
class ResultPerson: 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.firstName = firstName
self.lastName = lastName self.lastName = lastName
self.name = f"{firstName} {lastName}" self.name = f'{firstName} {lastName}'
self.club = club self.club = club
self.id = id self.id = id
self.group = group self.group = group
@staticmethod
def extractFromResultRow(row: ResultRow):
return ResultPerson(
firstName=row.firstName,
lastName=row.lastName,
club=row.club
)
def __eq__(self, o): def __eq__(self, o):
if not isinstance(o, ResultPerson): if not isinstance(o, ResultPerson):
return False return False
return ( return (
self.firstName == o.firstName self.firstName == o.firstName and
and self.lastName == o.lastName self.lastName == o.lastName and
and self.club == o.club self.club == o.club and
and self.id == o.id self.id == o.id
) )
def __repr__(self): def __repr__(self):
if self.id is None: if self.id is None:
return f"{self.name} ({self.club})" return f'{self.name} ({self.club})'
else: else:
return f"{self.name} ({self.club}) [{self.id}]" return f'{self.name} ({self.club}) [{self.id}]'
def __hash__(self): def __hash__(self):
text = str(self) text = str(self)
return text.__hash__() 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()
parser = html_parser.HtmlParser(text, filePair[0])
if filePair[1] is None:
parserTab = None
else:
with open(filePair[1], 'r') as fp:
textTab = fp.read()
parserTab = html_parser.HtmlParser(textTab, filePair[1])
try:
data = parser.guessDataFromHtmlTitle()
except:
self.l.error('Cannot parse HTML file %s to check if it is a valid result. Check manually.', filePair[0])
continue
try:
guessedClass = classParser.parseClass(data['class_'])
except:
self.l.error('Issue parsing class of file %s. Check manually.', filePair[0])
continue
self.l.debug('Fetched result data: %s, guessed class %s', data, guessedClass)
ret[filePair] = (parser, parserTab)
return ret
def _extractPlace(self, placeStr: str):
s = placeStr.replace('.', '')
matches = self.rePlaceSingle.fullmatch(s)
if matches is not None:
return (int(matches.group(1)), None)
matches = self.rePlaceDouble.fullmatch(s)
if matches is not None:
return (int(matches.group(1)), int(matches.group(2)))
self.l.error('Could not parse place string "%s"', placeStr)
raise Exception('Place cannot be parsed')
def _analyzeSingleParser(self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults):
data = parser.guessDataFromHtmlTitle()
competitionClass = data['class_']
competitionGroup = data['group']
dance = data['dance']
result = parser.parseResult()
self.l.log(5, 'Raw data extracted: %s', result)
for person in result.results.keys():
placeStr = result.results[person]
place, placeTo = self._extractPlace(placeStr)
competitionResult = types.HtmlSingleCompetitionResult(person.name, place, placeTo, person.finalist)
results.add(competitionGroup, competitionClass, dance, person.id, competitionResult)
#
def _analyzeIndividualResults(self, parser: html_parser.HtmlParser, results: types.HtmlCompetitionTotalResults):
data = parser.guessDataFromHtmlTitle()
competitionClass = data['class_']
competitionGroup = data['group']
dance = data['dance']
result = parser.parseIndividualResult(competitionGroup, competitionClass, dance)
self.l.log(5, 'Found individual results: %s', result.participants)
results.tabges.update(result.participants)
def extractAllData(self, parsers: ParserList_t) -> types.HtmlCompetitionTotalResults:
ret = types.HtmlCompetitionTotalResults()
for fileNameTuple in parsers:
fileName = fileNameTuple[0]
self.l.debug('Extracting data from file %s', fileName)
self._analyzeSingleParser(parsers[fileNameTuple][0], ret)
if parsers[fileNameTuple][1] is None:
self.l.info('Skipping extraction of individual result as class is not yet finished.')
else:
self.l.debug('Fetching individual result of combined competitions in %s', fileName)
self._analyzeIndividualResults(parsers[fileNameTuple][1], ret)
return ret
class DataWorker:
def __init__(self):
self.l = logging.getLogger('solo_turnier.worker.DataWorker')
def combineRowsByPerson(self, rows: list[ResultRow]) -> dict[ResultPerson, list[CompetitionResult]]:
ret = {}
for row in rows:
result = CompetitionResult.extractFromResultRow(row)
if result.place == '-' or result.placeTo == '-':
continue
person = ResultPerson.extractFromResultRow(row)
if person not in ret:
ret[person] = []
ret[person].append(result)
return ret
def checkUniqueIds(self, data: dict[ResultPerson, list[CompetitionResult]]) -> bool:
unique = True
for person in data:
ids = set([c.id for c in data[person]])
if len(ids) == 1:
person.id = list(ids)[0]
else:
unique = False
return unique
"""
Return a tuple
The first one is True, if all persons could be unambiguously identified a group
The second one is True if there was the need to override a group but it was possible to extract from other data
The second one can be seen as a warning
"""
def consolidateGroups(self, data:dict[ResultPerson, list[CompetitionResult]]) -> tuple[bool, bool]:
ambiguous = False
warnChange = False
unambiguousGroups = set(['Kin.', 'Jun.', 'Jug.'])
combinations = set(['Kin./Jun.', 'Jun./Jug.'])
for person in data:
groupsRaw = set([c.group for c in data[person]])
unknown = groupsRaw.difference(unambiguousGroups).difference(combinations)
if len(unknown) > 0:
raise Exception(f'There were unknown groups found for {person}: {unknown}')
numUnambiguousGroups = len(groupsRaw.intersection(unambiguousGroups))
if numUnambiguousGroups == 0:
if len(groupsRaw) == 2:
warnChange = True
person.group = 'Jun.'
else:
ambiguous = True
if len(groupsRaw) == 1:
person.group = list(groupsRaw)[0]
elif numUnambiguousGroups == 1:
if len(groupsRaw.intersection(combinations)) > 0:
warnChange = True
person.group = list(groupsRaw.intersection(unambiguousGroups))[0]
else:
raise Exception(f'{person} cannot have different groups.')
return (not ambiguous, warnChange)
def _createHtmlLUT(self, htmlImports: list[html_parser.HtmlImport]):
ret = {}
parser = html_parser.HtmlParser('')
for imp in htmlImports:
parsed = parser.guessDataFromHtmlTitle(imp.title)
key = (parsed['group'], parsed['class_'], parsed['dance'])
ret[key] = imp
self.l.debug('LUT[%s] = %s', key, imp)
self.l.debug('LUT completed')
return ret
def mergeHtmlData(self, data:dict[ResultPerson, list[CompetitionResult]], htmlImports: list[html_parser.HtmlImport]):
lut = self._createHtmlLUT(htmlImports)
for person in data:
for competition in data[person]:
key = (competition.competitionGroup, competition.competitionClass, competition.dance)
htmlImport = lut[key]
participant = htmlImport.participants[str(competition.id)]
if participant.name != person.name:
self.l.error(f'Names for {person} and participant in HTML import ({participant}) do not match. Please check carefully.')
competition.finalist = participant.finalist
def getAllDancesInCompetitions(self, data:dict[ResultPerson, list[CompetitionResult]]) -> list[str]:
allDances = [
'Samba', 'Cha Cha', 'Rumba', 'Paso Doble', 'Jive',
'Langs. Walzer', 'Tango', 'Wiener Walzer', 'Slowfox', 'Quickstep'
]
dancesPresent = {d: False for d in allDances}
for person in data:
for competition in data[person]:
dancesPresent[competition.dance] = True
return [d for d in allDances if dancesPresent[d]]
def collectPersonsInGroups(self, data:dict[ResultPerson, list[CompetitionResult]]) -> list[tuple[str, list[ResultPerson]]]:
groups = {
'Kin.': [p for p in data.keys() if p.group == 'Kin.'],
'Jun.': [p for p in data.keys() if p.group == 'Jun.'],
'Jug.': [p for p in data.keys() if p.group == 'Jug.'],
}
found = groups['Kin.'] + groups['Jun.'] + groups['Jug.']
groups['Sonst'] = [p for p in data.keys() if p not in found]
return groups
def sortPersonsInGroup(self, persons: list[ResultPerson]) -> list[ResultPerson]:
ids = [p.id for p in persons]
def decorateByName(p: ResultPerson):
return (f'{p.name} ({p.club})', p)
def decorateById(p: ResultPerson):
return (p.id, p)
if any([id == None for id in ids]):
# We need to sort by name
decorated = [decorateByName(p) for p in persons]
showIds = False
else:
decorated = [decorateById(p) for p in persons]
showIds = True
decorated.sort()
return ([d[1] for d in decorated], showIds)
def mapPersonResultsToDanceList(self, results: list[CompetitionResult], dances: list[str]) -> list[CompetitionResult|None]:
ret = []
for dance in dances:
competitions = [c for c in results if c.dance == dance]
if len(competitions) == 0:
ret.append(None)
elif len(competitions) > 1:
raise Exception(f'Multiple competitions with the same dance "{dance}" found.')
else:
ret.append(competitions[0])
return ret
class Worker:
def __init__(self):
self.l = logging.getLogger('solo_turnier.worker.Worker')
self._allDances = (
['Samba', 'Cha Cha', 'Rumba', 'Paso Doble', 'Jive'] +
['Langs. Walzer', 'Tango', 'Wiener Walzer', 'Slowfox', 'Quickstep']
)
def collectAllData(
self,
htmlCandidatesPreview: list[str],
htmlResultsFileNames: list[str]
) -> types.State3:
previewWorker = PreviewWorker()
self.l.info('Filtering for pure preview rounds.')
parsers = previewWorker.filterFilesPreview(htmlCandidatesPreview)
self.l.debug('Remaining files: %s', list(parsers.keys()))
self.l.info('Extracting person data from the preview rounds.')
previewImport = previewWorker.importAllData(parsers)
self.l.debug('Total preview imported participants: %s', pformat(previewImport.participants))
self.l.log(5, 'Total preview results: %s', pformat(previewImport.results))
resultExtractor = ResultExtractor()
resultParsers = resultExtractor.getAllParsers(htmlResultsFileNames)
htmlResults = resultExtractor.extractAllData(resultParsers)
self.l.info('Overall result data extracted: %s', pformat(htmlResults.results))
return types.State3(previewImport, htmlResults)
def combineData(self, importedData: types.State3):
self.l.info('Starting to build data sets.')
groups = self._extractGroups(importedData)
self.l.debug('Found groups in the dataset: %s', groups)
totalResult = {}
for group in groups:
self.l.debug('Collecting data for total result of group %s', group)
dances = self._extractDancesPerGroup(importedData, group)
self.l.log(5, 'Found dances in group %s: %s', group, dances)
participants = self._extractParticipantsPerGroup(importedData.previewImport, group)
self.l.log(5, 'Related participants %s', participants)
results = {}
for participant in participants:
self.l.log(5, 'Collecting data for %s', participant)
resultsOfParticipant = self._getResultOfSingleParticipant(
participant, group, importedData.previewImport,
importedData.htmlResults, dances
)
self.l.log(5, 'Obtained result %s', resultsOfParticipant)
results[participant] = resultsOfParticipant
self.l.log(5, 'Result before native fixing: %s', (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

@ -1,47 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,134 +0,0 @@
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

@ -1,547 +0,0 @@
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

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