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.
- pysportbot-0.0.1/LICENSE +21 -0
- pysportbot-0.0.1/PKG-INFO +148 -0
- pysportbot-0.0.1/README.md +125 -0
- pysportbot-0.0.1/pyproject.toml +121 -0
- pysportbot-0.0.1/pysportbot/__init__.py +116 -0
- pysportbot-0.0.1/pysportbot/activities.py +122 -0
- pysportbot-0.0.1/pysportbot/authenticator.py +118 -0
- pysportbot-0.0.1/pysportbot/bookings.py +87 -0
- pysportbot-0.0.1/pysportbot/centres.py +108 -0
- pysportbot-0.0.1/pysportbot/endpoints.py +40 -0
- pysportbot-0.0.1/pysportbot/service/__init__.py +0 -0
- pysportbot-0.0.1/pysportbot/service/__main__.py +32 -0
- pysportbot-0.0.1/pysportbot/service/booking.py +147 -0
- pysportbot-0.0.1/pysportbot/service/config_loader.py +7 -0
- pysportbot-0.0.1/pysportbot/service/config_validator.py +54 -0
- pysportbot-0.0.1/pysportbot/service/scheduling.py +61 -0
- pysportbot-0.0.1/pysportbot/service/service.py +47 -0
- pysportbot-0.0.1/pysportbot/session.py +47 -0
- pysportbot-0.0.1/pysportbot/utils/__init__.py +0 -0
- pysportbot-0.0.1/pysportbot/utils/errors.py +140 -0
- pysportbot-0.0.1/pysportbot/utils/logger.py +78 -0
- pysportbot-0.0.1/pysportbot/utils/time.py +39 -0
pysportbot-0.0.1/LICENSE
ADDED
@@ -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
|
+
[](https://img.shields.io/github/v/release/jbeirer/resasports-bot)
|
29
|
+
[](https://github.com/jbeirer/resasports-bot/actions/workflows/main.yml?query=branch%3Amain)
|
30
|
+
[](https://codecov.io/gh/jbeirer/resasports-bot)
|
31
|
+
[](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)
|
32
|
+
[](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
|
+
[](https://img.shields.io/github/v/release/jbeirer/resasports-bot)
|
7
|
+
[](https://github.com/jbeirer/resasports-bot/actions/workflows/main.yml?query=branch%3Amain)
|
8
|
+
[](https://codecov.io/gh/jbeirer/resasports-bot)
|
9
|
+
[](https://img.shields.io/github/commit-activity/m/jbeirer/resasports-bot)
|
10
|
+
[](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
|