All methods are in the right class
This commit is contained in:
parent
0d541e82a5
commit
7f59443b64
12 changed files with 585 additions and 1729 deletions
27
gd.json
Normal file
27
gd.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
|
@ -39,24 +39,12 @@ async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter)
|
||||||
await service.cancel_booking(user, club, booking_filter)
|
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]:
|
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 club: the club in which the tournaments are
|
||||||
:param user: the user who made the booking
|
:param user: a user of the club in order to retrieve the information
|
||||||
"""
|
"""
|
||||||
service = GestionSportsServices()
|
service = GestionSportsServices()
|
||||||
return await service.get_all_tournaments(user, club)
|
return await service.get_all_tournaments(user, club)
|
||||||
|
@ -74,17 +62,23 @@ def main() -> tuple[Court, User] | list[Tournament] | None:
|
||||||
club = config.get_club()
|
club = config.get_club()
|
||||||
users = config.get_users(club.id)
|
users = config.get_users(club.id)
|
||||||
booking_filter = config.get_booking_filter()
|
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))
|
court_booked, user = asyncio.run(book_court(club, users, booking_filter))
|
||||||
if court_booked:
|
if court_booked:
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Court %s booked successfully at %s for user %s",
|
f"Court of {booking_filter.sport_name} {court_booked} was booked "
|
||||||
court_booked,
|
f"successfully at {booking_filter.date} at club {club.name} "
|
||||||
booking_filter.date,
|
f"for user {user}"
|
||||||
user,
|
|
||||||
)
|
)
|
||||||
return court_booked, user
|
return court_booked, user
|
||||||
else:
|
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:
|
elif action == Action.CANCEL:
|
||||||
user = config.get_user()
|
user = config.get_user()
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
class ArgumentMissing(Exception):
|
class WrongResponseStatus(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingProperty(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -2,12 +2,11 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from aiohttp import ClientResponse, ClientSession
|
from aiohttp import ClientResponse, ClientSession
|
||||||
from bs4 import BeautifulSoup
|
from exceptions import WrongResponseStatus
|
||||||
from models import Booking, BookingFilter, Club, Court, Sport, User
|
from models import BookingFilter, Club, Sport, User
|
||||||
from payload_builders import PayloadBuilder
|
from payload_builders import PayloadBuilder
|
||||||
from pendulum import DateTime
|
from pendulum import DateTime
|
||||||
|
|
||||||
|
@ -18,8 +17,8 @@ POST_HEADERS = config.get_post_headers("gestion-sports")
|
||||||
|
|
||||||
class GestionSportsConnector:
|
class GestionSportsConnector:
|
||||||
"""
|
"""
|
||||||
The connector for the Gestion Sports platform.
|
The connector for the Gestion Sports platform handles all the HTTP requests to the
|
||||||
It handles all the requests to the website.
|
Gestion sports website.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, club: Club):
|
def __init__(self, club: Club):
|
||||||
|
@ -35,184 +34,139 @@ class GestionSportsConnector:
|
||||||
|
|
||||||
self.club = club
|
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
|
@property
|
||||||
def landing_url(self) -> str:
|
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
|
@property
|
||||||
def login_url(self) -> str:
|
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
|
@property
|
||||||
def login_template(self) -> Path:
|
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
|
@property
|
||||||
def booking_url(self) -> str:
|
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
|
@property
|
||||||
def booking_template(self) -> Path:
|
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: the payload template for booking a court
|
||||||
"""
|
"""
|
||||||
return self._get_payload_template("booking")
|
return self.club.booking_template
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_bookings_url(self) -> str:
|
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
|
@property
|
||||||
def user_bookings_template(self) -> Path:
|
def user_bookings_template(self) -> Path:
|
||||||
"""
|
"""
|
||||||
Get the payload template to send to get all the user's bookings that are
|
Get the payload template to get the bookings related to a user that are not yet
|
||||||
available
|
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
|
@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
|
@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
|
Get the payload template for cancelling a booking
|
||||||
available
|
|
||||||
|
|
||||||
: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
|
@property
|
||||||
def tournaments_sessions_url(self) -> str:
|
def sessions_url(self) -> str:
|
||||||
return self._get_url_path("tournament-sessions")
|
"""
|
||||||
|
Get the URL of the session containing all the tournaments
|
||||||
|
|
||||||
|
:return: the URL to get the session
|
||||||
|
"""
|
||||||
|
return self.club.sessions_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tournaments_sessions_template(self) -> Path:
|
def sessions_template(self) -> Path:
|
||||||
return self._get_payload_template("tournament-sessions")
|
"""
|
||||||
|
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
|
@property
|
||||||
def tournaments_list_url(self) -> str:
|
def tournaments_url(self) -> str:
|
||||||
return self._get_url_path("tournaments-list")
|
"""
|
||||||
|
Get the URL of all the tournaments list
|
||||||
|
|
||||||
|
:return: the URL to get the tournaments list
|
||||||
|
"""
|
||||||
|
return self.club.tournaments_url
|
||||||
|
|
||||||
@property
|
@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
|
Get a dictionary of all sports, the key is the sport name lowered case
|
||||||
:return: the dictionary of all sports
|
:return: the dictionary of all sports
|
||||||
"""
|
"""
|
||||||
return {
|
return self.club.sports
|
||||||
sport.name.lower(): sport for sport in self.club.booking_platform.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:
|
async def land(self, session: ClientSession) -> ClientResponse:
|
||||||
"""
|
"""
|
||||||
Perform the request to the landing page in order to get the cookie PHPSESSIONID
|
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
|
:return: the response from the landing page
|
||||||
"""
|
"""
|
||||||
LOGGER.info("Connecting to GestionSports API at %s", self.login_url)
|
LOGGER.info("Connecting to GestionSports API at %s", self.login_url)
|
||||||
async with session.get(self.landing_url) as response:
|
async with session.get(self.landing_url) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
await response.text()
|
await response.text()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -220,6 +174,8 @@ class GestionSportsConnector:
|
||||||
"""
|
"""
|
||||||
Perform the request to the log in the user
|
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
|
:return: the response from the login
|
||||||
"""
|
"""
|
||||||
LOGGER.info("Logging in to GestionSports API at %s", self.login_url)
|
LOGGER.info("Logging in to GestionSports API at %s", self.login_url)
|
||||||
|
@ -228,11 +184,12 @@ class GestionSportsConnector:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.login_url, data=payload, headers=POST_HEADERS, allow_redirects=False
|
self.login_url, data=payload, headers=POST_HEADERS, allow_redirects=False
|
||||||
) as response:
|
) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
resp_text = await response.text()
|
resp_text = await response.text()
|
||||||
LOGGER.debug("Connexion request response:\n%s", resp_text)
|
LOGGER.debug("Connexion request response:\n%s", resp_text)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def book_any_court(
|
async def send_all_booking_requests(
|
||||||
self, session: ClientSession, booking_filter: BookingFilter
|
self, session: ClientSession, booking_filter: BookingFilter
|
||||||
) -> list[tuple[int, dict]]:
|
) -> list[tuple[int, dict]]:
|
||||||
"""
|
"""
|
||||||
|
@ -241,7 +198,7 @@ class GestionSportsConnector:
|
||||||
The gestion-sports backend does not allow several bookings at the same time
|
The gestion-sports backend does not allow several bookings at the same time
|
||||||
so there is no need to make each request one after the other
|
so there is no need to make each request one after the other
|
||||||
|
|
||||||
:param session: the session to use
|
:param session: the client session shared among all connections
|
||||||
:param booking_filter: the booking conditions to meet
|
:param booking_filter: the booking conditions to meet
|
||||||
:return: the booked court, or None if no court was booked
|
: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
|
"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(
|
bookings = await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
|
@ -274,7 +231,7 @@ class GestionSportsConnector:
|
||||||
"""
|
"""
|
||||||
Book a single court that meets the conditions from the booking filter
|
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 date: the booking date
|
||||||
:param court_id: the id of the court to book
|
:param court_id: the id of the court to book
|
||||||
:param sport_id: the id of the sport
|
:param sport_id: the id of the sport
|
||||||
|
@ -293,168 +250,96 @@ class GestionSportsConnector:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.booking_url, data=payload, headers=POST_HEADERS
|
self.booking_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
assert response.status == 200
|
self.check_response_status(response.status)
|
||||||
resp_json = json.loads(await response.text())
|
resp_json = json.loads(await response.text())
|
||||||
|
|
||||||
LOGGER.debug("Response from booking request:\n'%s'", resp_json)
|
LOGGER.debug("Response from booking request:\n'%s'", resp_json)
|
||||||
return court_id, resp_json
|
return court_id, resp_json
|
||||||
|
|
||||||
def get_booked_court(
|
async def send_hash_request(self, session: ClientSession) -> ClientResponse:
|
||||||
self, bookings: list[tuple[int, dict]], sport_name: str
|
|
||||||
) -> Court | None:
|
|
||||||
"""
|
"""
|
||||||
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 session: the client session shared among all connections
|
||||||
: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
|
|
||||||
:return: the value of the hash
|
:return: the value of the hash
|
||||||
"""
|
"""
|
||||||
async with session.get(self.user_bookings_url) as response:
|
async with session.get(self.user_bookings_url) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
html = await response.text()
|
html = await response.text()
|
||||||
LOGGER.debug("Get bookings response: %s\n", html)
|
LOGGER.debug("Get bookings response: %s\n", html)
|
||||||
return self.get_hash_input(html)
|
return response
|
||||||
|
|
||||||
@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 send_user_bookings_request(
|
async def send_user_bookings_request(
|
||||||
self, session: ClientSession, payload: str
|
self, session: ClientSession, hash_value: str
|
||||||
) -> list[Booking]:
|
) -> 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 session: the client session shared among all connections
|
||||||
:param payload: the HTTP payload for the request
|
:param hash_value: the hash value to put in the payload
|
||||||
:return: a dictionary containing all the bookings
|
:return: a dictionary containing all the bookings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
payload = PayloadBuilder.build(self.user_bookings_template, hash=hash_value)
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.user_bookings_url, data=payload, headers=POST_HEADERS
|
self.user_bookings_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
resp = await response.text()
|
self.check_response_status(response.status)
|
||||||
LOGGER.debug("ongoing bookings response: %s\n", resp)
|
await response.text()
|
||||||
return [Booking(**booking) for booking in json.loads(resp)]
|
return response
|
||||||
|
|
||||||
async def cancel_booking_id(
|
async def send_cancellation_request(
|
||||||
self, session: ClientSession, booking_id: int
|
self, session: ClientSession, booking_id: int, hash_value: str
|
||||||
) -> ClientResponse:
|
) -> ClientResponse:
|
||||||
"""
|
"""
|
||||||
Send the HTTP request to cancel the booking
|
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
|
:param booking_id: the id of the booking to cancel
|
||||||
:return: the response from the client
|
:return: the response from the client
|
||||||
"""
|
"""
|
||||||
hash_value = await self.send_hash_request(session)
|
|
||||||
|
|
||||||
payload = PayloadBuilder.build(
|
payload = PayloadBuilder.build(
|
||||||
self.booking_cancel_template,
|
self.cancel_template,
|
||||||
booking_id=booking_id,
|
booking_id=booking_id,
|
||||||
hash=hash_value,
|
hash=hash_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.booking_cancellation_url, data=payload, headers=POST_HEADERS
|
self.cancel_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
await response.text()
|
await response.text()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def cancel_booking(
|
async def send_session_request(self, session: ClientSession) -> ClientResponse:
|
||||||
self, session: ClientSession, booking_filter: BookingFilter
|
|
||||||
) -> ClientResponse | None:
|
|
||||||
"""
|
"""
|
||||||
Cancel the booking that meets some conditions
|
Send a request to the platform to get the session id
|
||||||
|
|
||||||
:param session: the session
|
:param session: the client session shared among all connections
|
||||||
:param booking_filter: the conditions the booking to cancel should meet
|
:return: a client response containing HTML which has the session id
|
||||||
"""
|
"""
|
||||||
bookings = await self.get_ongoing_bookings(session)
|
payload = self.sessions_template.read_text()
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
async with session.post(
|
async with session.post(
|
||||||
self.tournaments_sessions_url, data=payload, headers=POST_HEADERS
|
self.sessions_url, data=payload, headers=POST_HEADERS
|
||||||
) as response:
|
) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
LOGGER.debug("tournament sessions: \n%s", await response.text())
|
LOGGER.debug("tournament sessions: \n%s", await response.text())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def send_tournaments_request(
|
async def send_tournaments_request(
|
||||||
self, session: ClientSession, tournement_session_id: str
|
self, session: ClientSession, session_id: str
|
||||||
) -> ClientResponse:
|
) -> 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)
|
LOGGER.debug("Getting tournaments list at %s", final_url)
|
||||||
async with session.get(final_url) as response:
|
async with session.get(final_url) as response:
|
||||||
|
self.check_response_status(response.status)
|
||||||
LOGGER.debug("tournaments: %s\n", await response.text())
|
LOGGER.debug("tournaments: %s\n", await response.text())
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -6,16 +6,24 @@ import pendulum
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from gestion_sport_connector import GestionSportsConnector
|
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
|
from pendulum import DateTime
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GestionSportsServices:
|
class GestionSportsServices:
|
||||||
@staticmethod
|
|
||||||
async def book(
|
async def book(
|
||||||
club: Club, user: User, booking_filter: BookingFilter
|
self, club: Club, user: User, booking_filter: BookingFilter
|
||||||
) -> Court | None:
|
) -> Court | None:
|
||||||
"""
|
"""
|
||||||
Perform a request for each court at the same time to increase the chances to get
|
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
|
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)
|
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
|
@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)
|
connector = GestionSportsConnector(club)
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
await connector.land(session)
|
await connector.land(session)
|
||||||
await connector.login(session, user)
|
await connector.login(session, user)
|
||||||
bookings = await connector.get_ongoing_bookings(session)
|
bookings = await self.get_ongoing_bookings(session, connector)
|
||||||
|
|
||||||
return bool(bookings)
|
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
|
@staticmethod
|
||||||
def wait_until_booking_time(
|
def wait_until_booking_time(
|
||||||
booking_filter: BookingFilter, booking_opening: BookingOpening
|
booking_filter: BookingFilter, booking_opening: BookingOpening
|
||||||
|
@ -128,6 +175,71 @@ class GestionSportsServices:
|
||||||
|
|
||||||
return booking_date.at(booking_hour, booking_minute)
|
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
|
@staticmethod
|
||||||
async def get_all_tournaments(user: User, club: Club) -> list[Tournament]:
|
async def get_all_tournaments(user: User, club: Club) -> list[Tournament]:
|
||||||
connector = GestionSportsConnector(club)
|
connector = GestionSportsConnector(club)
|
||||||
|
@ -135,7 +247,7 @@ class GestionSportsServices:
|
||||||
await connector.land(session)
|
await connector.land(session)
|
||||||
await connector.login(session, user)
|
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(
|
tournaments_id = GestionSportsServices.retrieve_tournament_session(
|
||||||
await session_html.text()
|
await session_html.text()
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import config
|
||||||
import pendulum
|
import pendulum
|
||||||
|
from exceptions import MissingProperty
|
||||||
from pendulum import Date, Time
|
from pendulum import Date, Time
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
from pydantic_extra_types.pendulum_dt import DateTime
|
from pydantic_extra_types.pendulum_dt import DateTime
|
||||||
|
@ -84,6 +88,166 @@ class BookingPlatform(BaseModel):
|
||||||
sports: list[Sport]
|
sports: list[Sport]
|
||||||
urls: dict[str, Url]
|
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):
|
class Club(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
@ -91,6 +255,109 @@ class Club(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
booking_platform: BookingPlatform = Field(alias="bookingPlatform")
|
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):
|
class PlatformDefinition(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -33,7 +33,7 @@ def test_cancellation(club, user, booking_filter):
|
||||||
"CLUB_ID": "tpc",
|
"CLUB_ID": "tpc",
|
||||||
"ACTION": "book",
|
"ACTION": "book",
|
||||||
"SPORT_NAME": "Padel",
|
"SPORT_NAME": "Padel",
|
||||||
"DATE_TIME": "2024-03-21T13:30:00+01:00",
|
"DATE_TIME": "2024-03-28T13:30:00+01:00",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
)
|
)
|
||||||
|
@ -49,7 +49,7 @@ def test_main_booking():
|
||||||
"CLUB_ID": "tpc",
|
"CLUB_ID": "tpc",
|
||||||
"ACTION": "cancel",
|
"ACTION": "cancel",
|
||||||
"SPORT_NAME": "Padel",
|
"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",
|
"LOGIN": "padel.testing@jouf.fr",
|
||||||
"PASSWORD": "ridicule",
|
"PASSWORD": "ridicule",
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,11 +33,11 @@ def test_urls(connector):
|
||||||
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
|
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.booking_cancellation_url
|
connector.cancel_url
|
||||||
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
|
== "https://toulousepadelclub.gestion-sports.com/membre/mesresas.html"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.tournaments_sessions_url
|
connector.sessions_url
|
||||||
== "https://toulousepadelclub.gestion-sports.com/membre/index.php"
|
== "https://toulousepadelclub.gestion-sports.com/membre/index.php"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,11 +56,11 @@ def test_urls_payload_templates(connector):
|
||||||
== resources_folder / "user-bookings-payload.txt"
|
== resources_folder / "user-bookings-payload.txt"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.booking_cancel_template
|
connector.cancel_template
|
||||||
== resources_folder / "booking-cancellation-payload.txt"
|
== resources_folder / "booking-cancellation-payload.txt"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.tournaments_sessions_template
|
connector.sessions_template
|
||||||
== resources_folder / "tournament-sessions-payload.txt"
|
== resources_folder / "tournament-sessions-payload.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ async def test_cancel_booking_id(connector, user):
|
||||||
await connector.land(session)
|
await connector.land(session)
|
||||||
await connector.login(session, user)
|
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
|
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:
|
async with ClientSession() as session:
|
||||||
await connector.land(session)
|
await connector.land(session)
|
||||||
await connector.login(session, user)
|
await connector.login(session, user)
|
||||||
response = await connector.send_tournaments_sessions_request(session)
|
response = await connector.send_session_request(session)
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ def set_tournaments_sessions_response(
|
||||||
aioresponses, connector: GestionSportsConnector, tournaments_sessions_response
|
aioresponses, connector: GestionSportsConnector, tournaments_sessions_response
|
||||||
):
|
):
|
||||||
aioresponses.post(
|
aioresponses.post(
|
||||||
connector.tournaments_sessions_url,
|
connector.sessions_url,
|
||||||
status=200,
|
status=200,
|
||||||
body=tournaments_sessions_response,
|
body=tournaments_sessions_response,
|
||||||
)
|
)
|
||||||
|
@ -73,7 +73,7 @@ def set_tournaments_list_response(
|
||||||
tournament_id,
|
tournament_id,
|
||||||
tournaments_list_response,
|
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)
|
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):
|
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):
|
def set_full_cancellation_by_id_responses(aioresponses, connector, responses):
|
||||||
|
|
|
@ -16,9 +16,9 @@ def test_urls(connector, club):
|
||||||
assert connector.login_url == f"{base_url}/login.html"
|
assert connector.login_url == f"{base_url}/login.html"
|
||||||
assert connector.booking_url == f"{base_url}/booking.html"
|
assert connector.booking_url == f"{base_url}/booking.html"
|
||||||
assert connector.user_bookings_url == f"{base_url}/user_bookings.html"
|
assert connector.user_bookings_url == f"{base_url}/user_bookings.html"
|
||||||
assert connector.booking_cancellation_url == f"{base_url}/cancel.html"
|
assert connector.cancel_url == f"{base_url}/cancel.html"
|
||||||
assert connector.tournaments_sessions_url == f"{base_url}/tournaments_sessions.php"
|
assert connector.sessions_url == f"{base_url}/tournaments_sessions.php"
|
||||||
assert connector.tournaments_list_url == f"{base_url}/tournaments_list.html?event="
|
assert connector.tournaments_url == f"{base_url}/tournaments_list.html?event="
|
||||||
|
|
||||||
|
|
||||||
@patch("config.get_resources_folder")
|
@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"
|
== path_to_resources / "gestion-sports/user-bookings-payload.txt"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.booking_cancel_template
|
connector.cancel_template
|
||||||
== path_to_resources / "gestion-sports/booking-cancellation-payload.txt"
|
== path_to_resources / "gestion-sports/booking-cancellation-payload.txt"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
connector.tournaments_sessions_template
|
connector.sessions_template
|
||||||
== path_to_resources / "gestion-sports/tournament-sessions-payload.txt"
|
== 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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_cancellation_request(
|
async def test_cancellation_request(
|
||||||
aioresponses, connector, user_bookings_get_response, cancellation_response
|
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)
|
responses.set_cancellation_response(aioresponses, connector, cancellation_response)
|
||||||
|
|
||||||
async with ClientSession() as session:
|
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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_tournament_sessions(
|
async def test_tournament_sessions(
|
||||||
aioresponses, connector, user, tournament_sessions_json
|
aioresponses, connector, user, tournament_sessions_json
|
||||||
|
@ -154,7 +111,7 @@ async def test_tournament_sessions(
|
||||||
aioresponses, connector, tournament_sessions_json
|
aioresponses, connector, tournament_sessions_json
|
||||||
)
|
)
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
response = await connector.send_tournaments_sessions_request(session)
|
response = await connector.send_session_request(session)
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from tests.unit_tests import responses
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_booking_success(
|
async def test_booking_success(
|
||||||
aioresponses,
|
aioresponses,
|
||||||
|
gs_services,
|
||||||
connector,
|
connector,
|
||||||
club,
|
club,
|
||||||
user,
|
user,
|
||||||
|
@ -21,7 +22,7 @@ async def test_booking_success(
|
||||||
aioresponses, connector, booking_success_from_start
|
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
|
assert court_booked.id == 2
|
||||||
|
|
||||||
|
@ -45,6 +46,11 @@ async def test_booking_failure(
|
||||||
assert court_booked is None
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_user_has_available_booking_slots(
|
async def test_user_has_available_booking_slots(
|
||||||
aioresponses,
|
aioresponses,
|
||||||
|
@ -98,22 +104,6 @@ async def test_cancel_booking(
|
||||||
await gs_services.cancel_booking(user, club, booking_filter)
|
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")
|
@patch("pendulum.now")
|
||||||
def test_wait_until_booking_time(mock_now, club, user):
|
def test_wait_until_booking_time(mock_now, club, user):
|
||||||
booking_filter = BookingFilter(
|
booking_filter = BookingFilter(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue