pysportbot 0.0.5__py3-none-any.whl → 0.0.7__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/activities.py +1 -1
- pysportbot/service/__main__.py +5 -7
- pysportbot/service/booking.py +103 -93
- pysportbot/service/config_validator.py +39 -20
- pysportbot/service/service.py +38 -28
- pysportbot/utils/errors.py +1 -5
- pysportbot/utils/logger.py +47 -7
- {pysportbot-0.0.5.dist-info → pysportbot-0.0.7.dist-info}/METADATA +6 -26
- {pysportbot-0.0.5.dist-info → pysportbot-0.0.7.dist-info}/RECORD +11 -11
- {pysportbot-0.0.5.dist-info → pysportbot-0.0.7.dist-info}/LICENSE +0 -0
- {pysportbot-0.0.5.dist-info → pysportbot-0.0.7.dist-info}/WHEEL +0 -0
pysportbot/activities.py
CHANGED
@@ -92,7 +92,7 @@ class Activities:
|
|
92
92
|
logger.warning(warning_msg)
|
93
93
|
return DataFrame()
|
94
94
|
|
95
|
-
logger.
|
95
|
+
logger.debug(f"Daily slots fetched for '{activity_name}' on {day}.")
|
96
96
|
|
97
97
|
# Filter desired columns
|
98
98
|
columns = [
|
pysportbot/service/__main__.py
CHANGED
@@ -10,11 +10,9 @@ 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("--
|
14
|
-
parser.add_argument("--retry-attempts", type=int, default=3, help="Number of retry attempts for
|
15
|
-
parser.add_argument(
|
16
|
-
"--retry-delay-minutes", type=int, default=2, help="Delay in minutes between retries for weekly bookings."
|
17
|
-
)
|
13
|
+
parser.add_argument("--booking-delay", type=int, default=5, 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=30, 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.")
|
20
18
|
args = parser.parse_args()
|
@@ -22,9 +20,9 @@ def main() -> None:
|
|
22
20
|
config: Dict[str, Any] = load_config(args.config)
|
23
21
|
run_service(
|
24
22
|
config,
|
25
|
-
|
23
|
+
booking_delay=args.booking_delay,
|
26
24
|
retry_attempts=args.retry_attempts,
|
27
|
-
|
25
|
+
retry_delay=args.retry_delay,
|
28
26
|
time_zone=args.time_zone,
|
29
27
|
log_level=args.log_level,
|
30
28
|
)
|
pysportbot/service/booking.py
CHANGED
@@ -1,147 +1,157 @@
|
|
1
1
|
import time
|
2
|
-
from
|
3
|
-
from
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Any, Dict, List
|
4
5
|
|
5
6
|
import pytz
|
6
|
-
import schedule
|
7
7
|
|
8
8
|
from pysportbot import SportBot
|
9
9
|
from pysportbot.utils.errors import ErrorMessages
|
10
10
|
from pysportbot.utils.logger import get_logger
|
11
11
|
|
12
|
-
from .config_validator import DAY_MAP
|
13
12
|
from .scheduling import calculate_class_day, calculate_next_execution
|
14
13
|
|
15
14
|
logger = get_logger(__name__)
|
16
15
|
|
17
16
|
|
18
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:
|
19
22
|
"""
|
20
|
-
|
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.
|
21
28
|
"""
|
22
|
-
|
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)
|
23
40
|
|
24
41
|
|
25
42
|
def attempt_booking(
|
26
43
|
bot: SportBot,
|
27
|
-
|
28
|
-
|
44
|
+
activity: str,
|
45
|
+
class_day: str,
|
46
|
+
class_time: str,
|
47
|
+
booking_delay: int,
|
29
48
|
retry_attempts: int = 1,
|
30
|
-
|
49
|
+
retry_delay: int = 0,
|
31
50
|
time_zone: str = "Europe/Madrid",
|
32
51
|
) -> None:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
52
|
+
"""
|
53
|
+
Attempt to book a slot for the given class.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
bot (SportBot): The SportBot instance.
|
57
|
+
activity (str): Activity name.
|
58
|
+
class_day (str): Day of the class.
|
59
|
+
class_time (str): Time of the class.
|
60
|
+
booking_delay (int): Delay before attempting booking.
|
61
|
+
retry_attempts (int): Number of retry attempts.
|
62
|
+
retry_delay (int): Delay between retries.
|
63
|
+
time_zone (str): Time zone for execution.
|
64
|
+
"""
|
38
65
|
for attempt_num in range(1, retry_attempts + 1):
|
39
66
|
booking_date = calculate_class_day(class_day, time_zone).strftime("%Y-%m-%d")
|
40
67
|
|
41
68
|
try:
|
42
|
-
logger.info(f"Fetching available slots for {activity} on {booking_date}")
|
69
|
+
logger.info(f"Fetching available slots for '{activity}' on {booking_date}.")
|
43
70
|
available_slots = bot.daily_slots(activity=activity, day=booking_date)
|
44
71
|
|
45
72
|
matching_slots = available_slots[available_slots["start_timestamp"] == f"{booking_date} {class_time}"]
|
46
73
|
if matching_slots.empty:
|
47
74
|
_raise_no_matching_slots_error(activity, class_time, booking_date)
|
48
75
|
|
49
|
-
if booking_execution != "now":
|
50
|
-
logger.info(f"Waiting {offset_seconds} seconds before attempting booking.")
|
51
|
-
time.sleep(offset_seconds)
|
52
|
-
|
53
76
|
slot_id = matching_slots.iloc[0]["start_timestamp"]
|
54
|
-
logger.info(f"Attempting to book
|
77
|
+
logger.info(f"Attempting to book '{activity}' at {slot_id} (Attempt {attempt_num}/{retry_attempts}).")
|
55
78
|
bot.book(activity=activity, start_time=slot_id)
|
56
|
-
logger.info(f"Successfully booked {activity} at {slot_id}")
|
57
79
|
|
58
80
|
except Exception as e:
|
59
81
|
error_str = str(e)
|
60
|
-
logger.warning(f"Attempt {attempt_num} failed
|
82
|
+
logger.warning(f"Attempt {attempt_num} failed: {error_str}")
|
61
83
|
|
62
84
|
if ErrorMessages.slot_already_booked() in error_str:
|
63
|
-
logger.warning(
|
85
|
+
logger.warning("Slot already booked; skipping further retries.")
|
64
86
|
return
|
65
87
|
|
66
88
|
if attempt_num < retry_attempts:
|
67
|
-
logger.info(f"Retrying in {
|
68
|
-
time.sleep(
|
89
|
+
logger.info(f"Retrying in {retry_delay} seconds...")
|
90
|
+
time.sleep(retry_delay)
|
69
91
|
else:
|
92
|
+
# If the booking attempt succeeds, log and exit
|
93
|
+
logger.info(f"Successfully booked '{activity}' at {slot_id}.")
|
70
94
|
return
|
71
95
|
|
72
|
-
|
96
|
+
# If all attempts fail, log an error
|
97
|
+
logger.error(f"Failed to book '{activity}' after {retry_attempts} attempts.")
|
73
98
|
|
74
99
|
|
75
100
|
def schedule_bookings(
|
76
101
|
bot: SportBot,
|
77
|
-
|
78
|
-
|
79
|
-
|
102
|
+
classes: List[Dict[str, Any]],
|
103
|
+
booking_execution: str,
|
104
|
+
booking_delay: int,
|
80
105
|
retry_attempts: int,
|
81
|
-
|
82
|
-
time_zone: str
|
106
|
+
retry_delay: int,
|
107
|
+
time_zone: str,
|
108
|
+
max_threads: int,
|
83
109
|
) -> None:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
110
|
+
"""
|
111
|
+
Execute bookings in parallel with a limit on the number of threads.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
bot (SportBot): The SportBot instance.
|
115
|
+
classes (list): List of class configurations.
|
116
|
+
booking_execution (str): Global execution time for all bookings.
|
117
|
+
booking_delay (int): Delay before each booking attempt.
|
118
|
+
retry_attempts (int): Number of retry attempts.
|
119
|
+
retry_delay (int): Delay between retries.
|
120
|
+
time_zone (str): Timezone for booking.
|
121
|
+
max_threads (int): Maximum number of threads to use.
|
122
|
+
"""
|
123
|
+
# Log planned bookings
|
124
|
+
for cls in classes:
|
125
|
+
logger.info(f"Scheduled to book '{cls['activity']}' next {cls['class_day']} at {cls['class_time']}.")
|
126
|
+
|
127
|
+
# Wait globally before starting bookings
|
128
|
+
wait_for_execution(booking_execution, time_zone)
|
129
|
+
|
130
|
+
# Global booking delay
|
131
|
+
logger.info(f"Waiting {booking_delay} seconds before attempting booking.")
|
132
|
+
time.sleep(booking_delay)
|
133
|
+
|
134
|
+
with ThreadPoolExecutor(max_workers=max_threads) as executor:
|
135
|
+
future_to_class = {
|
136
|
+
executor.submit(
|
137
|
+
attempt_booking,
|
138
|
+
bot,
|
139
|
+
cls["activity"],
|
140
|
+
cls["class_day"],
|
141
|
+
cls["class_time"],
|
142
|
+
booking_delay,
|
143
|
+
retry_attempts,
|
144
|
+
retry_delay,
|
145
|
+
time_zone,
|
146
|
+
): cls
|
147
|
+
for cls in classes
|
148
|
+
}
|
149
|
+
|
150
|
+
for future in as_completed(future_to_class):
|
151
|
+
cls = future_to_class[future]
|
152
|
+
activity, class_time = cls["activity"], cls["class_time"]
|
99
153
|
try:
|
100
|
-
|
101
|
-
|
102
|
-
logger.info("Re-authentication successful.")
|
103
|
-
attempt_booking(
|
104
|
-
bot,
|
105
|
-
cls,
|
106
|
-
offset_seconds,
|
107
|
-
retry_attempts,
|
108
|
-
retry_delay_minutes,
|
109
|
-
time_zone,
|
110
|
-
)
|
154
|
+
future.result()
|
155
|
+
logger.info(f"Booking for '{activity}' at {class_time} completed successfully.")
|
111
156
|
except Exception:
|
112
|
-
logger.exception(f"
|
113
|
-
|
114
|
-
# e.g., schedule.every().monday.at("HH:MM:SS").do(...)
|
115
|
-
getattr(schedule.every(), execution_day.lower()).at(execution_time).do(booking_task)
|
116
|
-
|
117
|
-
else:
|
118
|
-
# For one-off (non-weekly) bookings, calculate exact date/time
|
119
|
-
next_execution = calculate_next_execution(booking_execution, time_zone)
|
120
|
-
tz = pytz.timezone(time_zone)
|
121
|
-
|
122
|
-
day_of_week_target = DAY_MAP[class_day.lower().strip()]
|
123
|
-
execution_day_of_week = next_execution.weekday()
|
124
|
-
|
125
|
-
days_to_class = (day_of_week_target - execution_day_of_week + 7) % 7
|
126
|
-
planned_class_date_dt = next_execution + timedelta(days=days_to_class)
|
127
|
-
planned_class_date_str = planned_class_date_dt.strftime("%Y-%m-%d (%A)")
|
128
|
-
|
129
|
-
next_execution_str = next_execution.strftime("%Y-%m-%d (%A) %H:%M:%S %z")
|
130
|
-
|
131
|
-
logger.info(
|
132
|
-
f"Class '{activity}' on {planned_class_date_str} at {class_time} "
|
133
|
-
f"will be booked on {next_execution_str}."
|
134
|
-
)
|
135
|
-
|
136
|
-
# Wait until the next execution time
|
137
|
-
time_until_execution = (next_execution - datetime.now(tz)).total_seconds()
|
138
|
-
time.sleep(max(0, time_until_execution))
|
139
|
-
|
140
|
-
attempt_booking(
|
141
|
-
bot,
|
142
|
-
cls,
|
143
|
-
offset_seconds,
|
144
|
-
retry_attempts=retry_attempts,
|
145
|
-
retry_delay_minutes=retry_delay_minutes,
|
146
|
-
time_zone=time_zone,
|
147
|
-
)
|
157
|
+
logger.exception(f"Booking for '{activity}' at {class_time} failed.")
|
@@ -11,36 +11,54 @@ DAY_MAP = {"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3, "friday": 4
|
|
11
11
|
|
12
12
|
|
13
13
|
def validate_config(config: Dict[str, Any]) -> None:
|
14
|
-
|
14
|
+
"""
|
15
|
+
Validate the overall configuration structure and values.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
config (Dict[str, Any]): Configuration dictionary.
|
19
|
+
|
20
|
+
Raises:
|
21
|
+
ValueError: If the configuration is invalid.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def raise_invalid_booking_format_error() -> None:
|
25
|
+
"""Helper function to raise an error for invalid booking_execution format."""
|
26
|
+
raise ValueError(ErrorMessages.invalid_booking_execution_format())
|
27
|
+
|
28
|
+
required_keys = ["email", "password", "centre", "classes", "booking_execution"]
|
15
29
|
for key in required_keys:
|
16
30
|
if key not in config:
|
17
31
|
raise ValueError(ErrorMessages.missing_required_key(key))
|
18
32
|
|
33
|
+
# Validate global booking_execution
|
34
|
+
if config["booking_execution"] != "now":
|
35
|
+
try:
|
36
|
+
day_and_time = config["booking_execution"].split()
|
37
|
+
if len(day_and_time) != 2:
|
38
|
+
raise_invalid_booking_format_error()
|
39
|
+
|
40
|
+
_, exec_time = day_and_time
|
41
|
+
datetime.strptime(exec_time, "%H:%M:%S")
|
42
|
+
except ValueError:
|
43
|
+
raise_invalid_booking_format_error()
|
44
|
+
|
45
|
+
# Validate individual class definitions
|
19
46
|
for cls in config["classes"]:
|
20
|
-
if
|
21
|
-
"activity" not in cls
|
22
|
-
or "class_day" not in cls
|
23
|
-
or "class_time" not in cls
|
24
|
-
or "booking_execution" not in cls
|
25
|
-
or "weekly" not in cls
|
26
|
-
):
|
47
|
+
if "activity" not in cls or "class_day" not in cls or "class_time" not in cls:
|
27
48
|
raise ValueError(ErrorMessages.invalid_class_definition())
|
28
49
|
|
29
|
-
if cls["weekly"] and cls["booking_execution"] == "now":
|
30
|
-
raise ValueError(ErrorMessages.invalid_weekly_now())
|
31
50
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
raise ValueError(ErrorMessages.invalid_booking_execution_format())
|
36
|
-
_, exec_time = day_and_time
|
37
|
-
try:
|
38
|
-
datetime.strptime(exec_time, "%H:%M:%S")
|
39
|
-
except ValueError as err:
|
40
|
-
raise ValueError(ErrorMessages.invalid_booking_execution_format()) from err
|
51
|
+
def validate_activities(bot: SportBot, config: Dict[str, Any]) -> None:
|
52
|
+
"""
|
53
|
+
Validate that all activities specified in the configuration exist.
|
41
54
|
|
55
|
+
Args:
|
56
|
+
bot (SportBot): The SportBot instance.
|
57
|
+
config (Dict[str, Any]): Configuration dictionary.
|
42
58
|
|
43
|
-
|
59
|
+
Raises:
|
60
|
+
ValueError: If an activity is not found.
|
61
|
+
"""
|
44
62
|
logger.info("Fetching available activities for validation...")
|
45
63
|
available_activities = bot.activities()
|
46
64
|
available_activity_names = set(available_activities["name_activity"].tolist())
|
@@ -51,4 +69,5 @@ def validate_activities(bot: SportBot, config: Dict[str, Any]) -> None:
|
|
51
69
|
activity_name = cls["activity"]
|
52
70
|
if activity_name not in available_activity_names:
|
53
71
|
raise ValueError(ErrorMessages.activity_not_found(activity_name, list(available_activity_names)))
|
72
|
+
|
54
73
|
logger.info("All activities in the configuration file have been validated.")
|
pysportbot/service/service.py
CHANGED
@@ -1,49 +1,59 @@
|
|
1
|
-
import
|
1
|
+
import os
|
2
2
|
from typing import Any, Dict
|
3
3
|
|
4
|
-
import schedule
|
5
|
-
|
6
4
|
from pysportbot import SportBot
|
5
|
+
from pysportbot.service.booking import schedule_bookings
|
6
|
+
from pysportbot.service.config_validator import validate_activities, validate_config
|
7
7
|
from pysportbot.utils.logger import get_logger
|
8
8
|
|
9
|
-
from .booking import schedule_bookings
|
10
|
-
from .config_validator import validate_activities, validate_config
|
11
|
-
|
12
9
|
|
13
10
|
def run_service(
|
14
11
|
config: Dict[str, Any],
|
15
|
-
|
12
|
+
booking_delay: int,
|
16
13
|
retry_attempts: int,
|
17
|
-
|
14
|
+
retry_delay: int,
|
18
15
|
time_zone: str = "Europe/Madrid",
|
19
16
|
log_level: str = "INFO",
|
20
17
|
) -> None:
|
21
|
-
|
18
|
+
"""
|
19
|
+
Run the booking service with the given configuration.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
config (dict): Configuration dictionary for booking service.
|
23
|
+
booking_delay (int): Delay before each booking attempt.
|
24
|
+
retry_attempts (int): Number of retry attempts.
|
25
|
+
retry_delay (int): Delay between retry attempts in minutes.
|
26
|
+
time_zone (str): Time zone for the booking.
|
27
|
+
log_level (str): Logging level for the service.
|
28
|
+
"""
|
29
|
+
# Initialize logger
|
22
30
|
logger = get_logger(__name__)
|
23
31
|
logger.setLevel(log_level)
|
24
32
|
|
25
|
-
# Validate
|
33
|
+
# Validate configuration
|
26
34
|
validate_config(config)
|
27
|
-
|
35
|
+
|
36
|
+
# Initialize the SportBot and authenticate
|
28
37
|
bot = SportBot(log_level=log_level, time_zone=time_zone)
|
29
38
|
bot.login(config["email"], config["password"], config["centre"])
|
30
39
|
|
31
|
-
# Validate
|
40
|
+
# Validate activities in the configuration
|
32
41
|
validate_activities(bot, config)
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
+
|
47
|
+
# Schedule bookings in parallel
|
48
|
+
schedule_bookings(
|
49
|
+
bot,
|
50
|
+
config["classes"],
|
51
|
+
config["booking_execution"],
|
52
|
+
booking_delay,
|
53
|
+
retry_attempts,
|
54
|
+
retry_delay,
|
55
|
+
time_zone,
|
56
|
+
max_threads,
|
57
|
+
)
|
58
|
+
|
59
|
+
logger.info("All bookings completed.")
|
pysportbot/utils/errors.py
CHANGED
@@ -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'
|
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:
|
pysportbot/utils/logger.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import threading
|
2
3
|
from datetime import datetime
|
3
4
|
from typing import ClassVar, Optional
|
4
5
|
|
@@ -8,7 +9,7 @@ from .errors import ErrorMessages
|
|
8
9
|
|
9
10
|
|
10
11
|
class ColorFormatter(logging.Formatter):
|
11
|
-
"""Custom formatter to add color-coded log levels."""
|
12
|
+
"""Custom formatter to add color-coded log levels and thread information."""
|
12
13
|
|
13
14
|
COLORS: ClassVar[dict[str, str]] = {
|
14
15
|
"DEBUG": "\033[94m", # Blue
|
@@ -18,17 +19,34 @@ class ColorFormatter(logging.Formatter):
|
|
18
19
|
"RESET": "\033[0m", # Reset
|
19
20
|
}
|
20
21
|
|
21
|
-
|
22
|
+
THREAD_COLORS: ClassVar[list[str]] = [
|
23
|
+
"\033[95m", # Magenta
|
24
|
+
"\033[96m", # Cyan
|
25
|
+
"\033[93m", # Yellow
|
26
|
+
"\033[92m", # Green
|
27
|
+
"\033[94m", # Blue
|
28
|
+
"\033[90m", # Gray
|
29
|
+
"\033[37m", # White
|
30
|
+
"\033[33m", # Orange
|
31
|
+
"\033[35m", # Purple
|
32
|
+
]
|
33
|
+
|
34
|
+
thread_colors: dict[str, str]
|
35
|
+
|
36
|
+
def __init__(self, fmt: str, datefmt: str, tz: pytz.BaseTzInfo, include_threads: bool = False) -> None:
|
22
37
|
"""
|
23
|
-
Initialize the formatter with a specific timezone.
|
38
|
+
Initialize the formatter with a specific timezone and optional thread formatting.
|
24
39
|
|
25
40
|
Args:
|
26
41
|
fmt (str): The log message format.
|
27
42
|
datefmt (str): The date format.
|
28
43
|
tz (pytz.BaseTzInfo): The timezone for log timestamps.
|
44
|
+
include_threads (bool): Whether to include thread information in logs.
|
29
45
|
"""
|
30
46
|
super().__init__(fmt, datefmt)
|
31
47
|
self.timezone = tz
|
48
|
+
self.include_threads = include_threads
|
49
|
+
self.thread_colors = {} # Initialize as an empty dictionary
|
32
50
|
|
33
51
|
def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str:
|
34
52
|
"""
|
@@ -46,7 +64,7 @@ class ColorFormatter(logging.Formatter):
|
|
46
64
|
|
47
65
|
def format(self, record: logging.LogRecord) -> str:
|
48
66
|
"""
|
49
|
-
Format the log record with color-coded log levels.
|
67
|
+
Format the log record with color-coded log levels and optional thread information.
|
50
68
|
|
51
69
|
Args:
|
52
70
|
record (logging.LogRecord): The log record to format.
|
@@ -56,6 +74,24 @@ class ColorFormatter(logging.Formatter):
|
|
56
74
|
"""
|
57
75
|
color = self.COLORS.get(record.levelname, self.COLORS["RESET"])
|
58
76
|
record.levelname = f"{color}{record.levelname}{self.COLORS['RESET']}"
|
77
|
+
|
78
|
+
if self.include_threads:
|
79
|
+
thread_name = threading.current_thread().name
|
80
|
+
if thread_name == "MainThread":
|
81
|
+
# Skip adding thread info for the main thread
|
82
|
+
record.thread_info = ""
|
83
|
+
else:
|
84
|
+
# Map thread names to simplified format (Thread 0, Thread 1, etc.)
|
85
|
+
if thread_name not in self.thread_colors:
|
86
|
+
color_index = len(self.thread_colors) % len(self.THREAD_COLORS)
|
87
|
+
self.thread_colors[thread_name] = self.THREAD_COLORS[color_index]
|
88
|
+
|
89
|
+
thread_color = self.thread_colors[thread_name]
|
90
|
+
simplified_thread_name = thread_name.split("_")[-1]
|
91
|
+
record.thread_info = f"[{thread_color}Thread {simplified_thread_name}{self.COLORS['RESET']}] "
|
92
|
+
else:
|
93
|
+
record.thread_info = ""
|
94
|
+
|
59
95
|
return super().format(record)
|
60
96
|
|
61
97
|
|
@@ -74,12 +110,16 @@ def setup_logger(level: str = "INFO", timezone: str = "Europe/Madrid") -> None:
|
|
74
110
|
handler = logging.StreamHandler()
|
75
111
|
handler.setLevel(logging._nameToLevel[level.upper()])
|
76
112
|
tz = pytz.timezone(timezone)
|
77
|
-
|
78
|
-
|
113
|
+
|
114
|
+
# Default formatter for the main thread
|
115
|
+
thread_formatter = ColorFormatter(
|
116
|
+
"[%(asctime)s] [%(levelname)s] %(thread_info)s%(message)s",
|
79
117
|
datefmt="%Y-%m-%d %H:%M:%S",
|
80
118
|
tz=tz,
|
119
|
+
include_threads=True,
|
81
120
|
)
|
82
|
-
|
121
|
+
|
122
|
+
handler.setFormatter(thread_formatter)
|
83
123
|
root_logger.addHandler(handler)
|
84
124
|
|
85
125
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pysportbot
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.7
|
4
4
|
Summary: A python-based bot for automatic resasports slot booking
|
5
5
|
Home-page: https://github.com/jbeirer/resasports-bot
|
6
6
|
Author: Joshua Falco Beirer
|
@@ -15,7 +15,6 @@ 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
|
-
Requires-Dist: schedule (>=1.2.2,<2.0.0)
|
19
18
|
Project-URL: Documentation, https://jbeirer.github.io/resasports-bot/
|
20
19
|
Project-URL: Repository, https://github.com/jbeirer/resasports-bot
|
21
20
|
Description-Content-Type: text/markdown
|
@@ -70,7 +69,7 @@ You can easily run `pysportbot` as a service to manage your bookings automatical
|
|
70
69
|
```bash
|
71
70
|
python -m pysportbot.service --config config.json
|
72
71
|
```
|
73
|
-
The service requires a `json` configuration file that specifies your user data and how you would like to book your classes. Currently,
|
72
|
+
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:
|
74
73
|
|
75
74
|
##### 1. Book an upcoming class now
|
76
75
|
|
@@ -81,13 +80,13 @@ Let's say you would like to book Yoga next Monday at 18:00:00, then your `config
|
|
81
80
|
"email": "your-email",
|
82
81
|
"password": "your-password",
|
83
82
|
"center": "your-gym-name",
|
83
|
+
"booking_execution": "now",
|
84
|
+
|
84
85
|
"classes": [
|
85
86
|
{
|
86
87
|
"activity": "Yoga",
|
87
88
|
"class_day": "Monday",
|
88
89
|
"class_time": "18:00:00",
|
89
|
-
"booking_execution": "now",
|
90
|
-
"weekly": false
|
91
90
|
}
|
92
91
|
]
|
93
92
|
}
|
@@ -101,32 +100,13 @@ Let's say you would like to book Yoga next Monday at 18:00:00, but the execution
|
|
101
100
|
"email": "your-email",
|
102
101
|
"password": "your-password",
|
103
102
|
"center": "your-gym-name",
|
104
|
-
"
|
105
|
-
{
|
106
|
-
"activity": "Yoga",
|
107
|
-
"class_day": "Monday",
|
108
|
-
"class_time": "18:00:00",
|
109
|
-
"booking_execution": "Friday 07:30:00",
|
110
|
-
"weekly": false
|
111
|
-
}
|
112
|
-
]
|
113
|
-
}
|
114
|
-
```
|
115
|
-
##### 3. Schedule weekly booking at specific execution day and time
|
116
|
-
Let's say you would like to book Yoga every Monday at 18:00:00 and the booking execution should be every Friday at 07:30:00 then your `config.json` would look like:
|
103
|
+
"booking_execution": "Friday 07:30:00",
|
117
104
|
|
118
|
-
```json
|
119
|
-
{
|
120
|
-
"email": "your-email",
|
121
|
-
"password": "your-password",
|
122
|
-
"center": "your-gym-name",
|
123
105
|
"classes": [
|
124
106
|
{
|
125
107
|
"activity": "Yoga",
|
126
108
|
"class_day": "Monday",
|
127
109
|
"class_time": "18:00:00",
|
128
|
-
"booking_execution": "Friday 07:30:00",
|
129
|
-
"weekly": true
|
130
110
|
}
|
131
111
|
]
|
132
112
|
}
|
@@ -139,7 +119,7 @@ python -m pysportbot.service --help
|
|
139
119
|
```
|
140
120
|
Currently supported options include
|
141
121
|
1. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails
|
142
|
-
2. ```--retry-delay
|
122
|
+
2. ```--retry-delay``` sets the delay in seconds between retries booking retries
|
143
123
|
3. ```--time-zone``` sets the time zone for the service (e.g. Europe/Madrid)
|
144
124
|
4. ```--log-level``` sets the log-level of the service (e.g. DEBUG, INFO, WARNING, ERROR)
|
145
125
|
|
@@ -1,22 +1,22 @@
|
|
1
1
|
pysportbot/__init__.py,sha256=4z5KKwrnwj_uiZYTUNgZIw_7s8ZT9C-Olz8_hzhPutw,4919
|
2
|
-
pysportbot/activities.py,sha256=
|
2
|
+
pysportbot/activities.py,sha256=y2-CCdMoPTnKZleF_icYbngbnfpmHL5Cyadv1UuiCpE,4248
|
3
3
|
pysportbot/authenticator.py,sha256=xEU9O6W6Yp9DCueoUD0ASMkm17yvdUZqTvV4h6WlSnI,5012
|
4
4
|
pysportbot/bookings.py,sha256=vJ0kw74qyirZlbQ7M9XqlKtRoGzuHR0_t6-zUdgldkI,3123
|
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=
|
9
|
-
pysportbot/service/booking.py,sha256=
|
8
|
+
pysportbot/service/__main__.py,sha256=R03cbuus-BZ_dgdga94zqKblEkxJPuGuOP1JIz5Deyo,1274
|
9
|
+
pysportbot/service/booking.py,sha256=88sZLzX72m7vQXhJdPyiQlZXoi552dK7CCLywbrBYYE,5647
|
10
10
|
pysportbot/service/config_loader.py,sha256=elNNwcC7kwc0lmRqj95hAA50qSmLelWv5G2E62tDxP0,185
|
11
|
-
pysportbot/service/config_validator.py,sha256=
|
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=
|
13
|
+
pysportbot/service/service.py,sha256=Lwy5OTLeR2kneZZ1I3wCw6P4DItmN5Ih6ADSbZHU38M,1821
|
14
14
|
pysportbot/session.py,sha256=pTQrz3bGzLYBtzVOgKv04l4UXDSgtA3Infn368bjg5I,1529
|
15
15
|
pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
pysportbot/utils/errors.py,sha256=
|
17
|
-
pysportbot/utils/logger.py,sha256=
|
16
|
+
pysportbot/utils/errors.py,sha256=mpTeKjOUIxJbOqL6A6apdMb7Cpto86DkrKHWfROXjMc,4979
|
17
|
+
pysportbot/utils/logger.py,sha256=38rB0M6KPlelXPcgXPSEhXB36-_ZTERLJAQ8DRYIOTM,5189
|
18
18
|
pysportbot/utils/time.py,sha256=VZSW8AxFIoFD5ZSmLUPcwawp6PmpkcxNjP3Db-Hl_fw,1244
|
19
|
-
pysportbot-0.0.
|
20
|
-
pysportbot-0.0.
|
21
|
-
pysportbot-0.0.
|
22
|
-
pysportbot-0.0.
|
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,,
|
File without changes
|
File without changes
|