Merge pull request 'All methods are in the right class' (#21) from refactor-code-and-comments into main

Reviewed-on: https://jouf.fr/gitea/stanislas/resa-padel/pulls/21
This commit is contained in:
Stanislas Jouffroy 2024-03-24 13:37:39 +00:00
commit cea772371e
12 changed files with 585 additions and 1729 deletions

27
gd.json Normal file
View file

@ -0,0 +1,27 @@
[
{
"id": 3628098,
"chargeId": null,
"partners": [],
"dateResa": "28\/03\/2024",
"startTime": "13:30",
"endTime": "15:00",
"dayFr": "jeudi 28 mars 2024",
"codeLiveXperience": null,
"qrCodeSpartime": null,
"sport": "Padel",
"court": "court 11",
"creaPartie": 0,
"limitCreaPartie": "2024-03-28 11:30:00",
"cancel": true,
"bloquerRemplacementJoueur": 1,
"canRemovePartners": false,
"remainingPlaces": 3,
"isCaptain": true,
"dtStart": "2024-03-28T13:30:00+01:00",
"garantieCb": null,
"dureeValidCertif": null,
"playerStatus": 3,
"products": []
}
]

View file

@ -39,24 +39,12 @@ async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter)
await service.cancel_booking(user, club, booking_filter)
async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
"""
Cancel a booking that matches the booking id
:param club: the club in which the booking was made
:param user: the user who made the booking
:param booking_id: the id of the booking to cancel
"""
service = GestionSportsServices()
await service.cancel_booking_id(user, club, booking_id)
async def get_tournaments(club: Club, user: User) -> list[Tournament]:
"""
Cancel a booking that matches the booking id
Get the list of all current tournaments, their price, date and availability
:param club: the club in which the booking was made
:param user: the user who made the booking
:param club: the club in which the tournaments are
:param user: a user of the club in order to retrieve the information
"""
service = GestionSportsServices()
return await service.get_all_tournaments(user, club)
@ -74,17 +62,23 @@ def main() -> tuple[Court, User] | list[Tournament] | None:
club = config.get_club()
users = config.get_users(club.id)
booking_filter = config.get_booking_filter()
LOGGER.info(
f"Booking a court of {booking_filter.sport_name} at {booking_filter.date} "
f"at club {club.name}"
)
court_booked, user = asyncio.run(book_court(club, users, booking_filter))
if court_booked:
LOGGER.info(
"Court %s booked successfully at %s for user %s",
court_booked,
booking_filter.date,
user,
f"Court of {booking_filter.sport_name} {court_booked} was booked "
f"successfully at {booking_filter.date} at club {club.name} "
f"for user {user}"
)
return court_booked, user
else:
LOGGER.info("Booking did not work")
LOGGER.info(
f"No court of {booking_filter.sport_name} at {booking_filter.date} "
f"at club {club.name} was booked"
)
elif action == Action.CANCEL:
user = config.get_user()

View file

@ -1,2 +1,6 @@
class ArgumentMissing(Exception):
class WrongResponseStatus(Exception):
pass
class MissingProperty(Exception):
pass

View file

@ -2,12 +2,11 @@ import asyncio
import json
import logging
from pathlib import Path
from urllib.parse import urljoin
import config
from aiohttp import ClientResponse, ClientSession
from bs4 import BeautifulSoup
from models import Booking, BookingFilter, Club, Court, Sport, User
from exceptions import WrongResponseStatus
from models import BookingFilter, Club, Sport, User
from payload_builders import PayloadBuilder
from pendulum import DateTime
@ -18,8 +17,8 @@ POST_HEADERS = config.get_post_headers("gestion-sports")
class GestionSportsConnector:
"""
The connector for the Gestion Sports platform.
It handles all the requests to the website.
The connector for the Gestion Sports platform handles all the HTTP requests to the
Gestion sports website.
"""
def __init__(self, club: Club):
@ -35,184 +34,139 @@ class GestionSportsConnector:
self.club = club
def _get_url_path(self, name: str) -> str:
"""
Get the URL path for the service with the given name
:param name: the name of the service
:return: the URL path
"""
self._check_url_path_exists(name)
return urljoin(
self.club.booking_platform.url,
self.club.booking_platform.urls.get(name).path,
)
def _get_url_parameter(self, name: str) -> str:
self._check_url_path_exists(name)
return self.club.booking_platform.urls.get(name).parameter
def _get_payload_template(self, name: str) -> Path:
"""
Get the path to the template file for the service with the given name
:param name: the name of the service
:return: the path to the template file
"""
self._check_payload_template_exists(name)
return (
config.get_resources_folder()
/ self.club.booking_platform.urls.get(name).payload_template
)
def _check_url_path_exists(self, name: str) -> None:
"""
Check that the URL path for the given service is defined
:param name: the name of the service
"""
if (
self.club.booking_platform.urls is None
or self.club.booking_platform.urls.get(name) is None
or self.club.booking_platform.urls.get(name).path is None
):
raise ValueError(
f"The booking platform internal URL path for page {name} of club "
f"{self.club.name} are not set"
)
def _check_payload_template_exists(self, name: str) -> None:
"""
Check that the payload template for the given service is defined
:param name: the name of the service
"""
if (
self.club.booking_platform.urls is None
or self.club.booking_platform.urls.get(name) is None
or self.club.booking_platform.urls.get(name).path is None
):
raise ValueError(
f"The booking platform internal URL path for page {name} of club "
f"{self.club.name} are not set"
)
@property
def landing_url(self) -> str:
"""
Get the URL to the landing page of Gestion-Sports
Get the URL to for landing to the website
:return: the URL to the landing page
:return: the URL to landing
"""
return self._get_url_path("landing-page")
return self.club.landing_url
@property
def login_url(self) -> str:
"""
Get the URL to the connection login of Gestion-Sports
Get the URL to for logging in the website
:return: the URL to the login page
:return: the URL for logging in
"""
return self._get_url_path("login")
return self.club.login_url
@property
def login_template(self) -> Path:
"""
Get the payload template to send to log in the website
Get the payload template for logging in the website
:return: the payload template for logging in
:return: the payload template for logging
"""
return self._get_payload_template("login")
return self.club.login_template
@property
def booking_url(self) -> str:
"""
Get the URL to the booking page of Gestion-Sports
Get the URL used to book a court
:return: the URL to the booking page
:return: the URL to book a court
"""
return self._get_url_path("booking")
return self.club.booking_url
@property
def booking_template(self) -> Path:
"""
Get the payload template to send to book a court
Get the payload template for booking a court
:return: the payload template for booking a court
"""
return self._get_payload_template("booking")
return self.club.booking_template
@property
def user_bookings_url(self) -> str:
"""
Get the URL where all the user's bookings are available
Get the URL of the bookings related to a user that are not yet passed
:return: the URL to the user's bookings
:return: the URL to get the bookings related to a user
"""
return self._get_url_path("user-bookings")
return self.club.user_bookings_url
@property
def user_bookings_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
Get the payload template to get the bookings related to a user that are not yet
passed
:return: the payload template for the user's bookings
:return: the template for requesting the bookings related to a user
"""
return self._get_payload_template("user-bookings")
return self.club.user_bookings_template
@property
def booking_cancellation_url(self) -> str:
def cancel_url(self) -> str:
"""
Get the URL where all the user's bookings are available
Get the URL used to cancel a booking
:return: the URL to the user's bookings
:return: the URL to cancel a booking
"""
return self._get_url_path("cancellation")
return self.club.cancel_url
@property
def booking_cancel_template(self) -> Path:
def cancel_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
Get the payload template for cancelling a booking
:return: the payload template for the user's bookings
:return: the template for cancelling a booking
"""
return self._get_payload_template("cancellation")
return self.club.cancel_template
@property
def tournaments_sessions_url(self) -> str:
return self._get_url_path("tournament-sessions")
def sessions_url(self) -> str:
"""
Get the URL of the session containing all the tournaments
:return: the URL to get the session
"""
return self.club.sessions_url
@property
def tournaments_sessions_template(self) -> Path:
return self._get_payload_template("tournament-sessions")
def sessions_template(self) -> Path:
"""
Get the payload template for requesting the session containing all the
tournaments
:return: the template for requesting the session
"""
return self.club.sessions_template
@property
def tournaments_list_url(self) -> str:
return self._get_url_path("tournaments-list")
def tournaments_url(self) -> str:
"""
Get the URL of all the tournaments list
:return: the URL to get the tournaments list
"""
return self.club.tournaments_url
@property
def available_sports(self) -> dict[str, Sport]:
def sports(self) -> dict[str, Sport]:
"""
Get a dictionary of all sports, the key is the sport name lowered case
:return: the dictionary of all sports
"""
return {
sport.name.lower(): sport for sport in self.club.booking_platform.sports
}
return self.club.sports
@staticmethod
def check_response_status(response_status: int) -> None:
if response_status != 200:
raise WrongResponseStatus("GestionSports request failed")
async def land(self, session: ClientSession) -> ClientResponse:
"""
Perform the request to the landing page in order to get the cookie PHPSESSIONID
:param session: the client session shared among all connections
:return: the response from the landing page
"""
LOGGER.info("Connecting to GestionSports API at %s", self.login_url)
async with session.get(self.landing_url) as response:
self.check_response_status(response.status)
await response.text()
return response
@ -220,6 +174,8 @@ class GestionSportsConnector:
"""
Perform the request to the log in the user
:param session: the client session shared among all connections
:param user: the user to log in
:return: the response from the login
"""
LOGGER.info("Logging in to GestionSports API at %s", self.login_url)
@ -228,11 +184,12 @@ class GestionSportsConnector:
async with session.post(
self.login_url, data=payload, headers=POST_HEADERS, allow_redirects=False
) as response:
self.check_response_status(response.status)
resp_text = await response.text()
LOGGER.debug("Connexion request response:\n%s", resp_text)
return response
async def book_any_court(
async def send_all_booking_requests(
self, session: ClientSession, booking_filter: BookingFilter
) -> list[tuple[int, dict]]:
"""
@ -241,7 +198,7 @@ class GestionSportsConnector:
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 session: the session to use
:param session: the client session shared among all connections
:param booking_filter: the booking conditions to meet
:return: the booked court, or None if no court was booked
"""
@ -249,7 +206,7 @@ class GestionSportsConnector:
"Booking any available court from GestionSports API at %s", self.booking_url
)
sport = self.available_sports.get(booking_filter.sport_name)
sport = self.sports.get(booking_filter.sport_name)
bookings = await asyncio.gather(
*[
@ -274,7 +231,7 @@ class GestionSportsConnector:
"""
Book a single court that meets the conditions from the booking filter
:param session: the HTTP session that contains the user information and cookies
:param session: the client session shared among all connections
:param date: the booking date
:param court_id: the id of the court to book
:param sport_id: the id of the sport
@ -293,168 +250,96 @@ class GestionSportsConnector:
async with session.post(
self.booking_url, data=payload, headers=POST_HEADERS
) as response:
assert response.status == 200
self.check_response_status(response.status)
resp_json = json.loads(await response.text())
LOGGER.debug("Response from booking request:\n'%s'", resp_json)
return court_id, resp_json
def get_booked_court(
self, bookings: list[tuple[int, dict]], sport_name: str
) -> Court | None:
async def send_hash_request(self, session: ClientSession) -> ClientResponse:
"""
Parse the booking list and return the court that was booked
Get the hash value used in some other requests
:param bookings: a list of bookings
:param sport_name: the sport name
:return: the id of the booked court if any, None otherwise
"""
for court_id, response in bookings:
if self.is_booking_response_status_ok(response):
LOGGER.debug("Court %d is booked", court_id)
court_booked = self.find_court(court_id, sport_name)
LOGGER.info("Court '%s' is booked", court_booked.name)
return court_booked
LOGGER.debug("No booked court found")
return None
def find_court(self, court_id: int, sport_name: str) -> Court:
"""
Get all the court information based on the court id and the sport name
:param court_id: the court id
:param sport_name: the sport name
:return: the court that has the given id and sport name
"""
sport = self.available_sports.get(sport_name.lower())
for court in sport.courts:
if court.id == court_id:
return court
@staticmethod
def is_booking_response_status_ok(response: dict) -> bool:
"""
Check if the booking response is OK
:param response: the response as a string
:return: true if the status is ok, false otherwise
"""
return response["status"] == "ok"
async def get_ongoing_bookings(self, session: ClientSession) -> list[Booking]:
"""
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
"""
hash_value = await self.send_hash_request(session)
LOGGER.debug(f"Hash value: {hash_value}")
payload = PayloadBuilder.build(self.user_bookings_template, hash=hash_value)
LOGGER.debug(f"Payload to get ongoing bookings: {payload}")
return await self.send_user_bookings_request(session, payload)
async def send_hash_request(self, session: ClientSession) -> str:
"""
Get the hash value used in the request to get the user's bookings
:param session: the session in which the user logged in
:param session: the client session shared among all connections
:return: the value of the hash
"""
async with session.get(self.user_bookings_url) as response:
self.check_response_status(response.status)
html = await response.text()
LOGGER.debug("Get bookings response: %s\n", html)
return self.get_hash_input(html)
@staticmethod
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()
return response
async def send_user_bookings_request(
self, session: ClientSession, payload: str
) -> list[Booking]:
self, session: ClientSession, hash_value: str
) -> ClientResponse:
"""
Perform the HTTP request to get all bookings
Send a request to the platform to get all bookings of a user
:param session: the session in which the user logged in
:param payload: the HTTP payload for the request
:param session: the client session shared among all connections
:param hash_value: the hash value to put in the payload
:return: a dictionary containing all the bookings
"""
payload = PayloadBuilder.build(self.user_bookings_template, hash=hash_value)
async with 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 [Booking(**booking) for booking in json.loads(resp)]
self.check_response_status(response.status)
await response.text()
return response
async def cancel_booking_id(
self, session: ClientSession, booking_id: int
async def send_cancellation_request(
self, session: ClientSession, booking_id: int, hash_value: str
) -> ClientResponse:
"""
Send the HTTP request to cancel the booking
:param session: the HTTP session that contains the user information and cookies
:param session: the client session shared among all connections
:param booking_id: the id of the booking to cancel
:return: the response from the client
"""
hash_value = await self.send_hash_request(session)
payload = PayloadBuilder.build(
self.booking_cancel_template,
self.cancel_template,
booking_id=booking_id,
hash=hash_value,
)
async with session.post(
self.booking_cancellation_url, data=payload, headers=POST_HEADERS
self.cancel_url, data=payload, headers=POST_HEADERS
) as response:
self.check_response_status(response.status)
await response.text()
return response
async def cancel_booking(
self, session: ClientSession, booking_filter: BookingFilter
) -> ClientResponse | None:
async def send_session_request(self, session: ClientSession) -> ClientResponse:
"""
Cancel the booking that meets some conditions
Send a request to the platform to get the session id
:param session: the session
:param booking_filter: the conditions the booking to cancel should meet
:param session: the client session shared among all connections
:return: a client response containing HTML which has the session id
"""
bookings = await self.get_ongoing_bookings(session)
for booking in bookings:
if booking.matches(booking_filter):
return await self.cancel_booking_id(session, booking.id)
async def send_tournaments_sessions_request(
self, session: ClientSession
) -> ClientResponse:
payload = self.tournaments_sessions_template.read_text()
payload = self.sessions_template.read_text()
async with session.post(
self.tournaments_sessions_url, data=payload, headers=POST_HEADERS
self.sessions_url, data=payload, headers=POST_HEADERS
) as response:
self.check_response_status(response.status)
LOGGER.debug("tournament sessions: \n%s", await response.text())
return response
async def send_tournaments_request(
self, session: ClientSession, tournement_session_id: str
self, session: ClientSession, session_id: str
) -> ClientResponse:
final_url = self.tournaments_list_url + tournement_session_id
"""
Send a request to the platform to get the next tournaments
:param session: the client session shared among all connections
:param session_id: the tournaments are grouped in a session
:return: a client response containing the list of all the nex tournaments
"""
final_url = self.tournaments_url + session_id
LOGGER.debug("Getting tournaments list at %s", final_url)
async with session.get(final_url) as response:
self.check_response_status(response.status)
LOGGER.debug("tournaments: %s\n", await response.text())
return response

View file

@ -6,16 +6,24 @@ import pendulum
from aiohttp import ClientSession
from bs4 import BeautifulSoup
from gestion_sport_connector import GestionSportsConnector
from models import BookingFilter, BookingOpening, Club, Court, Tournament, User
from models import (
Booking,
BookingFilter,
BookingOpening,
Club,
Court,
Sport,
Tournament,
User,
)
from pendulum import DateTime
LOGGER = logging.getLogger(__name__)
class GestionSportsServices:
@staticmethod
async def book(
club: Club, user: User, booking_filter: BookingFilter
self, club: Club, user: User, booking_filter: BookingFilter
) -> Court | None:
"""
Perform a request for each court at the same time to increase the chances to get
@ -45,37 +53,76 @@ class GestionSportsServices:
booking_filter, booking_opening
)
bookings = await connector.book_any_court(session, booking_filter)
bookings = await connector.send_all_booking_requests(
session, booking_filter
)
LOGGER.debug("Booking results:\n'%s'", bookings)
return connector.get_booked_court(bookings, booking_filter.sport_name)
sport = club.sports.get(booking_filter.sport_name)
return self.get_booked_court(bookings, sport)
def get_booked_court(
self, bookings: list[tuple[int, dict]], sport: Sport
) -> Court | None:
"""
Parse the booking list and return the court that was booked
:param bookings: a list of bookings
:param sport: the sport of the club and all the courts it has
:return: the id of the booked court if any, None otherwise
"""
for court_id, response in bookings:
if self.is_booking_response_status_ok(response):
LOGGER.debug("Court %d is booked", court_id)
court_booked = self.find_court(court_id, sport)
LOGGER.info("Court '%s' is booked", court_booked.name)
return court_booked
LOGGER.debug("No booked court found")
return None
@staticmethod
async def has_user_available_slots(user: User, club: Club) -> bool:
def is_booking_response_status_ok(response: dict) -> bool:
"""
Check if the booking response is OK
:param response: the response as a string
:return: true if the status is ok, false otherwise
"""
return response["status"] == "ok"
@staticmethod
def find_court(court_id: int, sport: Sport) -> Court:
"""
Get all the court information based on the court id and the sport name
:param court_id: the court id
:param sport: the sport
:return: the court that has the given id and sport name
"""
for court in sport.courts:
if court.id == court_id:
return court
async def has_user_available_slots(self, user: User, club: Club) -> bool:
"""
Checks if a user has available booking slot.
If a user already has an ongoing booking, it is considered as no slot is
available
:param user: The user to check the booking availability
:param club: The club of the user
:return: True if the user has no ongoing booking, False otherwise
"""
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
bookings = await connector.get_ongoing_bookings(session)
bookings = await self.get_ongoing_bookings(session, connector)
return bool(bookings)
@staticmethod
async def cancel_booking(user: User, club: Club, booking_filter: BookingFilter):
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
await connector.cancel_booking(session, booking_filter)
@staticmethod
async def cancel_booking_id(user: User, club: Club, booking_id: int):
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
await connector.cancel_booking_id(session, booking_id)
@staticmethod
def wait_until_booking_time(
booking_filter: BookingFilter, booking_opening: BookingOpening
@ -128,6 +175,71 @@ class GestionSportsServices:
return booking_date.at(booking_hour, booking_minute)
async def cancel_booking(
self, user: User, club: Club, booking_filter: BookingFilter
):
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
bookings = await self.get_ongoing_bookings(session, connector)
for booking in bookings:
if booking.matches(booking_filter):
return await self.cancel_booking_id(session, connector, booking.id)
async def get_ongoing_bookings(
self, session: ClientSession, connector: GestionSportsConnector
) -> list[Booking]:
"""
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!!
:param session: the client session shared among all connections
:param connector: the connector used to send the requests
:return: the list of all ongoing bookings of a user
"""
response = await connector.send_hash_request(session)
hash_value = self.get_hash_input(await response.text())
LOGGER.debug(f"Hash value: {hash_value}")
response = await connector.send_user_bookings_request(session, hash_value)
return [Booking(**booking) for booking in json.loads(await response.text())]
@staticmethod
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()
async def cancel_booking_id(
self, session: ClientSession, connector: GestionSportsConnector, booking_id: int
) -> None:
"""
Send the HTTP request to cancel the booking
:param session: the client session shared among all connections
:param connector: the connector used to send the requests
:param booking_id: the id of the booking to cancel
:return: the response from the client
"""
response = await connector.send_hash_request(session)
hash_value = self.get_hash_input(await response.text())
LOGGER.debug(f"Hash value: {hash_value}")
await connector.send_cancellation_request(session, booking_id, hash_value)
@staticmethod
async def get_all_tournaments(user: User, club: Club) -> list[Tournament]:
connector = GestionSportsConnector(club)
@ -135,7 +247,7 @@ class GestionSportsServices:
await connector.land(session)
await connector.login(session, user)
session_html = await connector.send_tournaments_sessions_request(session)
session_html = await connector.send_session_request(session)
tournaments_id = GestionSportsServices.retrieve_tournament_session(
await session_html.text()
)

View file

@ -1,7 +1,11 @@
from enum import Enum
from pathlib import Path
from typing import Optional
from urllib.parse import urljoin
import config
import pendulum
from exceptions import MissingProperty
from pendulum import Date, Time
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic_extra_types.pendulum_dt import DateTime
@ -84,6 +88,166 @@ class BookingPlatform(BaseModel):
sports: list[Sport]
urls: dict[str, Url]
def get_url_path(self, name: str) -> str:
"""
Get the URL path for the service with the given name
:param name: the name of the service
:return: the URL path
"""
self.check_url_path_exists(name)
return urljoin(self.url, self.urls.get(name).path)
def get_payload_template(self, name: str) -> Path:
"""
Get the path to the template file for the service with the given name
:param name: the name of the service
:return: the path to the template file
"""
self.check_payload_template_exists(name)
return config.get_resources_folder() / self.urls.get(name).payload_template
def get_url_parameter(self, name: str) -> str:
self.check_url_path_exists(name)
return self.urls.get(name).parameter
def check_url_path_exists(self, name: str) -> None:
"""
Check that the URL path for the given service is defined
:param name: the name of the service
"""
if (
self.urls is None
or self.urls.get(name) is None
or self.urls.get(name).path is None
):
raise MissingProperty(
f"The booking platform internal URL path for page {name} are not set"
)
def check_payload_template_exists(self, name: str) -> None:
"""
Check that the payload template for the given service is defined
:param name: the name of the service
"""
if (
self.urls is None
or self.urls.get(name) is None
or self.urls.get(name).path is None
):
raise ValueError(
f"The booking platform internal URL path for page {name} are not set"
)
@property
def landing_url(self) -> str:
"""
Get the URL to the landing page of Gestion-Sports
:return: the URL to the landing page
"""
return self.get_url_path("landing-page")
@property
def login_url(self) -> str:
"""
Get the URL to the connection login of Gestion-Sports
:return: the URL to the login page
"""
return self.get_url_path("login")
@property
def login_template(self) -> Path:
"""
Get the payload template to send to log in the website
:return: the payload template for logging in
"""
return self.get_payload_template("login")
@property
def booking_url(self) -> str:
"""
Get the URL to the booking page of Gestion-Sports
:return: the URL to the booking page
"""
return self.get_url_path("booking")
@property
def booking_template(self) -> Path:
"""
Get the payload template to send to book a court
:return: the payload template for booking a court
"""
return self.get_payload_template("booking")
@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 self.get_url_path("user-bookings")
@property
def user_bookings_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
:return: the payload template for the user's bookings
"""
return self.get_payload_template("user-bookings")
@property
def booking_cancellation_url(self) -> str:
"""
Get the URL where all the user's bookings are available
:return: the URL to the user's bookings
"""
return self.get_url_path("cancellation")
@property
def booking_cancel_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
:return: the payload template for the user's bookings
"""
return self.get_payload_template("cancellation")
@property
def tournaments_sessions_url(self) -> str:
return self.get_url_path("tournament-sessions")
@property
def tournaments_sessions_template(self) -> Path:
return self.get_payload_template("tournament-sessions")
@property
def tournaments_list_url(self) -> str:
return self.get_url_path("tournaments-list")
@property
def available_sports(self) -> dict[str, Sport]:
"""
Get a dictionary of all sports, the key is the sport name lowered case
:return: the dictionary of all sports
"""
return {sport.name.lower(): sport for sport in self.sports}
class Club(BaseModel):
id: str
@ -91,6 +255,109 @@ class Club(BaseModel):
url: str
booking_platform: BookingPlatform = Field(alias="bookingPlatform")
@property
def landing_url(self) -> str:
"""
Get the URL to the landing page of Gestion-Sports
:return: the URL to the landing page
"""
return self.booking_platform.landing_url
@property
def login_url(self) -> str:
"""
Get the URL to the connection login of Gestion-Sports
:return: the URL to the login page
"""
return self.booking_platform.login_url
@property
def login_template(self) -> Path:
"""
Get the payload template to send to log in the website
:return: the payload template for logging in
"""
return self.booking_platform.login_template
@property
def booking_url(self) -> str:
"""
Get the URL to the booking page of Gestion-Sports
:return: the URL to the booking page
"""
return self.booking_platform.booking_url
@property
def booking_template(self) -> Path:
"""
Get the payload template to send to book a court
:return: the payload template for booking a court
"""
return self.booking_platform.booking_template
@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 self.booking_platform.user_bookings_url
@property
def user_bookings_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
:return: the payload template for the user's bookings
"""
return self.booking_platform.user_bookings_template
@property
def cancel_url(self) -> str:
"""
Get the URL where all the user's bookings are available
:return: the URL to the user's bookings
"""
return self.booking_platform.booking_cancellation_url
@property
def cancel_template(self) -> Path:
"""
Get the payload template to send to get all the user's bookings that are
available
:return: the payload template for the user's bookings
"""
return self.booking_platform.booking_cancel_template
@property
def sessions_url(self) -> str:
return self.booking_platform.tournaments_sessions_url
@property
def sessions_template(self) -> Path:
return self.booking_platform.tournaments_sessions_template
@property
def tournaments_url(self) -> str:
return self.booking_platform.tournaments_list_url
@property
def sports(self) -> dict[str, Sport]:
"""
Get a dictionary of all sports, the key is the sport name lowered case
:return: the dictionary of all sports
"""
return self.booking_platform.available_sports
class PlatformDefinition(BaseModel):
id: str

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ def test_cancellation(club, user, booking_filter):
"CLUB_ID": "tpc",
"ACTION": "book",
"SPORT_NAME": "Padel",
"DATE_TIME": "2024-03-21T13:30:00+01:00",
"DATE_TIME": "2024-03-28T13:30:00+01:00",
},
clear=True,
)
@ -49,7 +49,7 @@ def test_main_booking():
"CLUB_ID": "tpc",
"ACTION": "cancel",
"SPORT_NAME": "Padel",
"DATE_TIME": "2024-03-21T13:30:00+01:00",
"DATE_TIME": "2024-03-28T13:30:00+01:00",
"LOGIN": "padel.testing@jouf.fr",
"PASSWORD": "ridicule",
},

View file

@ -33,11 +33,11 @@ def test_urls(connector):
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
)
assert (
connector.booking_cancellation_url
connector.cancel_url
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
)
assert (
connector.tournaments_sessions_url
connector.sessions_url
== "https://toulousepadelclub.gestion-sports.com/membre/index.php"
)
@ -56,11 +56,11 @@ def test_urls_payload_templates(connector):
== resources_folder / "user-bookings-payload.txt"
)
assert (
connector.booking_cancel_template
connector.cancel_template
== resources_folder / "booking-cancellation-payload.txt"
)
assert (
connector.tournaments_sessions_template
connector.sessions_template
== resources_folder / "tournament-sessions-payload.txt"
)
@ -192,7 +192,7 @@ async def test_cancel_booking_id(connector, user):
await connector.land(session)
await connector.login(session, user)
await connector.cancel_booking_id(session, 666)
await connector.send_cancellation_request(session, 666)
assert len(await connector.get_ongoing_bookings(session)) == 0
@ -217,7 +217,7 @@ async def test_tournament_sessions(connector, user):
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
response = await connector.send_tournaments_sessions_request(session)
response = await connector.send_session_request(session)
assert response.status == 200

View file

@ -61,7 +61,7 @@ def set_tournaments_sessions_response(
aioresponses, connector: GestionSportsConnector, tournaments_sessions_response
):
aioresponses.post(
connector.tournaments_sessions_url,
connector.sessions_url,
status=200,
body=tournaments_sessions_response,
)
@ -73,7 +73,7 @@ def set_tournaments_list_response(
tournament_id,
tournaments_list_response,
):
url = f"{connector.tournaments_list_url}{tournament_id}"
url = f"{connector.tournaments_url}{tournament_id}"
aioresponses.get(url, status=200, body=tournaments_list_response)
@ -84,7 +84,7 @@ def set_full_user_bookings_responses(aioresponses, connector, responses):
def set_cancellation_response(aioresponses, connector, response):
aioresponses.post(connector.booking_cancellation_url, status=200, payload=response)
aioresponses.post(connector.cancel_url, status=200, payload=response)
def set_full_cancellation_by_id_responses(aioresponses, connector, responses):

View file

@ -16,9 +16,9 @@ def test_urls(connector, club):
assert connector.login_url == f"{base_url}/login.html"
assert connector.booking_url == f"{base_url}/booking.html"
assert connector.user_bookings_url == f"{base_url}/user_bookings.html"
assert connector.booking_cancellation_url == f"{base_url}/cancel.html"
assert connector.tournaments_sessions_url == f"{base_url}/tournaments_sessions.php"
assert connector.tournaments_list_url == f"{base_url}/tournaments_list.html?event="
assert connector.cancel_url == f"{base_url}/cancel.html"
assert connector.sessions_url == f"{base_url}/tournaments_sessions.php"
assert connector.tournaments_url == f"{base_url}/tournaments_list.html?event="
@patch("config.get_resources_folder")
@ -41,11 +41,11 @@ def test_urls_payload_templates(mock_resources, club):
== path_to_resources / "gestion-sports/user-bookings-payload.txt"
)
assert (
connector.booking_cancel_template
connector.cancel_template
== path_to_resources / "gestion-sports/booking-cancellation-payload.txt"
)
assert (
connector.tournaments_sessions_template
connector.sessions_template
== path_to_resources / "gestion-sports/tournament-sessions-payload.txt"
)
@ -90,29 +90,6 @@ async def test_login_failure(aioresponses, connector, user, login_failure_respon
assert await response.json() == login_failure_response
def test_get_booked_court(connector, booked_courts_response):
booked_court = connector.get_booked_court(booked_courts_response, "Sport1")
assert booked_court.number == 3
@pytest.mark.asyncio
async def test_get_ongoing_bookings(
aioresponses,
connector,
user,
user_bookings_get_response,
user_bookings_list,
):
responses.set_ongoing_bookings_response(
aioresponses, connector, user_bookings_get_response, user_bookings_list
)
async with ClientSession() as session:
bookings = await connector.get_ongoing_bookings(session)
assert len(bookings) == 2
@pytest.mark.asyncio
async def test_cancellation_request(
aioresponses, connector, user_bookings_get_response, cancellation_response
@ -121,31 +98,11 @@ async def test_cancellation_request(
responses.set_cancellation_response(aioresponses, connector, cancellation_response)
async with ClientSession() as session:
response = await connector.cancel_booking_id(session, 123)
response = await connector.send_cancellation_request(session, 123, "hash")
assert await response.json() == cancellation_response
@pytest.mark.asyncio
async def test_cancel_booking_success(
aioresponses,
connector,
user,
cancellation_success_booking_filter,
cancellation_success_from_start,
):
responses.set_full_cancellation_responses(
aioresponses, connector, cancellation_success_from_start
)
async with ClientSession() as session:
response = await connector.cancel_booking(
session, cancellation_success_booking_filter
)
assert await response.json() == cancellation_success_from_start[4]
@pytest.mark.asyncio
async def test_tournament_sessions(
aioresponses, connector, user, tournament_sessions_json
@ -154,7 +111,7 @@ async def test_tournament_sessions(
aioresponses, connector, tournament_sessions_json
)
async with ClientSession() as session:
response = await connector.send_tournaments_sessions_request(session)
response = await connector.send_session_request(session)
assert response.status == 200

View file

@ -11,6 +11,7 @@ from tests.unit_tests import responses
@pytest.mark.asyncio
async def test_booking_success(
aioresponses,
gs_services,
connector,
club,
user,
@ -21,7 +22,7 @@ async def test_booking_success(
aioresponses, connector, booking_success_from_start
)
court_booked = await GestionSportsServices.book(club, user, booking_filter)
court_booked = await gs_services.book(club, user, booking_filter)
assert court_booked.id == 2
@ -45,6 +46,11 @@ async def test_booking_failure(
assert court_booked is None
def test_get_booked_court(gs_services, booked_courts_response, sport1):
booked_court = gs_services.get_booked_court(booked_courts_response, sport1)
assert booked_court.number == 3
@pytest.mark.asyncio
async def test_user_has_available_booking_slots(
aioresponses,
@ -98,22 +104,6 @@ async def test_cancel_booking(
await gs_services.cancel_booking(user, club, booking_filter)
@pytest.mark.asyncio
async def test_cancel_booking_id(
aioresponses,
gs_services,
connector,
user,
club,
cancellation_success_from_start,
):
responses.set_full_cancellation_responses(
aioresponses, connector, cancellation_success_from_start
)
await gs_services.cancel_booking_id(user, club, 65464)
@patch("pendulum.now")
def test_wait_until_booking_time(mock_now, club, user):
booking_filter = BookingFilter(