From 559c3b6d694fadeea12c08ec61bb9e8cd1eb82d2 Mon Sep 17 00:00:00 2001 From: stanislas Date: Tue, 5 Mar 2024 00:24:28 +0100 Subject: [PATCH] Choose a user with booking availability among many --- Dockerfile | 2 +- poetry.lock | 238 +-- pyproject.toml | 5 + resa_padel/booking.py | 17 +- .../gestion_sports/gestion_sports_config.py | 2 + .../gestion_sports_connector.py | 61 +- .../gestion_sports_html_parser.py | 16 + .../gestion_sports/gestion_sports_platform.py | 34 +- resa_padel/gestion_sports/payload_builders.py | 78 +- .../gestion-sports/users_bookings.txt | 1 + tests/data/mes_resas.html | 1363 +++++++++++++++++ tests/fixtures.py | 9 + .../test_gestion_sports_connector.py | 69 +- .../test_gestion_sports_html_parser.py | 8 + .../test_gestion_sports_payload_builder.py | 11 + ...ons.py => test_gestion_sports_platform.py} | 0 tests/test_booking.py | 14 +- tests/utils.py | 29 +- 18 files changed, 1810 insertions(+), 147 deletions(-) create mode 100644 resa_padel/gestion_sports/gestion_sports_html_parser.py create mode 100644 resa_padel/resources/gestion-sports/users_bookings.txt create mode 100644 tests/data/mes_resas.html create mode 100644 tests/gestion_sports/test_gestion_sports_html_parser.py rename tests/gestion_sports/{test_gestion_sports_operations.py => test_gestion_sports_platform.py} (100%) diff --git a/Dockerfile b/Dockerfile index 592a1f2..08e0203 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" RUN python -m venv $VIRTUAL_ENV && \ pip install --no-cache-dir --upgrade -r requirements.txt -FROM python:3.10-alpine +FROM python:3.10-slim WORKDIR /app diff --git a/poetry.lock b/poetry.lock index ebf910e..4055470 100644 --- a/poetry.lock +++ b/poetry.lock @@ -165,6 +165,27 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "black" version = "24.2.0" @@ -827,18 +848,18 @@ virtualenv = ">=20.10.0" [[package]] name = "pydantic" -version = "2.6.1" +version = "2.6.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, - {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.2" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -846,90 +867,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.2" +version = "2.16.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, - {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, - {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, - {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, - {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, - {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, - {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, - {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, - {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, - {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, - {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, - {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, - {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, - {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, - {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, - {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, - {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, - {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, - {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, - {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, - {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, - {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, - {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, - {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, - {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, - {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, - {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, - {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, - {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, - {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, - {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -937,30 +958,30 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" -version = "2.5.0" +version = "2.6.0" description = "Extra Pydantic types." optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_extra_types-2.5.0-py3-none-any.whl", hash = "sha256:7346873019cac32061b471adf2cdac711664ddb7a6ede04219bed2da34888c4d"}, - {file = "pydantic_extra_types-2.5.0.tar.gz", hash = "sha256:46b85240093dc63ad4a8f3cab49e03d76ae0577e4f99e2bbff7d32f99d009bf9"}, + {file = "pydantic_extra_types-2.6.0-py3-none-any.whl", hash = "sha256:d291d521c2e2bf2e6f11971caf8d639518124ae26a76d2e712599e98c4ef2b2b"}, + {file = "pydantic_extra_types-2.6.0.tar.gz", hash = "sha256:e9a93cfb245158462acb76621785219f80ad112303a0a7784d2ada65e6ed6cba"}, ] [package.dependencies] pydantic = ">=2.5.2" [package.extras] -all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23,<24)", "python-ulid (>=1,<2)"] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] [[package]] name = "pytest" -version = "8.0.1" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, - {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] @@ -1045,13 +1066,13 @@ dev = ["black", "flake8", "pre-commit"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1159,19 +1180,19 @@ files = [ [[package]] name = "setuptools" -version = "69.1.0" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, - {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1184,6 +1205,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + [[package]] name = "termcolor" version = "2.4.0" @@ -1211,13 +1243,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -1357,4 +1389,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "dece321ad66871b7866b74fb4a14ad4b7bea46ac0e57f660986ddd85136715b9" +content-hash = "b542ec9550a6b162afec437cfeef6648cbe99de6831e80e4423fac7219d2a3e4" diff --git a/pyproject.toml b/pyproject.toml index fd2f2f2..e312206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ pydantic = "^2.6.1" pydantic-extra-types = "^2.5.0" python-dotenv = "^1.0.1" jinja2 = "^3.1.3" +beautifulsoup4 = "^4.12.3" [tool.poetry.group.dev.dependencies] black = "^24.1.1" @@ -39,6 +40,10 @@ pythonpath = [ ] log_cli = 1 log_cli_level = "DEBUG" +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "uncertain", +] [build-system] requires = ["poetry-core"] diff --git a/resa_padel/booking.py b/resa_padel/booking.py index ca9f87b..2a0b7ec 100644 --- a/resa_padel/booking.py +++ b/resa_padel/booking.py @@ -21,15 +21,30 @@ async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | N return await platform.book(user, booking_filter) +async def get_user_without_booking(club: Club, users: list[User]) -> User | None: + """ + Return the first user who has no booking + + :param club: the club where to book + :param users: the list of users + :return: any user who has no booking + """ + async with GestionSportsPlatform(club) as platform: + for user in users: + if await platform.user_has_no_ongoing_booking(user): + return user + return None + + def main() -> int | None: """ Main function used to book a court :return: the id of the booked court, or None if no court was booked """ - user = config.get_user() booking_filter = config.get_booking_filter() club = config.get_club() + user = asyncio.run(get_user_without_booking(club, config.get_available_users())) LOGGER.info( "Starting booking court of sport %s for user %s at club %s at %s", diff --git a/resa_padel/gestion_sports/gestion_sports_config.py b/resa_padel/gestion_sports/gestion_sports_config.py index 4f342c2..ea5b852 100644 --- a/resa_padel/gestion_sports/gestion_sports_config.py +++ b/resa_padel/gestion_sports/gestion_sports_config.py @@ -3,5 +3,7 @@ from pathlib import Path import config RESOURCES_DIR = Path(config.RESOURCES_DIR, "gestion-sports") + BOOKING_TEMPLATE = Path(RESOURCES_DIR, "booking-payload.txt") LOGIN_TEMPLATE = Path(RESOURCES_DIR, "login-payload.txt") +USERS_BOOKINGS_TEMPLATE = Path(RESOURCES_DIR, "users_bookings.txt") diff --git a/resa_padel/gestion_sports/gestion_sports_connector.py b/resa_padel/gestion_sports/gestion_sports_connector.py index 9e08e0a..de5dca3 100644 --- a/resa_padel/gestion_sports/gestion_sports_connector.py +++ b/resa_padel/gestion_sports/gestion_sports_connector.py @@ -5,9 +5,11 @@ from urllib.parse import urljoin import config from aiohttp import ClientResponse, ClientSession +from gestion_sports import gestion_sports_html_parser as html_parser from gestion_sports.payload_builders import ( GestionSportsBookingPayloadBuilder, GestionSportsLoginPayloadBuilder, + GestionSportsUsersBookingsPayloadBuilder, ) from models import BookingFilter, Club, User @@ -32,7 +34,7 @@ class GestionSportsConnector: :return: the URL to the landing page """ - return urljoin(self.url, "/connexion.php?") + return urljoin(self.url, "/connexion.php") @property def login_url(self) -> str: @@ -41,7 +43,7 @@ class GestionSportsConnector: :return: the URL to the login page """ - return urljoin(self.url, "/connexion.php?") + return urljoin(self.url, "/connexion.php") @property def booking_url(self) -> str: @@ -50,7 +52,16 @@ class GestionSportsConnector: :return: the URL to the booking page """ - return urljoin(self.url, "/membre/reservation.html?") + return urljoin(self.url, "/membre/reservation.html") + + @property + def user_bookings_url(self) -> str: + """ + Get the URL where all the user's bookings are available + + :return: the URL to the user's bookings + """ + return urljoin(self.url, "/membre/mesresas.html") async def land(self) -> ClientResponse: """ @@ -133,10 +144,16 @@ class GestionSportsConnector: ) as response: resp_json = await response.text() LOGGER.debug("Response from booking request:\n'%s'", resp_json) - return court_id, self.is_response_status_ok(resp_json) + return court_id, self.is_booking_response_status_ok(resp_json) @staticmethod - def get_booked_court(bookings): + def get_booked_court(bookings: list[tuple[int, bool]]) -> int | None: + """ + Parse the booking list and return the court that was booked + + :param bookings: a list of bookings + :return: the id of the booked court if any, None otherwise + """ for court, is_booked in bookings: if is_booked: LOGGER.debug("Court %s is booked", court) @@ -145,10 +162,38 @@ class GestionSportsConnector: return None @staticmethod - def is_response_status_ok(response: str) -> bool: + def is_booking_response_status_ok(response: str) -> bool: + """ + Check if the booking response is OK + + :param response: the response as a string + :return: true if the status is ok, false otherwise + """ formatted_result = response.removeprefix('"').removesuffix('"') result_json = json.loads(formatted_result) return result_json["status"] == "ok" - def get_user_information(self): - pass + async def get_ongoing_bookings(self) -> dict: + """ + Get the list of all ongoing bookings of a user. + The steps to perform this are to get the user's bookings page and get a hidden + property in the HTML to get a hash that will be used in the payload of the + POST request (sic) to get the user's bookings. + Gestion sports is really a mess!! + + :return: the list of all ongoing bookings of a user + """ + async with self.session.get(self.user_bookings_url) as get_resp: + html = await get_resp.text() + hash_value = html_parser.get_hash_input(html) + + payload_builder = GestionSportsUsersBookingsPayloadBuilder() + payload_builder.hash(hash_value) + payload = payload_builder.build() + + async with self.session.post( + self.user_bookings_url, data=payload, headers=POST_HEADERS + ) as response: + resp = await response.text() + LOGGER.debug("ongoing bookings response: %s\n", resp) + return json.loads(resp) diff --git a/resa_padel/gestion_sports/gestion_sports_html_parser.py b/resa_padel/gestion_sports/gestion_sports_html_parser.py new file mode 100644 index 0000000..ed7f12a --- /dev/null +++ b/resa_padel/gestion_sports/gestion_sports_html_parser.py @@ -0,0 +1,16 @@ +from bs4 import BeautifulSoup + + +def get_hash_input(html_doc: str) -> str: + """ + There is a secret hash generated by Gestion sports that is reused when trying to get + users bookings. This hash is stored in a hidden input with name "mesresas-hash" + + :param html_doc: the html document when getting the page mes-resas.html + :return: the value of the hash in the page + """ + soup = BeautifulSoup(html_doc, "html.parser") + inputs = soup.find_all("input") + for input_tag in inputs: + if input_tag.get("name") == "mesresas-hash": + return input_tag.get("value").strip() diff --git a/resa_padel/gestion_sports/gestion_sports_platform.py b/resa_padel/gestion_sports/gestion_sports_platform.py index 5ec95e7..35679ce 100644 --- a/resa_padel/gestion_sports/gestion_sports_platform.py +++ b/resa_padel/gestion_sports/gestion_sports_platform.py @@ -26,23 +26,49 @@ class GestionSportsPlatform: await self.session.close() async def book(self, user: User, booking_filter: BookingFilter) -> int | None: + """ + Book a court matching the booking filters for a user. + The steps to perform a booking are to go to the landing page, to log in, wait + and for the time when booking is open and then actually book the court + + :param user: the user that wants to book a court + :param booking_filter: the booking criteria + :return: the court number if the booking is successful, None otherwise + """ if self.connector is None: LOGGER.error("No connection to Gestion Sports is available") - return + return None if user is None or booking_filter is None: LOGGER.error("Not enough information available to book a court") - return + return None await self.connector.land() await self.connector.login(user, self.club) wait_until_booking_time(self.club, booking_filter) return await self.connector.book(booking_filter, self.club) - async def user_can_book(self, user: User, club: Club): + async def user_has_no_ongoing_booking(self, user: User) -> bool: + """ + Check if the user has any ongoing booking. + The steps to perform this task are to go to the landing page, to log in and + then retrieve user information and extract the ongoing bookings + + :param user: the user to check the bookings + :return: True if the user has ongoing bookings, false otherwise + """ + if self.connector is None: + LOGGER.error("No connection to Gestion Sports is available") + return False + + if user is None: + LOGGER.error("Not enough information available to book a court") + return False + await self.connector.land() await self.connector.login(user, self.club) - await self.connector.get_user_information() + bookings = await self.connector.get_ongoing_bookings() + return bookings == [] def wait_until_booking_time(club: Club, booking_filter: BookingFilter): diff --git a/resa_padel/gestion_sports/payload_builders.py b/resa_padel/gestion_sports/payload_builders.py index 4379f59..1509a52 100644 --- a/resa_padel/gestion_sports/payload_builders.py +++ b/resa_padel/gestion_sports/payload_builders.py @@ -1,23 +1,48 @@ from exceptions import ArgumentMissing -from gestion_sports.gestion_sports_config import BOOKING_TEMPLATE, LOGIN_TEMPLATE +from gestion_sports.gestion_sports_config import ( + BOOKING_TEMPLATE, + LOGIN_TEMPLATE, + USERS_BOOKINGS_TEMPLATE, +) from jinja2 import Environment, FileSystemLoader from models import BookingFilter, Club, User class GestionSportsLoginPayloadBuilder: + """ + Build the payload for the login page + """ + def __init__(self): self._user: User | None = None self._club: Club | None = None def user(self, user: User): + """ + Set the user + + :param user: the user + :return: the class itself + """ self._user = user return self def club(self, club: Club): + """ + Set the club + + :param club: the club + :return: the class itself + """ self._club = club return self - def build(self): + def build(self) -> str: + """ + Build the payload + + :return: the string representation of the payload + """ if self._user is None: raise ArgumentMissing("No user was provided") if self._club is None: @@ -35,14 +60,31 @@ class GestionSportsBookingPayloadBuilder: self._court_id: int | None = None def booking_filter(self, booking_filter: BookingFilter): + """ + Set the booking filter + + :param booking_filter: the booking filter + :return: the class itself + """ self._booking_filter = booking_filter return self def court_id(self, court_id: int): + """ + Set the court id + + :param court_id: the court id + :return: the class itself + """ self._court_id = court_id return self - def build(self): + def build(self) -> str: + """ + Build the payload + + :return: the string representation of the payload + """ if self._booking_filter is None: raise ArgumentMissing("No booking filter was provided") if self.court_id is None: @@ -54,3 +96,33 @@ class GestionSportsBookingPayloadBuilder: return template.render( court_id=self._court_id, booking_filter=self._booking_filter ) + + +class GestionSportsUsersBookingsPayloadBuilder: + def __init__(self): + self._hash: str | None = None + + def hash(self, hash_value: str): + """ + Set the hash + + :param hash_value: the hash + :return: the class itself + """ + self._hash = hash_value + + def build(self) -> str: + """ + Build the payload + + :return: the string representation of the payload + """ + if self._hash is None: + raise ArgumentMissing("No hash was provided") + + environment = Environment( + loader=FileSystemLoader(USERS_BOOKINGS_TEMPLATE.parent) + ) + template = environment.get_template(USERS_BOOKINGS_TEMPLATE.name) + + return template.render(hash=self._hash) diff --git a/resa_padel/resources/gestion-sports/users_bookings.txt b/resa_padel/resources/gestion-sports/users_bookings.txt new file mode 100644 index 0000000..bc971e6 --- /dev/null +++ b/resa_padel/resources/gestion-sports/users_bookings.txt @@ -0,0 +1 @@ +ajax=loadResa&hash={{ hash }} diff --git a/tests/data/mes_resas.html b/tests/data/mes_resas.html new file mode 100644 index 0000000..c59445a --- /dev/null +++ b/tests/data/mes_resas.html @@ -0,0 +1,1363 @@ + + + + + + Mes réservations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + + Retour + + + + + +
+ +
+ Mes réservations
+ +
+ + + +
+
+
+ + +
+
+
Mes événements
+
+
+
+
+
+ +
+
Mes réservations
+ +
+
+
+
+
Suivi de vos réservations par sport
+
+ + Heure pleines + Heure creuses +
+
+ Padel + +
+
0 /1
+
0 /1
+
+
+ +
+
illimitées
+
illimitées
+
+
+
+
+ Squash + +
+
0 /2
+
0 /2
+
+
+ +
+
illimitées
+
illimitées
+
+
+
+
+ Electrostimulation + +
+
illimitées
+
illimitées
+
+
+ +
+
illimitées
+
illimitées
+
+
+
+
+
+ + +
+ + + + + + + +
+ + +
+ + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + Accueil +
+
+ + +
+ + Actualités +
+
+ + +
+
+ + Réserver +
+
+ +
+ + +
+ + Compte +
+
+ + +
+ + Menu +
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ image + Le Mas + à l'instant +
+ + + +
+
+
+

Titre de la notif

+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. +
+
+
+
+
+ + + + + + + + + + + +
+
+ +
+ Votre inscription à la partie de Tennis a bien été enregistrée !
+ Rendez-vous le 14 Janvier de 16h30 à 17h30. +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures.py b/tests/fixtures.py index 220ee92..bef8442 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import pendulum import pytest @@ -42,6 +43,9 @@ booking_payload = ( .build() ) +html_file = Path(__file__).parent / "data" / "mes_resas.html" +_mes_resas_html = html_file.read_text(encoding="utf-8") + @pytest.fixture def a_user() -> User: @@ -71,3 +75,8 @@ def a_booking_failure_response() -> str: @pytest.fixture def a_booking_payload() -> str: return booking_payload + + +@pytest.fixture +def mes_resas_html() -> str: + return _mes_resas_html diff --git a/tests/gestion_sports/test_gestion_sports_connector.py b/tests/gestion_sports/test_gestion_sports_connector.py index 03da200..82bcf91 100644 --- a/tests/gestion_sports/test_gestion_sports_connector.py +++ b/tests/gestion_sports/test_gestion_sports_connector.py @@ -13,7 +13,25 @@ from tests.fixtures import ( ) tpc_url = "https://toulousepadelclub.gestion-sports.com" -o = "https://toulousepadelclub.gestion-sports.fr" +TPC_COURTS = [ + None, + 596, + 597, + 598, + 599, + 600, + 601, + 602, + 603, + 604, + 605, + 606, + 607, + 608, + 609, + 610, + 611, +] @pytest.mark.asyncio @@ -82,27 +100,7 @@ async def test_booking_url_should_be_reachable( court_booked = await gs_connector.book(a_booking_filter, a_club) # At 18:00 no chance to get a booking, any day of the week - assert court_booked in [ - None, - 597, - 598, - 599, - 600, - 601, - 602, - 603, - 604, - 605, - 606, - 607, - 608, - 609, - 610, - 611, - 612, - 613, - 614, - ] + assert court_booked in TPC_COURTS @pytest.mark.asyncio @@ -147,7 +145,9 @@ def test_response_status_should_be_ok(a_booking_success_response: str) -> None: :param a_booking_success_response: the success response mock """ - is_booked = GestionSportsConnector.is_response_status_ok(a_booking_success_response) + is_booked = GestionSportsConnector.is_booking_response_status_ok( + a_booking_success_response + ) assert is_booked @@ -158,5 +158,26 @@ def test_response_status_should_be_not_ok(a_booking_failure_response: str) -> No :param a_booking_failure_response: the failure response mock """ - is_booked = GestionSportsConnector.is_response_status_ok(a_booking_failure_response) + is_booked = GestionSportsConnector.is_booking_response_status_ok( + a_booking_failure_response + ) assert not is_booked + + +@pytest.mark.asyncio +async def test_get_user_ongoing_bookings(a_user: User, a_club: Club) -> None: + """ + Test that the user has 2 ongoing bookings + + :param a_user: + :param a_club: + :return: + """ + async with ClientSession() as session: + gs_connector = GestionSportsConnector(session, tpc_url) + await gs_connector.land() + await gs_connector.login(a_user, a_club) + + bookings = await gs_connector.get_ongoing_bookings() + + assert len(bookings) == 0 diff --git a/tests/gestion_sports/test_gestion_sports_html_parser.py b/tests/gestion_sports/test_gestion_sports_html_parser.py new file mode 100644 index 0000000..314e3e3 --- /dev/null +++ b/tests/gestion_sports/test_gestion_sports_html_parser.py @@ -0,0 +1,8 @@ +from gestion_sports import gestion_sports_html_parser as parser + +from tests.fixtures import mes_resas_html + + +def test_html_parser(mes_resas_html): + hash_value = parser.get_hash_input(mes_resas_html) + assert hash_value == "ef4403f4c44fa91060a92476aae011a2184323ec" diff --git a/tests/gestion_sports/test_gestion_sports_payload_builder.py b/tests/gestion_sports/test_gestion_sports_payload_builder.py index 0b917c3..4b7d23d 100644 --- a/tests/gestion_sports/test_gestion_sports_payload_builder.py +++ b/tests/gestion_sports/test_gestion_sports_payload_builder.py @@ -1,6 +1,7 @@ from resa_padel.gestion_sports.payload_builders import ( GestionSportsBookingPayloadBuilder, GestionSportsLoginPayloadBuilder, + GestionSportsUsersBookingsPayloadBuilder, ) from tests.fixtures import a_booking_filter, a_club, a_user @@ -48,3 +49,13 @@ def test_booking_payload_should_be_built(a_booking_filter): ) assert booking_payload == expected_payload + + +def test_users_bookings_payload_should_be_built(): + builder = GestionSportsUsersBookingsPayloadBuilder() + builder.hash("super_hash") + expected_payload = "ajax=loadResa&hash=super_hash" + + actual_payload = builder.build() + + assert actual_payload == expected_payload diff --git a/tests/gestion_sports/test_gestion_sports_operations.py b/tests/gestion_sports/test_gestion_sports_platform.py similarity index 100% rename from tests/gestion_sports/test_gestion_sports_operations.py rename to tests/gestion_sports/test_gestion_sports_platform.py diff --git a/tests/test_booking.py b/tests/test_booking.py index be3a11d..b60ae4d 100644 --- a/tests/test_booking.py +++ b/tests/test_booking.py @@ -8,10 +8,15 @@ from pendulum import Time from resa_padel import booking from tests import fixtures, utils -from tests.fixtures import a_booking_failure_response, a_booking_success_response +from tests.fixtures import ( + a_booking_failure_response, + a_booking_success_response, + mes_resas_html, +) login = "user" password = "password" +available_credentials = login + ":" + password + ",some_user:some_password" club_id = "88" court_id = "11" paris_tz = "Europe/Paris" @@ -31,11 +36,15 @@ datetime_to_book = ( "COURT_IDS": "7,8,10", "SPORT_ID": "217", "DATE_TIME": datetime_to_book.isoformat(), + "AVAILABLE_USERS_CREDENTIALS": available_credentials, }, clear=True, ) def test_main( - mock_now, a_booking_success_response: str, a_booking_failure_response: str + mock_now, + a_booking_success_response: str, + a_booking_failure_response: str, + mes_resas_html: str, ): """ Test the main function to book a court @@ -61,6 +70,7 @@ def test_main( fixtures.url, a_booking_failure_response, a_booking_success_response, + mes_resas_html, ) court_booked = booking.main() diff --git a/tests/utils.py b/tests/utils.py index 544c93e..c5383bb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,14 @@ from urllib.parse import urljoin from models import BookingFilter, Club from pendulum import DateTime +from tests.fixtures import ( + a_booking_failure_response, + a_booking_filter, + a_booking_success_response, + a_club, + mes_resas_html, +) + def mock_successful_connection(aio_mock, url): """ @@ -66,8 +74,20 @@ def retrieve_booking_datetime( ) +def mock_get_users_booking(aio_mock, url: str, booking_response: str): + return aio_mock.get(url, body=booking_response) + + +def mock_post_users_booking(aio_mock, url: str): + return aio_mock.post(url, payload=[]) + + def mock_rest_api_from_connection_to_booking( - aio_mock, url: str, a_booking_failure_response: str, a_booking_success_response: str + aio_mock, + url: str, + a_booking_failure_response: str, + a_booking_success_response: str, + mes_resas_html: str, ): """ Mock a REST API from a club. @@ -78,13 +98,20 @@ def mock_rest_api_from_connection_to_booking( :param url: the API root URL :param a_booking_success_response: the success json response :param a_booking_failure_response: the failure json response + :param mes_resas_html: the html response for getting the bookings :return: """ connexion_url = urljoin(url, "/connexion.php?") mock_successful_connection(aio_mock, connexion_url) + mock_successful_connection(aio_mock, connexion_url) login_url = urljoin(url, "/connexion.php?") mock_successful_login(aio_mock, login_url) + mock_successful_login(aio_mock, login_url) + + users_bookings_url = urljoin(url, "/membre/mesresas.html") + mock_get_users_booking(aio_mock, users_bookings_url, mes_resas_html) + mock_post_users_booking(aio_mock, users_bookings_url) booking_url = urljoin(url, "/membre/reservation.html?") mock_booking(aio_mock, booking_url, a_booking_failure_response)