Choose a user with booking availability among many
This commit is contained in:
parent
a8322d6be0
commit
559c3b6d69
18 changed files with 1810 additions and 147 deletions
|
@ -11,7 +11,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
RUN python -m venv $VIRTUAL_ENV && \
|
RUN python -m venv $VIRTUAL_ENV && \
|
||||||
pip install --no-cache-dir --upgrade -r requirements.txt
|
pip install --no-cache-dir --upgrade -r requirements.txt
|
||||||
|
|
||||||
FROM python:3.10-alpine
|
FROM python:3.10-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
238
poetry.lock
generated
238
poetry.lock
generated
|
@ -165,6 +165,27 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
|
||||||
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
||||||
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
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]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "24.2.0"
|
version = "24.2.0"
|
||||||
|
@ -827,18 +848,18 @@ virtualenv = ">=20.10.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.6.1"
|
version = "2.6.3"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"},
|
{file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"},
|
||||||
{file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"},
|
{file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
annotated-types = ">=0.4.0"
|
annotated-types = ">=0.4.0"
|
||||||
pydantic-core = "2.16.2"
|
pydantic-core = "2.16.3"
|
||||||
typing-extensions = ">=4.6.1"
|
typing-extensions = ">=4.6.1"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
@ -846,90 +867,90 @@ email = ["email-validator (>=2.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.16.2"
|
version = "2.16.3"
|
||||||
description = ""
|
description = ""
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"},
|
||||||
{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.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"},
|
{file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"},
|
{file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"},
|
||||||
{file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"},
|
{file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"},
|
||||||
{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.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"},
|
{file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"},
|
{file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"},
|
{file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"},
|
||||||
{file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"},
|
{file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"},
|
||||||
{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.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"},
|
{file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"},
|
{file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"},
|
{file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"},
|
||||||
{file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"},
|
{file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"},
|
||||||
{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.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"},
|
{file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"},
|
{file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"},
|
||||||
{file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"},
|
{file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"},
|
||||||
{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.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"},
|
{file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"},
|
{file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"},
|
||||||
{file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"},
|
{file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"},
|
||||||
{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.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"},
|
||||||
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"},
|
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"},
|
||||||
{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.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"},
|
||||||
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"},
|
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"},
|
||||||
{file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"},
|
{file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -937,30 +958,30 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-extra-types"
|
name = "pydantic-extra-types"
|
||||||
version = "2.5.0"
|
version = "2.6.0"
|
||||||
description = "Extra Pydantic types."
|
description = "Extra Pydantic types."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_extra_types-2.5.0-py3-none-any.whl", hash = "sha256:7346873019cac32061b471adf2cdac711664ddb7a6ede04219bed2da34888c4d"},
|
{file = "pydantic_extra_types-2.6.0-py3-none-any.whl", hash = "sha256:d291d521c2e2bf2e6f11971caf8d639518124ae26a76d2e712599e98c4ef2b2b"},
|
||||||
{file = "pydantic_extra_types-2.5.0.tar.gz", hash = "sha256:46b85240093dc63ad4a8f3cab49e03d76ae0577e4f99e2bbff7d32f99d009bf9"},
|
{file = "pydantic_extra_types-2.6.0.tar.gz", hash = "sha256:e9a93cfb245158462acb76621785219f80ad112303a0a7784d2ada65e6ed6cba"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pydantic = ">=2.5.2"
|
pydantic = ">=2.5.2"
|
||||||
|
|
||||||
[package.extras]
|
[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]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.0.1"
|
version = "8.0.2"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"},
|
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
||||||
{file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"},
|
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -1045,13 +1066,13 @@ dev = ["black", "flake8", "pre-commit"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.9.0.post0"
|
||||||
description = "Extensions to the standard Python datetime module"
|
description = "Extensions to the standard Python datetime module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -1159,19 +1180,19 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "69.1.0"
|
version = "69.1.1"
|
||||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"},
|
{file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
|
||||||
{file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"},
|
{file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[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"]
|
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 = ["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.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
|
@ -1184,6 +1205,17 @@ files = [
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -1211,13 +1243,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.9.0"
|
version = "4.10.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
{file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
|
||||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
{file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1357,4 +1389,4 @@ multidict = ">=4.0"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "dece321ad66871b7866b74fb4a14ad4b7bea46ac0e57f660986ddd85136715b9"
|
content-hash = "b542ec9550a6b162afec437cfeef6648cbe99de6831e80e4423fac7219d2a3e4"
|
||||||
|
|
|
@ -15,6 +15,7 @@ pydantic = "^2.6.1"
|
||||||
pydantic-extra-types = "^2.5.0"
|
pydantic-extra-types = "^2.5.0"
|
||||||
python-dotenv = "^1.0.1"
|
python-dotenv = "^1.0.1"
|
||||||
jinja2 = "^3.1.3"
|
jinja2 = "^3.1.3"
|
||||||
|
beautifulsoup4 = "^4.12.3"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^24.1.1"
|
black = "^24.1.1"
|
||||||
|
@ -39,6 +40,10 @@ pythonpath = [
|
||||||
]
|
]
|
||||||
log_cli = 1
|
log_cli = 1
|
||||||
log_cli_level = "DEBUG"
|
log_cli_level = "DEBUG"
|
||||||
|
markers = [
|
||||||
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||||
|
"uncertain",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
@ -21,15 +21,30 @@ async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | N
|
||||||
return await platform.book(user, booking_filter)
|
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:
|
def main() -> int | None:
|
||||||
"""
|
"""
|
||||||
Main function used to book a court
|
Main function used to book a court
|
||||||
|
|
||||||
:return: the id of the booked court, or None if no court was booked
|
:return: the id of the booked court, or None if no court was booked
|
||||||
"""
|
"""
|
||||||
user = config.get_user()
|
|
||||||
booking_filter = config.get_booking_filter()
|
booking_filter = config.get_booking_filter()
|
||||||
club = config.get_club()
|
club = config.get_club()
|
||||||
|
user = asyncio.run(get_user_without_booking(club, config.get_available_users()))
|
||||||
|
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Starting booking court of sport %s for user %s at club %s at %s",
|
"Starting booking court of sport %s for user %s at club %s at %s",
|
||||||
|
|
|
@ -3,5 +3,7 @@ from pathlib import Path
|
||||||
import config
|
import config
|
||||||
|
|
||||||
RESOURCES_DIR = Path(config.RESOURCES_DIR, "gestion-sports")
|
RESOURCES_DIR = Path(config.RESOURCES_DIR, "gestion-sports")
|
||||||
|
|
||||||
BOOKING_TEMPLATE = Path(RESOURCES_DIR, "booking-payload.txt")
|
BOOKING_TEMPLATE = Path(RESOURCES_DIR, "booking-payload.txt")
|
||||||
LOGIN_TEMPLATE = Path(RESOURCES_DIR, "login-payload.txt")
|
LOGIN_TEMPLATE = Path(RESOURCES_DIR, "login-payload.txt")
|
||||||
|
USERS_BOOKINGS_TEMPLATE = Path(RESOURCES_DIR, "users_bookings.txt")
|
||||||
|
|
|
@ -5,9 +5,11 @@ from urllib.parse import urljoin
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from aiohttp import ClientResponse, ClientSession
|
from aiohttp import ClientResponse, ClientSession
|
||||||
|
from gestion_sports import gestion_sports_html_parser as html_parser
|
||||||
from gestion_sports.payload_builders import (
|
from gestion_sports.payload_builders import (
|
||||||
GestionSportsBookingPayloadBuilder,
|
GestionSportsBookingPayloadBuilder,
|
||||||
GestionSportsLoginPayloadBuilder,
|
GestionSportsLoginPayloadBuilder,
|
||||||
|
GestionSportsUsersBookingsPayloadBuilder,
|
||||||
)
|
)
|
||||||
from models import BookingFilter, Club, User
|
from models import BookingFilter, Club, User
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
:return: the URL to the landing page
|
:return: the URL to the landing page
|
||||||
"""
|
"""
|
||||||
return urljoin(self.url, "/connexion.php?")
|
return urljoin(self.url, "/connexion.php")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def login_url(self) -> str:
|
def login_url(self) -> str:
|
||||||
|
@ -41,7 +43,7 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
:return: the URL to the login page
|
:return: the URL to the login page
|
||||||
"""
|
"""
|
||||||
return urljoin(self.url, "/connexion.php?")
|
return urljoin(self.url, "/connexion.php")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def booking_url(self) -> str:
|
def booking_url(self) -> str:
|
||||||
|
@ -50,7 +52,16 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
:return: the URL to the booking page
|
: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:
|
async def land(self) -> ClientResponse:
|
||||||
"""
|
"""
|
||||||
|
@ -133,10 +144,16 @@ class GestionSportsConnector:
|
||||||
) as response:
|
) as response:
|
||||||
resp_json = await response.text()
|
resp_json = await response.text()
|
||||||
LOGGER.debug("Response from booking request:\n'%s'", resp_json)
|
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
|
@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:
|
for court, is_booked in bookings:
|
||||||
if is_booked:
|
if is_booked:
|
||||||
LOGGER.debug("Court %s is booked", court)
|
LOGGER.debug("Court %s is booked", court)
|
||||||
|
@ -145,10 +162,38 @@ class GestionSportsConnector:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@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('"')
|
formatted_result = response.removeprefix('"').removesuffix('"')
|
||||||
result_json = json.loads(formatted_result)
|
result_json = json.loads(formatted_result)
|
||||||
return result_json["status"] == "ok"
|
return result_json["status"] == "ok"
|
||||||
|
|
||||||
def get_user_information(self):
|
async def get_ongoing_bookings(self) -> dict:
|
||||||
pass
|
"""
|
||||||
|
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)
|
||||||
|
|
16
resa_padel/gestion_sports/gestion_sports_html_parser.py
Normal file
16
resa_padel/gestion_sports/gestion_sports_html_parser.py
Normal file
|
@ -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()
|
|
@ -26,23 +26,49 @@ class GestionSportsPlatform:
|
||||||
await self.session.close()
|
await self.session.close()
|
||||||
|
|
||||||
async def book(self, user: User, booking_filter: BookingFilter) -> int | None:
|
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:
|
if self.connector is None:
|
||||||
LOGGER.error("No connection to Gestion Sports is available")
|
LOGGER.error("No connection to Gestion Sports is available")
|
||||||
return
|
return None
|
||||||
|
|
||||||
if user is None or booking_filter is None:
|
if user is None or booking_filter is None:
|
||||||
LOGGER.error("Not enough information available to book a court")
|
LOGGER.error("Not enough information available to book a court")
|
||||||
return
|
return None
|
||||||
|
|
||||||
await self.connector.land()
|
await self.connector.land()
|
||||||
await self.connector.login(user, self.club)
|
await self.connector.login(user, self.club)
|
||||||
wait_until_booking_time(self.club, booking_filter)
|
wait_until_booking_time(self.club, booking_filter)
|
||||||
return await self.connector.book(booking_filter, self.club)
|
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.land()
|
||||||
await self.connector.login(user, self.club)
|
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):
|
def wait_until_booking_time(club: Club, booking_filter: BookingFilter):
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
from exceptions import ArgumentMissing
|
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 jinja2 import Environment, FileSystemLoader
|
||||||
from models import BookingFilter, Club, User
|
from models import BookingFilter, Club, User
|
||||||
|
|
||||||
|
|
||||||
class GestionSportsLoginPayloadBuilder:
|
class GestionSportsLoginPayloadBuilder:
|
||||||
|
"""
|
||||||
|
Build the payload for the login page
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._user: User | None = None
|
self._user: User | None = None
|
||||||
self._club: Club | None = None
|
self._club: Club | None = None
|
||||||
|
|
||||||
def user(self, user: User):
|
def user(self, user: User):
|
||||||
|
"""
|
||||||
|
Set the user
|
||||||
|
|
||||||
|
:param user: the user
|
||||||
|
:return: the class itself
|
||||||
|
"""
|
||||||
self._user = user
|
self._user = user
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def club(self, club: Club):
|
def club(self, club: Club):
|
||||||
|
"""
|
||||||
|
Set the club
|
||||||
|
|
||||||
|
:param club: the club
|
||||||
|
:return: the class itself
|
||||||
|
"""
|
||||||
self._club = club
|
self._club = club
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def build(self):
|
def build(self) -> str:
|
||||||
|
"""
|
||||||
|
Build the payload
|
||||||
|
|
||||||
|
:return: the string representation of the payload
|
||||||
|
"""
|
||||||
if self._user is None:
|
if self._user is None:
|
||||||
raise ArgumentMissing("No user was provided")
|
raise ArgumentMissing("No user was provided")
|
||||||
if self._club is None:
|
if self._club is None:
|
||||||
|
@ -35,14 +60,31 @@ class GestionSportsBookingPayloadBuilder:
|
||||||
self._court_id: int | None = None
|
self._court_id: int | None = None
|
||||||
|
|
||||||
def booking_filter(self, booking_filter: BookingFilter):
|
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
|
self._booking_filter = booking_filter
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def court_id(self, court_id: int):
|
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
|
self._court_id = court_id
|
||||||
return self
|
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:
|
if self._booking_filter is None:
|
||||||
raise ArgumentMissing("No booking filter was provided")
|
raise ArgumentMissing("No booking filter was provided")
|
||||||
if self.court_id is None:
|
if self.court_id is None:
|
||||||
|
@ -54,3 +96,33 @@ class GestionSportsBookingPayloadBuilder:
|
||||||
return template.render(
|
return template.render(
|
||||||
court_id=self._court_id, booking_filter=self._booking_filter
|
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)
|
||||||
|
|
1
resa_padel/resources/gestion-sports/users_bookings.txt
Normal file
1
resa_padel/resources/gestion-sports/users_bookings.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ajax=loadResa&hash={{ hash }}
|
1363
tests/data/mes_resas.html
Normal file
1363
tests/data/mes_resas.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -42,6 +43,9 @@ booking_payload = (
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
html_file = Path(__file__).parent / "data" / "mes_resas.html"
|
||||||
|
_mes_resas_html = html_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def a_user() -> User:
|
def a_user() -> User:
|
||||||
|
@ -71,3 +75,8 @@ def a_booking_failure_response() -> str:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def a_booking_payload() -> str:
|
def a_booking_payload() -> str:
|
||||||
return booking_payload
|
return booking_payload
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mes_resas_html() -> str:
|
||||||
|
return _mes_resas_html
|
||||||
|
|
|
@ -13,7 +13,25 @@ from tests.fixtures import (
|
||||||
)
|
)
|
||||||
|
|
||||||
tpc_url = "https://toulousepadelclub.gestion-sports.com"
|
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
|
@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)
|
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
|
# At 18:00 no chance to get a booking, any day of the week
|
||||||
assert court_booked in [
|
assert court_booked in TPC_COURTS
|
||||||
None,
|
|
||||||
597,
|
|
||||||
598,
|
|
||||||
599,
|
|
||||||
600,
|
|
||||||
601,
|
|
||||||
602,
|
|
||||||
603,
|
|
||||||
604,
|
|
||||||
605,
|
|
||||||
606,
|
|
||||||
607,
|
|
||||||
608,
|
|
||||||
609,
|
|
||||||
610,
|
|
||||||
611,
|
|
||||||
612,
|
|
||||||
613,
|
|
||||||
614,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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
|
: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
|
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
|
: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
|
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
|
||||||
|
|
8
tests/gestion_sports/test_gestion_sports_html_parser.py
Normal file
8
tests/gestion_sports/test_gestion_sports_html_parser.py
Normal file
|
@ -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"
|
|
@ -1,6 +1,7 @@
|
||||||
from resa_padel.gestion_sports.payload_builders import (
|
from resa_padel.gestion_sports.payload_builders import (
|
||||||
GestionSportsBookingPayloadBuilder,
|
GestionSportsBookingPayloadBuilder,
|
||||||
GestionSportsLoginPayloadBuilder,
|
GestionSportsLoginPayloadBuilder,
|
||||||
|
GestionSportsUsersBookingsPayloadBuilder,
|
||||||
)
|
)
|
||||||
from tests.fixtures import a_booking_filter, a_club, a_user
|
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
|
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
|
||||||
|
|
|
@ -8,10 +8,15 @@ from pendulum import Time
|
||||||
|
|
||||||
from resa_padel import booking
|
from resa_padel import booking
|
||||||
from tests import fixtures, utils
|
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"
|
login = "user"
|
||||||
password = "password"
|
password = "password"
|
||||||
|
available_credentials = login + ":" + password + ",some_user:some_password"
|
||||||
club_id = "88"
|
club_id = "88"
|
||||||
court_id = "11"
|
court_id = "11"
|
||||||
paris_tz = "Europe/Paris"
|
paris_tz = "Europe/Paris"
|
||||||
|
@ -31,11 +36,15 @@ datetime_to_book = (
|
||||||
"COURT_IDS": "7,8,10",
|
"COURT_IDS": "7,8,10",
|
||||||
"SPORT_ID": "217",
|
"SPORT_ID": "217",
|
||||||
"DATE_TIME": datetime_to_book.isoformat(),
|
"DATE_TIME": datetime_to_book.isoformat(),
|
||||||
|
"AVAILABLE_USERS_CREDENTIALS": available_credentials,
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
)
|
)
|
||||||
def test_main(
|
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
|
Test the main function to book a court
|
||||||
|
@ -61,6 +70,7 @@ def test_main(
|
||||||
fixtures.url,
|
fixtures.url,
|
||||||
a_booking_failure_response,
|
a_booking_failure_response,
|
||||||
a_booking_success_response,
|
a_booking_success_response,
|
||||||
|
mes_resas_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
court_booked = booking.main()
|
court_booked = booking.main()
|
||||||
|
|
|
@ -3,6 +3,14 @@ from urllib.parse import urljoin
|
||||||
from models import BookingFilter, Club
|
from models import BookingFilter, Club
|
||||||
from pendulum import DateTime
|
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):
|
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(
|
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.
|
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 url: the API root URL
|
||||||
:param a_booking_success_response: the success json response
|
:param a_booking_success_response: the success json response
|
||||||
:param a_booking_failure_response: the failure json response
|
:param a_booking_failure_response: the failure json response
|
||||||
|
:param mes_resas_html: the html response for getting the bookings
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
connexion_url = urljoin(url, "/connexion.php?")
|
connexion_url = urljoin(url, "/connexion.php?")
|
||||||
mock_successful_connection(aio_mock, connexion_url)
|
mock_successful_connection(aio_mock, connexion_url)
|
||||||
|
mock_successful_connection(aio_mock, connexion_url)
|
||||||
|
|
||||||
login_url = urljoin(url, "/connexion.php?")
|
login_url = urljoin(url, "/connexion.php?")
|
||||||
mock_successful_login(aio_mock, login_url)
|
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?")
|
booking_url = urljoin(url, "/membre/reservation.html?")
|
||||||
mock_booking(aio_mock, booking_url, a_booking_failure_response)
|
mock_booking(aio_mock, booking_url, a_booking_failure_response)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue