pysportbot 0.0.12__py3-none-any.whl → 0.0.14__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 CHANGED
@@ -32,13 +32,27 @@ class Activities:
32
32
  """
33
33
  logger.info("Fetching activities...")
34
34
  response = self.session.post(Endpoints.ACTIVITIES, headers=self.headers)
35
+
35
36
  if response.status_code != 200:
36
37
  error_msg = ErrorMessages.failed_fetch("activities")
37
38
  logger.error(error_msg)
38
39
  raise RuntimeError(error_msg)
40
+
41
+ try:
42
+ activities = response.json().get("activities", {})
43
+ except json.JSONDecodeError as err:
44
+ error_msg = "Invalid JSON response while fetching activities."
45
+ logger.error(error_msg)
46
+ raise RuntimeError(error_msg) from err
47
+
48
+ if not activities:
49
+ logger.warning("No activities found in the response.")
50
+
51
+ df_activities = pd.DataFrame.from_dict(activities, orient="index")
52
+ df_activities.index = df_activities.index.astype(int) # Ensure index is integer for consistency
53
+
39
54
  logger.info("Activities fetched successfully.")
40
- activities = json.loads(response.content.decode("utf-8"))["activities"]
41
- return pd.DataFrame.from_dict(activities, orient="index")
55
+ return df_activities
42
56
 
43
57
  def daily_slots(self, df_activities: DataFrame, activity_name: str, day: str) -> DataFrame:
44
58
  """
@@ -67,9 +81,10 @@ class Activities:
67
81
  logger.error(error_msg)
68
82
  raise ValueError(error_msg)
69
83
 
70
- # Extract activity ID
71
- activity_id = str(activity_match.id_activity.iloc[0])
72
- id_category_activity = df_activities.loc[activity_id].activityCategoryId
84
+ # Extract activity ID and category ID
85
+ # Ensures activity_id is an integer
86
+ activity_id = activity_match.index[0]
87
+ id_category_activity = activity_match.at[activity_id, "activityCategoryId"]
73
88
 
74
89
  # Get Unix timestamp bounds for the day
75
90
  unix_day_bounds = get_unix_day_bounds(day)
@@ -81,12 +96,19 @@ class Activities:
81
96
  "end": unix_day_bounds[1],
82
97
  }
83
98
  response = self.session.get(Endpoints.SLOTS, headers=self.headers, params=params)
99
+
84
100
  if response.status_code != 200:
85
101
  error_msg = ErrorMessages.failed_fetch("slots")
86
102
  logger.error(error_msg)
87
103
  raise RuntimeError(error_msg)
88
104
 
89
- slots = json.loads(response.content.decode("utf-8"))
105
+ try:
106
+ slots = response.json()
107
+ except json.JSONDecodeError as err:
108
+ error_msg = "Invalid JSON response while fetching slots."
109
+ logger.error(error_msg)
110
+ raise RuntimeError(error_msg) from err
111
+
90
112
  if not slots:
91
113
  warning_msg = ErrorMessages.no_slots(activity_name, day)
92
114
  logger.warning(warning_msg)
@@ -110,10 +132,12 @@ class Activities:
110
132
  "trainer",
111
133
  ]
112
134
  df_slots = pd.DataFrame(slots)
113
- df_slots = df_slots[df_slots.columns.intersection(columns)] # Ensure no KeyError
135
+
136
+ # Ensure only desired columns are selected without KeyError
137
+ df_slots = df_slots.loc[:, df_slots.columns.intersection(columns)]
114
138
 
115
139
  # Only select rows of the specified activity
116
- df_slots = df_slots[df_slots.id_activity == activity_id]
140
+ df_slots = df_slots[df_slots["id_activity"] == activity_id]
117
141
  if df_slots.empty:
118
142
  warning_msg = ErrorMessages.no_matching_slots(activity_name, day)
119
143
  logger.warning(warning_msg)
@@ -51,7 +51,11 @@ class Authenticator:
51
51
  logger.error(f"Failed to fetch login popup: {response.status_code}")
52
52
  raise RuntimeError(ErrorMessages.failed_fetch("login popup"))
53
53
  logger.debug("Login popup fetched successfully.")
54
- csrf_token = BeautifulSoup(response.text, "html.parser").find("input", {"name": "_csrf_token"})["value"]
54
+ soup = BeautifulSoup(response.text, "html.parser")
55
+ csrf_tag = soup.find("input", {"name": "_csrf_token"})
56
+ if csrf_tag is None:
57
+ raise ValueError("CSRF token input not found on the page")
58
+ csrf_token = csrf_tag["value"] # type: ignore[index]
55
59
 
56
60
  # Step 2: Perform login
57
61
  payload = {
@@ -56,6 +56,7 @@ def calculate_class_day(class_day: str, time_zone: str = "Europe/Madrid") -> dat
56
56
  now = datetime.now(tz)
57
57
  target_weekday = DAY_MAP[class_day.lower().strip()]
58
58
  days_ahead = target_weekday - now.weekday()
59
- if days_ahead < 0:
59
+ # Ensure we always book for the upcoming week
60
+ if days_ahead <= 0:
60
61
  days_ahead += 7
61
62
  return now + timedelta(days=days_ahead)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pysportbot
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Summary: A python-based bot for automatic resasports slot booking
5
5
  Author: Joshua Falco Beirer
6
6
  Author-email: jbeirer@cern.ch
@@ -10,9 +10,9 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
13
+ Requires-Dist: beautifulsoup4 (>=4.13.3,<5.0.0)
14
14
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
15
- Requires-Dist: pytz (>=2024.2,<2025.0)
15
+ Requires-Dist: pytz (>=2025.1,<2026.0)
16
16
  Requires-Dist: requests (>=2.32.3,<3.0.0)
17
17
  Description-Content-Type: text/markdown
18
18
 
@@ -109,7 +109,11 @@ Let's say you would like to book Yoga next Monday at 18:00:00, but the execution
109
109
  }
110
110
  ```
111
111
 
112
- **Note:** By default, PySportBot will attempt to execute *N* bookings in parallel, where *N* is the number of available cores on your machine.
112
+ **Note:**
113
+ 1. Bookings are always scheduled for the following week. For example, if your booking execution is set for Friday at 07:30:00 and your class is on Friday at 18:00:00, the system will schedule the booking for **next Friday** at 18:00:00, not the same day.
114
+ 2. PySportBot automatically attempts to execute up to *N* bookings in parallel, where *N* corresponds to the number of available CPU cores on your machine.
115
+ 3. If the number of bookings exceeds the available parallel slots, they will be processed sequentially in the exact order specified in your JSON configuration file.
116
+
113
117
 
114
118
  The service also provides various other options that can be inspected with
115
119
 
@@ -125,6 +129,63 @@ Currently supported options include:
125
129
  5. `--log-level`: sets the log-level of the service [default: INFO]
126
130
  6. `--max-threads`: limits the number of used threads for parallel bookings [default: -1]
127
131
 
132
+ ## Automating Weekly Bookings with GitHub Actions
133
+
134
+ While you can use PySportBot as a continuously running service, doing so often becomes cumbersome because it requires a server (or similar machine) that is always online.
135
+
136
+ Using **[GitHub Actions](https://docs.github.com/en/actions)** is one of the most reliable ways to schedule **automated weekly bookings**. With a simple workflow file, you can ensure your bookings run at a specific time each week — no manual intervention required.
137
+
138
+ For example, if you have a **Yoga** class on **Monday at 18:00:00**, and you want the **booking** to be executed on **Friday at 07:30:00**, you can automate the process by configuring a **[GitHub Actions workflow](https://docs.github.com/en/actions/writing-workflows)**. Below is a sample workflow file that triggers every **Friday at 06:00 UTC (07:00 CET)**, ensuring there is enough time before the actual booking execution at 07:30:00:
139
+
140
+ ```yml
141
+ name: Weekly Booking
142
+
143
+ on:
144
+ # Runs at 06:00 UTC (07:00 CET) on Fridays
145
+ # Ensure enough time for the job to start well before 07:30 CET
146
+ schedule:
147
+ - cron: '0 6 * * 5'
148
+
149
+ jobs:
150
+ book:
151
+ runs-on: ubuntu-latest
152
+
153
+ steps:
154
+ - name: Check out repository
155
+ uses: actions/checkout@v2
156
+
157
+ - name: Set up Python 3.12
158
+ uses: actions/setup-python@v2
159
+ with:
160
+ python-version: '3.12'
161
+
162
+ - name: Install pysportbot
163
+ run: |
164
+ pip install pysportbot
165
+
166
+ - name: Create config file
167
+ run: |
168
+ echo '{
169
+ "email": "your-email",
170
+ "password": "your-password",
171
+ "centre": "your-gym",
172
+ "booking_execution": "Friday 07:30:00",
173
+ "classes": [
174
+ {
175
+ "activity": "Yoga",
176
+ "class_day": "Monday",
177
+ "class_time": "18:00:00"
178
+ }
179
+ ]
180
+ }' > config.json
181
+
182
+ - name: Run pysportbot
183
+ run: |
184
+ python -m pysportbot.service --config config.json --log-level INFO --booking-delay 1 --retry-attempts 3 --retry-delay 5
185
+ ```
186
+ **Note:** Make sure the workflow starts early enough — before your booking execution time (e.g., 07:30 CET) — to allow for potential delays in job startup or execution.
187
+
188
+
128
189
  ## LICENSE
129
190
 
130
191
  pysportbot is free of use and open-source. All versions are
@@ -1,6 +1,6 @@
1
1
  pysportbot/__init__.py,sha256=tZwOIuLO1-a3d4KKA5nNfXfN6PjJxem_hxyuff9utk4,6425
2
- pysportbot/activities.py,sha256=Aq8l570sxqEAKplsEMVkuDoPJJjoaZlIwPh6i64_SJ8,4252
3
- pysportbot/authenticator.py,sha256=xEU9O6W6Yp9DCueoUD0ASMkm17yvdUZqTvV4h6WlSnI,5012
2
+ pysportbot/activities.py,sha256=MATA0feSsDSoeFnHa4Y_dLv3gw4P6MFg-0kAEybqVIY,4991
3
+ pysportbot/authenticator.py,sha256=t_5x9anmsfh0SwpN9JSemzqKv72Hwt__Q8mQBowIOIo,5183
4
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
@@ -9,7 +9,7 @@ pysportbot/service/__main__.py,sha256=gsKfDMOmsVC3LHCQs0Dmp7YWJBlZeGcTsX-Mx4mk6r
9
9
  pysportbot/service/booking.py,sha256=oNLdY3nhs3ulAD-yFoc6YhgohbSN16xRYjEL8iyHkZg,5695
10
10
  pysportbot/service/config_loader.py,sha256=t086yaAyAKkCJTpxedwhyJ7QqSf5XROGDzjrFLsUxJE,179
11
11
  pysportbot/service/config_validator.py,sha256=0P_pcXU7s3T3asODqFtv3mSp1HyPoVBphE4Qe1mxcps,2615
12
- pysportbot/service/scheduling.py,sha256=bwqbiQQ9y4ss4UXZkWuOVCgCa9XlZt1n4TT_8z9bD7M,1973
12
+ pysportbot/service/scheduling.py,sha256=trz4zweZB2W9rwWAnW9Y6YkCo-f4dqtKyhaY_yqpJ-Y,2024
13
13
  pysportbot/service/service.py,sha256=eW-roFozDzkK7kTzYbSzNwZhbZpBr-22yUrAHVnrD-0,1933
14
14
  pysportbot/service/threading.py,sha256=j0tHgGty8iWDjpZtTzIu-5TUnDb9S6SJErm3QBpG-nY,1947
15
15
  pysportbot/session.py,sha256=pTQrz3bGzLYBtzVOgKv04l4UXDSgtA3Infn368bjg5I,1529
@@ -17,7 +17,7 @@ pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
17
17
  pysportbot/utils/errors.py,sha256=IRarfGtk1tETxUGAhPAIbeits_u2tJvVCGTtkHIdYFQ,5146
18
18
  pysportbot/utils/logger.py,sha256=ANayMEeeAIVGKvITAxOFm2EdCbzBBTpNywuytAr4Z90,5366
19
19
  pysportbot/utils/time.py,sha256=VZSW8AxFIoFD5ZSmLUPcwawp6PmpkcxNjP3Db-Hl_fw,1244
20
- pysportbot-0.0.12.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
21
- pysportbot-0.0.12.dist-info/METADATA,sha256=cnKqqZ72iDoVjPrnqrOxEy46gcNu0DsBdXr7BzbDIe8,5161
22
- pysportbot-0.0.12.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
23
- pysportbot-0.0.12.dist-info/RECORD,,
20
+ pysportbot-0.0.14.dist-info/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
21
+ pysportbot-0.0.14.dist-info/METADATA,sha256=aUx264Y6n5fGY4GvpK0tClB03Gm_XyJd0cBboTWzJ1c,7837
22
+ pysportbot-0.0.14.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ pysportbot-0.0.14.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.0
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any