refactor: Split code into many files
This commit is contained in:
310
booker.py
Normal file
310
booker.py
Normal file
@@ -0,0 +1,310 @@
|
||||
# Native modules
|
||||
import logging
|
||||
import traceback
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
# Third-party modules
|
||||
import requests
|
||||
from dateutil.parser import parse
|
||||
import pytz
|
||||
from dotenv import load_dotenv
|
||||
from urllib.parse import urlencode
|
||||
from typing import List, Dict, Optional, Any, Tuple
|
||||
|
||||
# Import the SessionNotifier class
|
||||
from session_notifier import SessionNotifier
|
||||
|
||||
# Import the preferred sessions from the session_config module
|
||||
from session_config import PREFERRED_SESSIONS
|
||||
|
||||
# Import the AuthHandler class
|
||||
from auth import AuthHandler
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration
|
||||
USERNAME = os.environ.get("CROSSFIT_USERNAME")
|
||||
PASSWORD = os.environ.get("CROSSFIT_PASSWORD")
|
||||
|
||||
if not all([USERNAME, PASSWORD]):
|
||||
raise ValueError("Missing environment variables: CROSSFIT_USERNAME and/or CROSSFIT_PASSWORD")
|
||||
|
||||
APPLICATION_ID = "81560887"
|
||||
CATEGORY_ID = "677" # Activity category ID for CrossFit
|
||||
TIMEZONE = "Europe/Paris" # Adjust to your timezone
|
||||
# Booking window configuration (can be overridden by environment variables)
|
||||
# TARGET_RESERVATION_TIME: string "HH:MM" local time when bookings open (default 20:01)
|
||||
# BOOKING_WINDOW_END_DELTA_MINUTES: int minutes after target time to stop booking (default 10)
|
||||
TARGET_RESERVATION_TIME = os.environ.get("TARGET_RESERVATION_TIME", "20:01")
|
||||
BOOKING_WINDOW_END_DELTA_MINUTES = int(os.environ.get("BOOKING_WINDOW_END_DELTA_MINUTES", "10"))
|
||||
DEVICE_TYPE = "3"
|
||||
|
||||
# Retry configuration
|
||||
RETRY_MAX = 3
|
||||
RETRY_BACKOFF = 1
|
||||
APP_VERSION = "5.09.21"
|
||||
|
||||
class Booker:
|
||||
"""
|
||||
A class for handling the main booking logic.
|
||||
This class is designed to be used as a standalone component
|
||||
that can be initialized with authentication and session management
|
||||
and used to perform the booking process.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_handler: AuthHandler, notifier: SessionNotifier) -> None:
|
||||
"""
|
||||
Initialize the Booker with necessary attributes.
|
||||
|
||||
Args:
|
||||
auth_handler (AuthHandler): AuthHandler instance for authentication.
|
||||
notifier (SessionNotifier): SessionNotifier instance for sending notifications.
|
||||
"""
|
||||
self.auth_handler = auth_handler
|
||||
self.notifier = notifier
|
||||
|
||||
# Initialize the session and headers
|
||||
self.session: requests.Session = requests.Session()
|
||||
self.base_headers: Dict[str, str] = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Nubapp-Origin": "user_apps",
|
||||
}
|
||||
self.session.headers.update(self.base_headers)
|
||||
|
||||
# Define mandatory parameters for API calls
|
||||
self.mandatory_params: Dict[str, str] = {
|
||||
"app_version": APP_VERSION,
|
||||
"device_type": DEVICE_TYPE,
|
||||
"id_application": APPLICATION_ID,
|
||||
"id_category_activity": CATEGORY_ID
|
||||
}
|
||||
|
||||
def get_auth_headers(self) -> Dict[str, str]:
|
||||
"""
|
||||
Return headers with authorization from the AuthHandler.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Headers dictionary with authorization if available.
|
||||
"""
|
||||
return self.auth_handler.get_auth_headers()
|
||||
|
||||
async def booker(self, current_time: datetime) -> None:
|
||||
"""
|
||||
Run one cycle of checking and booking sessions.
|
||||
|
||||
Args:
|
||||
current_time (datetime): Current time for comparison.
|
||||
"""
|
||||
# Calculate date range to check (current day, day + 1, and day + 2)
|
||||
start_date: date = current_time.date()
|
||||
end_date: date = start_date + timedelta(days=2) # Only go up to day + 2
|
||||
|
||||
# Get available sessions
|
||||
sessions_data: Optional[Dict[str, Any]] = self.get_available_sessions(start_date, end_date)
|
||||
if not sessions_data or not sessions_data.get("success", False):
|
||||
logging.error("No sessions available or error fetching sessions")
|
||||
return
|
||||
|
||||
activities: List[Dict[str, Any]] = sessions_data.get("data", {}).get("activities_calendar", [])
|
||||
|
||||
# Display all available sessions within the date range
|
||||
self.display_upcoming_sessions(activities, current_time)
|
||||
|
||||
# Find sessions to book (preferred only) within allowed date range
|
||||
found_preferred_sessions: List[Dict[str, Any]] = []
|
||||
|
||||
for session in activities:
|
||||
session_time: datetime = parse(session["start_timestamp"])
|
||||
if not session_time.tzinfo:
|
||||
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
||||
|
||||
# Check if session is within allowed date range (current day, day + 1, or day + 2)
|
||||
days_diff = (session_time.date() - current_time.date()).days
|
||||
if not (0 <= days_diff <= 2):
|
||||
continue # Skip sessions outside the allowed date range
|
||||
|
||||
# Check if session is preferred and bookable
|
||||
if self.is_session_bookable(session, current_time):
|
||||
if self.matches_preferred_session(session, current_time):
|
||||
found_preferred_sessions.append(session)
|
||||
|
||||
# Display preferred sessions found
|
||||
if found_preferred_sessions:
|
||||
logging.info("Preferred sessions found:")
|
||||
for session in found_preferred_sessions:
|
||||
session_time: datetime = parse(session["start_timestamp"])
|
||||
if not session_time.tzinfo:
|
||||
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
||||
logging.info(f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Time: {session_time.strftime('%Y-%m-%d %H:%M')}")
|
||||
else:
|
||||
logging.info("No matching preferred sessions found")
|
||||
|
||||
# Book preferred sessions
|
||||
if not found_preferred_sessions:
|
||||
logging.info("No matching sessions found to book")
|
||||
return
|
||||
|
||||
# Book sessions (preferred first)
|
||||
sessions_to_book = [("Preferred", session) for session in found_preferred_sessions]
|
||||
sessions_to_book.sort(key=lambda x: 0 if x[0] == "Preferred" else 1)
|
||||
booked_sessions = []
|
||||
|
||||
for session_type, session in sessions_to_book:
|
||||
session_time: datetime = parse(session["start_timestamp"])
|
||||
logging.info(f"Attempting to book {session_type} session at {session_time} ({session['name_activity']})")
|
||||
if self.book_session(session["id_activity_calendar"]):
|
||||
# Display booked session
|
||||
booked_sessions.append(session)
|
||||
logging.info(f"Successfully booked {session_type} session at {session_time}")
|
||||
|
||||
# Notify about booked session
|
||||
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
||||
await self.notifier.notify_session_booking(session_details)
|
||||
else:
|
||||
logging.error(f"Failed to book {session_type} session at {session_time}")
|
||||
|
||||
# Notify about failed booking
|
||||
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
||||
await self.notifier.notify_impossible_booking(session_details)
|
||||
logging.info(f"Notified about impossible booking for {session_type} session at {session_time}")
|
||||
|
||||
# Display all booked session(s)
|
||||
if booked_sessions:
|
||||
logging.info("Booked sessions:")
|
||||
for session in booked_sessions:
|
||||
session_time: datetime = parse(session["start_timestamp"])
|
||||
if not session_time.tzinfo:
|
||||
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
||||
logging.info(f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Time: {session_time.strftime('%Y-%m-%d %H:%M')}")
|
||||
else:
|
||||
logging.info("No sessions were booked")
|
||||
|
||||
async def run(self) -> None:
|
||||
"""
|
||||
Main execution loop.
|
||||
"""
|
||||
# Set up timezone
|
||||
tz: pytz.timezone = pytz.timezone(TIMEZONE)
|
||||
|
||||
# Parse TARGET_RESERVATION_TIME to get the target hour and minute
|
||||
target_hour, target_minute = map(int, TARGET_RESERVATION_TIME.split(":"))
|
||||
target_time = datetime.now(tz).replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
||||
booking_window_end = target_time + timedelta(minutes=BOOKING_WINDOW_END_DELTA_MINUTES)
|
||||
|
||||
# Initial login
|
||||
if not self.auth_handler.login():
|
||||
logging.error("Authentication failed - exiting program")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
current_time: datetime = datetime.now(tz)
|
||||
logging.info(f"Current time: {current_time}")
|
||||
|
||||
# Only book sessions if current time is within the booking window
|
||||
if target_time <= current_time <= booking_window_end:
|
||||
# Run booking cycle to check for preferred sessions and book
|
||||
await self.booker(current_time)
|
||||
# Wait for a short time before next check
|
||||
time.sleep(60)
|
||||
else:
|
||||
# Check again in 5 minutes if outside booking window
|
||||
time.sleep(300)
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error in booking cycle: {str(e)} - Traceback: {traceback.format_exc()}")
|
||||
time.sleep(60) # Wait before retrying after error
|
||||
except KeyboardInterrupt:
|
||||
self.quit()
|
||||
|
||||
def quit(self) -> None:
|
||||
"""
|
||||
Clean up resources and exit the script.
|
||||
"""
|
||||
logging.info("Script interrupted by user. Quitting...")
|
||||
exit(0)
|
||||
|
||||
def get_available_sessions(self, start_date: date, end_date: date) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch available sessions from the API.
|
||||
|
||||
Args:
|
||||
start_date (date): Start date for fetching sessions.
|
||||
end_date (date): End date for fetching sessions.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Dictionary containing available sessions if successful, None otherwise.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
return session_manager.get_available_sessions(start_date, end_date)
|
||||
|
||||
def book_session(self, session_id: str) -> bool:
|
||||
"""
|
||||
Book a specific session.
|
||||
|
||||
Args:
|
||||
session_id (str): ID of the session to book.
|
||||
|
||||
Returns:
|
||||
bool: True if booking is successful, False otherwise.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
return session_manager.book_session(session_id)
|
||||
|
||||
def get_booked_sessions(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get a list of booked sessions.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries containing information about the booked sessions.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
return session_manager.get_booked_sessions()
|
||||
|
||||
def is_session_bookable(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
||||
"""
|
||||
Check if a session is bookable based on user_info.
|
||||
|
||||
Args:
|
||||
session (Dict[str, Any]): Session data.
|
||||
current_time (datetime): Current time for comparison.
|
||||
|
||||
Returns:
|
||||
bool: True if the session is bookable, False otherwise.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
return session_manager.is_session_bookable(session, current_time)
|
||||
|
||||
def matches_preferred_session(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
||||
"""
|
||||
Check if session matches one of your preferred sessions with exact matching.
|
||||
|
||||
Args:
|
||||
session (Dict[str, Any]): Session data.
|
||||
current_time (datetime): Current time for comparison.
|
||||
|
||||
Returns:
|
||||
bool: True if the session matches a preferred session, False otherwise.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
return session_manager.matches_preferred_session(session, current_time)
|
||||
|
||||
def display_upcoming_sessions(self, sessions: List[Dict[str, Any]], current_time: datetime) -> None:
|
||||
"""
|
||||
Display upcoming sessions with ID, name, date, and time.
|
||||
|
||||
Args:
|
||||
sessions (List[Dict[str, Any]]): List of session data.
|
||||
current_time (datetime): Current time for comparison.
|
||||
"""
|
||||
from session_manager import SessionManager
|
||||
session_manager = SessionManager(self.auth_handler)
|
||||
session_manager.display_upcoming_sessions(sessions, current_time)
|
||||
Reference in New Issue
Block a user