otf-api 0.14.1__tar.gz → 0.15.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.
Files changed (65) hide show
  1. {otf_api-0.14.1/src/otf_api.egg-info → otf_api-0.15.1}/PKG-INFO +1 -1
  2. {otf_api-0.14.1 → otf_api-0.15.1}/pyproject.toml +1 -1
  3. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/__init__.py +1 -1
  4. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/bookings/booking_api.py +121 -12
  5. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/bookings/booking_client.py +1 -1
  6. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/client.py +1 -1
  7. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/studios/studio_api.py +39 -7
  8. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/workouts/workout_api.py +12 -9
  9. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/exceptions.py +7 -11
  10. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/bookings_v2.py +3 -3
  11. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/classes.py +5 -5
  12. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/telemetry.py +5 -4
  13. {otf_api-0.14.1 → otf_api-0.15.1/src/otf_api.egg-info}/PKG-INFO +1 -1
  14. {otf_api-0.14.1 → otf_api-0.15.1}/LICENSE +0 -0
  15. {otf_api-0.14.1 → otf_api-0.15.1}/README.md +0 -0
  16. {otf_api-0.14.1 → otf_api-0.15.1}/setup.cfg +0 -0
  17. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/__init__.py +0 -0
  18. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/_compat.py +0 -0
  19. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/api.py +0 -0
  20. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/bookings/__init__.py +0 -0
  21. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/members/__init__.py +0 -0
  22. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/members/member_api.py +0 -0
  23. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/members/member_client.py +0 -0
  24. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/studios/__init__.py +0 -0
  25. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/studios/studio_client.py +0 -0
  26. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/utils.py +0 -0
  27. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/workouts/__init__.py +0 -0
  28. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/api/workouts/workout_client.py +0 -0
  29. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/auth/__init__.py +0 -0
  30. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/auth/auth.py +0 -0
  31. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/auth/user.py +0 -0
  32. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/auth/utils.py +0 -0
  33. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/cache.py +0 -0
  34. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/__init__.py +0 -0
  35. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/base.py +0 -0
  36. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/__init__.py +0 -0
  37. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/bookings.py +0 -0
  38. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/enums.py +0 -0
  39. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/filters.py +0 -0
  40. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/bookings/ratings.py +0 -0
  41. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/members/__init__.py +0 -0
  42. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/members/member_detail.py +0 -0
  43. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/members/member_membership.py +0 -0
  44. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/members/member_purchases.py +0 -0
  45. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/members/notifications.py +0 -0
  46. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/mixins.py +0 -0
  47. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/studios/__init__.py +0 -0
  48. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/studios/enums.py +0 -0
  49. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/studios/studio_detail.py +0 -0
  50. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/studios/studio_services.py +0 -0
  51. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/__init__.py +0 -0
  52. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/body_composition_list.py +0 -0
  53. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/challenge_tracker_content.py +0 -0
  54. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/challenge_tracker_detail.py +0 -0
  55. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/enums.py +0 -0
  56. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/lifetime_stats.py +0 -0
  57. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/out_of_studio_workout_history.py +0 -0
  58. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/performance_summary.py +0 -0
  59. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/models/workouts/workout.py +0 -0
  60. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api/py.typed +0 -0
  61. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api.egg-info/SOURCES.txt +0 -0
  62. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api.egg-info/dependency_links.txt +0 -0
  63. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api.egg-info/requires.txt +0 -0
  64. {otf_api-0.14.1 → otf_api-0.15.1}/src/otf_api.egg-info/top_level.txt +0 -0
  65. {otf_api-0.14.1 → otf_api-0.15.1}/tests/test_filters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otf-api
3
- Version: 0.14.1
3
+ Version: 0.15.1
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  Author-email: Jessica Smith <j.smith.git1@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "otf-api"
3
- version = "0.14.1"
3
+ version = "0.15.1"
4
4
  description = "Python OrangeTheory Fitness API Client"
5
5
  authors = [{ name = "Jessica Smith", email = "j.smith.git1@gmail.com" }]
6
6
  requires-python = ">=3.11"
@@ -36,7 +36,7 @@ def _setup_logging() -> None:
36
36
 
37
37
  _setup_logging()
38
38
 
39
- __version__ = "0.14.1"
39
+ __version__ = "0.15.1"
40
40
 
41
41
 
42
42
  __all__ = ["Otf", "OtfUser", "models"]
@@ -80,11 +80,37 @@ class BookingApi:
80
80
  ends_before=end_date, starts_after=start_date, include_canceled=include_canceled, expand=expand
81
81
  )
82
82
 
83
- results = [models.BookingV2.create(**b, api=self.otf) for b in bookings_resp]
83
+ # filter out bookings with ids that start with "no-booking-id"
84
+ # no idea what these are, but I am praying for the poor sap stuck with maintaining OTF's data model
85
+ results: list[models.BookingV2] = []
86
+
87
+ for b in bookings_resp:
88
+ if not b.get("id", "").startswith("no-booking-id"):
89
+ try:
90
+ results.append(models.BookingV2.create(**b, api=self.otf))
91
+ except ValueError as e:
92
+ LOGGER.warning(f"Failed to create BookingV2 from response: {e}. Booking data:\n{b}")
93
+ continue
84
94
 
85
95
  if not remove_duplicates:
86
96
  return results
87
97
 
98
+ results = self._deduplicate_bookings(results, exclude_cancelled=exclude_cancelled)
99
+
100
+ return results
101
+
102
+ def _deduplicate_bookings(
103
+ self, results: list[models.BookingV2], exclude_cancelled: bool = True
104
+ ) -> list[models.BookingV2]:
105
+ """Deduplicate bookings by class_id, keeping the most recent booking.
106
+
107
+ Args:
108
+ results (list[BookingV2]): The list of bookings to deduplicate.
109
+ exclude_cancelled (bool): If True, will not include cancelled bookings in the results.
110
+
111
+ Returns:
112
+ list[BookingV2]: The deduplicated list of bookings.
113
+ """
88
114
  # remove duplicates by class_id, keeping the one with the most recent updated_at timestamp
89
115
  seen_classes: dict[str, models.BookingV2] = {}
90
116
 
@@ -188,7 +214,11 @@ class BookingApi:
188
214
  for c in classes_resp:
189
215
  c["studio"] = studio_dict[c["studio"]["id"]] # the one (?) place where ID actually means UUID
190
216
  c["is_home_studio"] = c["studio"].studio_uuid == self.otf.home_studio_uuid
191
- classes.append(models.OtfClass.create(**c, api=self.otf))
217
+ try:
218
+ classes.append(models.OtfClass.create(**c, api=self.otf))
219
+ except ValueError as e:
220
+ LOGGER.warning(f"Failed to create OtfClass from response: {e}. Class data:\n{c}")
221
+ continue
192
222
 
193
223
  # additional data filtering and enrichment
194
224
 
@@ -240,7 +270,7 @@ class BookingApi:
240
270
  Booking: The booking.
241
271
 
242
272
  Raises:
243
- BookingNotFoundError: If the booking does not exist.
273
+ ResourceNotFoundError: If the booking does not exist.
244
274
  ValueError: If class_uuid is None or empty string.
245
275
  """
246
276
  class_uuid = utils.get_class_uuid(otf_class)
@@ -250,7 +280,7 @@ class BookingApi:
250
280
  if booking := next((b for b in all_bookings if b.class_uuid == class_uuid), None):
251
281
  return booking
252
282
 
253
- raise exc.BookingNotFoundError(f"Booking for class {class_uuid} not found.")
283
+ raise exc.ResourceNotFoundError(f"Booking for class {class_uuid} not found.")
254
284
 
255
285
  def get_booking_from_class_new(self, otf_class: str | models.OtfClass | models.BookingV2Class) -> models.BookingV2:
256
286
  """Get a specific booking by class_uuid or OtfClass object.
@@ -262,7 +292,7 @@ class BookingApi:
262
292
  BookingV2: The booking.
263
293
 
264
294
  Raises:
265
- BookingNotFoundError: If the booking does not exist.
295
+ ResourceNotFoundError: If the booking does not exist.
266
296
  ValueError: If class_uuid is None or empty string.
267
297
  """
268
298
  class_uuid = utils.get_class_uuid(otf_class)
@@ -272,7 +302,78 @@ class BookingApi:
272
302
  if booking := next((b for b in all_bookings if b.class_uuid == class_uuid), None):
273
303
  return booking
274
304
 
275
- raise exc.BookingNotFoundError(f"Booking for class {class_uuid} not found.")
305
+ raise exc.ResourceNotFoundError(f"Booking for class {class_uuid} not found.")
306
+
307
+ def get_class_from_booking(self, booking: models.Booking | models.BookingV2) -> models.OtfClass:
308
+ """Get the class details from a Booking or BookingV2 object.
309
+
310
+ Args:
311
+ booking (Booking | BookingV2): The booking to get the class details from.
312
+
313
+ Returns:
314
+ OtfClass: The class details.
315
+
316
+ Raises:
317
+ ValueError: If the booking does not have a class_id.
318
+ """
319
+ if isinstance(booking, models.BookingV2):
320
+ return self.get_class_from_booking_new(booking)
321
+
322
+ if not booking.otf_class.class_uuid:
323
+ raise ValueError("Booking does not have a class_uuid")
324
+
325
+ if not booking.otf_class.studio:
326
+ LOGGER.warning("Booking does not have a studio, will attempt to use the home studio to get class details.")
327
+ studio_uuid = self.otf.home_studio_uuid
328
+ else:
329
+ studio_uuid = booking.otf_class.studio.studio_uuid
330
+
331
+ classes = self.otf.bookings.get_classes(
332
+ start_date=booking.starts_at.date(),
333
+ end_date=booking.starts_at.date(),
334
+ studio_uuids=[studio_uuid],
335
+ )
336
+ if classes:
337
+ otf_class = next((c for c in classes if c.class_uuid == booking.otf_class.class_uuid), None)
338
+ if otf_class:
339
+ return otf_class
340
+
341
+ raise exc.ResourceNotFoundError(
342
+ f"Class for booking {booking.otf_class.name} ({booking.booking_uuid}) not found."
343
+ )
344
+
345
+ def get_class_from_booking_new(self, booking: models.BookingV2) -> models.OtfClass:
346
+ """Get the class details from a BookingV2 object.
347
+
348
+ Args:
349
+ booking (BookingV2): The booking to get the class details from.
350
+
351
+ Returns:
352
+ OtfClass: The class details.
353
+
354
+ Raises:
355
+ ValueError: If the booking does not have a class_id.
356
+ """
357
+ if not booking.otf_class.class_id:
358
+ raise ValueError("Booking does not have a class_id")
359
+
360
+ if not booking.otf_class.studio:
361
+ LOGGER.warning("Booking does not have a studio, will attempt to use the home studio to get class details.")
362
+ studio_uuid = self.otf.home_studio_uuid
363
+ else:
364
+ studio_uuid = booking.otf_class.studio.studio_uuid
365
+
366
+ classes = self.otf.bookings.get_classes(
367
+ start_date=booking.starts_at.date(),
368
+ end_date=booking.starts_at.date(),
369
+ studio_uuids=[studio_uuid],
370
+ )
371
+ if classes:
372
+ otf_class = next((c for c in classes if c.class_id == booking.otf_class.class_id), None)
373
+ if otf_class:
374
+ return otf_class
375
+
376
+ raise exc.ResourceNotFoundError(f"Class for booking {booking.otf_class.name} ({booking.booking_id}) not found.")
276
377
 
277
378
  def book_class(self, otf_class: str | models.OtfClass) -> models.Booking:
278
379
  """Book a class by providing either the class_uuid or the OtfClass object.
@@ -287,7 +388,7 @@ class BookingApi:
287
388
  AlreadyBookedError: If the class is already booked.
288
389
  OutsideSchedulingWindowError: If the class is outside the scheduling window.
289
390
  ValueError: If class_uuid is None or empty string.
290
- OtfException: If there is an error booking the class.
391
+ OtfError: If there is an error booking the class.
291
392
  """
292
393
  class_uuid = utils.get_class_uuid(otf_class)
293
394
 
@@ -297,7 +398,7 @@ class BookingApi:
297
398
  raise exc.AlreadyBookedError(
298
399
  f"Class {class_uuid} is already booked.", booking_uuid=existing_booking.booking_uuid
299
400
  )
300
- except exc.BookingNotFoundError:
401
+ except exc.ResourceNotFoundError:
301
402
  pass
302
403
 
303
404
  if isinstance(otf_class, models.OtfClass):
@@ -328,7 +429,7 @@ class BookingApi:
328
429
  BookingV2: The booking.
329
430
 
330
431
  Raises:
331
- OtfException: If there is an error booking the class.
432
+ OtfError: If there is an error booking the class.
332
433
  TypeError: If the input is not a string or BookingV2Class.
333
434
  """
334
435
  class_id = utils.get_class_id(class_id)
@@ -349,7 +450,7 @@ class BookingApi:
349
450
 
350
451
  Raises:
351
452
  ValueError: If booking_uuid is None or empty string
352
- BookingNotFoundError: If the booking does not exist.
453
+ ResourceNotFoundError: If the booking does not exist.
353
454
  """
354
455
  if isinstance(booking, models.BookingV2):
355
456
  LOGGER.warning("BookingV2 object provided, using the new cancel booking endpoint (`cancel_booking_new`)")
@@ -370,7 +471,7 @@ class BookingApi:
370
471
 
371
472
  Raises:
372
473
  ValueError: If booking_id is None or empty string
373
- BookingNotFoundError: If the booking does not exist.
474
+ ResourceNotFoundError: If the booking does not exist.
374
475
  """
375
476
  if isinstance(booking, models.Booking):
376
477
  LOGGER.warning("Booking object provided, using the old cancel booking endpoint (`cancel_booking`)")
@@ -443,7 +544,15 @@ class BookingApi:
443
544
  b["class"]["studio"] = studios[b["class"]["studio"]["studioUUId"]]
444
545
  b["is_home_studio"] = b["class"]["studio"].studio_uuid == self.otf.home_studio_uuid
445
546
 
446
- bookings = [models.Booking.create(**b, api=self.otf) for b in resp]
547
+ bookings: list[models.Booking] = []
548
+
549
+ for b in resp:
550
+ try:
551
+ bookings.append(models.Booking.create(**b, api=self.otf))
552
+ except ValueError as e:
553
+ LOGGER.warning(f"Failed to create Booking from response: {e}. Booking data:\n{b}")
554
+ continue
555
+
447
556
  bookings = sorted(bookings, key=lambda x: x.otf_class.starts_at)
448
557
 
449
558
  if exclude_cancelled:
@@ -51,7 +51,7 @@ class BookingClient:
51
51
  Raises:
52
52
  AlreadyBookedError: If the class is already booked.
53
53
  OutsideSchedulingWindowError: If the class is outside the scheduling window.
54
- OtfException: If there is an error booking the class.
54
+ OtfError: If there is an error booking the class.
55
55
  """
56
56
  return self.client.default_request("PUT", f"/member/members/{self.member_uuid}/bookings", json=body)["data"]
57
57
 
@@ -152,7 +152,7 @@ class OtfClient:
152
152
 
153
153
  if re.match(r"^/member/members/.*?/bookings", path):
154
154
  if code == "NOT_AUTHORIZED" and error_msg.startswith("This class booking has been cancelled"):
155
- raise exc.BookingNotFoundError("Booking was already cancelled")
155
+ raise exc.ResourceNotFoundError("Booking was already cancelled")
156
156
  if error_code == "603":
157
157
  raise exc.AlreadyBookedError("Class is already booked")
158
158
  if error_code == "602":
@@ -56,7 +56,15 @@ class StudioApi:
56
56
 
57
57
  new_faves = resp.get("studios", [])
58
58
 
59
- return [models.StudioDetail.create(**studio, api=self.otf) for studio in new_faves]
59
+ studios: list[models.StudioDetail] = []
60
+ for studio in new_faves:
61
+ try:
62
+ studios.append(models.StudioDetail.create(**studio, api=self.otf))
63
+ except ValueError as e:
64
+ LOGGER.error(f"Failed to create StudioDetail for studio {studio}: {e}")
65
+ continue
66
+
67
+ return studios
60
68
 
61
69
  def remove_favorite_studio(self, studio_uuids: list[str] | str) -> None:
62
70
  """Remove a studio from the member's favorite studios.
@@ -141,7 +149,16 @@ class StudioApi:
141
149
  longitude = longitude or self.otf.home_studio.location.longitude
142
150
 
143
151
  results = self.client.get_studios_by_geo(latitude, longitude, distance)
144
- return [models.StudioDetail.create(**studio, api=self.otf) for studio in results]
152
+
153
+ studios: list[models.StudioDetail] = []
154
+ for studio in results:
155
+ try:
156
+ studios.append(models.StudioDetail.create(**studio, api=self.otf))
157
+ except ValueError as e:
158
+ LOGGER.error(f"Failed to create StudioDetail for studio {studio}: {e}")
159
+ continue
160
+
161
+ return studios
145
162
 
146
163
  def _get_all_studios(self) -> list[models.StudioDetail]:
147
164
  """Gets all studios. Marked as private to avoid random users calling it.
@@ -153,7 +170,16 @@ class StudioApi:
153
170
  """
154
171
  # long/lat being None will cause the endpoint to return all studios
155
172
  results = self.client.get_studios_by_geo(None, None)
156
- return [models.StudioDetail.create(**studio, api=self.otf) for studio in results]
173
+
174
+ studios: list[models.StudioDetail] = []
175
+ for studio in results:
176
+ try:
177
+ studios.append(models.StudioDetail.create(**studio, api=self.otf))
178
+ except ValueError as e:
179
+ LOGGER.error(f"Failed to create StudioDetail for studio {studio}: {e}")
180
+ continue
181
+
182
+ return studios
157
183
 
158
184
  def _get_studio_detail_threaded(self, studio_uuids: list[str]) -> dict[str, models.StudioDetail]:
159
185
  """Get detailed information about multiple studios in a threaded manner.
@@ -168,7 +194,13 @@ class StudioApi:
168
194
  dict[str, StudioDetail]: A dictionary mapping studio UUIDs to their detailed information.
169
195
  """
170
196
  studio_dicts = self.client.get_studio_detail_threaded(studio_uuids)
171
- return {
172
- studio_uuid: models.StudioDetail.create(**studio, api=self.otf)
173
- for studio_uuid, studio in studio_dicts.items()
174
- }
197
+
198
+ studios: dict[str, models.StudioDetail] = {}
199
+ for studio_uuid, studio in studio_dicts.items():
200
+ try:
201
+ studios[studio_uuid] = models.StudioDetail.create(**studio, api=self.otf)
202
+ except ValueError as e:
203
+ LOGGER.error(f"Failed to create StudioDetail for studio {studio_uuid}: {e}")
204
+ continue
205
+
206
+ return studios
@@ -221,7 +221,6 @@ class WorkoutApi:
221
221
  Workout: The member's workout.
222
222
 
223
223
  Raises:
224
- BookingNotFoundError: If the booking does not exist.
225
224
  ResourceNotFoundError: If the workout does not exist.
226
225
  TypeError: If the booking is an old Booking model, as these do not have the necessary fields.
227
226
  """
@@ -271,14 +270,18 @@ class WorkoutApi:
271
270
 
272
271
  workouts: list[models.Workout] = []
273
272
  for perf_id, perf_summary in perf_summaries_dict.items():
274
- workout = models.Workout.create(
275
- **perf_summary,
276
- v2_booking=bookings_dict[perf_id],
277
- telemetry=telemetry_dict.get(perf_id),
278
- class_uuid=perf_summary_to_class_uuid_map.get(perf_id),
279
- api=self.otf,
280
- )
281
- workouts.append(workout)
273
+ try:
274
+ workout = models.Workout.create(
275
+ **perf_summary,
276
+ v2_booking=bookings_dict[perf_id],
277
+ telemetry=telemetry_dict.get(perf_id),
278
+ class_uuid=perf_summary_to_class_uuid_map.get(perf_id),
279
+ api=self.otf,
280
+ )
281
+ workouts.append(workout)
282
+ except ValueError as e:
283
+ LOGGER.error(f"Failed to create Workout for performance summary {perf_id}: {e}")
284
+ continue
282
285
 
283
286
  return workouts
284
287
 
@@ -4,11 +4,11 @@ if typing.TYPE_CHECKING:
4
4
  from httpx import Request, Response
5
5
 
6
6
 
7
- class OtfException(Exception):
7
+ class OtfError(Exception):
8
8
  """Base class for all exceptions in this package."""
9
9
 
10
10
 
11
- class OtfRequestError(OtfException):
11
+ class OtfRequestError(OtfError):
12
12
  """Raised when an error occurs while making a request to the OTF API."""
13
13
 
14
14
  original_exception: Exception | None
@@ -29,7 +29,7 @@ class RetryableOtfRequestError(OtfRequestError):
29
29
  """
30
30
 
31
31
 
32
- class BookingError(OtfException):
32
+ class BookingError(OtfError):
33
33
  """Base class for booking-related errors, with an optional booking UUID attribute."""
34
34
 
35
35
  booking_uuid: str | None
@@ -53,21 +53,17 @@ class BookingAlreadyCancelledError(BookingError):
53
53
  """Raised when attempting to cancel a booking that is already cancelled."""
54
54
 
55
55
 
56
- class OutsideSchedulingWindowError(OtfException):
56
+ class OutsideSchedulingWindowError(OtfError):
57
57
  """Raised when attempting to book a class outside the scheduling window."""
58
58
 
59
59
 
60
- class BookingNotFoundError(OtfException):
61
- """Raised when a booking is not found."""
62
-
63
-
64
- class ResourceNotFoundError(OtfException):
60
+ class ResourceNotFoundError(OtfError):
65
61
  """Raised when a resource is not found."""
66
62
 
67
63
 
68
- class AlreadyRatedError(OtfException):
64
+ class AlreadyRatedError(OtfError):
69
65
  """Raised when attempting to rate a class that is already rated."""
70
66
 
71
67
 
72
- class ClassNotRatableError(OtfException):
68
+ class ClassNotRatableError(OtfError):
73
69
  """Raised when attempting to rate a class that is not ratable."""
@@ -93,7 +93,7 @@ class BookingV2Class(ApiMixin, OtfItemBase):
93
93
  """Returns a BookingV2 instance for this class.
94
94
 
95
95
  Raises:
96
- BookingNotFoundError: If the booking does not exist.
96
+ ResourceNotFoundError: If the booking does not exist.
97
97
  ValueError: If class_uuid is None or empty string or if the API instance is not set.
98
98
  """
99
99
  self.raise_if_api_not_set()
@@ -107,7 +107,7 @@ class BookingV2Class(ApiMixin, OtfItemBase):
107
107
  """Cancels the booking by calling the proper API method.
108
108
 
109
109
  Raises:
110
- BookingNotFoundError: If the booking does not exist.
110
+ ResourceNotFoundError: If the booking does not exist.
111
111
  ValueError: If class_uuid is None or empty string or if the API instance is not set.
112
112
  """
113
113
  self.raise_if_api_not_set()
@@ -161,7 +161,7 @@ class BookingV2(ApiMixin, OtfItemBase):
161
161
  repr=False,
162
162
  )
163
163
  updated_at: datetime = Field(
164
- ..., description="Date the booking was updated, not when the booking was made", exclude=True, repr=False
164
+ description="Date the booking was updated, not when the booking was made", exclude=True, repr=False
165
165
  )
166
166
 
167
167
  @property
@@ -18,7 +18,7 @@ class OtfClass(ApiMixin, OtfItemBase):
18
18
  class_uuid: str = Field(validation_alias="ot_base_class_uuid", description="The OTF class UUID")
19
19
  class_id: str | None = Field(None, validation_alias="id", description="Matches new booking endpoint class id")
20
20
 
21
- name: str | None = Field(None, description="The name of the class")
21
+ name: str = Field(..., description="The name of the class")
22
22
  class_type: ClassType = Field(validation_alias="type")
23
23
  coach: str | None = Field(None, validation_alias=AliasPath("coach", "first_name"))
24
24
  ends_at: datetime = Field(
@@ -90,7 +90,7 @@ class OtfClass(ApiMixin, OtfItemBase):
90
90
  AlreadyBookedError: If the class is already booked.
91
91
  OutsideSchedulingWindowError: If the class is outside the scheduling window.
92
92
  ValueError: If class_uuid is None or empty string.
93
- OtfException: If there is an error booking the class.
93
+ OtfError: If there is an error booking the class.
94
94
  """
95
95
  self.raise_if_api_not_set()
96
96
  new_booking = self._api.bookings.book_class(self.class_uuid)
@@ -101,7 +101,7 @@ class OtfClass(ApiMixin, OtfItemBase):
101
101
  """Cancels the class booking.
102
102
 
103
103
  Raises:
104
- BookingNotFoundError: If the booking does not exist.
104
+ ResourceNotFoundError: If the booking does not exist.
105
105
  ValueError: If booking_uuid is None or empty string or the API is not set.
106
106
  """
107
107
  self.raise_if_api_not_set()
@@ -114,11 +114,11 @@ class OtfClass(ApiMixin, OtfItemBase):
114
114
  Booking | BookingV2: The booking associated with this class.
115
115
 
116
116
  Raises:
117
- BookingNotFoundError: If the booking does not exist.
117
+ ResourceNotFoundError: If the booking does not exist.
118
118
  ValueError: If the API is not set.
119
119
  """
120
120
  self.raise_if_api_not_set()
121
121
  try:
122
122
  return self._api.bookings.get_booking_from_class(self)
123
- except exc.BookingNotFoundError:
123
+ except exc.ResourceNotFoundError:
124
124
  return self._api.bookings.get_booking_from_class_new(self)
@@ -59,16 +59,17 @@ class Telemetry(OtfItemBase):
59
59
  )
60
60
  class_start_time: datetime | None = Field(None, validation_alias="classStartTime")
61
61
  max_hr: int | None = Field(None, validation_alias="maxHr")
62
- zones: Zones
62
+ zones: Zones | None = Field(default=None, description="The zones associated with the telemetry.")
63
63
  window_size: int | None = Field(None, validation_alias="windowSize")
64
64
  telemetry: list[TelemetryItem] = Field(default_factory=list)
65
65
 
66
66
  def __init__(self, **data: dict[str, Any]):
67
67
  super().__init__(**data)
68
- for telem in self.telemetry:
69
- if self.class_start_time is None:
70
- continue
71
68
 
69
+ if self.class_start_time is None:
70
+ return
71
+
72
+ for telem in self.telemetry:
72
73
  telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp)
73
74
 
74
75
  @field_serializer("telemetry", when_used="json")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otf-api
3
- Version: 0.14.1
3
+ Version: 0.15.1
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  Author-email: Jessica Smith <j.smith.git1@gmail.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes