pysportbot 0.0.10__tar.gz → 0.0.12__tar.gz
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-0.0.10 → pysportbot-0.0.12}/PKG-INFO +1 -1
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pyproject.toml +4 -2
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/__init__.py +27 -8
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/booking.py +49 -47
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/utils/errors.py +5 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/LICENSE +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/README.md +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/activities.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/authenticator.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/bookings.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/centres.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/endpoints.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/__init__.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/__main__.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/config_loader.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/config_validator.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/scheduling.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/service.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/service/threading.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/session.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/utils/__init__.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/utils/logger.py +0 -0
- {pysportbot-0.0.10 → pysportbot-0.0.12}/pysportbot/utils/time.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pysportbot"
|
3
|
-
version = "v0.0.
|
3
|
+
version = "v0.0.12"
|
4
4
|
description = "A python-based bot for automatic resasports slot booking"
|
5
5
|
authors = [
|
6
6
|
{ name = "Joshua Falco Beirer", email = "jbeirer@cern.ch" }
|
@@ -110,7 +110,9 @@ lint.ignore = [
|
|
110
110
|
# Long exception message
|
111
111
|
"TRY003",
|
112
112
|
# Module shadows a Python standard-library module
|
113
|
-
"A005"
|
113
|
+
"A005",
|
114
|
+
# Use `logging.exception` instead of `logging.error`
|
115
|
+
"TRY400"
|
114
116
|
]
|
115
117
|
|
116
118
|
[tool.coverage.report]
|
@@ -49,7 +49,7 @@ class SportBot:
|
|
49
49
|
self._is_logged_in = True
|
50
50
|
self._logger.info("Login successful!")
|
51
51
|
except Exception:
|
52
|
-
self._is_logged_in = False
|
52
|
+
self._is_logged_in = False
|
53
53
|
self._logger.exception(ErrorMessages.login_failed())
|
54
54
|
raise
|
55
55
|
|
@@ -83,7 +83,6 @@ class SportBot:
|
|
83
83
|
|
84
84
|
def book(self, activity: str, start_time: str) -> None:
|
85
85
|
|
86
|
-
self._logger.debug(f"Attempting to book class '{activity}' on {start_time}")
|
87
86
|
if not self._is_logged_in:
|
88
87
|
self._logger.error(ErrorMessages.not_logged_in())
|
89
88
|
raise PermissionError(ErrorMessages.not_logged_in())
|
@@ -92,21 +91,42 @@ class SportBot:
|
|
92
91
|
self._logger.error(ErrorMessages.no_activities_loaded())
|
93
92
|
raise ValueError(ErrorMessages.no_activities_loaded())
|
94
93
|
|
94
|
+
# Fetch the daily slots for the activity
|
95
95
|
slots = self.daily_slots(activity, start_time.split(" ")[0])
|
96
|
+
|
97
|
+
# Find the slot that matches the start time
|
96
98
|
matching_slot = slots[slots["start_timestamp"] == start_time]
|
99
|
+
|
100
|
+
# If no matching slot is found, raise an error
|
97
101
|
if matching_slot.empty:
|
98
102
|
error_msg = ErrorMessages.slot_not_found(activity, start_time)
|
99
103
|
self._logger.error(error_msg)
|
100
104
|
raise IndexError(error_msg)
|
101
105
|
|
102
|
-
|
103
|
-
|
106
|
+
# The targeted slot
|
107
|
+
target_slot = matching_slot.iloc[0]
|
108
|
+
# The unique slot ID
|
109
|
+
slot_id = target_slot["id_activity_calendar"]
|
110
|
+
# The total member capacity of the slot
|
111
|
+
slot_capacity = target_slot["capacity"]
|
112
|
+
# The number of members already inscribed in the slot
|
113
|
+
slot_n_inscribed = target_slot["n_inscribed"]
|
114
|
+
# Log slot capacity
|
115
|
+
self._logger.info(
|
116
|
+
f"Attempting to book class '{activity}' on {start_time} with ID {slot_id} (Slot capacity: {slot_n_inscribed}/{slot_capacity})"
|
117
|
+
)
|
118
|
+
|
119
|
+
# Check if the slot is already booked out
|
120
|
+
if slot_n_inscribed >= slot_capacity:
|
121
|
+
self._logger.error(f"Activity '{activity}' on {start_time} with ID {slot_id} is booked out...")
|
122
|
+
raise ValueError(ErrorMessages.slot_capacity_full())
|
123
|
+
|
124
|
+
# Attempt to book the slot
|
104
125
|
try:
|
105
126
|
self._bookings.book(slot_id)
|
106
127
|
self._logger.info(f"Successfully booked class '{activity}' on {start_time}")
|
107
128
|
except ValueError:
|
108
|
-
self._logger.
|
109
|
-
raise
|
129
|
+
self._logger.error(f"Failed to book class '{activity}' on {start_time}")
|
110
130
|
|
111
131
|
def cancel(self, activity: str, start_time: str) -> None:
|
112
132
|
|
@@ -131,5 +151,4 @@ class SportBot:
|
|
131
151
|
self._bookings.cancel(slot_id)
|
132
152
|
self._logger.info(f"Successfully cancelled class '{activity}' on {start_time}")
|
133
153
|
except ValueError:
|
134
|
-
self._logger.
|
135
|
-
raise
|
154
|
+
self._logger.error(f"Failed to cancel class '{activity}' on {start_time}")
|
@@ -14,31 +14,6 @@ from .scheduling import calculate_class_day, calculate_next_execution
|
|
14
14
|
logger = get_logger(__name__)
|
15
15
|
|
16
16
|
|
17
|
-
def _raise_no_matching_slots_error(activity: str, class_time: str, booking_date: str) -> None:
|
18
|
-
raise ValueError(ErrorMessages.no_matching_slots_for_time(activity, class_time, booking_date))
|
19
|
-
|
20
|
-
|
21
|
-
def wait_for_execution(booking_execution: str, time_zone: str) -> None:
|
22
|
-
"""
|
23
|
-
Wait until the specified global execution time.
|
24
|
-
|
25
|
-
Args:
|
26
|
-
booking_execution (str): Global execution time in "Day HH:MM:SS" format.
|
27
|
-
time_zone (str): Timezone for calculation.
|
28
|
-
"""
|
29
|
-
tz = pytz.timezone(time_zone)
|
30
|
-
execution_time = calculate_next_execution(booking_execution, time_zone)
|
31
|
-
now = datetime.now(tz)
|
32
|
-
time_until_execution = (execution_time - now).total_seconds()
|
33
|
-
|
34
|
-
if time_until_execution > 0:
|
35
|
-
logger.info(
|
36
|
-
f"Waiting {time_until_execution:.2f} seconds until global execution time: "
|
37
|
-
f"{execution_time.strftime('%Y-%m-%d %H:%M:%S %z')}."
|
38
|
-
)
|
39
|
-
time.sleep(time_until_execution)
|
40
|
-
|
41
|
-
|
42
17
|
def attempt_booking(
|
43
18
|
bot: SportBot,
|
44
19
|
activity: str,
|
@@ -64,23 +39,19 @@ def attempt_booking(
|
|
64
39
|
booking_date = calculate_class_day(class_day, time_zone).strftime("%Y-%m-%d")
|
65
40
|
|
66
41
|
try:
|
67
|
-
|
68
|
-
|
69
|
-
matching_slots = available_slots[available_slots["start_timestamp"] == f"{booking_date} {class_time}"]
|
70
|
-
if matching_slots.empty:
|
71
|
-
_raise_no_matching_slots_error(activity, class_time, booking_date)
|
72
|
-
|
73
|
-
slot_id = matching_slots.iloc[0]["start_timestamp"]
|
74
|
-
logger.info(f"Attempting to book '{activity}' at {slot_id} (Attempt {attempt_num}/{retry_attempts}).")
|
75
|
-
bot.book(activity=activity, start_time=slot_id)
|
42
|
+
bot.book(activity=activity, start_time=f"{booking_date} {class_time}")
|
76
43
|
|
77
44
|
except Exception as e:
|
78
45
|
error_str = str(e)
|
79
46
|
logger.warning(f"Attempt {attempt_num} failed: {error_str}")
|
80
47
|
|
48
|
+
# Decide whether to retry based on the error message
|
81
49
|
if ErrorMessages.slot_already_booked() in error_str:
|
82
50
|
logger.warning("Slot already booked; skipping further retries.")
|
83
51
|
return
|
52
|
+
if ErrorMessages.slot_capacity_full() in error_str:
|
53
|
+
logger.warning("Slot capacity full; skipping further retries.")
|
54
|
+
return
|
84
55
|
|
85
56
|
if attempt_num < retry_attempts:
|
86
57
|
logger.info(f"Retrying in {retry_delay} seconds...")
|
@@ -119,20 +90,51 @@ def schedule_bookings(
|
|
119
90
|
for cls in config["classes"]:
|
120
91
|
logger.info(f"Scheduled to book '{cls['activity']}' next {cls['class_day']} at {cls['class_time']}.")
|
121
92
|
|
122
|
-
#
|
123
|
-
|
93
|
+
# Booking execution day and time
|
94
|
+
booking_execution = config["booking_execution"]
|
124
95
|
|
125
|
-
#
|
126
|
-
|
127
|
-
|
96
|
+
# Exact time when booking will be executed (modulo global booking delay)
|
97
|
+
execution_time = calculate_next_execution(booking_execution, time_zone)
|
98
|
+
|
99
|
+
# Get the time now
|
100
|
+
now = datetime.now(pytz.timezone(time_zone))
|
101
|
+
|
102
|
+
# Calculate the seconds until execution
|
103
|
+
time_until_execution = (execution_time - now).total_seconds()
|
104
|
+
|
105
|
+
if time_until_execution > 0:
|
106
|
+
|
107
|
+
logger.info(
|
108
|
+
f"Waiting {time_until_execution:.2f} seconds until global execution time: "
|
109
|
+
f"{execution_time.strftime('%Y-%m-%d %H:%M:%S %z')}."
|
110
|
+
)
|
111
|
+
# Re-authenticate 60 seconds before booking execution
|
112
|
+
reauth_time = time_until_execution - 60
|
113
|
+
|
114
|
+
if reauth_time <= 0:
|
115
|
+
logger.debug("Less than 60 seconds remain until execution; re-authenticating now.")
|
116
|
+
else:
|
117
|
+
logger.debug(f"Re-authenticating in {reauth_time:.2f} seconds.")
|
118
|
+
time.sleep(reauth_time)
|
128
119
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
120
|
+
# Re-authenticate before booking
|
121
|
+
logger.info("Re-authenticating before booking.")
|
122
|
+
try:
|
123
|
+
bot.login(config["email"], config["password"], config["centre"])
|
124
|
+
except Exception:
|
125
|
+
logger.warning("Re-authentication failed before booking execution.")
|
126
|
+
|
127
|
+
# Wait the remaining time until execution
|
128
|
+
now = datetime.now(pytz.timezone(time_zone))
|
129
|
+
remaining_time = (execution_time - now).total_seconds()
|
130
|
+
if remaining_time > 0:
|
131
|
+
logger.info(f"Waiting {remaining_time:.2f} seconds until booking execution.")
|
132
|
+
time.sleep(remaining_time)
|
133
|
+
|
134
|
+
# Global booking delay
|
135
|
+
if booking_delay > 0:
|
136
|
+
logger.info(f"Waiting {booking_delay} seconds before attempting booking.")
|
137
|
+
time.sleep(booking_delay)
|
136
138
|
|
137
139
|
# Submit bookings in parallel
|
138
140
|
with ThreadPoolExecutor(max_workers=max_threads) as executor:
|
@@ -156,4 +158,4 @@ def schedule_bookings(
|
|
156
158
|
try:
|
157
159
|
future.result()
|
158
160
|
except Exception:
|
159
|
-
logger.
|
161
|
+
logger.error(f"Booking for '{activity}' at {class_time} failed.")
|
@@ -115,6 +115,11 @@ class ErrorMessages:
|
|
115
115
|
"""Return an error message for a slot that is unavailable."""
|
116
116
|
return "The slot is not available."
|
117
117
|
|
118
|
+
@staticmethod
|
119
|
+
def slot_capacity_full() -> str:
|
120
|
+
"""Return an error message for a slot that is full."""
|
121
|
+
return "The slot is full. Cannot book."
|
122
|
+
|
118
123
|
@staticmethod
|
119
124
|
def cancellation_failed() -> str:
|
120
125
|
"""Return an error message for a failed cancellation."""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|