pysportbot 0.0.6__py3-none-any.whl → 0.0.8__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 CHANGED
@@ -18,7 +18,7 @@ class SportBot:
18
18
  """Unified interface for interacting with the booking system."""
19
19
 
20
20
  def __init__(self, log_level: str = "INFO", print_centres: bool = False, time_zone: str = "Europe/Madrid") -> None:
21
- setup_logger(log_level)
21
+ setup_logger(log_level, timezone=time_zone)
22
22
  self._logger = logging.getLogger("SportBot")
23
23
  self._logger.info("Initializing SportBot...")
24
24
  self._logger.info(f"Log level: {log_level}")
@@ -83,6 +83,8 @@ class SportBot:
83
83
  return df.head(limit) if limit else df
84
84
 
85
85
  def book(self, activity: str, start_time: str) -> None:
86
+
87
+ self._logger.debug(f"Attempting to book class '{activity}' on {start_time}")
86
88
  if not self._is_logged_in:
87
89
  self._logger.error(ErrorMessages.not_logged_in())
88
90
  raise PermissionError(ErrorMessages.not_logged_in())
@@ -98,9 +100,18 @@ class SportBot:
98
100
  self._logger.error(error_msg)
99
101
  raise IndexError(error_msg)
100
102
 
101
- self._bookings.book(matching_slot.iloc[0]["id_activity_calendar"])
103
+ slot_id = matching_slot.iloc[0]["id_activity_calendar"]
104
+
105
+ try:
106
+ self._bookings.book(slot_id)
107
+ self._logger.info(f"Successfully booked class '{activity}' on {start_time}")
108
+ except ValueError:
109
+ self._logger.exception(f"Failed to book class '{activity}' on {start_time}")
110
+ raise
102
111
 
103
112
  def cancel(self, activity: str, start_time: str) -> None:
113
+
114
+ self._logger.debug(f"Attempting to cancel class '{activity}' on {start_time}")
104
115
  if not self._is_logged_in:
105
116
  self._logger.error(ErrorMessages.not_logged_in())
106
117
  raise PermissionError(ErrorMessages.not_logged_in())
@@ -116,4 +127,10 @@ class SportBot:
116
127
  self._logger.error(error_msg)
117
128
  raise IndexError(error_msg)
118
129
 
119
- self._bookings.cancel(matching_slot.iloc[0]["id_activity_calendar"])
130
+ slot_id = matching_slot.iloc[0]["id_activity_calendar"]
131
+ try:
132
+ self._bookings.cancel(slot_id)
133
+ self._logger.info(f"Successfully cancelled class '{activity}' on {start_time}")
134
+ except ValueError:
135
+ self._logger.exception(f"Failed to cancel class '{activity}' on {start_time}")
136
+ raise
pysportbot/activities.py CHANGED
@@ -56,7 +56,7 @@ class Activities:
56
56
  ValueError: If the specified activity is not found.
57
57
  RuntimeError: If slots cannot be fetched.
58
58
  """
59
- logger.info(f"Fetching daily slots for '{activity_name}' on {day}...")
59
+ logger.info(f"Fetching available slots for '{activity_name}' on {day}...")
60
60
 
61
61
  # Check if the activity exists
62
62
  activity_match = df_activities[df_activities["name_activity"] == activity_name]
pysportbot/bookings.py CHANGED
@@ -27,7 +27,7 @@ class Bookings:
27
27
  ValueError: If the slot is already booked or unavailable.
28
28
  RuntimeError: If an unknown error occurs during booking.
29
29
  """
30
- logger.info(f"Attempting to book slot {slot_id}...")
30
+ logger.debug(f"Attempting to book slot {slot_id}...")
31
31
 
32
32
  # Payload for booking
33
33
  payload = {
@@ -46,7 +46,7 @@ class Bookings:
46
46
 
47
47
  # Handle response
48
48
  if response_json["error"] == 0:
49
- logger.info(f"Successfully booked slot {slot_id}.")
49
+ logger.debug(f"Successfully booked slot {slot_id}.")
50
50
  elif response_json["error"] == 5:
51
51
  logger.warning(f"Slot {slot_id} is already booked.")
52
52
  raise ValueError(ErrorMessages.slot_already_booked())
@@ -70,7 +70,7 @@ class Bookings:
70
70
  Raises:
71
71
  ValueError: If the cancellation fails.
72
72
  """
73
- logger.info(f"Attempting to cancel slot {slot_id}...")
73
+ logger.debug(f"Attempting to cancel slot {slot_id}...")
74
74
 
75
75
  # Payload for cancellation
76
76
  payload = {"id_activity_calendar": slot_id}
@@ -81,7 +81,7 @@ class Bookings:
81
81
 
82
82
  # Handle response
83
83
  if response_json["success"]:
84
- logger.info(f"Successfully cancelled slot {slot_id}.")
84
+ logger.debug(f"Successfully cancelled slot {slot_id}.")
85
85
  else:
86
86
  logger.warning(f"Slot {slot_id} was not booked.")
87
87
  raise ValueError(ErrorMessages.cancellation_failed())
@@ -10,23 +10,28 @@ from .service import run_service
10
10
  def main() -> None:
11
11
  parser = argparse.ArgumentParser(description="Run the pysportbot as a service.")
12
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=5, 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=1, help="Delay in minutes between retries for weekly bookings."
17
- )
13
+ parser.add_argument("--booking-delay", type=int, default=0, help="Global booking delay in seconds before booking.")
14
+ parser.add_argument("--retry-attempts", type=int, default=3, help="Number of retry attempts for bookings.")
15
+ parser.add_argument("--retry-delay", type=int, default=5, help="Delay in seconds between retries for bookings.")
18
16
  parser.add_argument("--time-zone", type=str, default="Europe/Madrid", help="Timezone for the service.")
19
17
  parser.add_argument("--log-level", type=str, default="INFO", help="Logging level for the service.")
18
+ parser.add_argument(
19
+ "--max-threads",
20
+ type=int,
21
+ default=-1,
22
+ help="Maxium number of threads to use for booking. -1 defaults to all available cores.",
23
+ )
20
24
  args = parser.parse_args()
21
25
 
22
26
  config: Dict[str, Any] = load_config(args.config)
23
27
  run_service(
24
28
  config,
25
- offset_seconds=args.offset_seconds,
29
+ booking_delay=args.booking_delay,
26
30
  retry_attempts=args.retry_attempts,
27
- retry_delay_minutes=args.retry_delay_minutes,
31
+ retry_delay=args.retry_delay,
28
32
  time_zone=args.time_zone,
29
33
  log_level=args.log_level,
34
+ max_threads=args.max_threads,
30
35
  )
31
36
 
32
37
 
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  from concurrent.futures import ThreadPoolExecutor, as_completed
3
3
  from datetime import datetime
4
- from typing import Any, Dict, List
4
+ from typing import Any, Dict
5
5
 
6
6
  import pytz
7
7
 
@@ -44,9 +44,8 @@ def attempt_booking(
44
44
  activity: str,
45
45
  class_day: str,
46
46
  class_time: str,
47
- offset_seconds: int,
48
47
  retry_attempts: int = 1,
49
- retry_delay_minutes: int = 0,
48
+ retry_delay: int = 0,
50
49
  time_zone: str = "Europe/Madrid",
51
50
  ) -> None:
52
51
  """
@@ -57,25 +56,20 @@ def attempt_booking(
57
56
  activity (str): Activity name.
58
57
  class_day (str): Day of the class.
59
58
  class_time (str): Time of the class.
60
- offset_seconds (int): Delay before attempting booking.
61
59
  retry_attempts (int): Number of retry attempts.
62
- retry_delay_minutes (int): Delay between retries.
60
+ retry_delay (int): Delay between retries.
63
61
  time_zone (str): Time zone for execution.
64
62
  """
65
63
  for attempt_num in range(1, retry_attempts + 1):
66
64
  booking_date = calculate_class_day(class_day, time_zone).strftime("%Y-%m-%d")
67
65
 
68
66
  try:
69
- logger.info(f"Fetching available slots for '{activity}' on {booking_date}.")
70
67
  available_slots = bot.daily_slots(activity=activity, day=booking_date)
71
68
 
72
69
  matching_slots = available_slots[available_slots["start_timestamp"] == f"{booking_date} {class_time}"]
73
70
  if matching_slots.empty:
74
71
  _raise_no_matching_slots_error(activity, class_time, booking_date)
75
72
 
76
- logger.info(f"Waiting {offset_seconds} seconds before attempting booking.")
77
- time.sleep(offset_seconds)
78
-
79
73
  slot_id = matching_slots.iloc[0]["start_timestamp"]
80
74
  logger.info(f"Attempting to book '{activity}' at {slot_id} (Attempt {attempt_num}/{retry_attempts}).")
81
75
  bot.book(activity=activity, start_time=slot_id)
@@ -89,24 +83,22 @@ def attempt_booking(
89
83
  return
90
84
 
91
85
  if attempt_num < retry_attempts:
92
- logger.info(f"Retrying in {retry_delay_minutes} minutes...")
93
- time.sleep(retry_delay_minutes * 60)
86
+ logger.info(f"Retrying in {retry_delay} seconds...")
87
+ time.sleep(retry_delay)
94
88
  else:
95
- # If the booking attempt succeeds, log and exit
96
- logger.info(f"Successfully booked '{activity}' at {slot_id}.")
97
89
  return
98
90
 
99
91
  # If all attempts fail, log an error
100
- logger.error(f"Failed to book '{activity}' after {retry_attempts} attempts.")
92
+ # Do not raise an exception to allow other bookings to proceed
93
+ logger.error(f"Failed to book '{activity}' at {class_time} on {booking_date} after {retry_attempts} attempts.")
101
94
 
102
95
 
103
- def schedule_bookings_parallel(
96
+ def schedule_bookings(
104
97
  bot: SportBot,
105
- classes: List[Dict[str, Any]],
106
- booking_execution: str,
107
- offset_seconds: int,
98
+ config: Dict[str, Any],
99
+ booking_delay: int,
108
100
  retry_attempts: int,
109
- retry_delay_minutes: int,
101
+ retry_delay: int,
110
102
  time_zone: str,
111
103
  max_threads: int,
112
104
  ) -> None:
@@ -117,19 +109,32 @@ def schedule_bookings_parallel(
117
109
  bot (SportBot): The SportBot instance.
118
110
  classes (list): List of class configurations.
119
111
  booking_execution (str): Global execution time for all bookings.
120
- offset_seconds (int): Delay before each booking attempt.
112
+ booking_delay (int): Delay before each booking attempt.
121
113
  retry_attempts (int): Number of retry attempts.
122
- retry_delay_minutes (int): Delay between retries.
114
+ retry_delay (int): Delay between retries.
123
115
  time_zone (str): Timezone for booking.
124
116
  max_threads (int): Maximum number of threads to use.
125
117
  """
126
118
  # Log planned bookings
127
- for cls in classes:
119
+ for cls in config["classes"]:
128
120
  logger.info(f"Scheduled to book '{cls['activity']}' next {cls['class_day']} at {cls['class_time']}.")
129
121
 
130
122
  # Wait globally before starting bookings
131
- wait_for_execution(booking_execution, time_zone)
123
+ wait_for_execution(config["booking_execution"], time_zone)
124
+
125
+ # Global booking delay
126
+ logger.info(f"Waiting {booking_delay} seconds before attempting booking.")
127
+ time.sleep(booking_delay)
128
+
129
+ # Re-authenticate before booking
130
+ logger.debug("Re-authenticating before booking.")
131
+ try:
132
+ bot.login(config["email"], config["password"], config["centre"])
133
+ except Exception:
134
+ logger.exception("Re-authentication failed before booking execution.")
135
+ raise
132
136
 
137
+ # Submit bookings in parallel
133
138
  with ThreadPoolExecutor(max_workers=max_threads) as executor:
134
139
  future_to_class = {
135
140
  executor.submit(
@@ -138,12 +143,11 @@ def schedule_bookings_parallel(
138
143
  cls["activity"],
139
144
  cls["class_day"],
140
145
  cls["class_time"],
141
- offset_seconds,
142
146
  retry_attempts,
143
- retry_delay_minutes,
147
+ retry_delay,
144
148
  time_zone,
145
149
  ): cls
146
- for cls in classes
150
+ for cls in config["classes"]
147
151
  }
148
152
 
149
153
  for future in as_completed(future_to_class):
@@ -151,6 +155,5 @@ def schedule_bookings_parallel(
151
155
  activity, class_time = cls["activity"], cls["class_time"]
152
156
  try:
153
157
  future.result()
154
- logger.info(f"Booking for '{activity}' at {class_time} completed successfully.")
155
158
  except Exception:
156
159
  logger.exception(f"Booking for '{activity}' at {class_time} failed.")
@@ -1,28 +1,29 @@
1
- import os
2
1
  from typing import Any, Dict
3
2
 
4
3
  from pysportbot import SportBot
5
- from pysportbot.service.booking import schedule_bookings_parallel
4
+ from pysportbot.service.booking import schedule_bookings
6
5
  from pysportbot.service.config_validator import validate_activities, validate_config
6
+ from pysportbot.service.threading import get_n_threads
7
7
  from pysportbot.utils.logger import get_logger
8
8
 
9
9
 
10
10
  def run_service(
11
11
  config: Dict[str, Any],
12
- offset_seconds: int,
12
+ booking_delay: int,
13
13
  retry_attempts: int,
14
- retry_delay_minutes: int,
14
+ retry_delay: int,
15
15
  time_zone: str = "Europe/Madrid",
16
16
  log_level: str = "INFO",
17
+ max_threads: int = -1,
17
18
  ) -> None:
18
19
  """
19
20
  Run the booking service with the given configuration.
20
21
 
21
22
  Args:
22
23
  config (dict): Configuration dictionary for booking service.
23
- offset_seconds (int): Delay before each booking attempt.
24
+ booking_delay (int): Delay before each booking attempt.
24
25
  retry_attempts (int): Number of retry attempts.
25
- retry_delay_minutes (int): Delay between retry attempts in minutes.
26
+ retry_delay (int): Delay between retry attempts in minutes.
26
27
  time_zone (str): Time zone for the booking.
27
28
  log_level (str): Logging level for the service.
28
29
  """
@@ -34,24 +35,25 @@ def run_service(
34
35
  validate_config(config)
35
36
 
36
37
  # Initialize the SportBot and authenticate
38
+ # Note: will re-authenticate before booking execution
39
+ # to ensure the session is still valid
37
40
  bot = SportBot(log_level=log_level, time_zone=time_zone)
38
41
  bot.login(config["email"], config["password"], config["centre"])
39
42
 
40
43
  # Validate activities in the configuration
41
44
  validate_activities(bot, config)
42
45
 
43
- # Determine the number of threads
44
- max_threads = min(len(config["classes"]), os.cpu_count() or 1)
45
- logger.info(f"Using up to {max_threads} threads for booking {len(config['classes'])} activities.")
46
+ # Determine the number of threads, where threads -1 defaults to all available cores
47
+ requested_bookings = len(config["classes"])
48
+ max_threads = get_n_threads(max_threads, requested_bookings)
46
49
 
47
50
  # Schedule bookings in parallel
48
- schedule_bookings_parallel(
51
+ schedule_bookings(
49
52
  bot,
50
- config["classes"],
51
- config["booking_execution"],
52
- offset_seconds,
53
+ config,
54
+ booking_delay,
53
55
  retry_attempts,
54
- retry_delay_minutes,
56
+ retry_delay,
55
57
  time_zone,
56
58
  max_threads,
57
59
  )
@@ -0,0 +1,51 @@
1
+ import os
2
+
3
+ from pysportbot.utils.logger import get_logger
4
+
5
+ logger = get_logger(__name__)
6
+
7
+
8
+ def get_n_threads(max_user_threads: int, requested_bookings: int) -> int:
9
+ """
10
+ Determine the number of threads to use based on user input and system resources.
11
+
12
+ Args:
13
+ max_user_threads (int): Maximum number of threads requested by the user (-1 for auto-detect).
14
+ requested_bookings (int): Number of bookings to process.
15
+
16
+ Returns:
17
+ int: The maximum number of threads to use.
18
+
19
+ Raises:
20
+ ValueError: If max_user_threads is 0.
21
+ """
22
+ logger.debug(f"Maximum number of user-requested threads: {max_user_threads}")
23
+ logger.debug(f"Requested bookings: {requested_bookings}")
24
+
25
+ available_threads: int = os.cpu_count() or 1 # Fallback to 1 if os.cpu_count() is None
26
+ logger.debug(f"Available threads: {available_threads}")
27
+
28
+ if max_user_threads == 0:
29
+ logger.error("The 'max_user_threads' argument cannot be 0.")
30
+ raise ValueError("The 'max_user_threads' argument cannot be 0.")
31
+
32
+ if max_user_threads > available_threads:
33
+ logger.warning(
34
+ f"User-requested threads ({max_user_threads}) exceed available threads ({available_threads}). "
35
+ f"Limiting to {available_threads} threads."
36
+ )
37
+
38
+ if requested_bookings <= 0:
39
+ logger.warning("No bookings requested. Returning 0 threads.")
40
+ return 0 # No threads needed if there are no bookings
41
+
42
+ # If max_user_threads is -1, use the lesser of available threads and requested bookings
43
+ if max_user_threads == -1:
44
+ max_threads: int = min(available_threads, requested_bookings)
45
+ else:
46
+ # Use the lesser of max_user_threads, available threads, and requested bookings
47
+ max_threads = min(max_user_threads, available_threads, requested_bookings)
48
+
49
+ logger.info(f"Using up to {max_threads} threads for booking {requested_bookings} activities.")
50
+
51
+ return max_threads
@@ -20,11 +20,7 @@ class ErrorMessages:
20
20
 
21
21
  @staticmethod
22
22
  def invalid_class_definition() -> str:
23
- return "Each class must include 'activity', 'class_day', " "'class_time', 'booking_execution', and 'weekly'."
24
-
25
- @staticmethod
26
- def invalid_weekly_now() -> str:
27
- return "Invalid combination: cannot use weekly=True with booking_execution='now'."
23
+ return "Each class must include 'activity', 'class_day', " "'class_time'"
28
24
 
29
25
  @staticmethod
30
26
  def invalid_booking_execution_format() -> str:
@@ -9,7 +9,9 @@ from .errors import ErrorMessages
9
9
 
10
10
 
11
11
  class ColorFormatter(logging.Formatter):
12
- """Custom formatter to add color-coded log levels and thread information."""
12
+ """Custom formatter to add color-coded log levels and thread information,
13
+ while aligning log levels in the output as `[INFO] message`.
14
+ """
13
15
 
14
16
  COLORS: ClassVar[dict[str, str]] = {
15
17
  "DEBUG": "\033[94m", # Blue
@@ -31,7 +33,9 @@ class ColorFormatter(logging.Formatter):
31
33
  "\033[35m", # Purple
32
34
  ]
33
35
 
34
- thread_colors: dict[str, str]
36
+ # The longest built-in level is WARNING = 7 letters => "[WARNING]" is 0 characters
37
+ # so let's set this to 9 to align them nicely.
38
+ _MAX_BRACKET_LEN = 9
35
39
 
36
40
  def __init__(self, fmt: str, datefmt: str, tz: pytz.BaseTzInfo, include_threads: bool = False) -> None:
37
41
  """
@@ -41,47 +45,46 @@ class ColorFormatter(logging.Formatter):
41
45
  fmt (str): The log message format.
42
46
  datefmt (str): The date format.
43
47
  tz (pytz.BaseTzInfo): The timezone for log timestamps.
44
- include_threads (bool): Whether to include thread information in logs.
48
+ include_threads (bool): Whether to include thread info in logs.
45
49
  """
46
50
  super().__init__(fmt, datefmt)
47
51
  self.timezone = tz
48
52
  self.include_threads = include_threads
49
- self.thread_colors = {} # Initialize as an empty dictionary
53
+ self.thread_colors: dict[str, str] = {} # Initialize empty dictionary
50
54
 
51
55
  def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str:
52
56
  """
53
57
  Override to format the time in the desired timezone.
54
-
55
- Args:
56
- record (logging.LogRecord): The log record.
57
- datefmt (Optional[str]): The date format.
58
-
59
- Returns:
60
- str: The formatted timestamp.
61
58
  """
62
59
  record_time = datetime.fromtimestamp(record.created, self.timezone)
63
60
  return record_time.strftime(datefmt or self.default_time_format)
64
61
 
65
62
  def format(self, record: logging.LogRecord) -> str:
66
63
  """
67
- Format the log record with color-coded log levels and optional thread information.
64
+ Format the log record with color-coded (and bracketed) log levels, plus optional thread info.
65
+ Example final output: "[INFO] message".
66
+ """
67
+ raw_level_name = record.levelname
68
68
 
69
- Args:
70
- record (logging.LogRecord): The log record to format.
69
+ # Build the bracketed level, e.g. "[INFO]"
70
+ bracketed_level = f"[{raw_level_name}]"
71
71
 
72
- Returns:
73
- str: The formatted log record as a string.
74
- """
75
- color = self.COLORS.get(record.levelname, self.COLORS["RESET"])
76
- record.levelname = f"{color}{record.levelname}{self.COLORS['RESET']}"
72
+ # Pad it to a fixed width (e.g. 10) so shorter levels also line up
73
+ padded_bracketed_level = f"{bracketed_level:<{self._MAX_BRACKET_LEN}}"
77
74
 
75
+ # Colorize the padded bracketed string
76
+ color = self.COLORS.get(raw_level_name, self.COLORS["RESET"])
77
+ colored_bracketed = f"{color}{padded_bracketed_level}{self.COLORS['RESET']}"
78
+
79
+ # We'll store this in a custom attribute
80
+ record.colored_bracketed_level = colored_bracketed
81
+
82
+ # Handle the thread info if requested
78
83
  if self.include_threads:
79
84
  thread_name = threading.current_thread().name
80
85
  if thread_name == "MainThread":
81
- # Skip adding thread info for the main thread
82
86
  record.thread_info = ""
83
87
  else:
84
- # Map thread names to simplified format (Thread 0, Thread 1, etc.)
85
88
  if thread_name not in self.thread_colors:
86
89
  color_index = len(self.thread_colors) % len(self.THREAD_COLORS)
87
90
  self.thread_colors[thread_name] = self.THREAD_COLORS[color_index]
@@ -101,25 +104,24 @@ def setup_logger(level: str = "INFO", timezone: str = "Europe/Madrid") -> None:
101
104
 
102
105
  Args:
103
106
  level (str): The desired logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
104
- timezone (str): The desired timezone for log timestamps (e.g., Europe/Madrid).
107
+ timezone (str): The desired timezone for log timestamps (e.g., 'Europe/Madrid').
105
108
  """
106
109
  root_logger = logging.getLogger()
107
110
  root_logger.setLevel(logging._nameToLevel[level.upper()])
108
111
 
109
- if not root_logger.hasHandlers(): # Avoid duplicate handlers
112
+ # Prevent adding multiple handlers if already set up
113
+ if not root_logger.hasHandlers():
110
114
  handler = logging.StreamHandler()
111
115
  handler.setLevel(logging._nameToLevel[level.upper()])
116
+
112
117
  tz = pytz.timezone(timezone)
113
118
 
114
- # Default formatter for the main thread
115
- thread_formatter = ColorFormatter(
116
- "[%(asctime)s] [%(levelname)s] %(thread_info)s%(message)s",
117
- datefmt="%Y-%m-%d %H:%M:%S",
118
- tz=tz,
119
- include_threads=True,
120
- )
119
+ # IMPORTANT: Note the %(colored_bracketed_level)s placeholder
120
+ fmt = "[%(asctime)s] %(colored_bracketed_level)s %(thread_info)s%(message)s"
121
+
122
+ formatter = ColorFormatter(fmt=fmt, datefmt="%Y-%m-%d %H:%M:%S", tz=tz, include_threads=True)
123
+ handler.setFormatter(formatter)
121
124
 
122
- handler.setFormatter(thread_formatter)
123
125
  root_logger.addHandler(handler)
124
126
 
125
127
 
@@ -143,11 +145,5 @@ def set_log_level(level: str) -> None:
143
145
  def get_logger(name: str) -> logging.Logger:
144
146
  """
145
147
  Retrieve a logger by name.
146
-
147
- Args:
148
- name (str): The name of the logger.
149
-
150
- Returns:
151
- logging.Logger: The logger instance.
152
148
  """
153
149
  return logging.getLogger(name)
@@ -1,22 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pysportbot
3
- Version: 0.0.6
4
- Summary: A python-based bot for automatic resasports slot booking
5
- Home-page: https://github.com/jbeirer/resasports-bot
3
+ Version: 0.0.8
4
+ Summary: A python-based bot for automatic resasports slot booking
6
5
  Author: Joshua Falco Beirer
7
6
  Author-email: jbeirer@cern.ch
8
- Requires-Python: >=3.9,<3.13
7
+ Requires-Python: >=3.9,<3.14
9
8
  Classifier: Programming Language :: Python :: 3
10
9
  Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
15
15
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
16
16
  Requires-Dist: pytz (>=2024.2,<2025.0)
17
17
  Requires-Dist: requests (>=2.32.3,<3.0.0)
18
- Project-URL: Documentation, https://jbeirer.github.io/resasports-bot/
19
- Project-URL: Repository, https://github.com/jbeirer/resasports-bot
20
18
  Description-Content-Type: text/markdown
21
19
 
22
20
  # No queues. Just gains.
@@ -32,10 +30,10 @@ Description-Content-Type: text/markdown
32
30
  [![Documentation](https://img.shields.io/badge/api-docs-blue)](https://jbeirer.github.io/resasports-bot/)
33
31
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/jbeirer/resasports-bot/blob/main/CODE_OF_CONDUCT.md)
34
32
 
35
- Welcome to pysportbot!
33
+ PySportBot empowers you to programmatically book fitness classes at any sports center that uses the [Resasports](https://social.resasports.com/en/) booking management software.
36
34
 
37
- ## Download pysportbot
38
- ```python
35
+ ## Install
36
+ ```bash
39
37
  pip install pysportbot
40
38
  ```
41
39
 
@@ -71,7 +69,7 @@ python -m pysportbot.service --config config.json
71
69
  ```
72
70
  The service requires a `json` configuration file that specifies your user data and how you would like to book your classes. Currently, two types of configuration are supported:
73
71
 
74
- ##### 1. Book an upcoming class now
72
+ ### 1. Book an upcoming class now
75
73
 
76
74
  Let's say you would like to book Yoga next Monday at 18:00:00, then your `config.json` would look like:
77
75
 
@@ -91,7 +89,7 @@ Let's say you would like to book Yoga next Monday at 18:00:00, then your `config
91
89
  ]
92
90
  }
93
91
  ```
94
- ##### 2. Book an upcoming class on a specific day and time
92
+ ### 2. Book an upcoming class on a specific day and time
95
93
 
96
94
  Let's say you would like to book Yoga next Monday at 18:00:00, but the execution of the booking should only happen on Friday at 07:30:00 then your `config.json` would look like:
97
95
 
@@ -112,19 +110,23 @@ Let's say you would like to book Yoga next Monday at 18:00:00, but the execution
112
110
  }
113
111
  ```
114
112
 
113
+ **Note:** By default, pysportbot will be attempt to execute *N* bookings in parallel, where *N* is the number of available cores on your machine.
114
+
115
115
  The service also provides various other options that can be inspected with
116
116
 
117
117
  ```bash
118
118
  python -m pysportbot.service --help
119
119
  ```
120
120
  Currently supported options include
121
- 1. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails
122
- 2. ```--retry-delay-minutes``` sets the delay in minutes between retries for weekly bookings
123
- 3. ```--time-zone``` sets the time zone for the service (e.g. Europe/Madrid)
124
- 4. ```--log-level``` sets the log-level of the service (e.g. DEBUG, INFO, WARNING, ERROR)
121
+ 1. ```--booking-delay``` sets a global delay in seconds before booking execution [default: 0]
122
+ 2. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails [default: 3]
123
+ 3. ```--retry-delay``` sets the delay in seconds between booking retries [default: 5]
124
+ 4. ```--time-zone``` sets the time zone for the service [default: Europe/Madrid]
125
+ 5. ```--log-level``` sets the log-level of the service [default: INFO]
126
+ 6. ```--max-threads``` limits the number of used threads for parallel bookings [default: -1]
125
127
 
126
128
  ## LICENSE
127
129
 
128
130
  pysportbot is free of use and open-source. All versions are
129
- published under the [MIT License](https://github.com/jbeirer/pysportbot/blob/main/LICENSE).
131
+ published under the [MIT License](https://github.com/jbeirer/resasports-bot/blob/main/LICENSE).
130
132
 
@@ -0,0 +1,23 @@
1
+ pysportbot/__init__.py,sha256=TomOCaCWzIuJtFsbglygUChn1wdNaARGiX5Ymt1v2aw,5651
2
+ pysportbot/activities.py,sha256=Aq8l570sxqEAKplsEMVkuDoPJJjoaZlIwPh6i64_SJ8,4252
3
+ pysportbot/authenticator.py,sha256=xEU9O6W6Yp9DCueoUD0ASMkm17yvdUZqTvV4h6WlSnI,5012
4
+ pysportbot/bookings.py,sha256=W91AYGPu0sCpGliuiVdlENsoGYzH8P6v-SyKisbSb7g,3127
5
+ pysportbot/centres.py,sha256=FTK-tXUOxiJvLCHP6Bk9XEQKODQZOwwkYLlioSJPBEk,3399
6
+ pysportbot/endpoints.py,sha256=ANh5JAbdzyZQ-i4ODrhYlskPpU1gkBrw9UhMC7kRSvU,1353
7
+ pysportbot/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pysportbot/service/__main__.py,sha256=J7c4O4QqrRyb3HvNEj3RLzCw-M-YEfU1aOrNPMlkTXM,1502
9
+ pysportbot/service/booking.py,sha256=sRV3cseOtgoMPl4Mnnt2SI0TReB4X0-jox5cxGvZBKo,5622
10
+ pysportbot/service/config_loader.py,sha256=elNNwcC7kwc0lmRqj95hAA50qSmLelWv5G2E62tDxP0,185
11
+ pysportbot/service/config_validator.py,sha256=ce7p-LgPtvWnTYvpyUEE60627DHHzx5mFhEI9hDNB-4,2621
12
+ pysportbot/service/scheduling.py,sha256=bwqbiQQ9y4ss4UXZkWuOVCgCa9XlZt1n4TT_8z9bD7M,1973
13
+ pysportbot/service/service.py,sha256=c7hE9xU9osNImprwTobR1wC9tLFdP_DfvIJtlHjqlE8,1939
14
+ pysportbot/service/threading.py,sha256=j0tHgGty8iWDjpZtTzIu-5TUnDb9S6SJErm3QBpG-nY,1947
15
+ pysportbot/session.py,sha256=pTQrz3bGzLYBtzVOgKv04l4UXDSgtA3Infn368bjg5I,1529
16
+ pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ pysportbot/utils/errors.py,sha256=mpTeKjOUIxJbOqL6A6apdMb7Cpto86DkrKHWfROXjMc,4979
18
+ pysportbot/utils/logger.py,sha256=BfR1COAmVr5v0T5JX-6-MsMXOx2XVzfib4Td1sSWYjI,5379
19
+ pysportbot/utils/time.py,sha256=VZSW8AxFIoFD5ZSmLUPcwawp6PmpkcxNjP3Db-Hl_fw,1244
20
+ pysportbot-0.0.8.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
21
+ pysportbot-0.0.8.dist-info/METADATA,sha256=-7JBOQq4lVwvVhwdr21JrdnTr_vG9v_V-wt2Yy5imTM,5228
22
+ pysportbot-0.0.8.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
23
+ pysportbot-0.0.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,22 +0,0 @@
1
- pysportbot/__init__.py,sha256=4z5KKwrnwj_uiZYTUNgZIw_7s8ZT9C-Olz8_hzhPutw,4919
2
- pysportbot/activities.py,sha256=y2-CCdMoPTnKZleF_icYbngbnfpmHL5Cyadv1UuiCpE,4248
3
- pysportbot/authenticator.py,sha256=xEU9O6W6Yp9DCueoUD0ASMkm17yvdUZqTvV4h6WlSnI,5012
4
- pysportbot/bookings.py,sha256=vJ0kw74qyirZlbQ7M9XqlKtRoGzuHR0_t6-zUdgldkI,3123
5
- pysportbot/centres.py,sha256=FTK-tXUOxiJvLCHP6Bk9XEQKODQZOwwkYLlioSJPBEk,3399
6
- pysportbot/endpoints.py,sha256=ANh5JAbdzyZQ-i4ODrhYlskPpU1gkBrw9UhMC7kRSvU,1353
7
- pysportbot/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pysportbot/service/__main__.py,sha256=PSaqYZ8wWlmFT0_taeOA2NLZfm9fIOy6MBhrv_IPSP4,1319
9
- pysportbot/service/booking.py,sha256=bjPb9BLF7XOhFnr1ZArxUHNO6-10yM1z-3S5-ZFpXjQ,5713
10
- pysportbot/service/config_loader.py,sha256=elNNwcC7kwc0lmRqj95hAA50qSmLelWv5G2E62tDxP0,185
11
- pysportbot/service/config_validator.py,sha256=ce7p-LgPtvWnTYvpyUEE60627DHHzx5mFhEI9hDNB-4,2621
12
- pysportbot/service/scheduling.py,sha256=bwqbiQQ9y4ss4UXZkWuOVCgCa9XlZt1n4TT_8z9bD7M,1973
13
- pysportbot/service/service.py,sha256=_LR7rJ4GAgp4saSFREiSJCm4GzzEor6ZYkIkuAq7PLM,1866
14
- pysportbot/session.py,sha256=pTQrz3bGzLYBtzVOgKv04l4UXDSgtA3Infn368bjg5I,1529
15
- pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- pysportbot/utils/errors.py,sha256=sUSs04yr_OmZeTywi1fzPsjVjkLd5iTpXlzlQqNKmgQ,5162
17
- pysportbot/utils/logger.py,sha256=38rB0M6KPlelXPcgXPSEhXB36-_ZTERLJAQ8DRYIOTM,5189
18
- pysportbot/utils/time.py,sha256=VZSW8AxFIoFD5ZSmLUPcwawp6PmpkcxNjP3Db-Hl_fw,1244
19
- pysportbot-0.0.6.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
20
- pysportbot-0.0.6.dist-info/METADATA,sha256=lECBuUu6DaV2CiVXWwHpmz1b--lL7DhxwpOsrXTITKU,4904
21
- pysportbot-0.0.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
22
- pysportbot-0.0.6.dist-info/RECORD,,