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
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue