pysportbot 0.0.15__tar.gz → 0.0.17__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.
Files changed (23) hide show
  1. {pysportbot-0.0.15 → pysportbot-0.0.17}/PKG-INFO +6 -4
  2. {pysportbot-0.0.15 → pysportbot-0.0.17}/README.md +5 -3
  3. {pysportbot-0.0.15 → pysportbot-0.0.17}/pyproject.toml +2 -2
  4. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/authenticator.py +29 -5
  5. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/booking.py +10 -6
  6. {pysportbot-0.0.15 → pysportbot-0.0.17}/LICENSE +0 -0
  7. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/__init__.py +0 -0
  8. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/activities.py +0 -0
  9. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/bookings.py +0 -0
  10. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/centres.py +0 -0
  11. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/endpoints.py +0 -0
  12. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/__init__.py +0 -0
  13. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/__main__.py +0 -0
  14. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/config_loader.py +0 -0
  15. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/config_validator.py +0 -0
  16. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/scheduling.py +0 -0
  17. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/service.py +0 -0
  18. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/service/threading.py +0 -0
  19. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/session.py +0 -0
  20. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/utils/__init__.py +0 -0
  21. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/utils/errors.py +0 -0
  22. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/utils/logger.py +0 -0
  23. {pysportbot-0.0.15 → pysportbot-0.0.17}/pysportbot/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pysportbot
3
- Version: 0.0.15
3
+ Version: 0.0.17
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
@@ -141,10 +141,12 @@ For example, if you have a **Yoga** class on **Monday at 18:00:00**, and you wan
141
141
  name: Weekly Booking
142
142
 
143
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
144
+ # Runs at 05:00 UTC (07:00 Madrid time during daylight saving time) on Fridays
145
+ # Note: Update to '0 6 * * 5' for winter (standard) time when Madrid shifts to UTC+1
146
+ # GitHub Actions cron expressions are always in UTC and do not support time zones
147
+ # Reference: https://github.com/orgs/community/discussions/13454
146
148
  schedule:
147
- - cron: '0 6 * * 5'
149
+ - cron: '0 5 * * 5'
148
150
 
149
151
  jobs:
150
152
  book:
@@ -123,10 +123,12 @@ For example, if you have a **Yoga** class on **Monday at 18:00:00**, and you wan
123
123
  name: Weekly Booking
124
124
 
125
125
  on:
126
- # Runs at 06:00 UTC (07:00 CET) on Fridays
127
- # Ensure enough time for the job to start well before 07:30 CET
126
+ # Runs at 05:00 UTC (07:00 Madrid time during daylight saving time) on Fridays
127
+ # Note: Update to '0 6 * * 5' for winter (standard) time when Madrid shifts to UTC+1
128
+ # GitHub Actions cron expressions are always in UTC and do not support time zones
129
+ # Reference: https://github.com/orgs/community/discussions/13454
128
130
  schedule:
129
- - cron: '0 6 * * 5'
131
+ - cron: '0 5 * * 5'
130
132
 
131
133
  jobs:
132
134
  book:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pysportbot"
3
- version = "v0.0.15"
3
+ version = "v0.0.17"
4
4
  description = "A python-based bot for automatic resasports slot booking"
5
5
  authors = [
6
6
  { name = "Joshua Falco Beirer", email = "jbeirer@cern.ch" }
@@ -37,7 +37,7 @@ mkdocs-material = "^9.6.9"
37
37
  mkdocstrings = {extras = ["python"], version = "^0.29.0"}
38
38
 
39
39
  [build-system]
40
- requires = ["poetry-core>=2.1.1"]
40
+ requires = ["poetry-core>=2.1.3"]
41
41
  build-backend = "poetry.core.masonry.api"
42
42
 
43
43
  [tool.black]
@@ -30,6 +30,27 @@ class Authenticator:
30
30
  self.user_id = None
31
31
  # Centre selected by the user
32
32
  self.centre = centre
33
+ # Timeout of requests
34
+ self.timeout = (5, 10)
35
+
36
+ def is_session_valid(self) -> bool:
37
+ """
38
+ Check if the current session is still valid.
39
+
40
+ Returns:
41
+ bool: True if session is valid, False otherwise.
42
+ """
43
+ try:
44
+ response = self.session.post(Endpoints.USER, headers=self.headers, timeout=self.timeout)
45
+ if response.status_code == 200:
46
+ response_dict = json.loads(response.content.decode("utf-8"))
47
+ return bool(response_dict.get("user"))
48
+ else:
49
+ logger.debug(f"Session validation failed with status code: {response.status_code}")
50
+ return False
51
+ except Exception as e:
52
+ logger.debug(f"Session validation failed with exception: {e}")
53
+ return False
33
54
 
34
55
  def login(self, email: str, password: str) -> None:
35
56
  """
@@ -46,11 +67,12 @@ class Authenticator:
46
67
 
47
68
  # Step 1: Fetch CSRF token
48
69
  logger.debug(f"GET {Endpoints.USER_LOGIN} | Headers: {json.dumps(self.headers, indent=2)}")
49
- response = self.session.get(Endpoints.USER_LOGIN, headers=self.headers)
70
+ response = self.session.get(Endpoints.USER_LOGIN, headers=self.headers, timeout=self.timeout)
50
71
  if response.status_code != 200:
51
72
  logger.error(f"Failed to fetch login popup: {response.status_code}")
52
73
  raise RuntimeError(ErrorMessages.failed_fetch("login popup"))
53
74
  logger.debug("Login popup fetched successfully.")
75
+
54
76
  soup = BeautifulSoup(response.text, "html.parser")
55
77
  csrf_tag = soup.find("input", {"name": "_csrf_token"})
56
78
  if csrf_tag is None:
@@ -70,7 +92,7 @@ class Authenticator:
70
92
  f"POST {Endpoints.LOGIN_CHECK} | Headers: {json.dumps(self.headers, indent=2)} | "
71
93
  f"Payload: {json.dumps(payload, indent=2)}"
72
94
  )
73
- response = self.session.post(Endpoints.LOGIN_CHECK, data=payload, headers=self.headers)
95
+ response = self.session.post(Endpoints.LOGIN_CHECK, data=payload, headers=self.headers, timeout=self.timeout)
74
96
  if response.status_code != 200:
75
97
  logger.error(f"Login failed: {response.status_code}, {response.text}")
76
98
  raise ValueError(ErrorMessages.failed_login())
@@ -79,7 +101,7 @@ class Authenticator:
79
101
  # Step 3: Retrieve credentials for Nubapp
80
102
  cred_endpoint = Endpoints.get_cred_endpoint(self.centre)
81
103
  logger.debug(f"GET {cred_endpoint} | Headers: {json.dumps(self.headers, indent=2)}")
82
- response = self.session.get(cred_endpoint, headers=self.headers)
104
+ response = self.session.get(cred_endpoint, headers=self.headers, timeout=self.timeout)
83
105
  if response.status_code != 200:
84
106
  logger.error(f"Failed to retrieve Nubapp credentials: {response.status_code}")
85
107
  raise RuntimeError(ErrorMessages.failed_fetch("credentials"))
@@ -94,14 +116,16 @@ class Authenticator:
94
116
  f"GET {Endpoints.NUBAP_LOGIN} | Headers: {json.dumps(self.headers, indent=2)} | "
95
117
  f"Params: {json.dumps(nubapp_creds, indent=2)}"
96
118
  )
97
- response = self.session.get(Endpoints.NUBAP_LOGIN, headers=self.headers, params=nubapp_creds)
119
+ response = self.session.get(
120
+ Endpoints.NUBAP_LOGIN, headers=self.headers, params=nubapp_creds, timeout=self.timeout
121
+ )
98
122
  if response.status_code != 200:
99
123
  logger.error(f"Login to Nubapp failed: {response.status_code}, {response.text}")
100
124
  raise ValueError(ErrorMessages.failed_login_nubapp())
101
125
  logger.info("Login to Nubapp successful!")
102
126
 
103
127
  # Step 5: Get user information
104
- response = self.session.post(Endpoints.USER, headers=self.headers, allow_redirects=True)
128
+ response = self.session.post(Endpoints.USER, headers=self.headers, allow_redirects=True, timeout=self.timeout)
105
129
 
106
130
  if response.status_code == 200:
107
131
  response_dict = json.loads(response.content.decode("utf-8"))
@@ -117,19 +117,23 @@ def schedule_bookings(
117
117
  logger.debug(f"Re-authenticating in {reauth_time:.2f} seconds.")
118
118
  time.sleep(reauth_time)
119
119
 
120
- # Re-authenticate before booking
121
- logger.info("Re-authenticating before booking.")
120
+ # Re-authenticate before booking if necessary
122
121
  try:
123
- bot.login(config["email"], config["password"], config["centre"])
124
- except Exception:
125
- logger.warning("Re-authentication failed before booking execution.")
122
+ if bot._auth and bot._auth.is_session_valid():
123
+ logger.info("Session still valid. Skipping re-authentication.")
124
+ else:
125
+ logger.info("Attempting re-authenticating before booking.")
126
+ bot.login(config["email"], config["password"], config["centre"])
127
+
128
+ except Exception as e:
129
+ logger.warning(f"Re-authentication failed before booking execution with {e}.")
126
130
 
127
131
  # Wait the remaining time until execution
128
132
  now = datetime.now(pytz.timezone(time_zone))
129
133
  remaining_time = (execution_time - now).total_seconds()
130
134
  if remaining_time > 0:
131
135
  logger.info(f"Waiting {remaining_time:.2f} seconds until booking execution.")
132
- time.sleep(remaining_time)
136
+ time.sleep(max(0, remaining_time))
133
137
 
134
138
  # Global booking delay
135
139
  if booking_delay > 0:
File without changes