from enum import Enum from pathlib import Path from typing import Optional from urllib.parse import urljoin import config import pendulum from exceptions import MissingProperty from pendulum import Date, Time from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic_extra_types.pendulum_dt import DateTime class User(BaseModel): login: str password: str = Field(repr=False) club_id: Optional[str] = Field(default=None) class BookingOpening(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) days_before: Optional[int] = Field(default=7, alias="daysBefore") opening_time: Optional[str] = Field(alias="time", default=None, repr=False) time_after_booking: Optional[str] = Field( alias="timeAfterBookingTime", default=None, repr=False ) def __repr__(self): base = super().__repr__() time = f", time: {self.time})" if self.time else "" time_after_booking = ( f", time_after_booking_time: {self.time_after_booking_time})" if self.time_after_booking_time else "" ) return base.removesuffix(")") + time + time_after_booking @property def time(self): return pendulum.parse(self.opening_time).time() @property def time_after_booking_time(self): return ( pendulum.parse(self.time_after_booking).time() if self.time_after_booking else None ) class TotalBookings(BaseModel): peak_hours: int | str = Field(alias="peakHours") off_peak_hours: int | str = Field(alias="offPeakHours") class Court(BaseModel): id: int name: str number: int is_indoor: Optional[bool] = Field(alias="isIndoor") class Sport(BaseModel): name: str id: int duration: int price: int players: int courts: list[Court] class Url(BaseModel): name: str path: str parameter: Optional[str] = Field(default=None) payload_template: Optional[str] = Field(default=None, alias="payloadTemplate") class BookingPlatform(BaseModel): id: str club_id: int = Field(alias="clubId") url: str hours_before_cancellation: int = Field(alias="hoursBeforeCancellation") booking_opening: BookingOpening = Field(alias="bookingOpening") total_bookings: TotalBookings = Field(alias="totalBookings") sports: list[Sport] 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): id: str name: str url: str 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): id: str name: str url: str urls: list[Url] class BookingFilter(BaseModel): date: DateTime sport_name: str @field_validator("sport_name", mode="before") @classmethod def to_lower_case(cls, d: str) -> str: return d.lower() class Booking(BaseModel): id: int booking_date: DateTime = Field(alias="dateResa") start_time: DateTime = Field(alias="startTime") sport: str court: str game_creation: Optional[int] = Field(default=None, alias="creaPartie") game_creation_limit: Optional[DateTime] = Field( default=None, alias="limitCreaPartie" ) cancel: Optional[bool] = Field(default=True) block_player_replacement: Optional[int] = Field( default=None, alias="bloquerRemplacementJoueur" ) can_remove_parteners: bool = Field(default=True, alias="canRemovePartners") end_time: Optional[DateTime] = Field(default=None, alias="endTime") day_fr: Optional[str] = Field(default=None, alias="dayFr") live_xperience_code: Optional[str] = Field(default=None, alias="codeLiveXperience") spartime_qr_code: Optional[str] = Field(default=None, alias="qrCodeSpartime") remaining_places: int = Field(default=3, alias="remainingPlaces") is_captain: bool = Field(default=True, alias="isCaptain") dt_start: Optional[DateTime] = Field(default=None, alias="dtStart") credit_card_guaranty: Optional[str] = Field(default=None, alias="garantieCb") certificate_validity_duration: Optional[int] = Field( alias="dureeValidCertif", default=None ) charge_id: Optional[str] = Field(default=None, alias="chargeId") partners: Optional[list] = Field(default=[]) player_status: Optional[int] = Field(default=None, alias="playerStatus") products: Optional[list] = Field(default=[]) @field_validator("booking_date", mode="before") @classmethod def validate_date(cls, d: str) -> DateTime: return pendulum.from_format( d, "DD/MM/YYYY", tz=pendulum.timezone("Europe/Paris") ) @field_validator("start_time", "end_time", mode="before") @classmethod def validate_time(cls, t: str) -> DateTime: return pendulum.from_format(t, "HH:mm", tz=pendulum.timezone("Europe/Paris")) @field_validator("game_creation_limit", mode="before") @classmethod def validate_datetime_add_tz(cls, dt: str) -> DateTime: return pendulum.parse(dt, tz=pendulum.timezone("Europe/Paris")) @field_validator("dt_start", mode="before") @classmethod def validate_datetime(cls, dt: str) -> DateTime: return pendulum.parse(dt) @field_validator("sport", mode="before") @classmethod def to_lower_case(cls, d: str) -> str: return d.lower() def matches(self, booking_filter: BookingFilter) -> bool: """ Check if the booking matches the booking filter :param booking_filter: the conditions the booking should meet :return: true if the booking matches the conditions, false otherwise """ return ( self.is_same_sport(booking_filter.sport_name) and self.is_same_date(booking_filter.date.date()) and self.is_same_time(booking_filter.date.time()) ) def is_same_sport(self, sport: str) -> bool: """ Check if the booking and the booking filter are about the same sport :param sport: the sport to test :return: true if the sport matches booking sport, false otherwise """ return self.sport == sport def is_same_date(self, date: Date) -> bool: """ Check if the booking filter has the same date as the booking :param date: the date to test :return: true if the date matches the booking date, false otherwise """ return self.booking_date.date() == date def is_same_time(self, time: Time) -> bool: """ Check if the booking filter has the same time as the booking :param time: the time to test :return: true if the time matches the booking time, false otherwise """ return self.start_time.time() == time 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