Merge pull request 'feature/refactor-request-building' (#9) from feature/refactor-request-building into main
Reviewed-on: https://jouf.fr/gitea/stanislas/resa-padel/pulls/9
This commit is contained in:
commit
466af1afd7
15 changed files with 380 additions and 184 deletions
88
poetry.lock
generated
88
poetry.lock
generated
|
@ -383,6 +383,92 @@ files = [
|
|||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
|
||||
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.0.5"
|
||||
|
@ -1151,4 +1237,4 @@ multidict = ">=4.0"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "c85317b4b8334d5d25f97ac426cfa6cd1f63c3c3d0726e313c97cf1de43c985a"
|
||||
content-hash = "eb2292ededdcd551249bd05bdc2fd3d38dfeeba495cb3ea8dd7ba5dfd9250980"
|
||||
|
|
|
@ -14,6 +14,7 @@ pendulum = "^3.0.0"
|
|||
pydantic = "^2.6.1"
|
||||
pydantic-extra-types = "^2.5.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
jinja2 = "^3.1.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.1.1"
|
||||
|
@ -30,7 +31,7 @@ line-length = 88
|
|||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = [
|
||||
".", "resa_padel", "rr"
|
||||
"resa_padel"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
|
@ -2,18 +2,25 @@ import asyncio
|
|||
import logging
|
||||
import time
|
||||
|
||||
import config
|
||||
import pendulum
|
||||
from aiohttp import ClientSession
|
||||
from pendulum import DateTime
|
||||
|
||||
import config
|
||||
from gestion_sports.gestion_sports_connector import GestionSportsConnector
|
||||
from models import BookingFilter, Club, User
|
||||
from pendulum import DateTime
|
||||
|
||||
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()
|
||||
|
@ -23,6 +30,15 @@ def wait_until_booking_time(club: Club, booking_filter: BookingFilter):
|
|||
|
||||
|
||||
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)
|
||||
|
||||
|
@ -33,15 +49,28 @@ def build_booking_datetime(booking_filter: BookingFilter, club: Club) -> DateTim
|
|||
|
||||
|
||||
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
|
||||
|
||||
:param club: the club where to book a court
|
||||
:param user: the user information
|
||||
:param booking_filter: the information related to the booking
|
||||
:return: the id of the booked court, or None if no court was booked
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
platform = GestionSportsConnector(session, club.url)
|
||||
await platform.connect()
|
||||
await platform.land()
|
||||
await platform.login(user, club)
|
||||
wait_until_booking_time(club, booking_filter)
|
||||
return await platform.book(booking_filter, club)
|
||||
|
||||
|
||||
def main() -> int | None:
|
||||
"""
|
||||
Main function used to book a court
|
||||
|
||||
:return: the id of the booked court, or None if no court was booked
|
||||
"""
|
||||
user = config.get_user()
|
||||
booking_filter = config.get_booking_filter()
|
||||
club = config.get_club()
|
||||
|
|
|
@ -6,13 +6,18 @@ from pathlib import Path
|
|||
import pendulum
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from models import BookingFilter, Club, User
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def get_club() -> Club:
|
||||
"""
|
||||
Read the environment variables related to the current club
|
||||
and build the Club object
|
||||
|
||||
:return: the club
|
||||
"""
|
||||
club_url = os.environ.get("CLUB_URL")
|
||||
court_ids_tmp = os.environ.get("COURT_IDS") or ""
|
||||
court_ids = (
|
||||
|
@ -34,6 +39,12 @@ def get_club() -> Club:
|
|||
|
||||
|
||||
def get_booking_filter() -> BookingFilter:
|
||||
"""
|
||||
Read the environment variables related to the current booking filter
|
||||
and build the BookingFilter object
|
||||
|
||||
:return: the club
|
||||
"""
|
||||
sport_id_tmp = os.environ.get("SPORT_ID")
|
||||
sport_id = int(sport_id_tmp) if sport_id_tmp else None
|
||||
date_time_tmp = os.environ.get("DATE_TIME")
|
||||
|
@ -42,12 +53,24 @@ def get_booking_filter() -> BookingFilter:
|
|||
|
||||
|
||||
def get_user() -> User:
|
||||
"""
|
||||
Read the environment variables related to the current user
|
||||
and build the User object
|
||||
|
||||
:return: the club
|
||||
"""
|
||||
login = os.environ.get("LOGIN")
|
||||
password = os.environ.get("PASSWORD")
|
||||
return User(login=login, password=password)
|
||||
|
||||
|
||||
def get_post_headers(platform_id: str) -> dict:
|
||||
"""
|
||||
Get the headers for the POST endpoint related to a specific booking platform
|
||||
|
||||
:param platform_id: the platform to which the headers apply
|
||||
:return: the headers as a dictionary
|
||||
"""
|
||||
root_path = Path(__file__).parent
|
||||
headers_file = Path(root_path, "resources", platform_id, "post-headers.json")
|
||||
with headers_file.open(mode="r", encoding="utf-8") as f:
|
||||
|
@ -57,9 +80,16 @@ def get_post_headers(platform_id: str) -> dict:
|
|||
|
||||
|
||||
def init_log_config():
|
||||
"""
|
||||
Read the logging.yaml file to initialize the logging configuration
|
||||
"""
|
||||
root_dir = os.path.realpath(os.path.dirname(__file__))
|
||||
logging_file = root_dir + "/logging.yaml"
|
||||
|
||||
with open(logging_file, "r") as f:
|
||||
logging_config = yaml.safe_load(f.read())
|
||||
logging.config.dictConfig(logging_config)
|
||||
|
||||
|
||||
ROOT_PATH = Path(__file__).parent.resolve()
|
||||
RESOURCES_DIR = Path(ROOT_PATH, "resources")
|
||||
|
|
7
resa_padel/gestion_sports/gestion_sports_config.py
Normal file
7
resa_padel/gestion_sports/gestion_sports_config.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from pathlib import Path
|
||||
|
||||
import config
|
||||
|
||||
RESOURCES_DIR = Path(config.RESOURCES_DIR, "gestion-sports")
|
||||
BOOKING_TEMPLATE = Path(RESOURCES_DIR, "booking-payload.txt")
|
||||
LOGIN_TEMPLATE = Path(RESOURCES_DIR, "login-payload.txt")
|
|
@ -3,10 +3,12 @@ import json
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from aiohttp import ClientResponse, ClientSession
|
||||
|
||||
import config
|
||||
from gestion_sports.gestion_sports_payload_builder import GestionSportsPayloadBuilder
|
||||
from aiohttp import ClientResponse, ClientSession
|
||||
from gestion_sports.payload_builders import (
|
||||
GestionSportsBookingPayloadBuilder,
|
||||
GestionSportsLoginPayloadBuilder,
|
||||
)
|
||||
from models import BookingFilter, Club, User
|
||||
|
||||
DATE_FORMAT = "%d/%m/%Y"
|
||||
|
@ -18,38 +20,61 @@ POST_HEADERS = config.get_post_headers("gestion-sports")
|
|||
|
||||
|
||||
class GestionSportsConnector:
|
||||
"""
|
||||
Handle the specific booking requests to Gestion-Sports
|
||||
"""
|
||||
|
||||
def __init__(self, session: ClientSession, url: str):
|
||||
LOGGER.info("Initializing connection to GestionSports API")
|
||||
self.url = url
|
||||
self.session = session
|
||||
self.payload_builder = GestionSportsPayloadBuilder()
|
||||
|
||||
@property
|
||||
def connection_url(self) -> str:
|
||||
def landing_url(self) -> str:
|
||||
"""
|
||||
Get the URL to the landing page of Gestion-Sports
|
||||
|
||||
:return: the URL to the landing page
|
||||
"""
|
||||
return urljoin(self.url, "/connexion.php?")
|
||||
|
||||
@property
|
||||
def login_url(self) -> str:
|
||||
"""
|
||||
Get the URL to the connection login of Gestion-Sports
|
||||
|
||||
:return: the URL to the login page
|
||||
"""
|
||||
return urljoin(self.url, "/connexion.php?")
|
||||
|
||||
@property
|
||||
def booking_url(self) -> str:
|
||||
"""
|
||||
Get the URL to the booking page of Gestion-Sports
|
||||
|
||||
:return: the URL to the booking page
|
||||
"""
|
||||
return urljoin(self.url, "/membre/reservation.html?")
|
||||
|
||||
async def connect(self) -> ClientResponse:
|
||||
async def land(self) -> ClientResponse:
|
||||
"""
|
||||
Perform the request to the landing page in order to get the cookie PHPSESSIONID
|
||||
|
||||
:return: the response from the landing page
|
||||
"""
|
||||
LOGGER.info("Connecting to GestionSports API")
|
||||
async with self.session.get(self.connection_url) as response:
|
||||
async with self.session.get(self.landing_url) as response:
|
||||
await response.text()
|
||||
return response
|
||||
|
||||
async def login(self, user: User, club: Club) -> ClientResponse:
|
||||
payload = (
|
||||
self.payload_builder.login(user.login)
|
||||
.password(user.password)
|
||||
.club_id(club.id)
|
||||
.build_login_payload()
|
||||
)
|
||||
"""
|
||||
Perform the request to the log in the user
|
||||
|
||||
:return: the response from the login
|
||||
"""
|
||||
payload_builder = GestionSportsLoginPayloadBuilder()
|
||||
payload = payload_builder.user(user).club(club).build()
|
||||
|
||||
async with self.session.post(
|
||||
self.login_url, data=payload, headers=POST_HEADERS
|
||||
|
@ -58,6 +83,15 @@ class GestionSportsConnector:
|
|||
return response
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
:param booking_filter: the booking information
|
||||
:param club: the club where to book the court
|
||||
:return: the booked court, or None if no court was booked
|
||||
"""
|
||||
# use asyncio to request a booking on every court
|
||||
# the gestion-sports backend is able to book only one court for a user
|
||||
bookings = await asyncio.gather(
|
||||
|
@ -68,10 +102,11 @@ class GestionSportsConnector:
|
|||
return_exceptions=True,
|
||||
)
|
||||
|
||||
return await self.get_booked_court(bookings)
|
||||
return self.get_booked_court(bookings)
|
||||
|
||||
@staticmethod
|
||||
async def get_booked_court(bookings):
|
||||
def get_booked_court(bookings):
|
||||
LOGGER.info(bookings)
|
||||
for court, is_booked in bookings:
|
||||
if is_booked:
|
||||
return court
|
||||
|
@ -80,23 +115,23 @@ class GestionSportsConnector:
|
|||
async def book_one_court(
|
||||
self, booking_filter: BookingFilter, court_id: int
|
||||
) -> tuple[int, bool]:
|
||||
payload_builder = GestionSportsBookingPayloadBuilder()
|
||||
payload = (
|
||||
self.payload_builder.date(booking_filter.date.date().strftime(DATE_FORMAT))
|
||||
.time(booking_filter.date.time().strftime(TIME_FORMAT))
|
||||
.sport_id(booking_filter.sport_id)
|
||||
.court_id(court_id)
|
||||
.build_booking_payload()
|
||||
payload_builder.booking_filter(booking_filter).court_id(court_id).build()
|
||||
)
|
||||
return court_id, await self.is_court_booked(payload)
|
||||
LOGGER.info(payload)
|
||||
|
||||
async def is_court_booked(self, payload: str) -> bool:
|
||||
async with self.session.post(
|
||||
self.booking_url, data=payload, headers=POST_HEADERS
|
||||
) as response:
|
||||
return self.is_response_status_ok(await response.text())
|
||||
resp_json = await response.text()
|
||||
|
||||
return court_id, self.is_response_status_ok(resp_json)
|
||||
|
||||
@staticmethod
|
||||
def is_response_status_ok(response: str) -> bool:
|
||||
|
||||
LOGGER.info(response)
|
||||
formatted_result = response.removeprefix('"').removesuffix('"')
|
||||
result_json = json.loads(formatted_result)
|
||||
return result_json["status"] == "ok"
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
from exceptions import ArgumentMissing
|
||||
|
||||
|
||||
class GestionSportsPayloadBuilder:
|
||||
def __init__(self):
|
||||
self._login = None
|
||||
self._password = None
|
||||
self._club_id = None
|
||||
self._date = None
|
||||
self._time = None
|
||||
self._sport_id = None
|
||||
self._court_id = None
|
||||
|
||||
def login(self, login: str):
|
||||
self._login = login
|
||||
return self
|
||||
|
||||
def password(self, password: str):
|
||||
self._password = password
|
||||
return self
|
||||
|
||||
def club_id(self, club_id: str):
|
||||
self._club_id = club_id
|
||||
return self
|
||||
|
||||
def date(self, date: str):
|
||||
self._date = date
|
||||
return self
|
||||
|
||||
def time(self, time: str):
|
||||
self._time = time
|
||||
return self
|
||||
|
||||
def sport_id(self, sport_id: int):
|
||||
self._sport_id = sport_id
|
||||
return self
|
||||
|
||||
def court_id(self, court_id: int):
|
||||
self._court_id = court_id
|
||||
return self
|
||||
|
||||
def build_login_payload(self):
|
||||
if self._login is None:
|
||||
raise ArgumentMissing("Login not provided")
|
||||
if self.password is None:
|
||||
raise ArgumentMissing("Password not provided")
|
||||
if self._club_id is None:
|
||||
raise ArgumentMissing("Club ID not provided")
|
||||
|
||||
return (
|
||||
f"ajax=connexionUser&id_club={self._club_id}&email={self._login}&form_ajax=1&pass={self._password}&compte"
|
||||
f"=user&playeridonesignal=0&identifiant=identifiant&externCo=true"
|
||||
).encode("utf-8")
|
||||
|
||||
def build_booking_payload(self):
|
||||
if self._date is None:
|
||||
raise ArgumentMissing("Date not provided")
|
||||
if self._time is None:
|
||||
raise ArgumentMissing("Time not provided")
|
||||
if self._sport_id is None:
|
||||
raise ArgumentMissing("Sport ID not provided")
|
||||
if self._court_id is None:
|
||||
raise ArgumentMissing("Court ID not provided")
|
||||
|
||||
return (
|
||||
f"ajax=addResa&date={self._date}&hour={self._time}&duration=90&partners=null|null|null"
|
||||
f"&paiement=facultatif&idSport={self._sport_id}&creaPartie=false&idCourt={self._court_id}"
|
||||
f"&pay=false&token=undefined&totalPrice=44&saveCard=0&foodNumber=0"
|
||||
).encode("utf-8")
|
58
resa_padel/gestion_sports/payload_builders.py
Normal file
58
resa_padel/gestion_sports/payload_builders.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from exceptions import ArgumentMissing
|
||||
from gestion_sports.gestion_sports_config import BOOKING_TEMPLATE, LOGIN_TEMPLATE
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from models import BookingFilter, Club, User
|
||||
|
||||
|
||||
class GestionSportsLoginPayloadBuilder:
|
||||
def __init__(self):
|
||||
self._user: User | None = None
|
||||
self._club: Club | None = None
|
||||
self._template = LOGIN_TEMPLATE
|
||||
|
||||
def user(self, user: User):
|
||||
self._user = user
|
||||
return self
|
||||
|
||||
def club(self, club: Club):
|
||||
self._club = club
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
if self._user is None:
|
||||
raise ArgumentMissing("No user was provided")
|
||||
if self._club is None:
|
||||
raise ArgumentMissing("No club was provided")
|
||||
|
||||
environment = Environment(loader=FileSystemLoader(self._template.parent))
|
||||
template = environment.get_template(self._template.name)
|
||||
|
||||
return template.render(club=self._club, user=self._user)
|
||||
|
||||
|
||||
class GestionSportsBookingPayloadBuilder:
|
||||
def __init__(self):
|
||||
self._booking_filter: BookingFilter | None = None
|
||||
self._court_id: int | None = None
|
||||
self._template = BOOKING_TEMPLATE
|
||||
|
||||
def booking_filter(self, booking_filter: BookingFilter):
|
||||
self._booking_filter = booking_filter
|
||||
return self
|
||||
|
||||
def court_id(self, court_id: int):
|
||||
self._court_id = court_id
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
if self._booking_filter is None:
|
||||
raise ArgumentMissing("No booking filter was provided")
|
||||
if self.court_id is None:
|
||||
raise ArgumentMissing("No court id was provided")
|
||||
|
||||
environment = Environment(loader=FileSystemLoader(self._template.parent))
|
||||
template = environment.get_template(self._template.name)
|
||||
|
||||
return template.render(
|
||||
court_id=self._court_id, booking_filter=self._booking_filter
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
from pendulum import Time
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic_extra_types.pendulum_dt import DateTime
|
||||
|
||||
|
||||
|
|
1
resa_padel/resources/gestion-sports/booking-payload.txt
Normal file
1
resa_padel/resources/gestion-sports/booking-payload.txt
Normal file
|
@ -0,0 +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
|
1
resa_padel/resources/gestion-sports/login-payload.txt
Normal file
1
resa_padel/resources/gestion-sports/login-payload.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ajax=connexionUser&id_club={{ club.id }}&email={{ user.login }}&form_ajax=1&pass={{ user.password }}&compte=user&playeridonesignal=0&identifiant=identifiant&externCo=true
|
|
@ -2,10 +2,8 @@ import json
|
|||
|
||||
import pendulum
|
||||
import pytest
|
||||
from gestion_sports.payload_builders import GestionSportsBookingPayloadBuilder
|
||||
|
||||
from resa_padel.gestion_sports.gestion_sports_payload_builder import (
|
||||
GestionSportsPayloadBuilder,
|
||||
)
|
||||
from resa_padel.models import BookingFilter, Club, User
|
||||
|
||||
user = User(login="padel.testing@jouf.fr", password="ridicule", club_id="123")
|
||||
|
@ -38,12 +36,10 @@ booking_success_response = json.dumps(
|
|||
date_format = "%d/%m/%Y"
|
||||
time_format = "%H:%M"
|
||||
booking_payload = (
|
||||
GestionSportsPayloadBuilder()
|
||||
.date(booking_date.date().strftime(date_format))
|
||||
.time(booking_date.time().strftime(time_format))
|
||||
.sport_id(sport_id)
|
||||
GestionSportsBookingPayloadBuilder()
|
||||
.booking_filter(booking_filter)
|
||||
.court_id(courts[0])
|
||||
.build_booking_payload()
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import pytest
|
||||
from aiohttp import ClientSession
|
||||
from models import BookingFilter, Club, User
|
||||
from yarl import URL
|
||||
|
||||
from resa_padel.gestion_sports.gestion_sports_connector import GestionSportsConnector
|
||||
from tests.fixtures import (
|
||||
a_booking_failure_response,
|
||||
a_booking_filter,
|
||||
a_booking_payload,
|
||||
a_booking_success_response,
|
||||
a_club,
|
||||
a_user,
|
||||
|
@ -16,13 +16,16 @@ tpc_url = "https://toulousepadelclub.gestion-sports.com"
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_connect_to_gestion_sports_website():
|
||||
async def test_should_reach_landing_page_to_gestion_sports_website() -> None:
|
||||
"""
|
||||
Test that landing page is reached
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
cookies = session.cookie_jar.filter_cookies(URL(tpc_url))
|
||||
assert cookies.get("PHPSESSID") is None
|
||||
gs_connector = GestionSportsConnector(session, tpc_url)
|
||||
|
||||
response = await gs_connector.connect()
|
||||
response = await gs_connector.land()
|
||||
|
||||
assert response.status == 200
|
||||
assert response.request_info.method == "GET"
|
||||
|
@ -35,10 +38,18 @@ async def test_should_connect_to_gestion_sports_website():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_login_to_gestion_sports_website(a_user, a_club):
|
||||
async def test_should_login_to_gestion_sports_website(
|
||||
a_user: User, a_club: Club
|
||||
) -> None:
|
||||
"""
|
||||
Test that a user can log in after reaching the landing page
|
||||
|
||||
:param a_user: the user that wants to book a court
|
||||
:param a_club: the club information
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
gs_connector = GestionSportsConnector(session, tpc_url)
|
||||
await gs_connector.connect()
|
||||
await gs_connector.land()
|
||||
|
||||
response = await gs_connector.login(a_user, a_club)
|
||||
|
||||
|
@ -53,10 +64,19 @@ async def test_should_login_to_gestion_sports_website(a_user, a_club):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_booking_url_should_be_reachable(a_user, a_booking_filter, a_club):
|
||||
async def test_booking_url_should_be_reachable(
|
||||
a_user: User, a_booking_filter: BookingFilter, a_club: Club
|
||||
) -> None:
|
||||
"""
|
||||
Test that a user can log in the booking platform and book a court
|
||||
|
||||
:param a_user: the user that wants to book a court
|
||||
:param a_booking_filter: the booking information
|
||||
:param a_club: the club information
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
gs_connector = GestionSportsConnector(session, tpc_url)
|
||||
await gs_connector.connect()
|
||||
await gs_connector.land()
|
||||
await gs_connector.login(a_user, a_club)
|
||||
|
||||
court_booked = await gs_connector.book(a_booking_filter, a_club)
|
||||
|
@ -67,11 +87,21 @@ async def test_booking_url_should_be_reachable(a_user, a_booking_filter, a_club)
|
|||
@pytest.mark.asyncio
|
||||
async def test_should_book_a_court_from_gestion_sports(
|
||||
aioresponses,
|
||||
a_booking_filter,
|
||||
a_club,
|
||||
a_booking_success_response,
|
||||
a_booking_failure_response,
|
||||
):
|
||||
a_booking_filter: BookingFilter,
|
||||
a_club: Club,
|
||||
a_booking_success_response: str,
|
||||
a_booking_failure_response: str,
|
||||
) -> None:
|
||||
"""
|
||||
Test that user can reach the landing page, then log in to the platform
|
||||
and eventually book a court
|
||||
|
||||
:param aioresponses: the http response mock
|
||||
:param a_booking_filter: the booking information
|
||||
:param a_club: the club information
|
||||
:param a_booking_success_response: the success response mock
|
||||
:param a_booking_failure_response: the failure response mock
|
||||
"""
|
||||
booking_url = URL(tpc_url + "/membre/reservation.html?")
|
||||
|
||||
# first booking request will fail
|
||||
|
@ -89,38 +119,23 @@ async def test_should_book_a_court_from_gestion_sports(
|
|||
assert court_booked == a_club.courts_ids[1]
|
||||
|
||||
|
||||
def test_response_status_should_be_ok(a_booking_success_response):
|
||||
def test_response_status_should_be_ok(a_booking_success_response: str) -> None:
|
||||
"""
|
||||
Test internal method to verify that the success response received by booking
|
||||
a gestion-sports court is still a JSON with a field 'status' set to 'ok'
|
||||
|
||||
:param a_booking_success_response: the success response mock
|
||||
"""
|
||||
is_booked = GestionSportsConnector.is_response_status_ok(a_booking_success_response)
|
||||
assert is_booked
|
||||
|
||||
|
||||
def test_response_status_should_be_not_ok(a_booking_failure_response):
|
||||
def test_response_status_should_be_not_ok(a_booking_failure_response: str) -> None:
|
||||
"""
|
||||
Test internal method to verify that the failure response received by booking
|
||||
a gestion-sports court is still a JSON with a field 'status' set to 'error'
|
||||
|
||||
:param a_booking_failure_response: the failure response mock
|
||||
"""
|
||||
is_booked = GestionSportsConnector.is_response_status_ok(a_booking_failure_response)
|
||||
assert not is_booked
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_court_should_not_be_booked(
|
||||
aioresponses, a_booking_payload, a_booking_failure_response
|
||||
):
|
||||
async with ClientSession() as session:
|
||||
tpc_connector = GestionSportsConnector(session, tpc_url)
|
||||
aioresponses.post(
|
||||
URL(tpc_connector.booking_url), status=200, body=a_booking_failure_response
|
||||
)
|
||||
is_booked = await tpc_connector.is_court_booked(a_booking_payload)
|
||||
assert not is_booked
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_court_should_be_booked(
|
||||
aioresponses, a_booking_payload, a_booking_success_response
|
||||
):
|
||||
async with ClientSession() as session:
|
||||
tpc_connector = GestionSportsConnector(session, tpc_url)
|
||||
|
||||
aioresponses.post(
|
||||
URL(tpc_connector.booking_url), status=200, body=a_booking_success_response
|
||||
)
|
||||
is_booked = await tpc_connector.is_court_booked(a_booking_payload)
|
||||
assert is_booked
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
import pendulum
|
||||
from resa_padel.gestion_sports.payload_builders import (
|
||||
GestionSportsBookingPayloadBuilder,
|
||||
GestionSportsLoginPayloadBuilder,
|
||||
)
|
||||
from tests.fixtures import a_booking_filter, a_club, a_user
|
||||
|
||||
from resa_padel.gestion_sports.gestion_sports_payload_builder import (
|
||||
GestionSportsPayloadBuilder,
|
||||
|
||||
def test_login_payload_should_be_built(a_user, a_club):
|
||||
"""
|
||||
Test that the login payload is filled with the right template
|
||||
and filled accordingly
|
||||
|
||||
:param a_user: the user information fixture
|
||||
:param a_club: the club information fixture
|
||||
"""
|
||||
payload_builder = GestionSportsLoginPayloadBuilder()
|
||||
login_payload = payload_builder.user(a_user).club(a_club).build()
|
||||
|
||||
expected_payload = (
|
||||
f"ajax=connexionUser&id_club={a_club.id}&email={a_user.login}&form_ajax=1"
|
||||
f"&pass={a_user.password}&compte=user&playeridonesignal=0"
|
||||
f"&identifiant=identifiant&externCo=true"
|
||||
)
|
||||
|
||||
|
||||
def test_login_payload_should_be_built():
|
||||
payload_builder = GestionSportsPayloadBuilder()
|
||||
login = "jacques"
|
||||
password = "chirac"
|
||||
club_id = "27"
|
||||
login_payload = (
|
||||
payload_builder.login(login)
|
||||
.password(password)
|
||||
.club_id(club_id)
|
||||
.build_login_payload()
|
||||
)
|
||||
|
||||
assert login_payload == (
|
||||
f"ajax=connexionUser&id_club={club_id}&email={login}&form_ajax=1&pass={password}&compte"
|
||||
f"=user&playeridonesignal=0&identifiant=identifiant&externCo=true"
|
||||
).encode("utf-8")
|
||||
assert login_payload == expected_payload
|
||||
|
||||
|
||||
def test_booking_payload_should_be_built():
|
||||
booking_builder = GestionSportsPayloadBuilder()
|
||||
booking_date = pendulum.now("Europe/Paris")
|
||||
sport_id = "27"
|
||||
court_id = "55"
|
||||
def test_booking_payload_should_be_built(a_booking_filter):
|
||||
"""
|
||||
Test that the booking payload is filled with the right template
|
||||
and filled accordingly
|
||||
|
||||
:param a_booking_filter: the booking information fixture
|
||||
"""
|
||||
booking_builder = GestionSportsBookingPayloadBuilder()
|
||||
booking_payload = (
|
||||
booking_builder.date(booking_date.date())
|
||||
.time(booking_date.time())
|
||||
.sport_id(sport_id)
|
||||
.court_id(court_id)
|
||||
.build_booking_payload()
|
||||
booking_builder.booking_filter(a_booking_filter).court_id(4).build()
|
||||
)
|
||||
|
||||
assert booking_payload == (
|
||||
f"ajax=addResa&date={booking_date.date()}&hour={booking_date.time()}&duration=90"
|
||||
f"&partners=null|null|null&paiement=facultatif&idSport={sport_id}"
|
||||
f"&creaPartie=false&idCourt={court_id}&pay=false&token=undefined&totalPrice=44&saveCard=0"
|
||||
f"&foodNumber=0"
|
||||
).encode("utf-8")
|
||||
expected_date = a_booking_filter.date.date().strftime("%d/%m/%Y")
|
||||
expected_time = a_booking_filter.date.time().strftime("%H:%M")
|
||||
expected_payload = (
|
||||
f"ajax=addResa&date={expected_date}"
|
||||
f"&hour={expected_time}&duration=90&partners=null|null|null"
|
||||
f"&paiement=facultatif&idSport={a_booking_filter.sport_id}"
|
||||
f"&creaPartie=false&idCourt=4&pay=false&token=undefined&totalPrice=44"
|
||||
f"&saveCard=0&foodNumber=0"
|
||||
)
|
||||
|
||||
assert booking_payload == expected_payload
|
||||
|
|
|
@ -5,9 +5,9 @@ from urllib.parse import urljoin
|
|||
|
||||
import pendulum
|
||||
from aioresponses import aioresponses
|
||||
from models import BookingFilter, Club, User
|
||||
from pendulum import DateTime, Time
|
||||
|
||||
from models import BookingFilter, Club, User
|
||||
from resa_padel import booking
|
||||
from tests import fixtures
|
||||
from tests.fixtures import (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue