diff --git a/gd.json b/gd.json new file mode 100644 index 0000000..5d6eb6b --- /dev/null +++ b/gd.json @@ -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": [] + } +] diff --git a/resa_padel/booking.py b/resa_padel/booking.py index b5a58f1..64b1983 100644 --- a/resa_padel/booking.py +++ b/resa_padel/booking.py @@ -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() diff --git a/resa_padel/exceptions.py b/resa_padel/exceptions.py index 6a17281..b777616 100644 --- a/resa_padel/exceptions.py +++ b/resa_padel/exceptions.py @@ -1,2 +1,6 @@ -class ArgumentMissing(Exception): +class WrongResponseStatus(Exception): + pass + + +class MissingProperty(Exception): pass diff --git a/resa_padel/gestion_sport_connector.py b/resa_padel/gestion_sport_connector.py index 55abcea..f7d1942 100644 --- a/resa_padel/gestion_sport_connector.py +++ b/resa_padel/gestion_sport_connector.py @@ -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 diff --git a/resa_padel/gestion_sports_services.py b/resa_padel/gestion_sports_services.py index d616484..6aa8f19 100644 --- a/resa_padel/gestion_sports_services.py +++ b/resa_padel/gestion_sports_services.py @@ -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() ) diff --git a/resa_padel/models.py b/resa_padel/models.py index 1fbb39c..4180fb4 100644 --- a/resa_padel/models.py +++ b/resa_padel/models.py @@ -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 diff --git a/tests/data/responses/tournament-sessions.html b/tests/data/responses/tournament-sessions.html deleted file mode 100644 index ff61262..0000000 --- a/tests/data/responses/tournament-sessions.html +++ /dev/null @@ -1,1380 +0,0 @@ - - - - - - Home - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- image - Le Mas - à l'instant -
- - - -
-
-
-

Titre de la notif

-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. -
-
-
-
-
- - - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
- - - - -
- -
- Accueil
-
- - - 37 - -
-
- - - - -
-
-
imageimageimageimageimageimage
imageimageimageimage
-
- -
-
-
imageimageimageimage
imageimageimageimage
imageimage
-
- - - - -
- -
-

Bonjour Audiard,

-
- -
- - - -
- Padel / Squash / Electrostimulation -
-
- -
- -
- - - - - - - - - - -
-
-
-
-
- -
- Évènements -
- -
- -
-
-
-
- -
- -
-
    -
-
- -
-
-
-
-
- - -
-
-
- -
Toulouse Padel Club
-

- 11 rue Marie Louise Dissard
- 31300
- Toulouse
-

- -
-
-
- -
- - -
-
-
-
-
-
-
-
- - - - -
- - -
- - - - - - - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - Accueil -
-
- - -
- - Actualités -
-
- - -
-
- - Réserver -
-
- -
- - -
- - Compte -
-
- - -
- - Menu -
-
- -
- - - - - - - - - - - - - - - - - - -
-
- -
- Votre inscription à la partie de Tennis a bien été enregistrée !
- Rendez-vous le 14 Janvier de 16h30 à 17h30. -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/integration_tests/test_booking.py b/tests/integration_tests/test_booking.py index db6c6e1..2e093cd 100644 --- a/tests/integration_tests/test_booking.py +++ b/tests/integration_tests/test_booking.py @@ -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", }, diff --git a/tests/integration_tests/test_gestion_sport_connector.py b/tests/integration_tests/test_gestion_sport_connector.py index 4e5bb44..5078e6a 100644 --- a/tests/integration_tests/test_gestion_sport_connector.py +++ b/tests/integration_tests/test_gestion_sport_connector.py @@ -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 diff --git a/tests/unit_tests/responses.py b/tests/unit_tests/responses.py index 8ee5c7e..57e8fee 100644 --- a/tests/unit_tests/responses.py +++ b/tests/unit_tests/responses.py @@ -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): diff --git a/tests/unit_tests/test_gestion_sports_connector.py b/tests/unit_tests/test_gestion_sports_connector.py index 82be8ee..b1b9bad 100644 --- a/tests/unit_tests/test_gestion_sports_connector.py +++ b/tests/unit_tests/test_gestion_sports_connector.py @@ -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 diff --git a/tests/unit_tests/test_gestion_sports_services.py b/tests/unit_tests/test_gestion_sports_services.py index 6cc8351..5ac584f 100644 --- a/tests/unit_tests/test_gestion_sports_services.py +++ b/tests/unit_tests/test_gestion_sports_services.py @@ -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(