pysportbot 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Joshua Falco Beirer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.1
2
+ Name: pysportbot
3
+ Version: 0.0.1
4
+ Summary: A python-based bot for automatic resasports slot booking
5
+ Home-page: https://github.com/jbeirer/pysportbot
6
+ Author: Joshua Falco Beirer
7
+ Author-email: jbeirer@cern.ch
8
+ Requires-Python: >=3.9,<3.13
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
15
+ Requires-Dist: pandas (>=2.2.3,<3.0.0)
16
+ Requires-Dist: pytz (>=2024.2,<2025.0)
17
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
18
+ Requires-Dist: schedule (>=1.2.2,<2.0.0)
19
+ Project-URL: Documentation, https://jbeirer.github.io/pysportbot/
20
+ Project-URL: Repository, https://github.com/jbeirer/pysportbot
21
+ Description-Content-Type: text/markdown
22
+
23
+ # No queues. Just gains.
24
+
25
+ <img src=https://github.com/jbeirer/resasports-bot/raw/main/docs/logo.png alt="Logo" width="250">
26
+
27
+
28
+ [![Release](https://img.shields.io/github/v/release/jbeirer/resasports-bot)](https://img.shields.io/github/v/release/jbeirer/resasports-bot)
29
+ [![Build status](https://img.shields.io/github/actions/workflow/status/jbeirer/resasports-bot/main.yml?branch=main)](https://github.com/jbeirer/resasports-bot/actions/workflows/main.yml?query=branch%3Amain)
30
+ [![codecov](https://codecov.io/gh/jbeirer/resasports-bot/graph/badge.svg?token=ZCJV384TXF)](https://codecov.io/gh/jbeirer/resasports-bot)
31
+ [![Commit activity](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)
32
+ [![License](https://img.shields.io/github/license/jbeirer/resasports-bot)](https://img.shields.io/github/license/jbeirer/resasports-bot)
33
+
34
+
35
+ Welcome to pysportbot!
36
+
37
+ ## Download pysportbot
38
+ ```python
39
+ pip install pysportbot
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from pysportbot import SportBot
46
+
47
+ # Create bot instance, will list available centres is requested
48
+ bot = SportBot(log_level='INFO', print_centres=False)
49
+
50
+ # Connect to service with email and password as well as the name of the centre
51
+ bot.login('email', 'password', 'centre')
52
+
53
+ # List available activites
54
+ bot.activities(limit = 10)
55
+
56
+ # List bookable slots for an activity on a specific day
57
+ bot.daily_slots(activity='YourFavouriteGymClass', day = '2025-01-03', limit = 10)
58
+
59
+ # Book an activity slot on a specific day and time
60
+ bot.book(activity='YourFavouriteGymClass', start_time = '2024-12-30 07:00:00')
61
+
62
+ # Cancel an activity slot ona specific day and time
63
+ bot.cancel(activity='YourFavouriteGymClass', start_time = '2024-12-30 07:00:00')
64
+ ```
65
+
66
+ ## Advanced usage as service
67
+
68
+ You can easily run `pysportbot` as a service to manage your bookings automatically with
69
+ ```bash
70
+ python -m pysportbot.service --config config.json
71
+ ```
72
+ The service requires a `json` configuration file that specifies your user data and how you would like to book your classes. Currently, three types of configuration are supported:
73
+
74
+ ##### 1. Book an upcoming class now
75
+
76
+ Let's say you would like to book Yoga next Monday at 18:00:00, then your `config.json` would look like:
77
+
78
+ ```json
79
+ {
80
+ "email": "your-email",
81
+ "password": "your-password",
82
+ "center": "your-gym-name",
83
+ "classes": [
84
+ {
85
+ "activity": "Yoga",
86
+ "class_day": "Monday",
87
+ "class_time": "18:00:00",
88
+ "booking_execution": "now",
89
+ "weekly": false
90
+ }
91
+ ]
92
+ }
93
+ ```
94
+ ##### 2. Book an upcoming class on a specific day and time
95
+
96
+ 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
+
98
+ ```json
99
+ {
100
+ "email": "your-email",
101
+ "password": "your-password",
102
+ "center": "your-gym-name",
103
+ "classes": [
104
+ {
105
+ "activity": "Yoga",
106
+ "class_day": "Monday",
107
+ "class_time": "18:00:00",
108
+ "booking_execution": "Friday 07:30:00",
109
+ "weekly": false
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+ ##### 3. Schedule weekly booking at specific execution day and time
115
+ 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:
116
+
117
+ ```json
118
+ {
119
+ "email": "your-email",
120
+ "password": "your-password",
121
+ "center": "your-gym-name",
122
+ "classes": [
123
+ {
124
+ "activity": "Yoga",
125
+ "class_day": "Monday",
126
+ "class_time": "18:00:00",
127
+ "booking_execution": "Friday 07:30:00",
128
+ "weekly": true
129
+ }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ The service also provides various other options that can be inspected with
135
+
136
+ ```bash
137
+ python -m pysportbot.service --help
138
+ ```
139
+ Currently supported options include
140
+ 1. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails
141
+ 2. ```--retry-delay-minutes``` sets the delay in minutes between retries for weekly bookings
142
+ 3. ```--time_zone``` sets the time zone for the service
143
+
144
+ ## LICENSE
145
+
146
+ pysportbot is free of use and open-source. All versions are
147
+ published under the [MIT License](https://github.com/jbeirer/pysportbot/blob/main/LICENSE).
148
+
@@ -0,0 +1,125 @@
1
+ # No queues. Just gains.
2
+
3
+ <img src=https://github.com/jbeirer/resasports-bot/raw/main/docs/logo.png alt="Logo" width="250">
4
+
5
+
6
+ [![Release](https://img.shields.io/github/v/release/jbeirer/resasports-bot)](https://img.shields.io/github/v/release/jbeirer/resasports-bot)
7
+ [![Build status](https://img.shields.io/github/actions/workflow/status/jbeirer/resasports-bot/main.yml?branch=main)](https://github.com/jbeirer/resasports-bot/actions/workflows/main.yml?query=branch%3Amain)
8
+ [![codecov](https://codecov.io/gh/jbeirer/resasports-bot/graph/badge.svg?token=ZCJV384TXF)](https://codecov.io/gh/jbeirer/resasports-bot)
9
+ [![Commit activity](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)
10
+ [![License](https://img.shields.io/github/license/jbeirer/resasports-bot)](https://img.shields.io/github/license/jbeirer/resasports-bot)
11
+
12
+
13
+ Welcome to pysportbot!
14
+
15
+ ## Download pysportbot
16
+ ```python
17
+ pip install pysportbot
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from pysportbot import SportBot
24
+
25
+ # Create bot instance, will list available centres is requested
26
+ bot = SportBot(log_level='INFO', print_centres=False)
27
+
28
+ # Connect to service with email and password as well as the name of the centre
29
+ bot.login('email', 'password', 'centre')
30
+
31
+ # List available activites
32
+ bot.activities(limit = 10)
33
+
34
+ # List bookable slots for an activity on a specific day
35
+ bot.daily_slots(activity='YourFavouriteGymClass', day = '2025-01-03', limit = 10)
36
+
37
+ # Book an activity slot on a specific day and time
38
+ bot.book(activity='YourFavouriteGymClass', start_time = '2024-12-30 07:00:00')
39
+
40
+ # Cancel an activity slot ona specific day and time
41
+ bot.cancel(activity='YourFavouriteGymClass', start_time = '2024-12-30 07:00:00')
42
+ ```
43
+
44
+ ## Advanced usage as service
45
+
46
+ You can easily run `pysportbot` as a service to manage your bookings automatically with
47
+ ```bash
48
+ python -m pysportbot.service --config config.json
49
+ ```
50
+ The service requires a `json` configuration file that specifies your user data and how you would like to book your classes. Currently, three types of configuration are supported:
51
+
52
+ ##### 1. Book an upcoming class now
53
+
54
+ Let's say you would like to book Yoga next Monday at 18:00:00, then your `config.json` would look like:
55
+
56
+ ```json
57
+ {
58
+ "email": "your-email",
59
+ "password": "your-password",
60
+ "center": "your-gym-name",
61
+ "classes": [
62
+ {
63
+ "activity": "Yoga",
64
+ "class_day": "Monday",
65
+ "class_time": "18:00:00",
66
+ "booking_execution": "now",
67
+ "weekly": false
68
+ }
69
+ ]
70
+ }
71
+ ```
72
+ ##### 2. Book an upcoming class on a specific day and time
73
+
74
+ 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:
75
+
76
+ ```json
77
+ {
78
+ "email": "your-email",
79
+ "password": "your-password",
80
+ "center": "your-gym-name",
81
+ "classes": [
82
+ {
83
+ "activity": "Yoga",
84
+ "class_day": "Monday",
85
+ "class_time": "18:00:00",
86
+ "booking_execution": "Friday 07:30:00",
87
+ "weekly": false
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+ ##### 3. Schedule weekly booking at specific execution day and time
93
+ 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:
94
+
95
+ ```json
96
+ {
97
+ "email": "your-email",
98
+ "password": "your-password",
99
+ "center": "your-gym-name",
100
+ "classes": [
101
+ {
102
+ "activity": "Yoga",
103
+ "class_day": "Monday",
104
+ "class_time": "18:00:00",
105
+ "booking_execution": "Friday 07:30:00",
106
+ "weekly": true
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ The service also provides various other options that can be inspected with
113
+
114
+ ```bash
115
+ python -m pysportbot.service --help
116
+ ```
117
+ Currently supported options include
118
+ 1. ```--retry-attempts``` sets the number of retries attempted in case a booking attempt fails
119
+ 2. ```--retry-delay-minutes``` sets the delay in minutes between retries for weekly bookings
120
+ 3. ```--time_zone``` sets the time zone for the service
121
+
122
+ ## LICENSE
123
+
124
+ pysportbot is free of use and open-source. All versions are
125
+ published under the [MIT License](https://github.com/jbeirer/pysportbot/blob/main/LICENSE).
@@ -0,0 +1,121 @@
1
+ [tool.poetry]
2
+ name = "pysportbot"
3
+ version = "v0.0.1"
4
+ description = " A python-based bot for automatic resasports slot booking"
5
+ authors = ["Joshua Falco Beirer <jbeirer@cern.ch>"]
6
+ repository = "https://github.com/jbeirer/pysportbot"
7
+ documentation = "https://jbeirer.github.io/pysportbot/"
8
+ readme = "README.md"
9
+ packages = [
10
+ {include = "pysportbot"}
11
+ ]
12
+
13
+ [tool.poetry.dependencies]
14
+ python = ">=3.9,<3.13"
15
+ requests = "^2.32.3"
16
+ beautifulsoup4 = "^4.12.3"
17
+ pandas = "^2.2.3"
18
+ pytz = "^2024.2"
19
+ schedule = "^1.2.2"
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ pytest = "^8.3.4"
23
+ pytest-cov = "^6.0.0"
24
+ deptry = "^0.21.2"
25
+ mypy = "^1.14.0"
26
+ pre-commit = "^4.0.1"
27
+ tox = "^4.23.2"
28
+ ipykernel = "^6.29.5"
29
+ types-pytz = "^2024.2.0.20241221"
30
+ types-requests = "^2.32.0.20241016"
31
+
32
+ [tool.poetry.group.docs.dependencies]
33
+ mkdocs = "^1.6.1"
34
+ mkdocs-material = "^9.5.49"
35
+ mkdocstrings = {extras = ["python"], version = "^0.27.0"}
36
+
37
+ [build-system]
38
+ requires = ["poetry-core>=1.9.1"]
39
+ build-backend = "poetry.core.masonry.api"
40
+
41
+ [tool.black]
42
+ line-length = 120
43
+ target-version = ['py37']
44
+ preview = true
45
+
46
+ [tool.mypy]
47
+ files = ["pysportbot"]
48
+ disallow_untyped_defs = "True"
49
+ no_implicit_optional = "True"
50
+ check_untyped_defs = "True"
51
+ warn_return_any = "True"
52
+ warn_unused_ignores = "True"
53
+ show_error_codes = "True"
54
+ ignore_missing_imports= "True"
55
+ disallow_any_unimported = "False"
56
+
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ # filter deprecation warnings from external packages
61
+ filterwarnings = [
62
+ "ignore::DeprecationWarning:(?!pysportbot).*",
63
+ ]
64
+
65
+ [tool.ruff]
66
+ target-version = "py37"
67
+ line-length = 120
68
+ fix = true
69
+ lint.select = [
70
+ # flake8-2020
71
+ "YTT",
72
+ # flake8-bandit
73
+ "S",
74
+ # flake8-bugbear
75
+ "B",
76
+ # flake8-builtins
77
+ "A",
78
+ # flake8-comprehensions
79
+ "C4",
80
+ # flake8-debugger
81
+ "T10",
82
+ # flake8-simplify
83
+ "SIM",
84
+ # isort
85
+ "I",
86
+ # mccabe
87
+ "C90",
88
+ # pycodestyle
89
+ "E", "W",
90
+ # pyflakes
91
+ "F",
92
+ # pygrep-hooks
93
+ "PGH",
94
+ # pyupgrade
95
+ "UP",
96
+ # ruff
97
+ "RUF",
98
+ # tryceratops
99
+ "TRY",
100
+ ]
101
+ lint.ignore = [
102
+ # LineTooLong
103
+ "E501",
104
+ # DoNotAssignLambda
105
+ "E731",
106
+ # Comparison to true should be 'if cond is true:'
107
+ "E712",
108
+ # Long exception message
109
+ "TRY003"
110
+ ]
111
+
112
+ [tool.coverage.report]
113
+ skip_empty = true
114
+
115
+ [tool.coverage.run]
116
+ branch = true
117
+ source = ["pysportbot"]
118
+
119
+
120
+ [tool.ruff.lint.per-file-ignores]
121
+ "tests/*" = ["S101"]
@@ -0,0 +1,116 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from pandas import DataFrame
5
+
6
+ from .activities import Activities
7
+ from .authenticator import Authenticator
8
+ from .bookings import Bookings
9
+ from .centres import Centres
10
+ from .session import Session
11
+ from .utils.errors import ErrorMessages
12
+ from .utils.logger import set_log_level, setup_logger
13
+
14
+
15
+ class SportBot:
16
+ """Unified interface for interacting with the booking system."""
17
+
18
+ def __init__(self, log_level: str = "INFO", print_centres: bool = False) -> None:
19
+ setup_logger(log_level)
20
+ self._logger = logging.getLogger("SportBot")
21
+ self._logger.info("Initializing SportBot...")
22
+ self._centres = Centres(print_centres)
23
+ self._session: Session = Session()
24
+ self._auth: Optional[Authenticator] = None
25
+ self._activities: Activities = Activities(self._session)
26
+ self._bookings: Bookings = Bookings(self._session)
27
+ self._df_activities: DataFrame | None = None
28
+ self._is_logged_in: bool = False # State variable for login status
29
+
30
+ def set_log_level(self, log_level: str) -> None:
31
+ set_log_level(log_level)
32
+ self._logger.info(f"Log level changed to {log_level}.")
33
+
34
+ def login(self, email: str, password: str, centre: str) -> None:
35
+
36
+ # Check if the selected centre is valid
37
+ self._centres.check_centre(centre)
38
+ self._logger.info(f"Selected centre: {centre}")
39
+
40
+ # Initialize the Authenticator
41
+ self._auth = Authenticator(self._session, centre)
42
+
43
+ self._logger.info("Attempting to log in...")
44
+ try:
45
+ self._auth.login(email, password)
46
+ self._df_activities = self._activities.fetch()
47
+ self._is_logged_in = True
48
+ self._logger.info("Login successful!")
49
+ except Exception:
50
+ self._is_logged_in = False # Ensure state is False on failure
51
+ self._logger.exception(ErrorMessages.login_failed())
52
+ raise
53
+
54
+ def is_logged_in(self) -> bool:
55
+ """Returns the login status."""
56
+ return self._is_logged_in
57
+
58
+ def activities(self, limit: int | None = None) -> DataFrame:
59
+ if not self._is_logged_in:
60
+ self._logger.error(ErrorMessages.not_logged_in())
61
+ raise PermissionError(ErrorMessages.not_logged_in())
62
+
63
+ if self._df_activities is None:
64
+ self._logger.error(ErrorMessages.no_activities_loaded())
65
+ raise ValueError(ErrorMessages.no_activities_loaded())
66
+
67
+ df = self._df_activities[["name_activity", "id_activity"]]
68
+ return df.head(limit) if limit else df
69
+
70
+ def daily_slots(self, activity: str, day: str, limit: int | None = None) -> DataFrame:
71
+ if not self._is_logged_in:
72
+ self._logger.error(ErrorMessages.not_logged_in())
73
+ raise PermissionError(ErrorMessages.not_logged_in())
74
+
75
+ if self._df_activities is None:
76
+ self._logger.error(ErrorMessages.no_activities_loaded())
77
+ raise ValueError(ErrorMessages.no_activities_loaded())
78
+
79
+ df = self._activities.daily_slots(self._df_activities, activity, day)
80
+ return df.head(limit) if limit else df
81
+
82
+ def book(self, activity: str, start_time: str) -> None:
83
+ if not self._is_logged_in:
84
+ self._logger.error(ErrorMessages.not_logged_in())
85
+ raise PermissionError(ErrorMessages.not_logged_in())
86
+
87
+ if self._df_activities is None:
88
+ self._logger.error(ErrorMessages.no_activities_loaded())
89
+ raise ValueError(ErrorMessages.no_activities_loaded())
90
+
91
+ slots = self.daily_slots(activity, start_time.split(" ")[0])
92
+ matching_slot = slots[slots["start_timestamp"] == start_time]
93
+ if matching_slot.empty:
94
+ error_msg = ErrorMessages.slot_not_found(activity, start_time)
95
+ self._logger.error(error_msg)
96
+ raise IndexError(error_msg)
97
+
98
+ self._bookings.book(matching_slot.iloc[0]["id_activity_calendar"])
99
+
100
+ def cancel(self, activity: str, start_time: str) -> None:
101
+ if not self._is_logged_in:
102
+ self._logger.error(ErrorMessages.not_logged_in())
103
+ raise PermissionError(ErrorMessages.not_logged_in())
104
+
105
+ if self._df_activities is None:
106
+ self._logger.error(ErrorMessages.no_activities_loaded())
107
+ raise ValueError(ErrorMessages.no_activities_loaded())
108
+
109
+ slots = self.daily_slots(activity, start_time.split(" ")[0])
110
+ matching_slot = slots[slots["start_timestamp"] == start_time]
111
+ if matching_slot.empty:
112
+ error_msg = ErrorMessages.slot_not_found(activity, start_time)
113
+ self._logger.error(error_msg)
114
+ raise IndexError(error_msg)
115
+
116
+ self._bookings.cancel(matching_slot.iloc[0]["id_activity_calendar"])
@@ -0,0 +1,122 @@
1
+ import json
2
+
3
+ import pandas as pd
4
+ from pandas import DataFrame
5
+
6
+ from .endpoints import Endpoints
7
+ from .session import Session
8
+ from .utils.errors import ErrorMessages
9
+ from .utils.logger import get_logger
10
+ from .utils.time import get_unix_day_bounds
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class Activities:
16
+ """Handles activity fetching and slot management."""
17
+
18
+ def __init__(self, session: Session) -> None:
19
+ """Initialize the Activities class."""
20
+ self.session = session.session
21
+ self.headers = session.headers
22
+
23
+ def fetch(self) -> DataFrame:
24
+ """
25
+ Fetch all available activities.
26
+
27
+ Returns:
28
+ DataFrame: A DataFrame containing activity details.
29
+
30
+ Raises:
31
+ RuntimeError: If the request fails.
32
+ """
33
+ logger.info("Fetching activities...")
34
+ response = self.session.post(Endpoints.ACTIVITIES, headers=self.headers)
35
+ if response.status_code != 200:
36
+ error_msg = ErrorMessages.failed_fetch("activities")
37
+ logger.error(error_msg)
38
+ raise RuntimeError(error_msg)
39
+ logger.info("Activities fetched successfully.")
40
+ activities = json.loads(response.content.decode("utf-8"))["activities"]
41
+ return pd.DataFrame.from_dict(activities, orient="index")
42
+
43
+ def daily_slots(self, df_activities: DataFrame, activity_name: str, day: str) -> DataFrame:
44
+ """
45
+ Fetch available slots for a specific activity on a given day.
46
+
47
+ Args:
48
+ df_activities (DataFrame): The DataFrame of activities.
49
+ activity_name (str): The name of the activity.
50
+ day (str): The day in 'YYYY-MM-DD' format.
51
+
52
+ Returns:
53
+ DataFrame: A DataFrame containing available slots.
54
+
55
+ Raises:
56
+ ValueError: If the specified activity is not found.
57
+ RuntimeError: If slots cannot be fetched.
58
+ """
59
+ logger.info(f"Fetching daily slots for '{activity_name}' on {day}...")
60
+
61
+ # Check if the activity exists
62
+ activity_match = df_activities[df_activities["name_activity"] == activity_name]
63
+ if activity_match.empty:
64
+ error_msg = ErrorMessages.activity_not_found(
65
+ activity_name, df_activities["name_activity"].unique().tolist()
66
+ )
67
+ logger.error(error_msg)
68
+ raise ValueError(error_msg)
69
+
70
+ # Extract activity ID
71
+ activity_id = str(activity_match.id_activity.iloc[0])
72
+ id_category_activity = df_activities.loc[activity_id].activityCategoryId
73
+
74
+ # Get Unix timestamp bounds for the day
75
+ unix_day_bounds = get_unix_day_bounds(day)
76
+
77
+ # Fetch slots
78
+ params = {
79
+ "id_category_activity": id_category_activity,
80
+ "start": unix_day_bounds[0],
81
+ "end": unix_day_bounds[1],
82
+ }
83
+ response = self.session.get(Endpoints.SLOTS, headers=self.headers, params=params)
84
+ if response.status_code != 200:
85
+ error_msg = ErrorMessages.failed_fetch("slots")
86
+ logger.error(error_msg)
87
+ raise RuntimeError(error_msg)
88
+
89
+ slots = json.loads(response.content.decode("utf-8"))
90
+ if not slots:
91
+ warning_msg = ErrorMessages.no_slots(activity_name, day)
92
+ logger.warning(warning_msg)
93
+ return DataFrame()
94
+
95
+ logger.info(f"Daily slots fetched for '{activity_name}' on {day}.")
96
+
97
+ # Filter desired columns
98
+ columns = [
99
+ "name_activity",
100
+ "id_activity_calendar",
101
+ "id_activity",
102
+ "id_category_activity",
103
+ "start_timestamp",
104
+ "end_timestamp",
105
+ "n_inscribed",
106
+ "capacity",
107
+ "n_waiting_list",
108
+ "cancelled",
109
+ "can_join",
110
+ "trainer",
111
+ ]
112
+ df_slots = pd.DataFrame(slots)
113
+ df_slots = df_slots[df_slots.columns.intersection(columns)] # Ensure no KeyError
114
+
115
+ # Only select rows of the specified activity
116
+ df_slots = df_slots[df_slots.id_activity == activity_id]
117
+ if df_slots.empty:
118
+ warning_msg = ErrorMessages.no_matching_slots(activity_name, day)
119
+ logger.warning(warning_msg)
120
+ return DataFrame()
121
+
122
+ return df_slots