pysportbot 0.0.7__py3-none-any.whl → 0.0.9__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,11 +10,17 @@ 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("--booking-delay", type=int, default=5, help="Global booking delay in seconds before booking.")
13
+ parser.add_argument("--booking-delay", type=int, default=0, help="Global booking delay in seconds before booking.")
14
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=30, help="Delay in seconds between retries for bookings.")
15
+ parser.add_argument("--retry-delay", type=int, default=5, help="Delay in seconds between retries for bookings.")
16
16
  parser.add_argument("--time-zone", type=str, default="Europe/Madrid", help="Timezone for the service.")
17
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
+ )
18
24
  args = parser.parse_args()
19
25
 
20
26
  config: Dict[str, Any] = load_config(args.config)
@@ -25,6 +31,7 @@ def main() -> None:
25
31
  retry_delay=args.retry_delay,
26
32
  time_zone=args.time_zone,
27
33
  log_level=args.log_level,
34
+ max_threads=args.max_threads,
28
35
  )
29
36
 
30
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,7 +44,6 @@ def attempt_booking(
44
44
  activity: str,
45
45
  class_day: str,
46
46
  class_time: str,
47
- booking_delay: int,
48
47
  retry_attempts: int = 1,
49
48
  retry_delay: int = 0,
50
49
  time_zone: str = "Europe/Madrid",
@@ -57,7 +56,6 @@ 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
- booking_delay (int): Delay before attempting booking.
61
59
  retry_attempts (int): Number of retry attempts.
62
60
  retry_delay (int): Delay between retries.
63
61
  time_zone (str): Time zone for execution.
@@ -66,7 +64,6 @@ def attempt_booking(
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}"]
@@ -89,18 +86,16 @@ def attempt_booking(
89
86
  logger.info(f"Retrying in {retry_delay} seconds...")
90
87
  time.sleep(retry_delay)
91
88
  else:
92
- # If the booking attempt succeeds, log and exit
93
- logger.info(f"Successfully booked '{activity}' at {slot_id}.")
94
89
  return
95
90
 
96
91
  # If all attempts fail, log an error
97
- 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.")
98
94
 
99
95
 
100
96
  def schedule_bookings(
101
97
  bot: SportBot,
102
- classes: List[Dict[str, Any]],
103
- booking_execution: str,
98
+ config: Dict[str, Any],
104
99
  booking_delay: int,
105
100
  retry_attempts: int,
106
101
  retry_delay: int,
@@ -121,16 +116,25 @@ def schedule_bookings(
121
116
  max_threads (int): Maximum number of threads to use.
122
117
  """
123
118
  # Log planned bookings
124
- for cls in classes:
119
+ for cls in config["classes"]:
125
120
  logger.info(f"Scheduled to book '{cls['activity']}' next {cls['class_day']} at {cls['class_time']}.")
126
121
 
127
122
  # Wait globally before starting bookings
128
- wait_for_execution(booking_execution, time_zone)
123
+ wait_for_execution(config["booking_execution"], time_zone)
129
124
 
130
125
  # Global booking delay
131
126
  logger.info(f"Waiting {booking_delay} seconds before attempting booking.")
132
127
  time.sleep(booking_delay)
133
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
136
+
137
+ # Submit bookings in parallel
134
138
  with ThreadPoolExecutor(max_workers=max_threads) as executor:
135
139
  future_to_class = {
136
140
  executor.submit(
@@ -139,12 +143,11 @@ def schedule_bookings(
139
143
  cls["activity"],
140
144
  cls["class_day"],
141
145
  cls["class_time"],
142
- booking_delay,
143
146
  retry_attempts,
144
147
  retry_delay,
145
148
  time_zone,
146
149
  ): cls
147
- for cls in classes
150
+ for cls in config["classes"]
148
151
  }
149
152
 
150
153
  for future in as_completed(future_to_class):
@@ -152,6 +155,5 @@ def schedule_bookings(
152
155
  activity, class_time = cls["activity"], cls["class_time"]
153
156
  try:
154
157
  future.result()
155
- logger.info(f"Booking for '{activity}' at {class_time} completed successfully.")
156
158
  except Exception:
157
159
  logger.exception(f"Booking for '{activity}' at {class_time} failed.")
@@ -1,9 +1,9 @@
1
- import os
2
1
  from typing import Any, Dict
3
2
 
4
3
  from pysportbot import SportBot
5
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
 
@@ -14,6 +14,7 @@ def run_service(
14
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.
@@ -34,21 +35,22 @@ 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
51
  schedule_bookings(
49
52
  bot,
50
- config["classes"],
51
- config["booking_execution"],
53
+ config,
52
54
  booking_delay,
53
55
  retry_attempts,
54
56
  retry_delay,
@@ -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
@@ -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.7
4
- Summary: A python-based bot for automatic resasports slot booking
5
- Home-page: https://github.com/jbeirer/resasports-bot
3
+ Version: 0.0.9
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,24 @@ 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 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
- Currently supported options include
121
- 1. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails
122
- 2. ```--retry-delay``` sets the delay in seconds between retries booking retries
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)
120
+ Currently supported options include:
121
+
122
+ 1. `--booking-delay`: sets a global delay in seconds before booking execution [default: 0]
123
+ 2. `--retry-attempts`: sets the number of retries attempted in case a booking attempt fails [default: 3]
124
+ 3. `--retry-delay`: sets the delay in seconds between booking retries [default: 5]
125
+ 4. `--time-zone`: sets the time zone for the service [default: Europe/Madrid]
126
+ 5. `--log-level`: sets the log-level of the service [default: INFO]
127
+ 6. `--max-threads`: limits the number of used threads for parallel bookings [default: -1]
125
128
 
126
129
  ## LICENSE
127
130
 
128
131
  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).
132
+ published under the [MIT License](https://github.com/jbeirer/resasports-bot/blob/main/LICENSE).
130
133
 
@@ -1,22 +1,23 @@
1
- pysportbot/__init__.py,sha256=4z5KKwrnwj_uiZYTUNgZIw_7s8ZT9C-Olz8_hzhPutw,4919
2
- pysportbot/activities.py,sha256=y2-CCdMoPTnKZleF_icYbngbnfpmHL5Cyadv1UuiCpE,4248
1
+ pysportbot/__init__.py,sha256=TomOCaCWzIuJtFsbglygUChn1wdNaARGiX5Ymt1v2aw,5651
2
+ pysportbot/activities.py,sha256=Aq8l570sxqEAKplsEMVkuDoPJJjoaZlIwPh6i64_SJ8,4252
3
3
  pysportbot/authenticator.py,sha256=xEU9O6W6Yp9DCueoUD0ASMkm17yvdUZqTvV4h6WlSnI,5012
4
- pysportbot/bookings.py,sha256=vJ0kw74qyirZlbQ7M9XqlKtRoGzuHR0_t6-zUdgldkI,3123
4
+ pysportbot/bookings.py,sha256=W91AYGPu0sCpGliuiVdlENsoGYzH8P6v-SyKisbSb7g,3127
5
5
  pysportbot/centres.py,sha256=FTK-tXUOxiJvLCHP6Bk9XEQKODQZOwwkYLlioSJPBEk,3399
6
6
  pysportbot/endpoints.py,sha256=ANh5JAbdzyZQ-i4ODrhYlskPpU1gkBrw9UhMC7kRSvU,1353
7
7
  pysportbot/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pysportbot/service/__main__.py,sha256=R03cbuus-BZ_dgdga94zqKblEkxJPuGuOP1JIz5Deyo,1274
9
- pysportbot/service/booking.py,sha256=88sZLzX72m7vQXhJdPyiQlZXoi552dK7CCLywbrBYYE,5647
8
+ pysportbot/service/__main__.py,sha256=J7c4O4QqrRyb3HvNEj3RLzCw-M-YEfU1aOrNPMlkTXM,1502
9
+ pysportbot/service/booking.py,sha256=sRV3cseOtgoMPl4Mnnt2SI0TReB4X0-jox5cxGvZBKo,5622
10
10
  pysportbot/service/config_loader.py,sha256=elNNwcC7kwc0lmRqj95hAA50qSmLelWv5G2E62tDxP0,185
11
11
  pysportbot/service/config_validator.py,sha256=ce7p-LgPtvWnTYvpyUEE60627DHHzx5mFhEI9hDNB-4,2621
12
12
  pysportbot/service/scheduling.py,sha256=bwqbiQQ9y4ss4UXZkWuOVCgCa9XlZt1n4TT_8z9bD7M,1973
13
- pysportbot/service/service.py,sha256=Lwy5OTLeR2kneZZ1I3wCw6P4DItmN5Ih6ADSbZHU38M,1821
13
+ pysportbot/service/service.py,sha256=c7hE9xU9osNImprwTobR1wC9tLFdP_DfvIJtlHjqlE8,1939
14
+ pysportbot/service/threading.py,sha256=j0tHgGty8iWDjpZtTzIu-5TUnDb9S6SJErm3QBpG-nY,1947
14
15
  pysportbot/session.py,sha256=pTQrz3bGzLYBtzVOgKv04l4UXDSgtA3Infn368bjg5I,1529
15
16
  pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  pysportbot/utils/errors.py,sha256=mpTeKjOUIxJbOqL6A6apdMb7Cpto86DkrKHWfROXjMc,4979
17
- pysportbot/utils/logger.py,sha256=38rB0M6KPlelXPcgXPSEhXB36-_ZTERLJAQ8DRYIOTM,5189
18
+ pysportbot/utils/logger.py,sha256=BfR1COAmVr5v0T5JX-6-MsMXOx2XVzfib4Td1sSWYjI,5379
18
19
  pysportbot/utils/time.py,sha256=VZSW8AxFIoFD5ZSmLUPcwawp6PmpkcxNjP3Db-Hl_fw,1244
19
- pysportbot-0.0.7.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
20
- pysportbot-0.0.7.dist-info/METADATA,sha256=rMxYNWwtB0qF_NHV4lDDXccasyi4LE8Ziuh6jO6wAxI,4892
21
- pysportbot-0.0.7.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
22
- pysportbot-0.0.7.dist-info/RECORD,,
20
+ pysportbot-0.0.9.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
21
+ pysportbot-0.0.9.dist-info/METADATA,sha256=AsgMiQqkPUFHjrTt_cN7HatrZd0ZipnyeoR8WeDUwpg,5209
22
+ pysportbot-0.0.9.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
23
+ pysportbot-0.0.9.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