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:
Stanislas Jouffroy 2024-02-20 09:30:47 +00:00
commit 466af1afd7
15 changed files with 380 additions and 184 deletions

88
poetry.lock generated
View file

@ -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"

View file

@ -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]

View file

@ -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()

View file

@ -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")

View 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")

View file

@ -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"

View file

@ -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")

View 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
)

View file

@ -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

View 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

View 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

View file

@ -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()
)

View file

@ -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

View file

@ -1,44 +1,50 @@
import pendulum
from resa_padel.gestion_sports.gestion_sports_payload_builder import (
GestionSportsPayloadBuilder,
from resa_padel.gestion_sports.payload_builders import (
GestionSportsBookingPayloadBuilder,
GestionSportsLoginPayloadBuilder,
)
from tests.fixtures import a_booking_filter, a_club, a_user
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()
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"
)
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

View file

@ -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 (