otf-api 0.14.1__tar.gz → 0.15.0__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.
- {otf_api-0.14.1/src/otf_api.egg-info → otf_api-0.15.0}/PKG-INFO +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/pyproject.toml +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/__init__.py +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/bookings/booking_api.py +121 -12
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/bookings/booking_client.py +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/client.py +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/studios/studio_api.py +39 -7
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/workouts/workout_api.py +12 -9
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/exceptions.py +7 -11
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/bookings_v2.py +3 -3
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/classes.py +5 -5
- {otf_api-0.14.1 → otf_api-0.15.0/src/otf_api.egg-info}/PKG-INFO +1 -1
- {otf_api-0.14.1 → otf_api-0.15.0}/LICENSE +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/README.md +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/setup.cfg +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/_compat.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/api.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/bookings/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/members/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/members/member_api.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/members/member_client.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/studios/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/studios/studio_client.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/utils.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/workouts/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/api/workouts/workout_client.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/auth/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/auth/auth.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/auth/user.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/auth/utils.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/cache.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/base.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/bookings.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/enums.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/filters.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/bookings/ratings.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/members/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/members/member_detail.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/members/member_membership.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/members/member_purchases.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/members/notifications.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/mixins.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/studios/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/studios/enums.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/studios/studio_detail.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/studios/studio_services.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/__init__.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/body_composition_list.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/challenge_tracker_content.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/challenge_tracker_detail.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/enums.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/lifetime_stats.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/out_of_studio_workout_history.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/performance_summary.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/telemetry.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/workout.py +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/py.typed +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api.egg-info/SOURCES.txt +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api.egg-info/dependency_links.txt +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api.egg-info/requires.txt +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api.egg-info/top_level.txt +0 -0
- {otf_api-0.14.1 → otf_api-0.15.0}/tests/test_filters.py +0 -0
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
7
|
+
class OtfError(Exception):
|
8
8
|
"""Base class for all exceptions in this package."""
|
9
9
|
|
10
10
|
|
11
|
-
class OtfRequestError(
|
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(
|
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(
|
56
|
+
class OutsideSchedulingWindowError(OtfError):
|
57
57
|
"""Raised when attempting to book a class outside the scheduling window."""
|
58
58
|
|
59
59
|
|
60
|
-
class
|
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(
|
64
|
+
class AlreadyRatedError(OtfError):
|
69
65
|
"""Raised when attempting to rate a class that is already rated."""
|
70
66
|
|
71
67
|
|
72
|
-
class ClassNotRatableError(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
123
|
+
except exc.ResourceNotFoundError:
|
124
124
|
return self._api.bookings.get_booking_from_class_new(self)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{otf_api-0.14.1 → otf_api-0.15.0}/src/otf_api/models/workouts/out_of_studio_workout_history.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|