created a gestion sports services class that handles the connection while the connector is dedicated to the requests
This commit is contained in:
parent
bcd8dc0733
commit
e6023e0687
12 changed files with 513 additions and 593 deletions
|
@ -2,17 +2,12 @@ import asyncio
|
|||
import logging
|
||||
|
||||
import config
|
||||
from connectors import Connector, GestionSportsConnector
|
||||
from gestion_sports_services import GestionSportsServices
|
||||
from models import Action, BookingFilter, Club, Court, User
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_connector(club: Club) -> Connector:
|
||||
if club.booking_platform.id == "gestion-sports":
|
||||
return GestionSportsConnector(club)
|
||||
|
||||
|
||||
async def book_court(
|
||||
club: Club, users: list[User], booking_filter: BookingFilter
|
||||
) -> tuple[Court, User]:
|
||||
|
@ -26,10 +21,10 @@ async def book_court(
|
|||
:return: a tuple containing the court that was booked and the user who made the
|
||||
booking
|
||||
"""
|
||||
connector = get_connector(club)
|
||||
service = GestionSportsServices()
|
||||
for user in users:
|
||||
if not await connector.has_user_ongoing_booking(user):
|
||||
return await connector.book(user, booking_filter), user
|
||||
if not await service.has_user_available_slots(user, club):
|
||||
return await service.book(club, user, booking_filter), user
|
||||
|
||||
|
||||
async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter) -> None:
|
||||
|
@ -40,8 +35,8 @@ async def cancel_booking(club: Club, user: User, booking_filter: BookingFilter)
|
|||
:param user: the user who made the booking
|
||||
:param booking_filter: the conditions to meet to cancel the booking
|
||||
"""
|
||||
connector = get_connector(club)
|
||||
await connector.cancel_booking(user, booking_filter)
|
||||
service = GestionSportsServices()
|
||||
await service.cancel_booking(user, club, booking_filter)
|
||||
|
||||
|
||||
async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
|
||||
|
@ -52,8 +47,8 @@ async def cancel_booking_id(club: Club, user: User, booking_id: int) -> None:
|
|||
:param user: the user who made the booking
|
||||
:param booking_id: the id of the booking to cancel
|
||||
"""
|
||||
connector = get_connector(club)
|
||||
await connector.cancel_booking_id(user, booking_id)
|
||||
service = GestionSportsServices()
|
||||
await service.cancel_booking_id(user, club, booking_id)
|
||||
|
||||
|
||||
def main() -> tuple[Court, User] | None:
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import config
|
||||
import pendulum
|
||||
from aiohttp import ClientResponse, ClientSession
|
||||
from bs4 import BeautifulSoup
|
||||
from models import Booking, BookingFilter, Club, Court, Sport, User
|
||||
|
@ -19,55 +16,7 @@ LOGGER = logging.getLogger(__name__)
|
|||
POST_HEADERS = config.get_post_headers("gestion-sports")
|
||||
|
||||
|
||||
class Connector(ABC):
|
||||
"""
|
||||
Abstract class that defines the method a connector
|
||||
to a website for sport booking should have
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def book(self, user: User, booking_filter: BookingFilter) -> Court | None:
|
||||
"""
|
||||
Book a court matching the filter for a user
|
||||
|
||||
:param user: the user who will have the booking
|
||||
:param booking_filter: the conditions to book (date, time, court)
|
||||
:return: the court booked
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def has_user_ongoing_booking(self, user: User) -> bool:
|
||||
"""
|
||||
Test whether the user has ongoing bookings
|
||||
|
||||
:param user: the user who will have the booking
|
||||
:return: true if the user has at least one ongoing booking, false otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def cancel_booking_id(self, user: User, booking_id: int) -> None:
|
||||
"""
|
||||
Cancel the booking for a given user
|
||||
|
||||
:param user: the user who has the booking
|
||||
:param booking_id: the id of the booking
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def cancel_booking(self, user: User, booking_filter: BookingFilter) -> None:
|
||||
"""
|
||||
Cancel the booking that meet some conditions for a given user
|
||||
|
||||
:param user: the user who has the booking
|
||||
:param booking_filter: the booking conditions to meet to cancel the booking
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GestionSportsConnector(Connector):
|
||||
class GestionSportsConnector:
|
||||
"""
|
||||
The connector for the Gestion Sports platform.
|
||||
It handles all the requests to the website.
|
||||
|
@ -266,14 +215,16 @@ class GestionSportsConnector(Connector):
|
|||
LOGGER.debug("Connexion request response:\n%s", resp_text)
|
||||
return response
|
||||
|
||||
async def book(self, user: User, booking_filter: BookingFilter) -> Court | None:
|
||||
async def book_any_court(
|
||||
self, session: ClientSession, booking_filter: BookingFilter
|
||||
) -> list[tuple[int, dict]]:
|
||||
"""
|
||||
Perform a request for each court at the same time to increase the chances to get
|
||||
a booking.
|
||||
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 user: the user that wants to book the court
|
||||
:param session: the session to use
|
||||
:param booking_filter: the booking conditions to meet
|
||||
:return: the booked court, or None if no court was booked
|
||||
"""
|
||||
|
@ -283,24 +234,18 @@ class GestionSportsConnector(Connector):
|
|||
|
||||
sport = self.available_sports.get(booking_filter.sport_name)
|
||||
|
||||
async with ClientSession() as session:
|
||||
# use asyncio to request a booking on every court
|
||||
# the gestion-sports backend is able to book only one court for a user
|
||||
await self.land(session)
|
||||
await self.login(session, user)
|
||||
self.wait_until_booking_time(booking_filter)
|
||||
bookings = await asyncio.gather(
|
||||
*[
|
||||
self.send_booking_request(
|
||||
session, booking_filter.date, court.id, sport.id
|
||||
)
|
||||
for court in sport.courts
|
||||
],
|
||||
return_exceptions=True,
|
||||
)
|
||||
bookings = await asyncio.gather(
|
||||
*[
|
||||
self.send_booking_request(
|
||||
session, booking_filter.date, court.id, sport.id
|
||||
)
|
||||
for court in sport.courts
|
||||
],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
LOGGER.debug("Booking results:\n'%s'", bookings)
|
||||
return self.get_booked_court(bookings, sport.name)
|
||||
return bookings
|
||||
|
||||
async def send_booking_request(
|
||||
self,
|
||||
|
@ -308,7 +253,7 @@ class GestionSportsConnector(Connector):
|
|||
date: DateTime,
|
||||
court_id: int,
|
||||
sport_id: int,
|
||||
) -> tuple[ClientResponse, int, bool]:
|
||||
) -> tuple[int, dict]:
|
||||
"""
|
||||
Book a single court that meets the conditions from the booking filter
|
||||
|
||||
|
@ -316,7 +261,7 @@ class GestionSportsConnector(Connector):
|
|||
:param date: the booking date
|
||||
:param court_id: the id of the court to book
|
||||
:param sport_id: the id of the sport
|
||||
:return: a tuple containing the court id and the booking status
|
||||
:return: a tuple containing the court id and the response
|
||||
"""
|
||||
LOGGER.debug("Booking court %s at %s", court_id, date.to_w3c_string())
|
||||
payload = PayloadBuilder.build(
|
||||
|
@ -332,12 +277,12 @@ class GestionSportsConnector(Connector):
|
|||
self.booking_url, data=payload, headers=POST_HEADERS
|
||||
) as response:
|
||||
assert response.status == 200
|
||||
resp_json = await response.text()
|
||||
resp_json = json.loads(await response.text())
|
||||
LOGGER.debug("Response from booking request:\n'%s'", resp_json)
|
||||
return response, court_id, self.is_booking_response_status_ok(resp_json)
|
||||
return court_id, resp_json
|
||||
|
||||
def get_booked_court(
|
||||
self, bookings: list[tuple[ClientSession, int, bool]], sport_name: str
|
||||
self, bookings: list[tuple[int, dict]], sport_name: str
|
||||
) -> Court | None:
|
||||
"""
|
||||
Parse the booking list and return the court that was booked
|
||||
|
@ -346,8 +291,8 @@ class GestionSportsConnector(Connector):
|
|||
:param sport_name: the sport name
|
||||
:return: the id of the booked court if any, None otherwise
|
||||
"""
|
||||
for _, court_id, is_booked in bookings:
|
||||
if is_booked:
|
||||
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)
|
||||
|
@ -369,70 +314,14 @@ class GestionSportsConnector(Connector):
|
|||
return court
|
||||
|
||||
@staticmethod
|
||||
def is_booking_response_status_ok(response: str) -> 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
|
||||
"""
|
||||
formatted_result = response.removeprefix('"').removesuffix('"')
|
||||
result_json = json.loads(formatted_result)
|
||||
return result_json["status"] == "ok"
|
||||
|
||||
def build_booking_datetime(self, booking_filter: BookingFilter) -> DateTime:
|
||||
"""
|
||||
Build the date and time when the booking is open for a given match date.
|
||||
The booking filter contains the date and time of the booking.
|
||||
The club has the information about when the booking is open for that date.
|
||||
|
||||
:param booking_filter: the booking information
|
||||
:return: the date and time when the booking is open
|
||||
"""
|
||||
date_to_book = booking_filter.date
|
||||
booking_opening = self.club.booking_platform.booking_opening
|
||||
booking_date = date_to_book.subtract(days=booking_opening.days_before)
|
||||
|
||||
opening_time = pendulum.parse(booking_opening.opening_time)
|
||||
booking_hour = opening_time.hour
|
||||
booking_minute = opening_time.minute
|
||||
|
||||
return booking_date.at(booking_hour, booking_minute)
|
||||
|
||||
def wait_until_booking_time(self, booking_filter: BookingFilter) -> None:
|
||||
"""
|
||||
Wait until the booking is open.
|
||||
The booking filter contains the date and time of the booking.
|
||||
The club has the information about when the booking is open for that date.
|
||||
|
||||
:param booking_filter: the booking information
|
||||
"""
|
||||
LOGGER.info("Waiting for booking time")
|
||||
booking_datetime = self.build_booking_datetime(booking_filter)
|
||||
now = pendulum.now()
|
||||
duration_until_booking = booking_datetime - now
|
||||
LOGGER.debug(f"Current time: {now}, Datetime to book: {booking_datetime}")
|
||||
LOGGER.debug(
|
||||
f"Time to wait before booking: {duration_until_booking.hours:0>2}"
|
||||
f":{duration_until_booking.minutes:0>2}"
|
||||
f":{duration_until_booking.seconds:0>2}"
|
||||
)
|
||||
|
||||
while now < booking_datetime:
|
||||
time.sleep(1)
|
||||
now = pendulum.now()
|
||||
LOGGER.info("It's booking time!")
|
||||
|
||||
async def has_user_ongoing_booking(self, user: User) -> bool:
|
||||
"""
|
||||
Check if the user currently has bookings in the future
|
||||
:param user: the user to check the bookings
|
||||
:return: true if the user has some bookings, false otherwise
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
await self.land(session)
|
||||
await self.login(session, user)
|
||||
return bool(await self.get_ongoing_bookings(session))
|
||||
return response["status"] == "ok"
|
||||
|
||||
async def get_ongoing_bookings(self, session: ClientSession) -> list[Booking]:
|
||||
"""
|
||||
|
@ -494,21 +383,7 @@ class GestionSportsConnector(Connector):
|
|||
LOGGER.debug("ongoing bookings response: %s\n", resp)
|
||||
return [Booking(**booking) for booking in json.loads(resp)]
|
||||
|
||||
async def cancel_booking_id(self, user: User, booking_id: int) -> ClientResponse:
|
||||
"""
|
||||
Cancel a booking based on its id for a given user
|
||||
|
||||
:param user: the user that has the booking
|
||||
:param booking_id: the id of the booking to cancel
|
||||
:return: the response from the client
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
await self.land(session)
|
||||
await self.login(session, user)
|
||||
|
||||
return await self.send_cancellation_request(session, booking_id)
|
||||
|
||||
async def send_cancellation_request(
|
||||
async def cancel_booking_id(
|
||||
self, session: ClientSession, booking_id: int
|
||||
) -> ClientResponse:
|
||||
"""
|
||||
|
@ -533,20 +408,16 @@ class GestionSportsConnector(Connector):
|
|||
return response
|
||||
|
||||
async def cancel_booking(
|
||||
self, user: User, booking_filter: BookingFilter
|
||||
self, session: ClientSession, booking_filter: BookingFilter
|
||||
) -> ClientResponse | None:
|
||||
"""
|
||||
Cancel the booking that meets some conditions
|
||||
|
||||
:param user: the user who owns the booking
|
||||
:param session: the session
|
||||
:param booking_filter: the conditions the booking to cancel should meet
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
await self.land(session)
|
||||
await self.login(session, user)
|
||||
bookings = await self.get_ongoing_bookings(session)
|
||||
|
||||
bookings = await self.get_ongoing_bookings(session)
|
||||
|
||||
for booking in bookings:
|
||||
if booking.matches(booking_filter):
|
||||
return await self.send_cancellation_request(session, booking.id)
|
||||
for booking in bookings:
|
||||
if booking.matches(booking_filter):
|
||||
return await self.cancel_booking_id(session, booking.id)
|
||||
|
|
127
resa_padel/gestion_sports_services.py
Normal file
127
resa_padel/gestion_sports_services.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import pendulum
|
||||
from aiohttp import ClientSession
|
||||
from connectors import GestionSportsConnector
|
||||
from models import BookingFilter, BookingOpening, Club, Court, User
|
||||
from pendulum import DateTime
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GestionSportsServices:
|
||||
@staticmethod
|
||||
async def book(
|
||||
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
|
||||
a booking.
|
||||
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 club: the club in which the booking will be made
|
||||
:param user: the user that wants to book the court
|
||||
:param booking_filter: the booking conditions to meet
|
||||
:return: the booked court, or None if no court was booked
|
||||
"""
|
||||
connector = GestionSportsConnector(club)
|
||||
LOGGER.info(
|
||||
"Booking any available court from GestionSports API at %s",
|
||||
connector.booking_url,
|
||||
)
|
||||
|
||||
async with ClientSession() as session:
|
||||
# use asyncio to request a booking on every court
|
||||
# the gestion-sports backend is able to book only one court for a user
|
||||
await connector.land(session)
|
||||
await connector.login(session, user)
|
||||
|
||||
booking_opening = club.booking_platform.booking_opening
|
||||
GestionSportsServices.wait_until_booking_time(
|
||||
booking_filter, booking_opening
|
||||
)
|
||||
|
||||
bookings = await connector.book_any_court(session, booking_filter)
|
||||
|
||||
LOGGER.debug("Booking results:\n'%s'", bookings)
|
||||
return connector.get_booked_court(bookings, booking_filter.sport_name)
|
||||
|
||||
@staticmethod
|
||||
async def has_user_available_slots(user: User, club: Club) -> bool:
|
||||
connector = GestionSportsConnector(club)
|
||||
async with ClientSession() as session:
|
||||
await connector.land(session)
|
||||
await connector.login(session, user)
|
||||
bookings = await connector.get_ongoing_bookings(session)
|
||||
|
||||
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
|
||||
) -> None:
|
||||
"""
|
||||
Wait until the booking is open.
|
||||
The booking filter contains the date and time of the booking.
|
||||
The club has the information about when the booking is open for that date.
|
||||
|
||||
:param booking_opening:
|
||||
:param booking_filter: the booking information
|
||||
"""
|
||||
LOGGER.info("Waiting for booking time")
|
||||
booking_datetime = GestionSportsServices.build_booking_datetime(
|
||||
booking_filter, booking_opening
|
||||
)
|
||||
now = pendulum.now()
|
||||
duration_until_booking = booking_datetime - now
|
||||
LOGGER.debug(f"Current time: {now}, Datetime to book: {booking_datetime}")
|
||||
LOGGER.debug(
|
||||
f"Time to wait before booking: {duration_until_booking.hours:0>2}"
|
||||
f":{duration_until_booking.minutes:0>2}"
|
||||
f":{duration_until_booking.seconds:0>2}"
|
||||
)
|
||||
|
||||
while now < booking_datetime:
|
||||
time.sleep(1)
|
||||
now = pendulum.now()
|
||||
LOGGER.info("It's booking time!")
|
||||
|
||||
@staticmethod
|
||||
def build_booking_datetime(
|
||||
booking_filter: BookingFilter, booking_opening: BookingOpening
|
||||
) -> DateTime:
|
||||
"""
|
||||
Build the date and time when the booking is open for a given match date.
|
||||
The booking filter contains the date and time of the booking.
|
||||
The club has the information about when the booking is open for that date.
|
||||
|
||||
:param booking_opening:the booking opening conditions
|
||||
:param booking_filter: the booking information
|
||||
:return: the date and time when the booking is open
|
||||
"""
|
||||
date_to_book = booking_filter.date
|
||||
booking_date = date_to_book.subtract(days=booking_opening.days_before)
|
||||
|
||||
opening_time = pendulum.parse(booking_opening.opening_time)
|
||||
booking_hour = opening_time.hour
|
||||
booking_minute = opening_time.minute
|
||||
|
||||
return booking_date.at(booking_hour, booking_minute)
|
Loading…
Add table
Add a link
Reference in a new issue