Merge pull request 'feature/choose-user-with-available-slot' (#15) from feature/choose-user-with-available-slot into main
Reviewed-on: https://jouf.fr/gitea/stanislas/resa-padel/pulls/15
This commit is contained in:
commit
dbda5a158e
27 changed files with 2415 additions and 403 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -163,4 +163,3 @@ cython_debug/
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
|
25
.pre-commit-config.yaml
Normal file
25
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-toml
|
||||||
|
- id: detect-private-key
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.13.2
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
|
rev: v1.10.0
|
||||||
|
hooks:
|
||||||
|
- id: python-use-type-annotations
|
19
Dockerfile
19
Dockerfile
|
@ -1,18 +1,25 @@
|
||||||
FROM python:3.10 as build
|
FROM python:3.10-slim as build
|
||||||
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
|
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
COPY ./pyproject.toml ./poetry.lock* /tmp/
|
COPY ./pyproject.toml ./poetry.lock* /tmp/
|
||||||
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
|
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes --without dev
|
||||||
|
|
||||||
FROM python:3.10
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
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-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=build /tmp/requirements.txt /code/requirements.txt
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
COPY --from=build $VIRTUAL_ENV $VIRTUAL_ENV
|
||||||
|
|
||||||
COPY resa_padel/ /app/
|
COPY resa_padel/ /app/
|
||||||
|
|
||||||
CMD python .
|
CMD python .
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
# resa-padel
|
# resa-padel
|
||||||
|
|
||||||
|
|
436
poetry.lock
generated
436
poetry.lock
generated
|
@ -165,35 +165,56 @@ 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.1.1"
|
version = "24.2.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"},
|
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
|
||||||
{file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"},
|
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
|
||||||
{file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"},
|
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
|
||||||
{file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"},
|
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
|
||||||
{file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"},
|
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
|
||||||
{file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"},
|
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
|
||||||
{file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"},
|
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
|
||||||
{file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"},
|
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
|
||||||
{file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"},
|
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
|
||||||
{file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"},
|
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
|
||||||
{file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"},
|
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
|
||||||
{file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"},
|
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
|
||||||
{file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"},
|
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
|
||||||
{file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"},
|
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
|
||||||
{file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"},
|
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
|
||||||
{file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"},
|
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
|
||||||
{file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"},
|
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
|
||||||
{file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"},
|
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
|
||||||
{file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"},
|
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
|
||||||
{file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"},
|
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
|
||||||
{file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"},
|
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
|
||||||
{file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"},
|
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -211,6 +232,17 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.4.0"
|
||||||
|
description = "Validate configuration and produce human readable error messages."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||||
|
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
|
@ -236,6 +268,17 @@ files = [
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.8"
|
||||||
|
description = "Distribution utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
||||||
|
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -250,6 +293,22 @@ files = [
|
||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["pytest (>=6)"]
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.13.1"
|
||||||
|
description = "A platform independent file lock."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
|
||||||
|
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
|
||||||
|
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||||
|
typing = ["typing-extensions (>=4.8)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
@ -347,6 +406,20 @@ files = [
|
||||||
{file = "icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f"},
|
{file = "icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.5.35"
|
||||||
|
description = "File identification library for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
|
||||||
|
{file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
license = ["ukkonen"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.6"
|
version = "3.6"
|
||||||
|
@ -579,6 +652,20 @@ files = [
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.8.0"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||||
|
files = [
|
||||||
|
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
||||||
|
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
setuptools = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "23.2"
|
version = "23.2"
|
||||||
|
@ -741,20 +828,38 @@ files = [
|
||||||
{file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"},
|
{file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "3.6.2"
|
||||||
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"},
|
||||||
|
{file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cfgv = ">=2.0.0"
|
||||||
|
identify = ">=1.0.0"
|
||||||
|
nodeenv = ">=0.11.1"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
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]
|
||||||
|
@ -762,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]
|
||||||
|
@ -853,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.0"
|
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.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
|
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
||||||
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
|
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -961,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]
|
||||||
|
@ -1049,30 +1154,46 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
|
||||||
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
|
||||||
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
|
||||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
|
||||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
|
||||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
|
||||||
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
|
||||||
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
|
||||||
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
|
||||||
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
|
||||||
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
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.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)", "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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -1084,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"
|
||||||
|
@ -1111,26 +1243,46 @@ 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]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2023.4"
|
version = "2024.1"
|
||||||
description = "Provider of IANA time zone data"
|
description = "Provider of IANA time zone data"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2"
|
python-versions = ">=2"
|
||||||
files = [
|
files = [
|
||||||
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
|
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
|
||||||
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
|
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.25.1"
|
||||||
|
description = "Virtual Python Environment builder"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
|
||||||
|
{file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
distlib = ">=0.3.7,<1"
|
||||||
|
filelock = ">=3.12.2,<4"
|
||||||
|
platformdirs = ">=3.9.1,<5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||||
|
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
version = "1.9.4"
|
version = "1.9.4"
|
||||||
|
@ -1237,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 = "eb2292ededdcd551249bd05bdc2fd3d38dfeeba495cb3ea8dd7ba5dfd9250980"
|
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"
|
||||||
|
@ -25,14 +26,24 @@ pytest-sugar = "^1.0.0"
|
||||||
pytest-icdiff = "^0.9"
|
pytest-icdiff = "^0.9"
|
||||||
pytest-asyncio = "^0.23.5"
|
pytest-asyncio = "^0.23.5"
|
||||||
pytest-aioresponses = "^0.2.0"
|
pytest-aioresponses = "^0.2.0"
|
||||||
|
pre-commit = "^3.6.2"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = [
|
pythonpath = [
|
||||||
"resa_padel"
|
"resa_padel"
|
||||||
]
|
]
|
||||||
|
log_cli = 1
|
||||||
|
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"]
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,53 +1,13 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import pendulum
|
from gestion_sports.gestion_sports_platform import GestionSportsPlatform
|
||||||
from aiohttp import ClientSession
|
|
||||||
from gestion_sports.gestion_sports_connector import GestionSportsConnector
|
|
||||||
from models import BookingFilter, Club, User
|
from models import BookingFilter, Club, User
|
||||||
from pendulum import DateTime
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def wait_until_booking_time(club: Club, booking_filter: BookingFilter):
|
|
||||||
"""
|
|
||||||
Wait until the booking is open.
|
|
||||||
The booking filter contains the date and time of the booking.
|
|
||||||
The club has the information about when the booking is open for that date.
|
|
||||||
|
|
||||||
:param club: the club where to book a court
|
|
||||||
:param booking_filter: the booking information
|
|
||||||
"""
|
|
||||||
LOGGER.info("Waiting booking time")
|
|
||||||
booking_datetime = build_booking_datetime(booking_filter, club)
|
|
||||||
now = pendulum.now()
|
|
||||||
while now < booking_datetime:
|
|
||||||
time.sleep(1)
|
|
||||||
now = pendulum.now()
|
|
||||||
|
|
||||||
|
|
||||||
def build_booking_datetime(booking_filter: BookingFilter, club: Club) -> DateTime:
|
|
||||||
"""
|
|
||||||
Build the date and time when the booking is open for a given match date.
|
|
||||||
The booking filter contains the date and time of the booking.
|
|
||||||
The club has the information about when the booking is open for that date.
|
|
||||||
|
|
||||||
:param booking_filter: the booking information
|
|
||||||
:param club: the club where to book a court
|
|
||||||
:return: the date and time when the booking is open
|
|
||||||
"""
|
|
||||||
date_to_book = booking_filter.date
|
|
||||||
booking_date = date_to_book.subtract(days=club.booking_open_days_before)
|
|
||||||
|
|
||||||
booking_hour = club.booking_opening_time.hour
|
|
||||||
booking_minute = club.booking_opening_time.minute
|
|
||||||
|
|
||||||
return booking_date.at(booking_hour, booking_minute)
|
|
||||||
|
|
||||||
|
|
||||||
async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | None:
|
async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | None:
|
||||||
"""
|
"""
|
||||||
Book a court for a user to a club following a booking filter
|
Book a court for a user to a club following a booking filter
|
||||||
|
@ -57,12 +17,23 @@ async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | N
|
||||||
:param booking_filter: the information related to the booking
|
:param booking_filter: the information related to the booking
|
||||||
: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
|
||||||
"""
|
"""
|
||||||
async with ClientSession() as session:
|
async with GestionSportsPlatform(club) as platform:
|
||||||
platform = GestionSportsConnector(session, club.url)
|
return await platform.book(user, booking_filter)
|
||||||
await platform.land()
|
|
||||||
await platform.login(user, club)
|
|
||||||
wait_until_booking_time(club, booking_filter)
|
async def get_user_without_booking(club: Club, users: list[User]) -> User | None:
|
||||||
return await platform.book(booking_filter, club)
|
"""
|
||||||
|
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:
|
||||||
|
@ -71,12 +42,12 @@ def main() -> int | None:
|
||||||
|
|
||||||
: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 %s for user %s at club %s at %s",
|
"Starting booking court of sport %s for user %s at club %s at %s",
|
||||||
booking_filter.sport_id,
|
booking_filter.sport_id,
|
||||||
user.login,
|
user.login,
|
||||||
club.id,
|
club.id,
|
||||||
|
|
|
@ -64,6 +64,25 @@ def get_user() -> User:
|
||||||
return User(login=login, password=password)
|
return User(login=login, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_users() -> list[User]:
|
||||||
|
"""
|
||||||
|
Read the environment variables to get all the available users in order
|
||||||
|
to increase the chance of having a user with a free slot for a booking
|
||||||
|
|
||||||
|
:return: the list of all users that can book a court
|
||||||
|
"""
|
||||||
|
available_users_credentials = os.environ.get("AVAILABLE_USERS_CREDENTIALS")
|
||||||
|
available_users = [
|
||||||
|
credential for credential in available_users_credentials.split(",")
|
||||||
|
]
|
||||||
|
users = []
|
||||||
|
for user in available_users:
|
||||||
|
login, password = user.split(":")
|
||||||
|
users.append(User(login=login, password=password))
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
def get_post_headers(platform_id: str) -> dict:
|
def get_post_headers(platform_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Get the headers for the POST endpoint related to a specific booking platform
|
Get the headers for the POST endpoint related to a specific booking platform
|
||||||
|
|
|
@ -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,16 +5,14 @@ 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
|
||||||
|
|
||||||
DATE_FORMAT = "%d/%m/%Y"
|
|
||||||
|
|
||||||
TIME_FORMAT = "%H:%M"
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
POST_HEADERS = config.get_post_headers("gestion-sports")
|
POST_HEADERS = config.get_post_headers("gestion-sports")
|
||||||
|
|
||||||
|
@ -36,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:
|
||||||
|
@ -45,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:
|
||||||
|
@ -54,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:
|
||||||
"""
|
"""
|
||||||
|
@ -62,7 +69,7 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
:return: the response from the landing page
|
:return: the response from the landing page
|
||||||
"""
|
"""
|
||||||
LOGGER.info("Connecting to GestionSports API")
|
LOGGER.info("Connecting to GestionSports API at %s", self.login_url)
|
||||||
async with self.session.get(self.landing_url) as response:
|
async with self.session.get(self.landing_url) as response:
|
||||||
await response.text()
|
await response.text()
|
||||||
return response
|
return response
|
||||||
|
@ -73,18 +80,21 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
:return: the response from the login
|
:return: the response from the login
|
||||||
"""
|
"""
|
||||||
|
LOGGER.info("Logging in to GestionSports API at %s", self.login_url)
|
||||||
payload_builder = GestionSportsLoginPayloadBuilder()
|
payload_builder = GestionSportsLoginPayloadBuilder()
|
||||||
payload = payload_builder.user(user).club(club).build()
|
payload = payload_builder.user(user).club(club).build()
|
||||||
|
|
||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
self.login_url, data=payload, headers=POST_HEADERS
|
self.login_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
await response.text()
|
resp_text = await response.text()
|
||||||
|
LOGGER.debug("Connexion request response:\n%s", resp_text)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def book(self, booking_filter: BookingFilter, club: Club) -> int | None:
|
async def book(self, booking_filter: BookingFilter, club: Club) -> int | None:
|
||||||
"""
|
"""
|
||||||
Perform a request for each court at the same time to increase the chances to get a booking.
|
Perform a request for each court at the same time to increase the chances to get
|
||||||
|
a booking.
|
||||||
The gestion-sports backend does not allow several bookings at the same time
|
The gestion-sports backend does not allow several bookings at the same time
|
||||||
so there is no need to make each request one after the other
|
so there is no need to make each request one after the other
|
||||||
|
|
||||||
|
@ -92,6 +102,9 @@ class GestionSportsConnector:
|
||||||
:param club: the club where to book the court
|
:param club: the club where to book the court
|
||||||
:return: the booked court, or None if no court was booked
|
:return: the booked court, or None if no court was booked
|
||||||
"""
|
"""
|
||||||
|
LOGGER.info(
|
||||||
|
"Booking any available court from GestionSports API at %s", self.booking_url
|
||||||
|
)
|
||||||
# use asyncio to request a booking on every court
|
# use asyncio to request a booking on every court
|
||||||
# the gestion-sports backend is able to book only one court for a user
|
# the gestion-sports backend is able to book only one court for a user
|
||||||
bookings = await asyncio.gather(
|
bookings = await asyncio.gather(
|
||||||
|
@ -102,36 +115,85 @@ class GestionSportsConnector:
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOGGER.debug("Booking results:\n'%s'", bookings)
|
||||||
return self.get_booked_court(bookings)
|
return self.get_booked_court(bookings)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_booked_court(bookings):
|
|
||||||
LOGGER.info(bookings)
|
|
||||||
for court, is_booked in bookings:
|
|
||||||
if is_booked:
|
|
||||||
return court
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def book_one_court(
|
async def book_one_court(
|
||||||
self, booking_filter: BookingFilter, court_id: int
|
self, booking_filter: BookingFilter, court_id: int
|
||||||
) -> tuple[int, bool]:
|
) -> tuple[int, bool]:
|
||||||
|
"""
|
||||||
|
Book a single court according to the information provided in the booking filter
|
||||||
|
|
||||||
|
:param booking_filter: the booking information
|
||||||
|
:param court_id: the id of the court to book
|
||||||
|
:return: a tuple containing the court id and the booking status
|
||||||
|
"""
|
||||||
|
LOGGER.debug(
|
||||||
|
"Booking court %s at %s",
|
||||||
|
court_id,
|
||||||
|
booking_filter.date.to_w3c_string(),
|
||||||
|
)
|
||||||
payload_builder = GestionSportsBookingPayloadBuilder()
|
payload_builder = GestionSportsBookingPayloadBuilder()
|
||||||
payload = (
|
payload = (
|
||||||
payload_builder.booking_filter(booking_filter).court_id(court_id).build()
|
payload_builder.booking_filter(booking_filter).court_id(court_id).build()
|
||||||
)
|
)
|
||||||
LOGGER.info(payload)
|
LOGGER.debug("Booking court %s request:\n'%s'", court_id, payload)
|
||||||
|
|
||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
self.booking_url, data=payload, headers=POST_HEADERS
|
self.booking_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
resp_json = await response.text()
|
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
|
@staticmethod
|
||||||
def is_response_status_ok(response: str) -> bool:
|
def get_booked_court(bookings: list[tuple[int, bool]]) -> int | None:
|
||||||
|
"""
|
||||||
|
Parse the booking list and return the court that was booked
|
||||||
|
|
||||||
LOGGER.info(response)
|
: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)
|
||||||
|
return court
|
||||||
|
LOGGER.debug("No booked court found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
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()
|
116
resa_padel/gestion_sports/gestion_sports_platform.py
Normal file
116
resa_padel/gestion_sports/gestion_sports_platform.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pendulum
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from gestion_sports.gestion_sports_connector import GestionSportsConnector
|
||||||
|
from models import BookingFilter, Club, User
|
||||||
|
from pendulum import DateTime
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GestionSportsPlatform:
|
||||||
|
def __init__(self, club: Club):
|
||||||
|
LOGGER.info("Initializing Gestion Sports platform at url %s", club.url)
|
||||||
|
self.connector: GestionSportsConnector | None = None
|
||||||
|
self.club: Club = club
|
||||||
|
self.session: ClientSession | None = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.session = ClientSession()
|
||||||
|
self.connector = GestionSportsConnector(self.session, self.club.url)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
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 None
|
||||||
|
|
||||||
|
if user is None or booking_filter is None:
|
||||||
|
LOGGER.error("Not enough information available to book a court")
|
||||||
|
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_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)
|
||||||
|
bookings = await self.connector.get_ongoing_bookings()
|
||||||
|
return bookings == []
|
||||||
|
|
||||||
|
|
||||||
|
def wait_until_booking_time(club: Club, booking_filter: BookingFilter):
|
||||||
|
"""
|
||||||
|
Wait until the booking is open.
|
||||||
|
The booking filter contains the date and time of the booking.
|
||||||
|
The club has the information about when the booking is open for that date.
|
||||||
|
|
||||||
|
:param club: the club where to book a court
|
||||||
|
:param booking_filter: the booking information
|
||||||
|
"""
|
||||||
|
LOGGER.info("Waiting for booking time")
|
||||||
|
booking_datetime = build_booking_datetime(booking_filter, club)
|
||||||
|
now = pendulum.now()
|
||||||
|
duration_until_booking = booking_datetime - now
|
||||||
|
LOGGER.debug(
|
||||||
|
"Time to wait before booking: %s:%s:%s",
|
||||||
|
"{:0>2}".format(duration_until_booking.hours),
|
||||||
|
"{:0>2}".format(duration_until_booking.minutes),
|
||||||
|
"{:0>2}".format(duration_until_booking.seconds),
|
||||||
|
)
|
||||||
|
|
||||||
|
while now < booking_datetime:
|
||||||
|
time.sleep(1)
|
||||||
|
now = pendulum.now()
|
||||||
|
LOGGER.info("It's booking time!")
|
||||||
|
|
||||||
|
|
||||||
|
def build_booking_datetime(booking_filter: BookingFilter, club: Club) -> DateTime:
|
||||||
|
"""
|
||||||
|
Build the date and time when the booking is open for a given match date.
|
||||||
|
The booking filter contains the date and time of the booking.
|
||||||
|
The club has the information about when the booking is open for that date.
|
||||||
|
|
||||||
|
:param booking_filter: the booking information
|
||||||
|
:param club: the club where to book a court
|
||||||
|
:return: the date and time when the booking is open
|
||||||
|
"""
|
||||||
|
date_to_book = booking_filter.date
|
||||||
|
booking_date = date_to_book.subtract(days=club.booking_open_days_before)
|
||||||
|
|
||||||
|
booking_hour = club.booking_opening_time.hour
|
||||||
|
booking_minute = club.booking_opening_time.minute
|
||||||
|
|
||||||
|
return booking_date.at(booking_hour, booking_minute)
|
|
@ -1,31 +1,55 @@
|
||||||
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
|
||||||
self._template = LOGIN_TEMPLATE
|
|
||||||
|
|
||||||
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:
|
||||||
raise ArgumentMissing("No club was provided")
|
raise ArgumentMissing("No club was provided")
|
||||||
|
|
||||||
environment = Environment(loader=FileSystemLoader(self._template.parent))
|
environment = Environment(loader=FileSystemLoader(LOGIN_TEMPLATE.parent))
|
||||||
template = environment.get_template(self._template.name)
|
template = environment.get_template(LOGIN_TEMPLATE.name)
|
||||||
|
|
||||||
return template.render(club=self._club, user=self._user)
|
return template.render(club=self._club, user=self._user)
|
||||||
|
|
||||||
|
@ -34,25 +58,71 @@ class GestionSportsBookingPayloadBuilder:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._booking_filter: BookingFilter | None = None
|
self._booking_filter: BookingFilter | None = None
|
||||||
self._court_id: int | None = None
|
self._court_id: int | None = None
|
||||||
self._template = BOOKING_TEMPLATE
|
|
||||||
|
|
||||||
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:
|
||||||
raise ArgumentMissing("No court id was provided")
|
raise ArgumentMissing("No court id was provided")
|
||||||
|
|
||||||
environment = Environment(loader=FileSystemLoader(self._template.parent))
|
environment = Environment(loader=FileSystemLoader(BOOKING_TEMPLATE.parent))
|
||||||
template = environment.get_template(self._template.name)
|
template = environment.get_template(BOOKING_TEMPLATE.name)
|
||||||
|
|
||||||
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 +1 @@
|
||||||
ajax=addResa&date={{ booking_filter.date.date().strftime("%d/%m/%Y") }}&hour={{ booking_filter.date.time().strftime("%H:%M") }}&duration=90&partners=null|null|null&paiement=facultatif&idSport={{ booking_filter.sport_id }}&creaPartie=false&idCourt={{ court_id }}&pay=false&token=undefined&totalPrice=44&saveCard=0&foodNumber=0
|
ajax=addResa&date={{ booking_filter.date.date().strftime("%d/%m/%Y") }}&hour={{ booking_filter.date.time().strftime("%H:%M") }}&duration=90&partners=null|null|null&paiement=facultatif&idSport={{ booking_filter.sport_id }}&creaPartie=false&idCourt={{ court_id }}&pay=false&token=undefined&totalPrice=44&saveCard=0&foodNumber=0
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
ajax=connexionUser&id_club={{ club.id }}&email={{ user.login }}&form_ajax=1&pass={{ user.password }}&compte=user&playeridonesignal=0&identifiant=identifiant&externCo=true
|
ajax=connexionUser&id_club={{ club.id }}&email={{ user.login }}&form_ajax=1&pass={{ user.password }}&compte=user&playeridonesignal=0&identifiant=identifiant&externCo=true
|
||||||
|
|
|
@ -9,4 +9,4 @@
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Sec-Fetch-Dest": "empty",
|
||||||
"Sec-Fetch-Mode": "cors",
|
"Sec-Fetch-Mode": "cors",
|
||||||
"Sec-Fetch-Site": "same-origin"
|
"Sec-Fetch-Site": "same-origin"
|
||||||
}
|
}
|
||||||
|
|
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,6 +13,25 @@ from tests.fixtures import (
|
||||||
)
|
)
|
||||||
|
|
||||||
tpc_url = "https://toulousepadelclub.gestion-sports.com"
|
tpc_url = "https://toulousepadelclub.gestion-sports.com"
|
||||||
|
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
|
||||||
|
@ -64,6 +83,7 @@ async def test_should_login_to_gestion_sports_website(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.slow
|
||||||
async def test_booking_url_should_be_reachable(
|
async def test_booking_url_should_be_reachable(
|
||||||
a_user: User, a_booking_filter: BookingFilter, a_club: Club
|
a_user: User, a_booking_filter: BookingFilter, a_club: Club
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -81,7 +101,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 is None
|
assert court_booked in TPC_COURTS
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -126,7 +146,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -137,5 +159,27 @@ 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
|
||||||
|
@pytest.mark.slow
|
||||||
|
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
|
||||||
|
|
87
tests/gestion_sports/test_gestion_sports_platform.py
Normal file
87
tests/gestion_sports/test_gestion_sports_platform.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pendulum
|
||||||
|
import pytest
|
||||||
|
from aioresponses import aioresponses
|
||||||
|
from gestion_sports.gestion_sports_platform import (
|
||||||
|
GestionSportsPlatform,
|
||||||
|
wait_until_booking_time,
|
||||||
|
)
|
||||||
|
from models import BookingFilter, Club, User
|
||||||
|
|
||||||
|
from tests import fixtures, utils
|
||||||
|
from tests.fixtures import (
|
||||||
|
a_booking_failure_response,
|
||||||
|
a_booking_filter,
|
||||||
|
a_booking_success_response,
|
||||||
|
a_club,
|
||||||
|
a_user,
|
||||||
|
mes_resas_html,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("pendulum.now")
|
||||||
|
async def test_booking(
|
||||||
|
mock_now,
|
||||||
|
a_booking_success_response: str,
|
||||||
|
a_booking_failure_response: str,
|
||||||
|
a_user: User,
|
||||||
|
a_club: Club,
|
||||||
|
a_booking_filter: BookingFilter,
|
||||||
|
mes_resas_html: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test a single court booking without reading the conf from environment variables
|
||||||
|
|
||||||
|
:param mock_now: the pendulum.now() mock
|
||||||
|
:param a_booking_success_response: the success json response
|
||||||
|
:param a_booking_failure_response: the failure json response
|
||||||
|
:param a_user: a test user
|
||||||
|
:param a_club:a test club
|
||||||
|
:param a_booking_filter: a test booking filter
|
||||||
|
"""
|
||||||
|
booking_datetime = utils.retrieve_booking_datetime(a_booking_filter, a_club)
|
||||||
|
mock_now.side_effect = [booking_datetime]
|
||||||
|
|
||||||
|
# mock connection to the booking platform
|
||||||
|
with aioresponses() as aio_mock:
|
||||||
|
utils.mock_rest_api_from_connection_to_booking(
|
||||||
|
aio_mock,
|
||||||
|
fixtures.url,
|
||||||
|
a_booking_failure_response,
|
||||||
|
a_booking_success_response,
|
||||||
|
mes_resas_html,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with GestionSportsPlatform(a_club) as gs_operations:
|
||||||
|
court_booked = await gs_operations.book(a_user, a_booking_filter)
|
||||||
|
assert court_booked == a_club.courts_ids[1]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("pendulum.now")
|
||||||
|
def test_wait_until_booking_time(
|
||||||
|
mock_now, a_club: Club, a_booking_filter: BookingFilter
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the function that waits until the booking can be performed
|
||||||
|
|
||||||
|
:param mock_now: the pendulum.now() mock
|
||||||
|
:param a_club: a club
|
||||||
|
:param a_booking_filter: a booking filter
|
||||||
|
"""
|
||||||
|
booking_datetime = utils.retrieve_booking_datetime(a_booking_filter, a_club)
|
||||||
|
|
||||||
|
seconds = [
|
||||||
|
booking_datetime.subtract(seconds=3),
|
||||||
|
booking_datetime.subtract(seconds=2),
|
||||||
|
booking_datetime.subtract(seconds=1),
|
||||||
|
booking_datetime,
|
||||||
|
booking_datetime.add(microseconds=1),
|
||||||
|
booking_datetime.add(microseconds=2),
|
||||||
|
]
|
||||||
|
mock_now.side_effect = seconds
|
||||||
|
|
||||||
|
wait_until_booking_time(a_club, a_booking_filter)
|
||||||
|
|
||||||
|
assert pendulum.now() == booking_datetime.add(microseconds=1)
|
|
@ -1,25 +1,22 @@
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from aioresponses import aioresponses
|
from aioresponses import aioresponses
|
||||||
from models import BookingFilter, Club, User
|
from models import BookingFilter, Club
|
||||||
from pendulum import DateTime, Time
|
from pendulum import Time
|
||||||
|
|
||||||
from resa_padel import booking
|
from resa_padel import booking
|
||||||
from tests import fixtures
|
from tests import fixtures, utils
|
||||||
from tests.fixtures import (
|
from tests.fixtures import (
|
||||||
a_booking_failure_response,
|
a_booking_failure_response,
|
||||||
a_booking_filter,
|
|
||||||
a_booking_success_response,
|
a_booking_success_response,
|
||||||
a_club,
|
mes_resas_html,
|
||||||
a_user,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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"
|
||||||
|
@ -28,160 +25,6 @@ datetime_to_book = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def mock_successful_connection(aio_mock, url):
|
|
||||||
"""
|
|
||||||
Mock a call to the connection endpoint
|
|
||||||
|
|
||||||
:param aio_mock: the aioresponses mock object
|
|
||||||
:param url: the URL of the connection endpoint
|
|
||||||
"""
|
|
||||||
aio_mock.get(
|
|
||||||
url,
|
|
||||||
status=200,
|
|
||||||
headers={"Set-Cookie": f"connection_called=True; Domain={url}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mock_successful_login(aio_mock, url):
|
|
||||||
"""
|
|
||||||
Mock a call to the login endpoint
|
|
||||||
|
|
||||||
:param aio_mock: the aioresponses mock object
|
|
||||||
:param url: the URL of the login endpoint
|
|
||||||
"""
|
|
||||||
aio_mock.post(
|
|
||||||
url,
|
|
||||||
status=200,
|
|
||||||
headers={"Set-Cookie": f"login_called=True; Domain={url}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mock_booking(aio_mock, url, response):
|
|
||||||
"""
|
|
||||||
Mock a call to the booking endpoint
|
|
||||||
|
|
||||||
:param aio_mock: the aioresponses mock object
|
|
||||||
:param url: the URL of the booking endpoint
|
|
||||||
:param response: the response from the booking endpoint
|
|
||||||
"""
|
|
||||||
aio_mock.post(
|
|
||||||
url,
|
|
||||||
status=200,
|
|
||||||
headers={"Set-Cookie": f"booking_called=True; Domain={url}"},
|
|
||||||
body=response,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mock_rest_api_from_connection_to_booking(
|
|
||||||
aio_mock, url: str, a_booking_failure_response: str, a_booking_success_response: str
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Mock a REST API from a club.
|
|
||||||
It mocks the calls to the connexion to the website, a call to log in the user
|
|
||||||
and 2 calls to the booking endpoint
|
|
||||||
|
|
||||||
:param mock_now: the pendulum.now() mock
|
|
||||||
:param url: the API root URL
|
|
||||||
:param a_booking_success_response: the success json response
|
|
||||||
:param a_booking_failure_response: the failure json response
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
connexion_url = urljoin(url, "/connexion.php?")
|
|
||||||
mock_successful_connection(aio_mock, connexion_url)
|
|
||||||
|
|
||||||
login_url = urljoin(url, "/connexion.php?")
|
|
||||||
mock_successful_login(aio_mock, login_url)
|
|
||||||
|
|
||||||
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_success_response)
|
|
||||||
|
|
||||||
|
|
||||||
@patch("pendulum.now")
|
|
||||||
def test_wait_until_booking_time(
|
|
||||||
mock_now, a_club: Club, a_booking_filter: BookingFilter
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Test the function that waits until the booking can be performed
|
|
||||||
|
|
||||||
:param mock_now: the pendulum.now() mock
|
|
||||||
:param a_club: a club
|
|
||||||
:param a_booking_filter: a booking filter
|
|
||||||
"""
|
|
||||||
booking_datetime = retrieve_booking_datetime(a_booking_filter, a_club)
|
|
||||||
|
|
||||||
seconds = [
|
|
||||||
booking_datetime.subtract(seconds=3),
|
|
||||||
booking_datetime.subtract(seconds=2),
|
|
||||||
booking_datetime.subtract(seconds=1),
|
|
||||||
booking_datetime,
|
|
||||||
booking_datetime.add(microseconds=1),
|
|
||||||
booking_datetime.add(microseconds=2),
|
|
||||||
]
|
|
||||||
mock_now.side_effect = seconds
|
|
||||||
|
|
||||||
booking.wait_until_booking_time(a_club, a_booking_filter)
|
|
||||||
|
|
||||||
assert pendulum.now() == booking_datetime.add(microseconds=1)
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_booking_datetime(
|
|
||||||
a_booking_filter: BookingFilter, a_club: Club
|
|
||||||
) -> DateTime:
|
|
||||||
"""
|
|
||||||
Utility to retrieve the booking datetime from the booking filter and the club
|
|
||||||
|
|
||||||
:param a_booking_filter: the booking filter that contains the date to book
|
|
||||||
:param a_club: the club which has the number of days before the date and the booking time
|
|
||||||
"""
|
|
||||||
booking_hour = a_club.booking_opening_time.hour
|
|
||||||
booking_minute = a_club.booking_opening_time.minute
|
|
||||||
|
|
||||||
date_to_book = a_booking_filter.date
|
|
||||||
return date_to_book.subtract(days=a_club.booking_open_days_before).at(
|
|
||||||
booking_hour, booking_minute
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@patch("pendulum.now")
|
|
||||||
def test_booking_does_the_rights_calls(
|
|
||||||
mock_now,
|
|
||||||
a_booking_success_response: str,
|
|
||||||
a_booking_failure_response: str,
|
|
||||||
a_user: User,
|
|
||||||
a_club: Club,
|
|
||||||
a_booking_filter: BookingFilter,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Test a single court booking without reading the conf from environment variables
|
|
||||||
|
|
||||||
:param mock_now: the pendulum.now() mock
|
|
||||||
:param a_booking_success_response: the success json response
|
|
||||||
:param a_booking_failure_response: the failure json response
|
|
||||||
:param a_user: a test user
|
|
||||||
:param a_club:a test club
|
|
||||||
:param a_booking_filter: a test booking filter
|
|
||||||
"""
|
|
||||||
booking_datetime = retrieve_booking_datetime(a_booking_filter, a_club)
|
|
||||||
mock_now.side_effect = [booking_datetime]
|
|
||||||
|
|
||||||
# mock connection to the booking platform
|
|
||||||
with aioresponses() as aio_mock:
|
|
||||||
mock_rest_api_from_connection_to_booking(
|
|
||||||
aio_mock,
|
|
||||||
fixtures.url,
|
|
||||||
a_booking_failure_response,
|
|
||||||
a_booking_success_response,
|
|
||||||
)
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
court_booked = loop.run_until_complete(
|
|
||||||
booking.book(a_club, a_user, a_booking_filter)
|
|
||||||
)
|
|
||||||
assert court_booked == a_club.courts_ids[1]
|
|
||||||
|
|
||||||
|
|
||||||
@patch("pendulum.now")
|
@patch("pendulum.now")
|
||||||
@patch.dict(
|
@patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
|
@ -193,11 +36,15 @@ def test_booking_does_the_rights_calls(
|
||||||
"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
|
||||||
|
@ -214,15 +61,16 @@ def test_main(
|
||||||
booking_open_days_before=7,
|
booking_open_days_before=7,
|
||||||
booking_opening_time=Time(hour=0, minute=0),
|
booking_opening_time=Time(hour=0, minute=0),
|
||||||
)
|
)
|
||||||
booking_datetime = retrieve_booking_datetime(booking_filter, club)
|
booking_datetime = utils.retrieve_booking_datetime(booking_filter, club)
|
||||||
mock_now.side_effect = [booking_datetime]
|
mock_now.side_effect = [booking_datetime]
|
||||||
|
|
||||||
with aioresponses() as aio_mock:
|
with aioresponses() as aio_mock:
|
||||||
mock_rest_api_from_connection_to_booking(
|
utils.mock_rest_api_from_connection_to_booking(
|
||||||
aio_mock,
|
aio_mock,
|
||||||
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()
|
||||||
|
|
74
tests/test_config.py
Normal file
74
tests/test_config.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import config
|
||||||
|
from pendulum import DateTime, Time, Timezone
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"CLUB_URL": "club.url",
|
||||||
|
"COURT_IDS": "7,8,10",
|
||||||
|
"CLUB_ID": "666",
|
||||||
|
"BOOKING_OPEN_DAYS_BEFORE": "5",
|
||||||
|
"BOOKING_OPENING_TIME": "18:37",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_get_club():
|
||||||
|
club = config.get_club()
|
||||||
|
assert club.url == "club.url"
|
||||||
|
assert club.courts_ids == [7, 8, 10]
|
||||||
|
assert club.id == "666"
|
||||||
|
assert club.booking_open_days_before == 5
|
||||||
|
assert club.booking_opening_time == Time(hour=18, minute=37)
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"SPORT_ID": "666",
|
||||||
|
"DATE_TIME": "2024-02-03T22:38:45Z",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_get_booking_filter():
|
||||||
|
booking_filter = config.get_booking_filter()
|
||||||
|
assert booking_filter.sport_id == 666
|
||||||
|
assert booking_filter.date == DateTime(
|
||||||
|
year=2024,
|
||||||
|
month=2,
|
||||||
|
day=3,
|
||||||
|
hour=23,
|
||||||
|
minute=38,
|
||||||
|
second=45,
|
||||||
|
tzinfo=Timezone("Europe/Paris"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"LOGIN": "login@user.tld",
|
||||||
|
"PASSWORD": "gloups",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_get_available_user():
|
||||||
|
user = config.get_user()
|
||||||
|
assert user.login == "login@user.tld"
|
||||||
|
assert user.password == "gloups"
|
||||||
|
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"AVAILABLE_USERS_CREDENTIALS": "login@user.tld:gloups,other@user.tld:patatras"},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
def test_user():
|
||||||
|
users = config.get_available_users()
|
||||||
|
assert users[0].login == "login@user.tld"
|
||||||
|
assert users[0].password == "gloups"
|
||||||
|
assert users[1].login == "other@user.tld"
|
||||||
|
assert users[1].password == "patatras"
|
119
tests/utils.py
Normal file
119
tests/utils.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Mock a call to the connection endpoint
|
||||||
|
|
||||||
|
:param aio_mock: the aioresponses mock object
|
||||||
|
:param url: the URL of the connection endpoint
|
||||||
|
"""
|
||||||
|
aio_mock.get(
|
||||||
|
url,
|
||||||
|
status=200,
|
||||||
|
headers={"Set-Cookie": f"connection_called=True; Domain={url}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mock_successful_login(aio_mock, url):
|
||||||
|
"""
|
||||||
|
Mock a call to the login endpoint
|
||||||
|
|
||||||
|
:param aio_mock: the aioresponses mock object
|
||||||
|
:param url: the URL of the login endpoint
|
||||||
|
"""
|
||||||
|
aio_mock.post(
|
||||||
|
url,
|
||||||
|
status=200,
|
||||||
|
headers={"Set-Cookie": f"login_called=True; Domain={url}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mock_booking(aio_mock, url, response):
|
||||||
|
"""
|
||||||
|
Mock a call to the booking endpoint
|
||||||
|
|
||||||
|
:param aio_mock: the aioresponses mock object
|
||||||
|
:param url: the URL of the booking endpoint
|
||||||
|
:param response: the response from the booking endpoint
|
||||||
|
"""
|
||||||
|
aio_mock.post(
|
||||||
|
url,
|
||||||
|
status=200,
|
||||||
|
headers={"Set-Cookie": f"booking_called=True; Domain={url}"},
|
||||||
|
body=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_booking_datetime(
|
||||||
|
a_booking_filter: BookingFilter, a_club: Club
|
||||||
|
) -> DateTime:
|
||||||
|
"""
|
||||||
|
Utility to retrieve the booking datetime from the booking filter and the club
|
||||||
|
|
||||||
|
:param a_booking_filter: the booking filter that contains the date to book
|
||||||
|
:param a_club: the club which has the number of days before the date and the booking time
|
||||||
|
"""
|
||||||
|
booking_hour = a_club.booking_opening_time.hour
|
||||||
|
booking_minute = a_club.booking_opening_time.minute
|
||||||
|
|
||||||
|
date_to_book = a_booking_filter.date
|
||||||
|
return date_to_book.subtract(days=a_club.booking_open_days_before).at(
|
||||||
|
booking_hour, booking_minute
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
mes_resas_html: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mock a REST API from a club.
|
||||||
|
It mocks the calls to the connexion to the website, a call to log in the user
|
||||||
|
and 2 calls to the booking endpoint
|
||||||
|
|
||||||
|
:param aio_mock: the pendulum.now() mock
|
||||||
|
: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)
|
||||||
|
mock_booking(aio_mock, booking_url, a_booking_success_response)
|
||||||
|
mock_booking(aio_mock, booking_url, a_booking_failure_response)
|
Loading…
Add table
Add a link
Reference in a new issue