pysportbot 0.0.19__py3-none-any.whl → 0.0.20__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.
@@ -1,7 +1,4 @@
1
1
  import json
2
- from urllib.parse import parse_qs, urlparse
3
-
4
- from bs4 import BeautifulSoup
5
2
 
6
3
  from .endpoints import Endpoints
7
4
  from .session import Session
@@ -12,206 +9,296 @@ logger = get_logger(__name__)
12
9
 
13
10
 
14
11
  class Authenticator:
15
- """Handles user authentication and Nubapp login functionality."""
12
+ """
13
+ Handles user authentication and Nubapp login functionality.
16
14
 
17
- def __init__(self, session: Session, centre: str) -> None:
18
- """
19
- Initialize the Authenticator.
15
+ Flow overview:
20
16
 
21
- Args:
22
- session (Session): An instance of the Session class.
23
- centre (str): The centre selected by the user.
24
- """
17
+ 1. Login to Resasocial (api.resasocial.com) via /user/login
18
+ -> get Resasocial JWT + (id_user, id_application).
19
+
20
+ 2. Store (id_user, id_application) in self.creds for use by Activities.
21
+
22
+ 3. Call /secure/user/getSportUserToken with the Resasocial JWT
23
+ -> get Nubapp (sport.nubapp.com) JWT.
24
+
25
+ 4. Store Nubapp JWT in self.headers["Authorization"].
26
+
27
+ 5. Use Nubapp JWT to call /api/v4/users/getUser.php and verify the user.
28
+ """
29
+
30
+ def __init__(self, session: Session, centre: str) -> None:
25
31
  self.session = session.session
32
+ # Base headers; will be enriched with Nubapp JWT after login
26
33
  self.headers = session.headers
27
- self.creds: dict[str, str] = {}
34
+ # Centre is still passed in from the bot, but not used in the new flow
28
35
  self.centre = centre
29
36
  self.timeout = (5, 10)
30
37
 
31
38
  # Authentication state
32
- self.authenticated = False
39
+ self.authenticated: bool = False
33
40
  self.user_id: str | None = None
34
41
 
35
- def is_session_valid(self) -> bool:
36
- """
37
- Check if the current session is still valid.
42
+ # Minimal "credentials" object used by Activities
43
+ # (id_application, id_user) are filled after /user/login.
44
+ self.creds: dict[str, str] = {}
38
45
 
39
- Returns:
40
- bool: True if session is valid, False otherwise.
41
- """
42
- try:
43
- response = self.session.post(Endpoints.USER, headers=self.headers, timeout=self.timeout)
44
- if response.status_code == 200:
45
- response_dict = json.loads(response.content.decode("utf-8"))
46
- return bool(response_dict.get("user"))
46
+ # Resasocial (resasports) JWT state
47
+ self.resasocial_jwt: str | None = None
48
+ self.resasocial_refresh: str | None = None
49
+ self.id_user: str | None = None
50
+ self.id_application: str | None = None
47
51
 
48
- except Exception as e:
49
- logger.debug(f"Session validation failed with exception: {e}")
50
- return False
52
+ # Nubapp JWT tokens used for authenticated sport.nubapp.com requests
53
+ self.sport_jwt: str | None = None
54
+ self.sport_refresh: str | None = None
51
55
 
52
- logger.debug(f"Session validation failed with status code: {response.status_code}")
53
- return False
56
+ # ------------------------------------------------------------------
57
+ # Public API
58
+ # ------------------------------------------------------------------
54
59
 
55
60
  def login(self, email: str, password: str) -> None:
56
61
  """
57
- Authenticate the user with email and password and log in to Nubapp.
58
-
59
- Args:
60
- email (str): The user's email address.
61
- password (str): The user's password.
62
+ Full login flow:
62
63
 
63
- Raises:
64
- ValueError: If login credentials are invalid or authentication fails.
65
- RuntimeError: If the login process fails due to system errors.
64
+ 1. Resasocial JSON login (/user/login)
65
+ 2. Fill self.creds with (id_application, id_user) for Activities
66
+ 3. /secure/user/getSportUserToken -> Nubapp JWT
67
+ 4. Store Nubapp JWT in headers and fetch user info to confirm identity
66
68
  """
67
69
  logger.info("Starting login process...")
68
70
 
69
71
  try:
70
- # Fetch the CSRF token and perform login
71
- csrf_token = self._fetch_csrf_token()
72
- # Resasport login with CSRF token
73
- self._resasports_login(email, password, csrf_token)
74
- # Retrieve Nubapp credentials
75
- self._retrieve_nubapp_credentials()
76
- bearer_token = self._login_to_nubapp()
77
- # Authenticate with the bearer token
78
- self._authenticate_with_bearer_token(bearer_token)
79
- # Fetch user information to complete the login process
72
+ self._resasocial_jwt_login(email, password)
73
+
74
+ # Expose IDs in the same structure Activities expects
75
+ self.creds = {
76
+ "id_application": str(self.id_application),
77
+ "id_user": str(self.id_user),
78
+ }
79
+
80
+ self._get_sport_user_token()
81
+ self._authenticate_with_bearer_token(self.sport_jwt)
80
82
  self._fetch_user_information()
81
83
 
84
+ self.authenticated = True
82
85
  logger.info("Login process completed successfully!")
83
86
 
84
- except Exception as e:
87
+ except Exception as exc:
85
88
  self.authenticated = False
86
89
  self.user_id = None
87
- logger.error(f"Login process failed: {e}")
88
- raise
90
+ logger.error(f"Login process failed: {exc}")
91
+ # Normalize to a consistent login error for callers/tests
92
+ raise ValueError(ErrorMessages.failed_login()) from exc
89
93
 
90
- def _fetch_csrf_token(self) -> str:
91
- """Fetch CSRF token from the login page."""
92
- logger.debug(f"Fetching CSRF token from {Endpoints.USER_LOGIN}")
94
+ def is_session_valid(self) -> bool:
95
+ """
96
+ Check whether the current Nubapp JWT is still valid by probing USER.
97
+ """
98
+ if not self.sport_jwt:
99
+ return False
93
100
 
94
- response = self.session.get(Endpoints.USER_LOGIN, headers=self.headers, timeout=self.timeout)
95
- if response.status_code != 200:
96
- raise RuntimeError(ErrorMessages.failed_fetch("login popup"))
97
-
98
- soup = BeautifulSoup(response.text, "html.parser")
99
- csrf_tag = soup.find("input", {"name": "_csrf_token"})
100
- if csrf_tag is None:
101
- raise ValueError("CSRF token input not found on the page")
102
-
103
- csrf_token = str(csrf_tag["value"]) # type: ignore[index]
104
- logger.debug("CSRF token fetched successfully")
105
- return csrf_token
106
-
107
- def _resasports_login(self, email: str, password: str, csrf_token: str) -> None:
108
- """Perform login to the main site."""
109
- logger.debug("Performing site login")
110
-
111
- payload = {
112
- "_username": email,
113
- "_password": password,
114
- "_csrf_token": csrf_token,
115
- "_submit": "",
116
- "_force": "true",
117
- }
101
+ try:
102
+ # At this point self.headers already contains Nubapp Authorization
103
+ response = self.session.post(
104
+ Endpoints.USER,
105
+ headers=self.headers,
106
+ timeout=self.timeout,
107
+ )
108
+
109
+ if response.status_code != 200:
110
+ return False
111
+
112
+ data = response.json()
113
+ return bool(data.get("data", {}).get("user"))
114
+
115
+ except Exception as exc:
116
+ logger.debug(f"Session validation failed: {exc}")
117
+ return False
118
118
 
119
+ # ------------------------------------------------------------------
120
+ # Step 1: Resasocial JWT login
121
+ # ------------------------------------------------------------------
122
+
123
+ def _resasocial_jwt_login(self, email: str, password: str) -> None:
124
+ """Perform login via the /user/login JSON endpoint on api.resasocial.com."""
125
+ logger.debug("Logging in via Resasocial /user/login (JWT flow)")
126
+
127
+ payload = {"username": email, "password": password}
128
+ # Use a copy of the base headers: no Authorization here.
119
129
  headers = self.headers.copy()
120
- headers.update({"Content-Type": "application/x-www-form-urlencoded"})
121
130
 
122
- response = self.session.post(Endpoints.LOGIN_CHECK, data=payload, headers=headers, timeout=self.timeout)
131
+ response = self.session.post(
132
+ Endpoints.USER_LOGIN,
133
+ json=payload,
134
+ headers=headers,
135
+ timeout=self.timeout,
136
+ )
123
137
 
124
138
  if response.status_code != 200:
125
- logger.error(f"Site login failed: {response.status_code}")
139
+ logger.error(
140
+ "JWT login failed with status %s. Body (truncated): %r",
141
+ response.status_code,
142
+ response.text[:200],
143
+ )
126
144
  raise ValueError(ErrorMessages.failed_login())
127
145
 
128
- logger.info("Site login successful!")
146
+ try:
147
+ data = response.json()
148
+ except Exception as exc:
149
+ logger.error(
150
+ "JWT login returned non-JSON response. Body (truncated): %r",
151
+ response.text[:200],
152
+ )
153
+ raise ValueError(ErrorMessages.failed_login()) from exc
154
+
155
+ logger.debug(
156
+ "/user/login response keys: %s; applications count=%d",
157
+ list(data.keys()),
158
+ len(data.get("applications") or []),
159
+ )
129
160
 
130
- def _retrieve_nubapp_credentials(self) -> None:
131
- """Retrieve Nubapp credentials from the centre endpoint."""
132
- logger.debug("Retrieving Nubapp credentials")
161
+ self.resasocial_jwt = data.get("jwt_token")
162
+ self.resasocial_refresh = data.get("refresh_token")
133
163
 
134
- cred_endpoint = Endpoints.get_cred_endpoint(self.centre)
135
- response = self.session.get(cred_endpoint, headers=self.headers, timeout=self.timeout)
164
+ apps = data.get("applications") or []
165
+ if not apps:
166
+ logger.error("No applications returned in /user/login response")
167
+ raise ValueError(ErrorMessages.failed_login())
136
168
 
137
- if response.status_code != 200:
138
- raise RuntimeError(ErrorMessages.failed_fetch("credentials"))
169
+ first_app = apps[0]
170
+ self.id_application = first_app.get("id_application")
171
+ self.id_user = first_app.get("id_user")
139
172
 
140
- try:
141
- response_data = json.loads(response.content.decode("utf-8"))
142
- creds_payload = response_data.get("payload", "")
143
- creds = {k: v[0] for k, v in parse_qs(creds_payload).items()}
144
- creds.update({"platform": "resasocial", "network": "resasports"})
173
+ if not self.resasocial_jwt or not self.id_user or not self.id_application:
174
+ logger.error(
175
+ "Missing required fields in /user/login response: "
176
+ f"jwt_token={self.resasocial_jwt}, id_user={self.id_user}, "
177
+ f"id_application={self.id_application}"
178
+ )
179
+ raise ValueError(ErrorMessages.failed_login())
180
+
181
+ logger.info(
182
+ "JWT login successful. id_user=%s, id_application=%s",
183
+ self.id_user,
184
+ self.id_application,
185
+ )
186
+
187
+ # ------------------------------------------------------------------
188
+ # Step 3: getSportUserToken -> Nubapp JWT
189
+ # ------------------------------------------------------------------
145
190
 
146
- self.creds = creds
147
- logger.debug("Nubapp credentials retrieved successfully")
191
+ def _get_sport_user_token(self) -> None:
192
+ """
193
+ Request the Nubapp JWT token using the Resasocial JWT.
194
+ This replaces the old login_from_social.php redirect method.
195
+ """
196
+ logger.debug("Fetching Nubapp JWT via getSportUserToken")
148
197
 
149
- except (ValueError, KeyError, SyntaxError) as e:
150
- raise RuntimeError(f"Failed to parse credentials: {e}") from e
198
+ if not self.resasocial_jwt or not self.id_user or not self.id_application:
199
+ logger.error("Cannot fetch sport user token without resasocial_jwt, id_user, id_application")
200
+ raise ValueError(ErrorMessages.failed_login())
151
201
 
152
- def _login_to_nubapp(self) -> str:
153
- """Login to Nubapp and extract bearer token."""
154
- logger.debug("Logging in to Nubapp")
202
+ # Local headers specific to this Resasocial-authenticated call
203
+ social_auth_headers = self.headers.copy()
204
+ social_auth_headers["Authorization"] = f"Bearer {self.resasocial_jwt}"
205
+
206
+ params = {
207
+ "id_user": self.id_user,
208
+ "id_application": self.id_application,
209
+ }
155
210
 
156
211
  response = self.session.get(
157
- Endpoints.NUBAPP_LOGIN,
158
- headers=self.headers,
159
- params=self.creds,
212
+ Endpoints.SPORT_USER_TOKEN,
213
+ params=params,
214
+ headers=social_auth_headers,
160
215
  timeout=self.timeout,
161
- allow_redirects=False,
162
216
  )
163
217
 
164
- if response.status_code != 302:
165
- logger.error(f"Nubapp login failed: {response.status_code}")
218
+ if response.status_code != 200:
219
+ logger.error(
220
+ "getSportUserToken failed with status %s. Body (truncated): %r",
221
+ response.status_code,
222
+ response.text[:200],
223
+ )
166
224
  raise ValueError(ErrorMessages.failed_login())
167
225
 
168
- # Extract bearer token from redirect URL
169
- redirect_url = response.headers.get("Location", "")
170
- if not redirect_url:
226
+ try:
227
+ data = response.json()
228
+ except Exception as exc:
229
+ logger.error(
230
+ "getSportUserToken returned non-JSON response. Body (truncated): %r",
231
+ response.text[:200],
232
+ )
233
+ raise ValueError(ErrorMessages.failed_login()) from exc
234
+
235
+ logger.debug("response keys: %s", list(data.keys()))
236
+
237
+ self.sport_jwt = data.get("jwt_token")
238
+ self.sport_refresh = data.get("refresh_token")
239
+
240
+ if not self.sport_jwt:
241
+ logger.error("No jwt_token found in getSportUserToken response")
171
242
  raise ValueError(ErrorMessages.failed_login())
172
243
 
173
- parsed_url = urlparse(redirect_url)
174
- token = parse_qs(parsed_url.query).get("token", [None])[0]
244
+ logger.info("Nubapp JWT obtained successfully.")
245
+
246
+ # ------------------------------------------------------------------
247
+ # Step 4: Set Authorization header for Nubapp
248
+ # ------------------------------------------------------------------
175
249
 
250
+ def _authenticate_with_bearer_token(self, token: str | None) -> None:
251
+ """
252
+ Store the Nubapp JWT in self.headers so that all subsequent
253
+ Nubapp API calls (including Activities) share the same auth.
254
+ """
176
255
  if not token:
177
256
  raise ValueError(ErrorMessages.failed_login())
178
257
 
179
- logger.info("Nubapp login successful!")
180
- return token
181
-
182
- def _authenticate_with_bearer_token(self, token: str) -> None:
183
- """Add bearer token to headers for authentication."""
184
- logger.debug("Setting up bearer token authentication")
258
+ logger.debug("Setting Nubapp Authorization header")
185
259
  self.headers["Authorization"] = f"Bearer {token}"
186
260
 
261
+ # ------------------------------------------------------------------
262
+ # Step 5: Nubapp User info
263
+ # ------------------------------------------------------------------
264
+
187
265
  def _fetch_user_information(self) -> None:
188
- """Fetch and validate user information."""
189
- logger.debug("Fetching user information")
266
+ """
267
+ Fetch and validate user information from Nubapp.
190
268
 
191
- payload = {
192
- "id_application": self.creds["id_application"],
193
- "id_user": self.creds["id_user"],
194
- }
269
+ No extra payload we just rely on the Nubapp JWT already set in self.headers.
270
+ """
271
+ logger.debug("Fetching user info from Nubapp")
272
+
273
+ if not self.sport_jwt:
274
+ raise ValueError(ErrorMessages.failed_login())
195
275
 
196
- response = self.session.post(Endpoints.USER, headers=self.headers, data=payload, timeout=self.timeout)
276
+ response = self.session.post(
277
+ Endpoints.USER,
278
+ headers=self.headers,
279
+ timeout=self.timeout,
280
+ )
197
281
 
198
282
  if response.status_code != 200:
283
+ logger.error(
284
+ "Fetching user info failed with status %s. Body (truncated): %r",
285
+ response.status_code,
286
+ response.text[:200],
287
+ )
199
288
  raise ValueError(ErrorMessages.failed_login())
200
289
 
201
290
  try:
202
- response_dict = json.loads(response.content.decode("utf-8"))
203
- user_data = response_dict.get("data", {}).get("user")
291
+ data = response.json()
292
+ except (json.JSONDecodeError, ValueError) as exc:
293
+ raise ValueError(f"Failed to parse user information: {exc}") from exc
204
294
 
205
- if not user_data:
206
- raise ValueError("No user data found in response")
295
+ user_data = data.get("data", {}).get("user")
296
+ if not user_data:
297
+ raise ValueError("No user data found in response")
207
298
 
208
- user_id = user_data.get("id_user")
209
- if not user_id:
210
- raise ValueError("No user ID found in response")
211
-
212
- self.user_id = str(user_id)
213
- self.authenticated = True
214
- logger.info(f"Authentication successful. User ID: {self.user_id}")
299
+ user_id = user_data.get("id_user")
300
+ if not user_id:
301
+ raise ValueError("No user ID found in response")
215
302
 
216
- except (json.JSONDecodeError, KeyError) as e:
217
- raise ValueError(f"Failed to parse user information: {e}") from e
303
+ self.user_id = str(user_id)
304
+ logger.info("Authentication successful. User ID: %s", self.user_id)
pysportbot/endpoints.py CHANGED
@@ -3,56 +3,67 @@ from enum import Enum
3
3
 
4
4
  class Endpoints(str, Enum):
5
5
  """
6
- API endpoints used throughout the application.
6
+ Centralized collection of API endpoints used by the bot.
7
7
 
8
- This enum provides type-safe access to all API endpoints with clear organization
9
- and automatic string conversion for use in HTTP requests.
8
+ This enum provides type-safe access to all URLs with clear structure.
10
9
  """
11
10
 
12
- # === Base URLs ===
13
- BASE_SOCIAL = "https://social.resasports.com"
11
+ # ============================================================
12
+ # Base URLs
13
+ # ============================================================
14
+
15
+ # Resasocial / Resasports API (used for login, centre data, etc.)
16
+ BASE_SOCIAL = "https://api.resasocial.com"
17
+
18
+ # Nubapp API (used for bookings, user data, etc.)
14
19
  BASE_NUBAPP = "https://sport.nubapp.com"
15
20
 
16
- # === URL Components ===
17
- NUBAPP_RESOURCES = "web/resources"
21
+ # Path used for all Nubapp JSON API endpoints
18
22
  NUBAPP_API = "api/v4"
19
23
 
20
- # === Centre Management ===
24
+ # ============================================================
25
+ # Centre Management
26
+ # ============================================================
27
+
28
+ # Returns the list of centres with their bounds / metadata.
29
+ # Used by Centres.fetch_centres() to populate the centre list (slug, name, etc).
21
30
  CENTRE = f"{BASE_SOCIAL}/ajax/applications/bounds/"
22
31
 
23
- # === Authentication ===
24
- USER_LOGIN = f"{BASE_SOCIAL}/popup/login"
25
- LOGIN_CHECK = f"{BASE_SOCIAL}/popup/login_check"
26
- NUBAPP_LOGIN = f"{BASE_NUBAPP}/{NUBAPP_RESOURCES}/login_from_social.php"
32
+ # ============================================================
33
+ # Authentication
34
+ # ============================================================
35
+
36
+ # Resasports login endpoint
37
+ USER_LOGIN = f"{BASE_SOCIAL}/user/login"
38
+
39
+ # Nubapp authentification via JWT token
40
+ SPORT_USER_TOKEN = f"{BASE_SOCIAL}/secure/user/getSportUserToken"
41
+
42
+ # ============================================================
43
+ # Nubapp User & Application
44
+ # ============================================================
27
45
 
28
- # === User Management ===
46
+ # User information endpoint (requires Nubapp JWT)
29
47
  USER = f"{BASE_NUBAPP}/{NUBAPP_API}/users/getUser.php"
30
48
 
31
- # === Activities & Scheduling ===
49
+ # ============================================================
50
+ # Activities & Scheduling
51
+ # ============================================================
52
+
32
53
  ACTIVITIES = f"{BASE_NUBAPP}/{NUBAPP_API}/activities/getActivities.php"
33
54
  SLOTS = f"{BASE_NUBAPP}/{NUBAPP_API}/activities/getActivitiesCalendar.php"
34
55
 
35
- # === Booking Management ===
56
+ # ============================================================
57
+ # Booking
58
+ # ============================================================
59
+
36
60
  BOOKING = f"{BASE_NUBAPP}/{NUBAPP_API}/activities/bookActivityCalendar.php"
37
61
  CANCELLATION = f"{BASE_NUBAPP}/{NUBAPP_API}/activities/leaveActivityCalendar.php"
38
62
 
39
- @classmethod
40
- def get_cred_endpoint(cls, centre_slug: str) -> str:
41
- """
42
- Generate the credentials endpoint for a specific centre.
43
-
44
- Args:
45
- centre_slug (str): The unique identifier for the sports centre
46
-
47
- Returns:
48
- str: Complete URL for fetching centre credentials
49
-
50
- Example:
51
- >>> Endpoints.get_cred_endpoint("kirolklub")
52
- "https://social.resasports.com/ajax/application/kirolklub/book/request"
53
- """
54
- return f"{cls.BASE_SOCIAL}/ajax/application/{centre_slug}/book/request"
63
+ # ============================================================
64
+ # Utility
65
+ # ============================================================
55
66
 
56
67
  def __str__(self) -> str:
57
- """Return the URL string for direct use in HTTP requests."""
68
+ """Return URL string for direct HTTP usage."""
58
69
  return str(self.value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysportbot
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: A python-based bot for automatic resasports slot booking
5
5
  License-File: LICENSE
6
6
  Author: Joshua Falco Beirer
@@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Requires-Dist: beautifulsoup4 (>=4.13.5,<5.0.0)
15
14
  Requires-Dist: pandas (>=2.3.2,<3.0.0)
16
15
  Requires-Dist: pytz (>=2025.2,<2026.0)
17
16
  Requires-Dist: requests (>=2.32.5,<3.0.0)
@@ -1,9 +1,9 @@
1
1
  pysportbot/__init__.py,sha256=PdDUmkEBSOkdw3BaKMv1sXAUgYSTl6_qUOnAEj9UDBM,6570
2
2
  pysportbot/activities.py,sha256=fvj1Pnf3xxDuNY2FBGDOaach6tcZ_HJxCfoyGVju0oM,7598
3
- pysportbot/authenticator.py,sha256=MadHojTiDRcpPNABgT76j-fJ4cw9yydibHbEtDNktPg,7950
3
+ pysportbot/authenticator.py,sha256=vsY_ZZ9ONfv5qNAk0stNshKbXckj8JulLN91Nt_bpK8,10891
4
4
  pysportbot/bookings.py,sha256=yJPgCTECXBEVtQzwj3lUJ8QR3h43ycCUw80mTx-Ck60,3189
5
5
  pysportbot/centres.py,sha256=FTK-tXUOxiJvLCHP6Bk9XEQKODQZOwwkYLlioSJPBEk,3399
6
- pysportbot/endpoints.py,sha256=vysTBx6fEn6LtjOlYx2Dj-LLe1ts7JmeEbFZiWcE5qU,1965
6
+ pysportbot/endpoints.py,sha256=yLhejLpyI5tP1PtxeIkbKszUqah_hwtSHjLfmV1EF70,2548
7
7
  pysportbot/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  pysportbot/service/__main__.py,sha256=gsKfDMOmsVC3LHCQs0Dmp7YWJBlZeGcTsX-Mx4mk6ro,1496
9
9
  pysportbot/service/booking.py,sha256=ifeo1LbtPvls-hpQOZW5TvoqeZ1MJxIipim4RHTH7nI,5908
@@ -17,7 +17,7 @@ pysportbot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
17
17
  pysportbot/utils/errors.py,sha256=hPbWJT_uOIgYjxqm5VojlRy3wWnFj9HLZGIcbv9tq8c,4956
18
18
  pysportbot/utils/logger.py,sha256=ANayMEeeAIVGKvITAxOFm2EdCbzBBTpNywuytAr4Z90,5366
19
19
  pysportbot/utils/time.py,sha256=GKfFU5WwRSaWz2xNJ6EM0w1lHOVo8of5qhqnfh9xbJM,1926
20
- pysportbot-0.0.19.dist-info/METADATA,sha256=rOfr_KIlMFExYU7vYIuPaXE9aVfwdIu1vRebsNnq65s,8191
21
- pysportbot-0.0.19.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
22
- pysportbot-0.0.19.dist-info/licenses/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
23
- pysportbot-0.0.19.dist-info/RECORD,,
20
+ pysportbot-0.0.20.dist-info/METADATA,sha256=rqdC7PNwFSPswnq8vpJQcbJXTdZZDdpRhOJFNCsHCVM,8143
21
+ pysportbot-0.0.20.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
22
+ pysportbot-0.0.20.dist-info/licenses/LICENSE,sha256=6ov3DypdEVYpp2pn_B1MniKWO5C9iDA4O6PGcbork6c,1077
23
+ pysportbot-0.0.20.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any