# Native modules import logging import pytz import time from datetime import date from typing import List, Dict, Optional, Any from datetime import datetime, timedelta, date # Third-party modules import requests from dateutil.parser import parse # Import the preferred sessions from the session_config module from session_config import PREFERRED_SESSIONS # Import the AuthHandler class from auth import AuthHandler class SessionManager: """ A class for managing CrossFit sessions. This class handles session availability checking, booking, and session-related operations. """ def __init__(self, auth_handler: AuthHandler) -> None: """ Initialize the SessionManager with necessary attributes. Args: auth_handler (AuthHandler): AuthHandler instance for authentication. """ self.auth_handler = auth_handler self.session = requests.Session() self.base_headers = { "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 = { "app_version": "5.09.21", "device_type": "3", "id_application": "81560887", "id_category_activity": "677" } 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() 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. """ if not self.auth_handler.auth_token or not self.auth_handler.user_id: logging.error("Authentication required - missing token or user ID") return None url = "https://sport.nubapp.com/api/v4/activities/getActivitiesCalendar.php" # Prepare request with mandatory parameters request_data = self.mandatory_params.copy() request_data.update({ "id_user": self.auth_handler.user_id, "start_timestamp": start_date.strftime("%d-%m-%Y"), "end_timestamp": end_date.strftime("%d-%m-%Y") }) # Add retry logic for retry in range(3): try: response = self.session.post( url, headers=self.get_auth_headers(), data=request_data, timeout=10 ) if response.status_code == 200: return response.json() elif response.status_code == 401: logging.error("401 Unauthorized - token may be expired or invalid") return None elif 500 <= response.status_code < 600: logging.error(f"Server error {response.status_code}") raise requests.exceptions.ConnectionError(f"Server error {response.status_code}") else: logging.error(f"Unexpected status code: {response.status_code} - {response.text[:100]}") return None except requests.exceptions.RequestException as e: if retry == 2: logging.error(f"Final retry failed: {str(e)}") raise wait_time = 1 * (2 ** retry) logging.warning(f"Request failed (attempt {retry+1}/3): {str(e)}. Retrying in {wait_time}s...") time.sleep(wait_time) return None 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. """ url = "https://sport.nubapp.com/api/v4/activities/bookActivityCalendar.php" data = { **self.mandatory_params, "id_activity_calendar": session_id, "id_user": self.auth_handler.user_id, "action_by": self.auth_handler.user_id, "n_guests": "0", "booked_on": "1", "device_type": self.mandatory_params["device_type"], "token": self.auth_handler.auth_token } for retry in range(3): try: response = self.session.post( url, headers=self.get_auth_headers(), data=data, timeout=10 ) if response.status_code == 200: json_response = response.json() if json_response.get("success", False): logging.info(f"Successfully booked session {session_id}") return True else: logging.error(f"API returned success:false: {json_response} - Session ID: {session_id}") return False logging.error(f"HTTP {response.status_code}: {response.text[:100]}") return False except requests.exceptions.RequestException as e: if retry == 2: logging.error(f"Final retry failed: {str(e)}") raise wait_time = 1 * (2 ** retry) logging.warning(f"Request failed (attempt {retry+1}/3): {str(e)}. Retrying in {wait_time}s...") time.sleep(wait_time) logging.error(f"Failed to complete request after 3 attempts") return False 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. """ url = "https://sport.nubapp.com/api/v4/activities/getBookedActivities.php" data = { **self.mandatory_params, "id_user": self.auth_handler.user_id, "action_by": self.auth_handler.user_id } for retry in range(3): try: response = self.session.post( url, headers=self.get_auth_headers(), data=data, timeout=10 ) if response.status_code == 200: json_response = response.json() if json_response.get("success", False): return json_response.get("data", []) logging.error(f"API returned success:false: {json_response}") return [] logging.error(f"HTTP {response.status_code}: {response.text[:100]}") return [] except requests.exceptions.RequestException as e: if retry == 2: logging.error(f"Final retry failed: {str(e)}") raise wait_time = 1 * (2 ** retry) logging.warning(f"Request failed (attempt {retry+1}/3): {str(e)}. Retrying in {wait_time}s...") time.sleep(wait_time) logging.error(f"Failed to complete request after 3 attempts") return [] 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. """ user_info = session.get("user_info", {}) # First check if can_join is true (primary condition) if user_info.get("can_join", False): return True # Default case: not bookable return False 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. """ try: session_time = parse(session["start_timestamp"]) if not session_time.tzinfo: session_time = pytz.timezone("Europe/Paris").localize(session_time) day_of_week = session_time.weekday() session_time_str = session_time.strftime("%H:%M") session_name = session.get("name_activity", "").upper() for preferred_day, preferred_time, preferred_name in PREFERRED_SESSIONS: # Exact match if (day_of_week == preferred_day and session_time_str == preferred_time and preferred_name in session_name): return True return False except Exception as e: logging.error(f"Failed to check session: {str(e)} - Session: {session}") return False 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. """ if not sessions: logging.info("No sessions to display") return logging.info("Upcoming sessions:") logging.info("ID\t\tName\t\tDate\t\tTime") logging.info("="*50) for session in sessions: session_time = parse(session["start_timestamp"]) if not session_time.tzinfo: session_time = pytz.timezone("Europe/Paris").localize(session_time) # Format session details session_id = session.get("id_activity_calendar", "N/A") session_name = session.get("name_activity", "N/A") session_date = session_time.strftime("%Y-%m-%d") session_time_str = session_time.strftime("%H:%M") # Display session details logging.info(f"{session_id}\t{session_name}\t{session_date}\t{session_time_str}")