pysportbot 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pysportbot/__init__.py ADDED
@@ -0,0 +1,116 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from pandas import DataFrame
5
+
6
+ from .activities import Activities
7
+ from .authenticator import Authenticator
8
+ from .bookings import Bookings
9
+ from .centres import Centres
10
+ from .session import Session
11
+ from .utils.errors import ErrorMessages
12
+ from .utils.logger import set_log_level, setup_logger
13
+
14
+
15
+ class SportBot:
16
+ """Unified interface for interacting with the booking system."""
17
+
18
+ def __init__(self, log_level: str = "INFO", print_centres: bool = False) -> None:
19
+ setup_logger(log_level)
20
+ self._logger = logging.getLogger("SportBot")
21
+ self._logger.info("Initializing SportBot...")
22
+ self._centres = Centres(print_centres)
23
+ self._session: Session = Session()
24
+ self._auth: Optional[Authenticator] = None
25
+ self._activities: Activities = Activities(self._session)
26
+ self._bookings: Bookings = Bookings(self._session)
27
+ self._df_activities: DataFrame | None = None
28
+ self._is_logged_in: bool = False # State variable for login status
29
+
30
+ def set_log_level(self, log_level: str) -> None:
31
+ set_log_level(log_level)
32
+ self._logger.info(f"Log level changed to {log_level}.")
33
+
34
+ def login(self, email: str, password: str, centre: str) -> None:
35
+
36
+ # Check if the selected centre is valid
37
+ self._centres.check_centre(centre)
38
+ self._logger.info(f"Selected centre: {centre}")
39
+
40
+ # Initialize the Authenticator
41
+ self._auth = Authenticator(self._session, centre)
42
+
43
+ self._logger.info("Attempting to log in...")
44
+ try:
45
+ self._auth.login(email, password)
46
+ self._df_activities = self._activities.fetch()
47
+ self._is_logged_in = True
48
+ self._logger.info("Login successful!")
49
+ except Exception:
50
+ self._is_logged_in = False # Ensure state is False on failure
51
+ self._logger.exception(ErrorMessages.login_failed())
52
+ raise
53
+
54
+ def is_logged_in(self) -> bool:
55
+ """Returns the login status."""
56
+ return self._is_logged_in
57
+
58
+ def activities(self, limit: int | None = None) -> DataFrame:
59
+ if not self._is_logged_in:
60
+ self._logger.error(ErrorMessages.not_logged_in())
61
+ raise PermissionError(ErrorMessages.not_logged_in())
62
+
63
+ if self._df_activities is None:
64
+ self._logger.error(ErrorMessages.no_activities_loaded())
65
+ raise ValueError(ErrorMessages.no_activities_loaded())
66
+
67
+ df = self._df_activities[["name_activity", "id_activity"]]
68
+ return df.head(limit) if limit else df
69
+
70
+ def daily_slots(self, activity: str, day: str, limit: int | None = None) -> DataFrame:
71
+ if not self._is_logged_in:
72
+ self._logger.error(ErrorMessages.not_logged_in())
73
+ raise PermissionError(ErrorMessages.not_logged_in())
74
+
75
+ if self._df_activities is None:
76
+ self._logger.error(ErrorMessages.no_activities_loaded())
77
+ raise ValueError(ErrorMessages.no_activities_loaded())
78
+
79
+ df = self._activities.daily_slots(self._df_activities, activity, day)
80
+ return df.head(limit) if limit else df
81
+
82
+ def book(self, activity: str, start_time: str) -> None:
83
+ if not self._is_logged_in:
84
+ self._logger.error(ErrorMessages.not_logged_in())
85
+ raise PermissionError(ErrorMessages.not_logged_in())
86
+
87
+ if self._df_activities is None:
88
+ self._logger.error(ErrorMessages.no_activities_loaded())
89
+ raise ValueError(ErrorMessages.no_activities_loaded())
90
+
91
+ slots = self.daily_slots(activity, start_time.split(" ")[0])
92
+ matching_slot = slots[slots["start_timestamp"] == start_time]
93
+ if matching_slot.empty:
94
+ error_msg = ErrorMessages.slot_not_found(activity, start_time)
95
+ self._logger.error(error_msg)
96
+ raise IndexError(error_msg)
97
+
98
+ self._bookings.book(matching_slot.iloc[0]["id_activity_calendar"])
99
+
100
+ def cancel(self, activity: str, start_time: str) -> None:
101
+ if not self._is_logged_in:
102
+ self._logger.error(ErrorMessages.not_logged_in())
103
+ raise PermissionError(ErrorMessages.not_logged_in())
104
+
105
+ if self._df_activities is None:
106
+ self._logger.error(ErrorMessages.no_activities_loaded())
107
+ raise ValueError(ErrorMessages.no_activities_loaded())
108
+
109
+ slots = self.daily_slots(activity, start_time.split(" ")[0])
110
+ matching_slot = slots[slots["start_timestamp"] == start_time]
111
+ if matching_slot.empty:
112
+ error_msg = ErrorMessages.slot_not_found(activity, start_time)
113
+ self._logger.error(error_msg)
114
+ raise IndexError(error_msg)
115
+
116
+ self._bookings.cancel(matching_slot.iloc[0]["id_activity_calendar"])
@@ -0,0 +1,122 @@
1
+ import json
2
+
3
+ import pandas as pd
4
+ from pandas import DataFrame
5
+
6
+ from .endpoints import Endpoints
7
+ from .session import Session
8
+ from .utils.errors import ErrorMessages
9
+ from .utils.logger import get_logger
10
+ from .utils.time import get_unix_day_bounds
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class Activities:
16
+ """Handles activity fetching and slot management."""
17
+
18
+ def __init__(self, session: Session) -> None:
19
+ """Initialize the Activities class."""
20
+ self.session = session.session
21
+ self.headers = session.headers
22
+
23
+ def fetch(self) -> DataFrame:
24
+ """
25
+ Fetch all available activities.
26
+
27
+ Returns:
28
+ DataFrame: A DataFrame containing activity details.
29
+
30
+ Raises:
31
+ RuntimeError: If the request fails.
32
+ """
33
+ logger.info("Fetching activities...")
34
+ response = self.session.post(Endpoints.ACTIVITIES, headers=self.headers)
35
+ if response.status_code != 200:
36
+ error_msg = ErrorMessages.failed_fetch("activities")
37
+ logger.error(error_msg)
38
+ raise RuntimeError(error_msg)
39
+ logger.info("Activities fetched successfully.")
40
+ activities = json.loads(response.content.decode("utf-8"))["activities"]
41
+ return pd.DataFrame.from_dict(activities, orient="index")
42
+
43
+ def daily_slots(self, df_activities: DataFrame, activity_name: str, day: str) -> DataFrame:
44
+ """
45
+ Fetch available slots for a specific activity on a given day.
46
+
47
+ Args:
48
+ df_activities (DataFrame): The DataFrame of activities.
49
+ activity_name (str): The name of the activity.
50
+ day (str): The day in 'YYYY-MM-DD' format.
51
+
52
+ Returns:
53
+ DataFrame: A DataFrame containing available slots.
54
+
55
+ Raises:
56
+ ValueError: If the specified activity is not found.
57
+ RuntimeError: If slots cannot be fetched.
58
+ """
59
+ logger.info(f"Fetching daily slots for '{activity_name}' on {day}...")
60
+
61
+ # Check if the activity exists
62
+ activity_match = df_activities[df_activities["name_activity"] == activity_name]
63
+ if activity_match.empty:
64
+ error_msg = ErrorMessages.activity_not_found(
65
+ activity_name, df_activities["name_activity"].unique().tolist()
66
+ )
67
+ logger.error(error_msg)
68
+ raise ValueError(error_msg)
69
+
70
+ # Extract activity ID
71
+ activity_id = str(activity_match.id_activity.iloc[0])
72
+ id_category_activity = df_activities.loc[activity_id].activityCategoryId
73
+
74
+ # Get Unix timestamp bounds for the day
75
+ unix_day_bounds = get_unix_day_bounds(day)
76
+
77
+ # Fetch slots
78
+ params = {
79
+ "id_category_activity": id_category_activity,
80
+ "start": unix_day_bounds[0],
81
+ "end": unix_day_bounds[1],
82
+ }
83
+ response = self.session.get(Endpoints.SLOTS, headers=self.headers, params=params)
84
+ if response.status_code != 200:
85
+ error_msg = ErrorMessages.failed_fetch("slots")
86
+ logger.error(error_msg)
87
+ raise RuntimeError(error_msg)
88
+
89
+ slots = json.loads(response.content.decode("utf-8"))
90
+ if not slots:
91
+ warning_msg = ErrorMessages.no_slots(activity_name, day)
92
+ logger.warning(warning_msg)
93
+ return DataFrame()
94
+
95
+ logger.info(f"Daily slots fetched for '{activity_name}' on {day}.")
96
+
97
+ # Filter desired columns
98
+ columns = [
99
+ "name_activity",
100
+ "id_activity_calendar",
101
+ "id_activity",
102
+ "id_category_activity",
103
+ "start_timestamp",
104
+ "end_timestamp",
105
+ "n_inscribed",
106
+ "capacity",
107
+ "n_waiting_list",
108
+ "cancelled",
109
+ "can_join",
110
+ "trainer",
111
+ ]
112
+ df_slots = pd.DataFrame(slots)
113
+ df_slots = df_slots[df_slots.columns.intersection(columns)] # Ensure no KeyError
114
+
115
+ # Only select rows of the specified activity
116
+ df_slots = df_slots[df_slots.id_activity == activity_id]
117
+ if df_slots.empty:
118
+ warning_msg = ErrorMessages.no_matching_slots(activity_name, day)
119
+ logger.warning(warning_msg)
120
+ return DataFrame()
121
+
122
+ return df_slots
@@ -0,0 +1,118 @@
1
+ import ast
2
+ import json
3
+ from urllib.parse import parse_qs
4
+
5
+ from bs4 import BeautifulSoup
6
+
7
+ from .endpoints import Endpoints
8
+ from .session import Session
9
+ from .utils.errors import ErrorMessages
10
+ from .utils.logger import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class Authenticator:
16
+ """Handles user authentication and Nubapp login functionality."""
17
+
18
+ def __init__(self, session: Session, centre: str) -> None:
19
+ """
20
+ Initialize the Authenticator.
21
+
22
+ Args:
23
+ session (Session): An instance of the Session class.
24
+ """
25
+ self.session = session.session
26
+ self.headers = session.headers
27
+ # Has the user successfully authenticated?
28
+ self.authenticated = False
29
+ # User ID for the authenticated user
30
+ self.user_id = None
31
+ # Centre selected by the user
32
+ self.centre = centre
33
+
34
+ def login(self, email: str, password: str) -> None:
35
+ """
36
+ Authenticate the user with email and password and log in to Nubapp.
37
+
38
+ Args:
39
+ email (str): The user's email address.
40
+ password (str): The user's password.
41
+
42
+ Raises:
43
+ RuntimeError: If the login process fails at any stage.
44
+ """
45
+ logger.info("Starting login process...")
46
+
47
+ # Step 1: Fetch CSRF token
48
+ logger.debug(f"GET {Endpoints.USER_LOGIN} | Headers: {json.dumps(self.headers, indent=2)}")
49
+ response = self.session.get(Endpoints.USER_LOGIN, headers=self.headers)
50
+ if response.status_code != 200:
51
+ logger.error(f"Failed to fetch login popup: {response.status_code}")
52
+ raise RuntimeError(ErrorMessages.failed_fetch("login popup"))
53
+ logger.debug("Login popup fetched successfully.")
54
+ csrf_token = BeautifulSoup(response.text, "html.parser").find("input", {"name": "_csrf_token"})["value"]
55
+
56
+ # Step 2: Perform login
57
+ payload = {
58
+ "_username": email,
59
+ "_password": password,
60
+ "_csrf_token": csrf_token,
61
+ "_submit": "",
62
+ "_force": "true",
63
+ }
64
+ self.headers.update({"Content-Type": "application/x-www-form-urlencoded"})
65
+ logger.debug(
66
+ f"POST {Endpoints.LOGIN_CHECK} | Headers: {json.dumps(self.headers, indent=2)} | "
67
+ f"Payload: {json.dumps(payload, indent=2)}"
68
+ )
69
+ response = self.session.post(Endpoints.LOGIN_CHECK, data=payload, headers=self.headers)
70
+ if response.status_code != 200:
71
+ logger.error(f"Login failed: {response.status_code}, {response.text}")
72
+ raise ValueError(ErrorMessages.failed_login())
73
+ logger.info("Login successful!")
74
+
75
+ # Step 3: Retrieve credentials for Nubapp
76
+ cred_endpoint = Endpoints.get_cred_endpoint(self.centre)
77
+ logger.debug(f"GET {cred_endpoint} | Headers: {json.dumps(self.headers, indent=2)}")
78
+ response = self.session.get(cred_endpoint, headers=self.headers)
79
+ if response.status_code != 200:
80
+ logger.error(f"Failed to retrieve Nubapp credentials: {response.status_code}")
81
+ raise RuntimeError(ErrorMessages.failed_fetch("credentials"))
82
+ nubapp_creds = ast.literal_eval(response.content.decode("utf-8"))["payload"]
83
+ nubapp_creds = {k: v[0] for k, v in parse_qs(nubapp_creds).items()}
84
+ nubapp_creds["platform"] = "resasocial"
85
+ nubapp_creds["network"] = "resasports"
86
+ logger.debug(f"Nubapp credentials retrieved: {json.dumps(nubapp_creds, indent=2)}")
87
+
88
+ # Step 4: Log in to Nubapp
89
+ logger.debug(
90
+ f"GET {Endpoints.NUBAP_LOGIN} | Headers: {json.dumps(self.headers, indent=2)} | "
91
+ f"Params: {json.dumps(nubapp_creds, indent=2)}"
92
+ )
93
+ response = self.session.get(Endpoints.NUBAP_LOGIN, headers=self.headers, params=nubapp_creds)
94
+ if response.status_code != 200:
95
+ logger.error(f"Login to Nubapp failed: {response.status_code}, {response.text}")
96
+ raise ValueError(ErrorMessages.failed_login_nubapp())
97
+ logger.info("Login to Nubapp successful!")
98
+
99
+ # Step 5: Get user information
100
+ response = self.session.post(Endpoints.USER, headers=self.headers, allow_redirects=True)
101
+
102
+ if response.status_code == 200:
103
+ response_dict = json.loads(response.content.decode("utf-8"))
104
+
105
+ if response_dict["user"]:
106
+ self.user_id = response_dict.get("user", {}).get("id_user")
107
+ if self.user_id:
108
+ self.authenticated = True
109
+ logger.info(f"Authentication successful. User ID: {self.user_id}")
110
+ else:
111
+ self.authenticated = False
112
+ raise ValueError()
113
+ else:
114
+ self.authenticated = False
115
+ raise ValueError(ErrorMessages.failed_login())
116
+ else:
117
+ logger.error(f"Failed to retrieve user information: {response.status_code}, {response.text}")
118
+ raise ValueError(ErrorMessages.failed_login())
pysportbot/bookings.py ADDED
@@ -0,0 +1,87 @@
1
+ import json
2
+
3
+ from .endpoints import Endpoints
4
+ from .session import Session
5
+ from .utils.errors import ErrorMessages
6
+ from .utils.logger import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class Bookings:
12
+ """Handles booking and cancellation of activity slots."""
13
+
14
+ def __init__(self, session: Session) -> None:
15
+ """Initialize the Bookings class."""
16
+ self.session = session.session
17
+ self.headers = session.headers
18
+
19
+ def book(self, slot_id: str) -> None:
20
+ """
21
+ Book a specific slot by its ID.
22
+
23
+ Args:
24
+ slot_id (str): The unique ID of the activity slot.
25
+
26
+ Raises:
27
+ ValueError: If the slot is already booked or unavailable.
28
+ RuntimeError: If an unknown error occurs during booking.
29
+ """
30
+ logger.info(f"Attempting to book slot {slot_id}...")
31
+
32
+ # Payload for booking
33
+ payload = {
34
+ "items[activities][0][id_activity_calendar]": slot_id,
35
+ "items[activities][0][unit_price]": "0",
36
+ "items[activities][0][n_guests]": "0",
37
+ "items[activities][0][id_resource]": "false",
38
+ "discount_code": "false",
39
+ "form": "",
40
+ "formIntoNotes": "",
41
+ }
42
+
43
+ # Send booking request
44
+ response = self.session.post(Endpoints.BOOKING, data=payload, headers=self.headers)
45
+ response_json = json.loads(response.content.decode("utf-8"))
46
+
47
+ # Handle response
48
+ if response_json["error"] == 0:
49
+ logger.info(f"Successfully booked slot {slot_id}.")
50
+ elif response_json["error"] == 5:
51
+ logger.warning(f"Slot {slot_id} is already booked.")
52
+ raise ValueError(ErrorMessages.slot_already_booked())
53
+ elif response_json["error"] == 6:
54
+ logger.warning(f"Slot {slot_id} is not available.")
55
+ raise ValueError(ErrorMessages.slot_unavailable())
56
+ elif response_json["error"] == 28:
57
+ logger.warning(f"Slot {slot_id} is not bookable yet.")
58
+ raise ValueError(ErrorMessages.slot_not_bookable_yet())
59
+ else:
60
+ logger.error(f"Booking failed with error code: {response_json['error']}")
61
+ raise RuntimeError(ErrorMessages.unknown_error("booking"))
62
+
63
+ def cancel(self, slot_id: str) -> None:
64
+ """
65
+ Cancel a specific slot by its ID.
66
+
67
+ Args:
68
+ slot_id (str): The unique ID of the activity slot.
69
+
70
+ Raises:
71
+ ValueError: If the cancellation fails.
72
+ """
73
+ logger.info(f"Attempting to cancel slot {slot_id}...")
74
+
75
+ # Payload for cancellation
76
+ payload = {"id_activity_calendar": slot_id}
77
+
78
+ # Send cancellation request
79
+ response = self.session.post(Endpoints.CANCELLATION, data=payload, headers=self.headers)
80
+ response_json = json.loads(response.content.decode("utf-8"))
81
+
82
+ # Handle response
83
+ if response_json["success"]:
84
+ logger.info(f"Successfully cancelled slot {slot_id}.")
85
+ else:
86
+ logger.warning(f"Slot {slot_id} was not booked.")
87
+ raise ValueError(ErrorMessages.cancellation_failed())
pysportbot/centres.py ADDED
@@ -0,0 +1,108 @@
1
+ # centres.py
2
+
3
+ import json
4
+ import logging
5
+
6
+ import pandas as pd
7
+ import requests
8
+ from pandas import DataFrame
9
+
10
+ from pysportbot.utils.logger import get_logger
11
+
12
+ from .endpoints import Endpoints
13
+ from .utils.errors import ErrorMessages
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class Centres:
19
+ """
20
+ Manages fetching and storing the list of available centres
21
+ from the Resasports service.
22
+ """
23
+
24
+ def __init__(self, print_centres: bool = False) -> None:
25
+ # Coordinates for the bounding box of the world
26
+ # Set to the entire world by default
27
+ self.bounds: dict = {
28
+ "bounds": {
29
+ "south": -90,
30
+ "west": -180,
31
+ "north": 90,
32
+ "east": 180,
33
+ }
34
+ }
35
+ self._df_centres = self.fetch_centres()
36
+ if print_centres:
37
+ self.print_centres()
38
+
39
+ # list of centre (slugs)
40
+ self.centre_list = self._df_centres["slug"].tolist()
41
+
42
+ def check_centre(self, centre: str) -> None:
43
+ """
44
+ Set the user selected centre.
45
+ """
46
+ if centre not in self.centre_list:
47
+ logger.error(ErrorMessages.centre_not_found(centre))
48
+ self.print_centres()
49
+ raise ValueError(ErrorMessages.centre_not_found(centre))
50
+
51
+ def fetch_centres(self) -> DataFrame:
52
+ """
53
+ Fetches the info of available centres from Resasports and returns a DataFrame.
54
+ """
55
+ try:
56
+ response = requests.post(
57
+ Endpoints.CENTRE,
58
+ json=self.bounds,
59
+ timeout=10,
60
+ )
61
+ response.raise_for_status()
62
+
63
+ # Parse the JSON content
64
+ response_json = json.loads(response.content.decode("utf-8"))
65
+ # Flatten and extract the desired columns
66
+ df = pd.json_normalize(response_json["applications"])
67
+ df = df[["slug", "name", "address.town", "address.country", "address.street_line"]]
68
+ df.columns = ["slug", "name", "town", "country", "address"]
69
+
70
+ if df is None or df.empty:
71
+ logging.error("Failed to fetch centres.")
72
+ self._raise_fetch_error() # Fix for TRY301: abstract raise
73
+ else:
74
+ return df
75
+
76
+ except Exception:
77
+ logging.exception("Failed to fetch centres")
78
+ return pd.DataFrame()
79
+
80
+ def _raise_fetch_error(self) -> None:
81
+ """
82
+ Helper function to raise a ValueError for failed centre fetches.
83
+ """
84
+ raise ValueError("Failed to fetch centres.")
85
+
86
+ def print_centres(self, cols: int = 4, col_width: int = 40) -> None:
87
+ """
88
+ Prints the stored list of centres to the console.
89
+ """
90
+ lines = []
91
+ for i, name in enumerate(self.centre_list):
92
+ # Use ljust or rjust to format columns
93
+ name_column = name[:col_width].ljust(col_width)
94
+ # Insert a newline after every 'cols' items
95
+ if (i + 1) % cols == 0:
96
+ lines.append(name_column + "\n")
97
+ else:
98
+ lines.append(name_column)
99
+
100
+ final_str = "".join(lines)
101
+ logger.info(f"Available centres:\n{final_str}")
102
+
103
+ @property
104
+ def df_centres(self) -> DataFrame | None:
105
+ """
106
+ Returns the stored DataFrame of centres (or None if fetch_centres hasn't been called).
107
+ """
108
+ return self._df_centres
@@ -0,0 +1,40 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Endpoints(str, Enum):
5
+ """
6
+ Enum class for API endpoints used in the application.
7
+ Each member is a string, so you can use them directly.
8
+ """
9
+
10
+ # Base URLs
11
+ BASE_SOCIAL = "https://social.resasports.com"
12
+ BASE_NUBAPP = "https://sport.nubapp.com/web"
13
+
14
+ # Centre list
15
+ CENTRE = f"{BASE_SOCIAL}/ajax/applications/bounds/"
16
+
17
+ # Authentication Endpoints
18
+ USER_LOGIN = f"{BASE_SOCIAL}/popup/login"
19
+ LOGIN_CHECK = f"{BASE_SOCIAL}/popup/login_check"
20
+ NUBAP_LOGIN = f"{BASE_NUBAPP}/resources/login_from_social.php"
21
+
22
+ # User, activities, and slots
23
+ USER = f"{BASE_NUBAPP}/ajax/users/getUser.php"
24
+ ACTIVITIES = f"{BASE_NUBAPP}/ajax/application/getActivities.php"
25
+ SLOTS = f"{BASE_NUBAPP}/ajax/activities/getActivitiesCalendar.php"
26
+
27
+ # Booking and Cancellation
28
+ BOOKING = f"{BASE_NUBAPP}/ajax/bookings/bookBookings.php"
29
+ CANCELLATION = f"{BASE_NUBAPP}/ajax/activities/leaveActivityCalendar.php"
30
+
31
+ @classmethod
32
+ def get_cred_endpoint(cls, centre_slug: str) -> str:
33
+ """
34
+ Build the booking request URL for a given centre slug.
35
+ """
36
+ return f"{cls.BASE_SOCIAL}/ajax/application/{centre_slug}/book/request"
37
+
38
+ def __str__(self) -> str:
39
+ """Return the underlying string value instead of the member name."""
40
+ return str(self.value)
File without changes
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ from typing import Any, Dict
5
+
6
+ from .config_loader import load_config
7
+ from .service import run_service
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(description="Run the pysportbot as a service.")
12
+ parser.add_argument("--config", type=str, required=True, help="Path to the JSON configuration file.")
13
+ parser.add_argument("--offset-seconds", type=int, default=10, help="Time offset in seconds before booking.")
14
+ parser.add_argument("--retry-attempts", type=int, default=3, help="Number of retry attempts for weekly bookings.")
15
+ parser.add_argument(
16
+ "--retry-delay-minutes", type=int, default=2, help="Delay in minutes between retries for weekly bookings."
17
+ )
18
+ parser.add_argument("--time_zone", type=str, default="Europe/Madrid", help="Timezone for the service.")
19
+ args = parser.parse_args()
20
+
21
+ config: Dict[str, Any] = load_config(args.config)
22
+ run_service(
23
+ config,
24
+ offset_seconds=args.offset_seconds,
25
+ retry_attempts=args.retry_attempts,
26
+ retry_delay_minutes=args.retry_delay_minutes,
27
+ time_zone=args.time_zone,
28
+ )
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()