@@ -80,11 +80,37 @@ def get_bookings_new(
8080 ends_before = end_date , starts_after = start_date , include_canceled = include_canceled , expand = expand
8181 )
8282
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
8494
8595 if not remove_duplicates :
8696 return results
8797
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+ """
88114 # remove duplicates by class_id, keeping the one with the most recent updated_at timestamp
89115 seen_classes : dict [str , models .BookingV2 ] = {}
90116
@@ -188,7 +214,11 @@ def get_classes(
188214 for c in classes_resp :
189215 c ["studio" ] = studio_dict [c ["studio" ]["id" ]] # the one (?) place where ID actually means UUID
190216 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
192222
193223 # additional data filtering and enrichment
194224
@@ -240,7 +270,7 @@ def get_booking_from_class(self, otf_class: str | models.OtfClass) -> models.Boo
240270 Booking: The booking.
241271
242272 Raises:
243- BookingNotFoundError : If the booking does not exist.
273+ ResourceNotFoundError : If the booking does not exist.
244274 ValueError: If class_uuid is None or empty string.
245275 """
246276 class_uuid = utils .get_class_uuid (otf_class )
@@ -250,7 +280,7 @@ def get_booking_from_class(self, otf_class: str | models.OtfClass) -> models.Boo
250280 if booking := next ((b for b in all_bookings if b .class_uuid == class_uuid ), None ):
251281 return booking
252282
253- raise exc .BookingNotFoundError (f"Booking for class { class_uuid } not found." )
283+ raise exc .ResourceNotFoundError (f"Booking for class { class_uuid } not found." )
254284
255285 def get_booking_from_class_new (self , otf_class : str | models .OtfClass | models .BookingV2Class ) -> models .BookingV2 :
256286 """Get a specific booking by class_uuid or OtfClass object.
@@ -262,7 +292,7 @@ def get_booking_from_class_new(self, otf_class: str | models.OtfClass | models.B
262292 BookingV2: The booking.
263293
264294 Raises:
265- BookingNotFoundError : If the booking does not exist.
295+ ResourceNotFoundError : If the booking does not exist.
266296 ValueError: If class_uuid is None or empty string.
267297 """
268298 class_uuid = utils .get_class_uuid (otf_class )
@@ -272,7 +302,78 @@ def get_booking_from_class_new(self, otf_class: str | models.OtfClass | models.B
272302 if booking := next ((b for b in all_bookings if b .class_uuid == class_uuid ), None ):
273303 return booking
274304
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." )
276377
277378 def book_class (self , otf_class : str | models .OtfClass ) -> models .Booking :
278379 """Book a class by providing either the class_uuid or the OtfClass object.
@@ -287,7 +388,7 @@ def book_class(self, otf_class: str | models.OtfClass) -> models.Booking:
287388 AlreadyBookedError: If the class is already booked.
288389 OutsideSchedulingWindowError: If the class is outside the scheduling window.
289390 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.
291392 """
292393 class_uuid = utils .get_class_uuid (otf_class )
293394
@@ -297,7 +398,7 @@ def book_class(self, otf_class: str | models.OtfClass) -> models.Booking:
297398 raise exc .AlreadyBookedError (
298399 f"Class { class_uuid } is already booked." , booking_uuid = existing_booking .booking_uuid
299400 )
300- except exc .BookingNotFoundError :
401+ except exc .ResourceNotFoundError :
301402 pass
302403
303404 if isinstance (otf_class , models .OtfClass ):
@@ -328,7 +429,7 @@ def book_class_new(self, class_id: str | models.BookingV2Class) -> models.Bookin
328429 BookingV2: The booking.
329430
330431 Raises:
331- OtfException : If there is an error booking the class.
432+ OtfError : If there is an error booking the class.
332433 TypeError: If the input is not a string or BookingV2Class.
333434 """
334435 class_id = utils .get_class_id (class_id )
@@ -349,7 +450,7 @@ def cancel_booking(self, booking: str | models.Booking) -> None:
349450
350451 Raises:
351452 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.
353454 """
354455 if isinstance (booking , models .BookingV2 ):
355456 LOGGER .warning ("BookingV2 object provided, using the new cancel booking endpoint (`cancel_booking_new`)" )
@@ -370,7 +471,7 @@ def cancel_booking_new(self, booking: str | models.BookingV2) -> None:
370471
371472 Raises:
372473 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.
374475 """
375476 if isinstance (booking , models .Booking ):
376477 LOGGER .warning ("Booking object provided, using the old cancel booking endpoint (`cancel_booking`)" )
@@ -443,7 +544,15 @@ def get_bookings(
443544 b ["class" ]["studio" ] = studios [b ["class" ]["studio" ]["studioUUId" ]]
444545 b ["is_home_studio" ] = b ["class" ]["studio" ].studio_uuid == self .otf .home_studio_uuid
445546
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+
447556 bookings = sorted (bookings , key = lambda x : x .otf_class .starts_at )
448557
449558 if exclude_cancelled :
0 commit comments