Booking can be made as early as possible depending on the club booking opening

This commit is contained in:
Stanislas Jouffroy 2024-02-17 16:23:53 +01:00
parent fc11a1e1eb
commit 8562e101b4
5 changed files with 171 additions and 13 deletions

View file

@ -1,19 +1,43 @@
import asyncio import asyncio
import logging import logging
import time
import pendulum
from aiohttp import ClientSession
from pendulum import DateTime
import config import config
from aiohttp import ClientSession
from gestion_sports.gestion_sports_connector import GestionSportsConnector from gestion_sports.gestion_sports_connector import GestionSportsConnector
from models import BookingFilter, Club, User from models import BookingFilter, Club, User
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
def wait_until_booking_time(club: Club, booking_filter: BookingFilter):
LOGGER.info("Waiting booking time")
booking_datetime = build_booking_datetime(booking_filter, club)
now = pendulum.now()
while now < booking_datetime:
time.sleep(1)
now = pendulum.now()
def build_booking_datetime(booking_filter: BookingFilter, club: Club) -> DateTime:
date_to_book = booking_filter.date
booking_date = date_to_book.subtract(days=club.booking_open_days_before)
booking_hour = club.booking_opening_time.hour
booking_minute = club.booking_opening_time.minute
return booking_date.at(booking_hour, booking_minute)
async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | None: async def book(club: Club, user: User, booking_filter: BookingFilter) -> int | None:
async with ClientSession() as session: async with ClientSession() as session:
platform = GestionSportsConnector(session, club.url) platform = GestionSportsConnector(session, club.url)
await platform.connect() await platform.connect()
await platform.login(user, club) await platform.login(user, club)
wait_until_booking_time(club, booking_filter)
return await platform.book(booking_filter, club) return await platform.book(booking_filter, club)

View file

@ -21,7 +21,16 @@ def get_club() -> Club:
else [] else []
) )
club_id = os.environ.get("CLUB_ID") club_id = os.environ.get("CLUB_ID")
return Club(id=club_id, url=club_url, courts_ids=court_ids) booking_open_days_before = int(os.environ.get("BOOKING_OPEN_DAYS_BEFORE", "7"))
booking_opening_time_str = os.environ.get("BOOKING_OPENING_TIME", "00:00")
booking_opening_time = pendulum.parse(booking_opening_time_str)
return Club(
id=club_id,
url=club_url,
courts_ids=court_ids,
booking_open_days_before=booking_open_days_before,
booking_opening_time=booking_opening_time.time(),
)
def get_booking_filter() -> BookingFilter: def get_booking_filter() -> BookingFilter:

View file

@ -1,11 +1,15 @@
from pydantic import BaseModel, Field from pendulum import Time
from pydantic import BaseModel, Field, ConfigDict
from pydantic_extra_types.pendulum_dt import DateTime from pydantic_extra_types.pendulum_dt import DateTime
class Club(BaseModel): class Club(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: str = Field() id: str = Field()
url: str = Field() url: str = Field()
courts_ids: list[int] = Field(default_factory=list) courts_ids: list[int] = Field(default_factory=list)
booking_open_days_before: int = Field(default=7)
booking_opening_time: Time = Field(default=Time(hour=0, minute=0))
class BookingFilter(BaseModel): class BookingFilter(BaseModel):

View file

@ -14,7 +14,8 @@ club = Club(id="123", url=url, courts_ids=[606, 607, 608])
courts = [606, 607, 608] courts = [606, 607, 608]
sport_id = 217 sport_id = 217
booking_date = pendulum.now().add(days=6).set(hour=18, minute=0, second=0) tz_info = "Europe/Paris"
booking_date = pendulum.now().add(days=6).set(hour=18, minute=0, second=0, tz=tz_info)
booking_filter = BookingFilter(sport_id=sport_id, date=booking_date) booking_filter = BookingFilter(sport_id=sport_id, date=booking_date)
booking_failure_response = json.dumps( booking_failure_response = json.dumps(

View file

@ -3,8 +3,11 @@ import os
from unittest.mock import patch from unittest.mock import patch
from urllib.parse import urljoin from urllib.parse import urljoin
import pendulum
from aioresponses import aioresponses from aioresponses import aioresponses
from pendulum import DateTime, Time
from models import BookingFilter, Club, User
from resa_padel import booking from resa_padel import booking
from tests import fixtures from tests import fixtures
from tests.fixtures import ( from tests.fixtures import (
@ -19,9 +22,19 @@ login = "user"
password = "password" password = "password"
club_id = "88" club_id = "88"
court_id = "11" court_id = "11"
paris_tz = "Europe/Paris"
datetime_to_book = (
pendulum.now().add(days=6).set(hour=18, minute=0, second=0, tz=paris_tz)
)
def mock_successful_connection(aio_mock, url): def mock_successful_connection(aio_mock, url):
"""
Mock a call to the connection endpoint
:param aio_mock: the aioresponses mock object
:param url: the URL of the connection endpoint
"""
aio_mock.get( aio_mock.get(
url, url,
status=200, status=200,
@ -30,6 +43,12 @@ def mock_successful_connection(aio_mock, url):
def mock_successful_login(aio_mock, url): def mock_successful_login(aio_mock, url):
"""
Mock a call to the login endpoint
:param aio_mock: the aioresponses mock object
:param url: the URL of the login endpoint
"""
aio_mock.post( aio_mock.post(
url, url,
status=200, status=200,
@ -38,6 +57,13 @@ def mock_successful_login(aio_mock, url):
def mock_booking(aio_mock, url, response): def mock_booking(aio_mock, url, response):
"""
Mock a call to the booking endpoint
:param aio_mock: the aioresponses mock object
:param url: the URL of the booking endpoint
:param response: the response from the booking endpoint
"""
aio_mock.post( aio_mock.post(
url, url,
status=200, status=200,
@ -47,24 +73,98 @@ def mock_booking(aio_mock, url, response):
def mock_rest_api_from_connection_to_booking( def mock_rest_api_from_connection_to_booking(
aio_mock, url, a_booking_failure_response, a_booking_success_response aio_mock, url: str, a_booking_failure_response: str, a_booking_success_response: str
): ):
"""
Mock a REST API from a club.
It mocks the calls to the connexion to the website, a call to log in the user
and 2 calls to the booking endpoint
:param mock_now: the pendulum.now() mock
:param url: the API root URL
:param a_booking_success_response: the success json response
:param a_booking_failure_response: the failure json response
:return:
"""
connexion_url = urljoin(url, "/connexion.php?") connexion_url = urljoin(url, "/connexion.php?")
mock_successful_connection(aio_mock, connexion_url) mock_successful_connection(aio_mock, connexion_url)
login_url = urljoin(url, "/connexion.php?") login_url = urljoin(url, "/connexion.php?")
mock_successful_login(aio_mock, login_url) mock_successful_login(aio_mock, login_url)
booking_url = urljoin(url, "/membre/reservation.html?") booking_url = urljoin(url, "/membre/reservation.html?")
mock_booking(aio_mock, booking_url, a_booking_failure_response) mock_booking(aio_mock, booking_url, a_booking_failure_response)
mock_booking(aio_mock, booking_url, a_booking_success_response) mock_booking(aio_mock, booking_url, a_booking_success_response)
def test_booking_does_the_rights_calls( @patch("pendulum.now")
a_booking_success_response, def test_wait_until_booking_time(
a_booking_failure_response, mock_now, a_club: Club, a_booking_filter: BookingFilter
a_user,
a_club,
a_booking_filter,
): ):
"""
Test the function that waits until the booking can be performed
:param mock_now: the pendulum.now() mock
:param a_club: a club
:param a_booking_filter: a booking filter
"""
booking_datetime = retrieve_booking_datetime(a_booking_filter, a_club)
seconds = [
booking_datetime.subtract(seconds=3),
booking_datetime.subtract(seconds=2),
booking_datetime.subtract(seconds=1),
booking_datetime,
booking_datetime.add(microseconds=1),
booking_datetime.add(microseconds=2),
]
mock_now.side_effect = seconds
booking.wait_until_booking_time(a_club, a_booking_filter)
assert pendulum.now() == booking_datetime.add(microseconds=1)
def retrieve_booking_datetime(
a_booking_filter: BookingFilter, a_club: Club
) -> DateTime:
"""
Utility to retrieve the booking datetime from the booking filter and the club
:param a_booking_filter: the booking filter that contains the date to book
:param a_club: the club which has the number of days before the date and the booking time
"""
booking_hour = a_club.booking_opening_time.hour
booking_minute = a_club.booking_opening_time.minute
date_to_book = a_booking_filter.date
return date_to_book.subtract(days=a_club.booking_open_days_before).at(
booking_hour, booking_minute
)
@patch("pendulum.now")
def test_booking_does_the_rights_calls(
mock_now,
a_booking_success_response: str,
a_booking_failure_response: str,
a_user: User,
a_club: Club,
a_booking_filter: BookingFilter,
):
"""
Test a single court booking without reading the conf from environment variables
:param mock_now: the pendulum.now() mock
:param a_booking_success_response: the success json response
:param a_booking_failure_response: the failure json response
:param a_user: a test user
:param a_club:a test club
:param a_booking_filter: a test booking filter
"""
booking_datetime = retrieve_booking_datetime(a_booking_filter, a_club)
mock_now.side_effect = [booking_datetime]
# mock connection to the booking platform # mock connection to the booking platform
with aioresponses() as aio_mock: with aioresponses() as aio_mock:
mock_rest_api_from_connection_to_booking( mock_rest_api_from_connection_to_booking(
@ -82,6 +182,7 @@ def test_booking_does_the_rights_calls(
assert court_booked == a_club.courts_ids[1] assert court_booked == a_club.courts_ids[1]
@patch("pendulum.now")
@patch.dict( @patch.dict(
os.environ, os.environ,
{ {
@ -91,11 +192,30 @@ def test_booking_does_the_rights_calls(
"CLUB_URL": fixtures.url, "CLUB_URL": fixtures.url,
"COURT_IDS": "7,8,10", "COURT_IDS": "7,8,10",
"SPORT_ID": "217", "SPORT_ID": "217",
"DATE_TIME": "2024-04-23T15:00:00Z", "DATE_TIME": datetime_to_book.isoformat(),
}, },
clear=True, clear=True,
) )
def test_main(a_booking_success_response, a_booking_failure_response): def test_main(
mock_now, a_booking_success_response: str, a_booking_failure_response: str
):
"""
Test the main function to book a court
:param mock_now: the pendulum.now() mock
:param a_booking_success_response: the success json response
:param a_booking_failure_response: the failure json response
"""
booking_filter = BookingFilter(sport_id=666, date=datetime_to_book)
club = Club(
id="club",
url="some.url",
courts_ids=[7, 8, 10],
booking_open_days_before=7,
booking_opening_time=Time(hour=0, minute=0),
)
booking_datetime = retrieve_booking_datetime(booking_filter, club)
mock_now.side_effect = [booking_datetime]
with aioresponses() as aio_mock: with aioresponses() as aio_mock:
mock_rest_api_from_connection_to_booking( mock_rest_api_from_connection_to_booking(