Added a service that can get all current tournaments list

This commit is contained in:
Stanislas Jouffroy 2024-03-23 11:56:31 +01:00
parent e6023e0687
commit 3d0bd47079
26 changed files with 4305 additions and 204 deletions

View file

@ -3,7 +3,7 @@ import logging
import config
from gestion_sports_services import GestionSportsServices
from models import Action, BookingFilter, Club, Court, User
from models import Action, BookingFilter, Club, Court, Tournament, User
LOGGER = logging.getLogger(__name__)
@ -51,7 +51,18 @@ async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
await service.cancel_booking_id(user, club, booking_id)
def main() -> tuple[Court, User] | None:
async def get_tournaments(club: Club, user: User) -> list[Tournament]:
"""
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
"""
service = GestionSportsServices()
return await service.get_all_tournaments(user, club)
def main() -> tuple[Court, User] | list[Tournament] | None:
"""
Main function used to book a court
@ -80,3 +91,8 @@ def main() -> tuple[Court, User] | None:
club = config.get_club()
booking_filter = config.get_booking_filter()
asyncio.run(cancel_booking(club, user, booking_filter))
elif action == Action.TOURNAMENTS:
user = config.get_user()
club = config.get_club()
return asyncio.run(get_tournaments(club, user))

View file

@ -1,42 +0,0 @@
import logging
from aiohttp import ClientSession
from connectors import Connector
from models import BookingFilter, Club, User
LOGGER = logging.getLogger(__name__)
class BookingService:
def __init__(self, club: Club, connector: Connector):
LOGGER.info("Initializing booking service at for club", club.name)
self.club = club
self.connector = connector
self.session: ClientSession | None = None
async def __aenter__(self):
self.session = ClientSession()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.session.close()
async def book(self, user: User, booking_filter: BookingFilter) -> int | None:
"""
Book a court matching the booking filters for a user.
The steps to perform a booking are to go to the landing page, to log in, wait
and for the time when booking is open and then actually book the court
:param user: the user that wants to book a court
:param booking_filter: the booking criteria
:return: the court number if the booking is successful, None otherwise
"""
if self.connector is None:
LOGGER.error("No connection to Gestion Sports is available")
return None
if user is None or booking_filter is None:
LOGGER.error("Not enough information available to book a court")
return None
self.connector.book(user, booking_filter)

View file

@ -143,4 +143,4 @@ def get_action() -> Action:
Get the action to perform from an environment variable
:return: the action to perform
"""
return Action(os.environ.get("ACTION"))
return Action(os.environ.get("ACTION").lower())

View file

@ -49,6 +49,11 @@ class GestionSportsConnector:
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
@ -178,6 +183,18 @@ class GestionSportsConnector:
"""
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]:
"""
@ -209,7 +226,7 @@ class GestionSportsConnector:
payload = PayloadBuilder.build(self.login_template, user=user, club=self.club)
async with session.post(
self.login_url, data=payload, headers=POST_HEADERS
self.login_url, data=payload, headers=POST_HEADERS, allow_redirects=False
) as response:
resp_text = await response.text()
LOGGER.debug("Connexion request response:\n%s", resp_text)
@ -421,3 +438,23 @@ class GestionSportsConnector:
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(
self.tournaments_sessions_url, data=payload, headers=POST_HEADERS
) as response:
LOGGER.debug("tournament sessions: \n%s", await response.text())
return response
async def send_tournaments_request(
self, session: ClientSession, tournement_session_id: str
) -> ClientResponse:
final_url = self.tournaments_list_url + tournement_session_id
LOGGER.debug("Getting tournaments list at %s", final_url)
async with session.get(final_url) as response:
LOGGER.debug("tournaments: %s\n", await response.text())
return response

View file

@ -1,10 +1,12 @@
import json
import logging
import time
import pendulum
from aiohttp import ClientSession
from connectors import GestionSportsConnector
from models import BookingFilter, BookingOpening, Club, Court, User
from bs4 import BeautifulSoup
from gestion_sport_connector import GestionSportsConnector
from models import BookingFilter, BookingOpening, Club, Court, Tournament, User
from pendulum import DateTime
LOGGER = logging.getLogger(__name__)
@ -125,3 +127,62 @@ class GestionSportsServices:
booking_minute = opening_time.minute
return booking_date.at(booking_hour, booking_minute)
@staticmethod
async def get_all_tournaments(user: User, club: Club) -> list[Tournament]:
connector = GestionSportsConnector(club)
async with ClientSession() as session:
await connector.land(session)
await connector.login(session, user)
session_html = await connector.send_tournaments_sessions_request(session)
tournaments_id = GestionSportsServices.retrieve_tournament_session(
await session_html.text()
)
tournaments = await connector.send_tournaments_request(
session, tournaments_id
)
return GestionSportsServices.retrieve_tournaments(await tournaments.text())
@staticmethod
def retrieve_tournament_session(sessions: str) -> str:
session_object = json.loads(sessions).get("Inscription tournois:school-outline")
return list(session_object.keys())[0]
@staticmethod
def retrieve_tournaments(html: str) -> list[Tournament]:
soup = BeautifulSoup(html, "html.parser")
tournaments = []
cards = soup.find_all("div", {"class": "card-body"})
for card in cards:
title = card.find("h5")
price = title.find("span").get_text().strip()
name = title.get_text().strip().removesuffix(price).strip()
elements = card.find("div", {"class": "row"}).find_all("li")
date = elements[0].get_text().strip()
start_time, end_time = (
elements[2].get_text().strip().replace("h", ":").split(" - ")
)
start_datetime = pendulum.from_format(
f"{date} {start_time}", "DD/MM/YYYY HH:mm"
)
end_datetime = pendulum.from_format(
f"{date} {end_time}", "DD/MM/YYYY HH:mm"
)
gender = elements[1].get_text().strip()
places_left = (
card.find("span", {"class": "nb_place_libre"}).get_text().strip()
)
tournament = Tournament(
name=name,
price=price,
start_date=start_datetime,
end_date=end_datetime,
gender=gender,
places_left=places_left,
)
tournaments.append(tournament)
return tournaments

View file

@ -70,6 +70,7 @@ class Sport(BaseModel):
class Url(BaseModel):
name: str
path: str
parameter: Optional[str] = Field(default=None)
payload_template: Optional[str] = Field(default=None, alias="payloadTemplate")
@ -210,3 +211,13 @@ class Booking(BaseModel):
class Action(Enum):
BOOK = "book"
CANCEL = "cancel"
TOURNAMENTS = "tournaments"
class Tournament(BaseModel):
name: str
price: str
start_date: DateTime
end_date: DateTime
gender: str
places_left: str | int

View file

@ -1,12 +1,16 @@
{
"Connection": "keep-alive",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1",
"Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
"Connection": "keep-alive",
"DNT": "1",
"Origin": "https://toulousepadelclub.gestion-sports.com",
"Pragma": "no-cache",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin"
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
"X-Requested-With": "XMLHttpRequest"
}

View file

@ -0,0 +1 @@
ajax=loadSessionForSpecDay&date=all

View file

@ -17,3 +17,8 @@ platforms:
- name: cancellation
path: /membre/mesresas.html
payloadTemplate: gestion-sports/booking-cancellation-payload.txt
- name: tournament-sessions
path: /membre/index.php
payloadTemplate: gestion-sports/tournament-sessions-payload.txt
- name: tournaments-list
path: /membre/events/event.html?event=