otf-api 0.11.0rc1__py3-none-any.whl → 0.12.0__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.
otf_api/__init__.py CHANGED
@@ -4,7 +4,7 @@ from otf_api.api import Otf
4
4
  from otf_api import models
5
5
  from otf_api.auth import OtfUser
6
6
 
7
- __version__ = "0.11.0-rc1"
7
+ __version__ = "0.12.0"
8
8
 
9
9
 
10
10
  __all__ = ["Otf", "OtfUser", "models"]
otf_api/api.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import atexit
2
2
  import contextlib
3
3
  from concurrent.futures import ThreadPoolExecutor
4
- from datetime import date, datetime, timedelta
4
+ from datetime import date, datetime, time, timedelta
5
5
  from functools import partial
6
6
  from json import JSONDecodeError
7
7
  from logging import getLogger
@@ -18,7 +18,7 @@ from otf_api import exceptions as exc
18
18
  from otf_api import filters, models
19
19
  from otf_api.auth import OtfUser
20
20
  from otf_api.models.enums import HISTORICAL_BOOKING_STATUSES
21
- from otf_api.utils import ensure_date, ensure_datetime, ensure_list, get_booking_uuid, get_class_uuid # get_booking_id
21
+ from otf_api.utils import ensure_date, ensure_datetime, ensure_list, get_booking_id, get_booking_uuid, get_class_uuid
22
22
 
23
23
  API_BASE_URL = "api.orangetheory.co"
24
24
  API_IO_BASE_URL = "api.orangetheory.io"
@@ -69,7 +69,7 @@ class Otf:
69
69
  return hash(self.member_uuid)
70
70
 
71
71
  @retry(
72
- retry=retry_if_exception_type(exc.OtfRequestError),
72
+ retry=retry_if_exception_type((exc.OtfRequestError, httpx.HTTPStatusError)),
73
73
  stop=stop_after_attempt(3),
74
74
  wait=wait_exponential(multiplier=1, min=4, max=10),
75
75
  reraise=True,
@@ -123,8 +123,11 @@ class Otf:
123
123
  raise
124
124
 
125
125
  if not response.text:
126
- # insanely enough, at least one endpoint (get perf summary) returns None without error instead of 404
127
- raise exc.OtfRequestError("Empty response", None, response=response, request=request)
126
+ if method == "GET":
127
+ raise exc.OtfRequestError("Empty response", None, response=response, request=request)
128
+
129
+ LOGGER.debug(f"Request {method!r} to {full_url!r} returned no content")
130
+ return None
128
131
 
129
132
  try:
130
133
  resp = response.json()
@@ -144,10 +147,15 @@ class Otf:
144
147
  return resp
145
148
 
146
149
  def _classes_request(
147
- self, method: str, url: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None
150
+ self,
151
+ method: str,
152
+ url: str,
153
+ params: dict[str, Any] | None = None,
154
+ headers: dict[str, Any] | None = None,
155
+ **kwargs: Any,
148
156
  ) -> Any:
149
157
  """Perform an API request to the classes API."""
150
- return self._do(method, API_IO_BASE_URL, url, params, headers=headers)
158
+ return self._do(method, API_IO_BASE_URL, url, params, headers=headers, **kwargs)
151
159
 
152
160
  def _default_request(
153
161
  self,
@@ -175,7 +183,7 @@ class Otf:
175
183
 
176
184
  return self._do(method, API_IO_BASE_URL, url, params, headers=headers)
177
185
 
178
- def _get_classes_raw(self, studio_uuids: list[str] | None) -> dict:
186
+ def _get_classes_raw(self, studio_uuids: list[str]) -> dict:
179
187
  """Retrieve raw class data."""
180
188
  return self._classes_request("GET", "/v1/classes", params={"studio_ids": studio_uuids})
181
189
 
@@ -203,6 +211,11 @@ class Otf:
203
211
  raise exc.OtfException(f"Error booking class {class_uuid}: {e}")
204
212
  return resp
205
213
 
214
+ def _book_class_new_raw(self, body: dict[str, str | bool]) -> dict:
215
+ """Book a class by class_id."""
216
+
217
+ return self._classes_request("POST", "/v1/bookings/me", json=body)
218
+
206
219
  def _get_booking_raw(self, booking_uuid: str) -> dict:
207
220
  """Retrieve raw booking data."""
208
221
  return self._default_request("GET", f"/member/members/{self.member_uuid}/bookings/{booking_uuid}")
@@ -240,15 +253,7 @@ class Otf:
240
253
 
241
254
  def _cancel_booking_new_raw(self, booking_id: str) -> dict:
242
255
  """Cancel a booking by booking_id."""
243
- return self._classes_request("DELETE", f"/v1/bookings/me/{booking_id}", headers={"SIGV4AUTH_REQUIRED": "true"})
244
-
245
- def _get_booking_new_raw(self, booking_id: str) -> dict:
246
- """Retrieve raw booking data."""
247
- return self._classes_request("GET", f"/v1/bookings/me/{booking_id}", headers={"SIGV4AUTH_REQUIRED": "true"})
248
-
249
- def _book_class_new_raw(self, class_id: str) -> dict:
250
- """Book a class by class_id."""
251
- return self._classes_request("POST", f"/v1/bookings/me/{class_id}", headers={"SIGV4AUTH_REQUIRED": "true"})
256
+ return self._classes_request("DELETE", f"/v1/bookings/me/{booking_id}")
252
257
 
253
258
  def _get_member_detail_raw(self) -> dict:
254
259
  """Retrieve raw member details."""
@@ -432,13 +437,16 @@ class Otf:
432
437
  """Get bookings from the new endpoint with no date filters."""
433
438
  start_date = pendulum.datetime(1970, 1, 1)
434
439
  end_date = pendulum.today().start_of("day").add(days=45)
435
- return self.get_bookings_new(start_date, end_date, exclude_canceled=False)
440
+ return self.get_bookings_new(start_date, end_date, exclude_cancelled=False)
441
+
442
+ def _get_app_config_raw(self) -> dict[str, Any]:
443
+ return self._default_request("GET", "/member/app-configurations", headers={"SIGV4AUTH_REQUIRED": "true"})
436
444
 
437
445
  def get_bookings_new(
438
446
  self,
439
- start_dtme: datetime | str | None = None,
440
- end_dtme: datetime | str | None = None,
441
- exclude_canceled: bool = True,
447
+ start_date: datetime | str | None = None,
448
+ end_date: datetime | str | None = None,
449
+ exclude_cancelled: bool = True,
442
450
  ) -> list[models.BookingV2]:
443
451
  """Get the bookings for the user. If no dates are provided, it will return all bookings
444
452
  between today and 45 days from now.
@@ -450,9 +458,9 @@ class Otf:
450
458
  new class, which is normally transparent to the user.
451
459
 
452
460
  Args:
453
- start_dtme (datetime | str | None): The start date for the bookings. Default is None.
454
- end_dtme (datetime | str | None): The end date for the bookings. Default is None.
455
- exclude_canceled (bool): Whether to exclude canceled bookings. Default is True.
461
+ start_dtme (datetime | date | str | None): The start date for the bookings. Default is None.
462
+ end_dtme (datetime | date | str | None): The end date for the bookings. Default is None.
463
+ exclude_cancelled (bool): Whether to exclude canceled bookings. Default is True.
456
464
  Returns:
457
465
  list[BookingV2]: The bookings for the user.
458
466
  """
@@ -460,24 +468,27 @@ class Otf:
460
468
  expand = True # this doesn't seem to have an effect? so leaving it out of the argument list
461
469
 
462
470
  # leaving the parameter as `exclude_canceled` for backwards compatibility
463
- include_canceled = not exclude_canceled
471
+ include_canceled = not exclude_cancelled
464
472
 
465
- end_dtme = ensure_datetime(end_dtme)
466
- start_dtme = ensure_datetime(start_dtme)
473
+ end_date = ensure_datetime(end_date, time(23, 59, 59))
474
+ start_date = ensure_datetime(start_date)
467
475
 
468
- end_dtme = end_dtme or pendulum.today().start_of("day").add(days=45)
469
- start_dtme = start_dtme or pendulum.datetime(1970, 1, 1).start_of("day")
476
+ end_date = end_date or pendulum.today().start_of("day").add(days=45)
477
+ start_date = start_date or pendulum.datetime(1970, 1, 1).start_of("day")
470
478
 
471
479
  bookings_resp = self._get_bookings_new_raw(
472
- ends_before=end_dtme, starts_after=start_dtme, include_canceled=include_canceled, expand=expand
480
+ ends_before=end_date, starts_after=start_date, include_canceled=include_canceled, expand=expand
473
481
  )
474
482
 
475
483
  return [models.BookingV2(**b) for b in bookings_resp["items"]]
476
484
 
477
- # def get_booking_new(self, booking_id: str) -> models.BookingV2:
478
- # """Get a booking by ID."""
479
- # booking_resp = self._get_booking_new_raw(booking_id)
480
- # return models.BookingV2(**booking_resp)
485
+ def get_booking_new(self, booking_id: str) -> models.BookingV2:
486
+ """Get a booking by ID."""
487
+ all_bookings = self._get_all_bookings_new()
488
+ booking = next((b for b in all_bookings if b.booking_id == booking_id), None)
489
+ if not booking:
490
+ raise exc.ResourceNotFoundError(f"Booking with ID {booking_id} not found")
491
+ return booking
481
492
 
482
493
  def get_classes(
483
494
  self,
@@ -493,11 +504,11 @@ class Otf:
493
504
  UUIDs are provided, it will default to the user's home studio.
494
505
 
495
506
  Args:
496
- start_date (date | None): The start date for the classes. Default is None.
497
- end_date (date | None): The end date for the classes. Default is None.
507
+ start_date (date | str | None): The start date for the classes. Default is None.
508
+ end_date (date | str | None): The end date for the classes. Default is None.
498
509
  studio_uuids (list[str] | None): The studio UUIDs to get the classes for. Default is None, which will\
499
510
  default to the user's home studio only.
500
- include_home_studio (bool): Whether to include the home studio in the classes. Default is True.
511
+ include_home_studio (bool | None): Whether to include the home studio in the classes. Default is True.
501
512
  filters (list[ClassFilter] | ClassFilter | None): A list of filters to apply to the classes, or a single\
502
513
  filter. Filters are applied as an OR operation. Default is None.
503
514
 
@@ -726,6 +737,26 @@ class Otf:
726
737
 
727
738
  return booking
728
739
 
740
+ def book_class_new(self, class_id: str) -> models.BookingV2:
741
+ """Book a class by providing the class_id.
742
+
743
+ Args:
744
+ class_id (str): The class ID to book.
745
+
746
+ Returns:
747
+ BookingV2: The booking.
748
+ """
749
+ if not class_id:
750
+ raise ValueError("class_id is required")
751
+
752
+ body = {"class_id": class_id, "confirmed": False, "waitlist": False}
753
+
754
+ resp = self._book_class_new_raw(body)
755
+
756
+ new_booking = models.BookingV2(**resp)
757
+
758
+ return new_booking
759
+
729
760
  def _check_class_already_booked(self, class_uuid: str) -> None:
730
761
  """Check if the class is already booked.
731
762
 
@@ -779,17 +810,14 @@ class Otf:
779
810
  ValueError: If booking_uuid is None or empty string
780
811
  BookingNotFoundError: If the booking does not exist.
781
812
  """
782
- # if isinstance(booking, models.BookingV2):
783
- # LOGGER.warning("BookingV2 object provided, using the new cancel booking endpoint (`cancel_booking_new`)")
784
- # self.cancel_booking_new(booking)
813
+ if isinstance(booking, models.BookingV2):
814
+ LOGGER.warning("BookingV2 object provided, using the new cancel booking endpoint (`cancel_booking_new`)")
815
+ self.cancel_booking_new(booking)
785
816
 
786
817
  booking_uuid = get_booking_uuid(booking)
787
818
 
788
819
  if booking == booking_uuid: # ensure this booking exists by calling the booking endpoint
789
- try:
790
- self.get_booking(booking_uuid)
791
- except Exception:
792
- raise exc.BookingNotFoundError(f"Booking {booking_uuid} does not exist.")
820
+ _ = self.get_booking(booking_uuid) # allow the exception to be raised if it doesn't exist
793
821
 
794
822
  resp = self._cancel_booking_raw(booking_uuid)
795
823
  if resp["code"] == "NOT_AUTHORIZED" and resp["message"].startswith("This class booking has"):
@@ -797,25 +825,27 @@ class Otf:
797
825
  f"Booking {booking_uuid} is already cancelled.", booking_uuid=booking_uuid
798
826
  )
799
827
 
800
- # def cancel_booking_new(self, booking: str | models.BookingV2) -> None:
801
- # """Cancel a booking by providing either the booking_id or the BookingV2 object.
802
- # Args:
803
- # booking (str | BookingV2): The booking ID or the BookingV2 object to cancel.
804
- # Raises:
805
- # ValueError: If booking_id is None or empty string
806
- # BookingNotFoundError: If the booking does not exist.
807
- # """
828
+ def cancel_booking_new(self, booking: str | models.BookingV2) -> None:
829
+ """Cancel a booking by providing either the booking_id or the BookingV2 object.
830
+
831
+ Args:
832
+ booking (str | BookingV2): The booking ID or the BookingV2 object to cancel.
808
833
 
809
- # if isinstance(booking, models.Booking):
810
- # LOGGER.warning("Booking object provided, using the old cancel booking endpoint (`cancel_booking`)")
811
- # self.cancel_booking(booking)
834
+ Raises:
835
+ ValueError: If booking_id is None or empty string
836
+ BookingNotFoundError: If the booking does not exist.
837
+ """
812
838
 
813
- # booking_id = get_booking_id(booking)
839
+ if isinstance(booking, models.Booking):
840
+ LOGGER.warning("Booking object provided, using the old cancel booking endpoint (`cancel_booking`)")
841
+ self.cancel_booking(booking)
814
842
 
815
- # if booking == booking_id: # ensure this booking exists by calling the booking endpoint
816
- # self.get_booking_new(booking_id)
843
+ booking_id = get_booking_id(booking)
817
844
 
818
- # self._cancel_booking_new_raw(booking_id)
845
+ if booking == booking_id:
846
+ _ = self.get_booking_new(booking_id) # allow the exception to be raised if it doesn't exist
847
+
848
+ self._cancel_booking_new_raw(booking_id)
819
849
 
820
850
  def get_bookings(
821
851
  self,
@@ -1483,32 +1513,31 @@ class Otf:
1483
1513
  raise exc.AlreadyRatedError(f"Workout {performance_summary_id} is already rated.") from None
1484
1514
  raise
1485
1515
 
1486
- # def get_workout_from_booking(self, booking: str | models.BookingV2) -> models.Workout:
1487
- # """Get a workout for a specific booking.
1516
+ def get_workout_from_booking(self, booking: str | models.BookingV2) -> models.Workout:
1517
+ """Get a workout for a specific booking.
1518
+
1519
+ Args:
1520
+ booking (str | Booking): The booking ID or BookingV2 object to get the workout for.
1488
1521
 
1489
- # Args:
1490
- # booking_id (str | Booking): The booking ID or Booking object to get the workout for.
1522
+ Returns:
1523
+ Workout: The member's workout.
1491
1524
 
1492
- # Returns:
1493
- # Workout: The member's workout.
1525
+ Raises:
1526
+ BookingNotFoundError: If the booking does not exist.
1527
+ ResourceNotFoundError: If the workout does not exist.
1528
+ """
1529
+ booking_id = get_booking_id(booking)
1494
1530
 
1495
- # Raises:
1496
- # BookingNotFoundError: If the booking does not exist.
1497
- # ResourceNotFoundError: If the workout does not exist.
1498
- # """
1499
- # booking_id = booking if isinstance(booking, str) else booking.booking_id
1531
+ booking = self.get_booking_new(booking_id)
1500
1532
 
1501
- # booking = self.get_booking_new(booking_id)
1502
- # if not booking:
1503
- # raise exc.BookingNotFoundError(f"Booking {booking_id} not found.")
1533
+ if not booking.workout or not booking.workout.performance_summary_id:
1534
+ raise exc.ResourceNotFoundError(f"Workout for booking {booking_id} not found.")
1504
1535
 
1505
- # if not booking.workout or not booking.workout.performance_summary_id:
1506
- # raise exc.ResourceNotFoundError(f"Workout for booking {booking_id} not found.")
1536
+ perf_summary = self._get_performance_summary_raw(booking.workout.performance_summary_id)
1537
+ telemetry = self.get_telemetry(booking.workout.performance_summary_id)
1538
+ workout = models.Workout(**perf_summary, v2_booking=booking, telemetry=telemetry)
1507
1539
 
1508
- # perf_summary = self._get_performance_summary_raw(booking.workout.performance_summary_id)
1509
- # telemetry = self.get_telemetry(booking.workout.performance_summary_id)
1510
- # workout = models.Workout(**perf_summary, v2_booking=booking, telemetry=telemetry)
1511
- # return workout
1540
+ return workout
1512
1541
 
1513
1542
  def get_workouts(
1514
1543
  self, start_date: date | str | None = None, end_date: date | str | None = None
@@ -1516,8 +1545,8 @@ class Otf:
1516
1545
  """Get the member's workouts, using the new bookings endpoint and the performance summary endpoint.
1517
1546
 
1518
1547
  Args:
1519
- start_date (date | None): The start date for the workouts. If None, defaults to 30 days ago.
1520
- end_date (date | None): The end date for the workouts. If None, defaults to today.
1548
+ start_date (date | str | None): The start date for the workouts. If None, defaults to 30 days ago.
1549
+ end_date (date | str | None): The end date for the workouts. If None, defaults to today.
1521
1550
 
1522
1551
  Returns:
1523
1552
  list[Workout]: The member's workouts.
@@ -1528,7 +1557,7 @@ class Otf:
1528
1557
  start_dtme = pendulum.datetime(start_date.year, start_date.month, start_date.day, 0, 0, 0)
1529
1558
  end_dtme = pendulum.datetime(end_date.year, end_date.month, end_date.day, 23, 59, 59)
1530
1559
 
1531
- bookings = self.get_bookings_new(start_dtme, end_dtme, exclude_canceled=False)
1560
+ bookings = self.get_bookings_new(start_dtme, end_dtme, exclude_cancelled=False)
1532
1561
  bookings_dict = {b.workout.id: b for b in bookings if b.workout}
1533
1562
 
1534
1563
  perf_summaries_dict = self._get_perf_summaries_threaded(list(bookings_dict.keys()))
@@ -1621,19 +1650,7 @@ class Otf:
1621
1650
 
1622
1651
  self.rate_class(workout.class_uuid, workout.performance_summary_id, class_rating, coach_rating)
1623
1652
 
1624
- # TODO: use this once we get it working, getting workout list is substitute for now
1625
- # return self.get_workout_from_booking(workout.booking_id)
1626
-
1627
- workout_date = pendulum.instance(workout.otf_class.starts_at).start_of("day")
1628
- workouts = self.get_workouts(workout_date.subtract(days=1), workout_date.add(days=1))
1629
-
1630
- selected_workout = next(
1631
- (w for w in workouts if w.performance_summary_id == workout.performance_summary_id), None
1632
- )
1633
-
1634
- assert selected_workout is not None, "Workout not found in the list of workouts"
1635
-
1636
- return selected_workout
1653
+ return self.get_workout_from_booking(workout.booking_id)
1637
1654
 
1638
1655
  # the below do not return any data for me, so I can't test them
1639
1656
 
otf_api/auth/auth.py CHANGED
@@ -29,7 +29,6 @@ LOGGER = getLogger(__name__)
29
29
  CLIENT_ID = "1457d19r0pcjgmp5agooi0rb1b" # from android app
30
30
  USER_POOL_ID = "us-east-1_dYDxUeyL1"
31
31
  REGION = "us-east-1"
32
- COGNITO_IDP_URL = f"https://cognito-idp.{REGION}.amazonaws.com/"
33
32
 
34
33
  ID_POOL_ID = "us-east-1:4943c880-fb02-4fd7-bc37-2f4c32ecb2a3"
35
34
  PROVIDER_KEY = f"cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}"
@@ -296,6 +295,8 @@ class HttpxCognitoAuth(httpx.Auth):
296
295
 
297
296
  request.headers[self.http_header] = self.http_header_prefix + token
298
297
 
298
+ # If the request has the SIGV4AUTH_REQUIRED header, sign the request
299
+ # used by very few endpoints, but I expect that may change in the future
299
300
  if request.headers.get("SIGV4AUTH_REQUIRED"):
300
301
  del request.headers["SIGV4AUTH_REQUIRED"]
301
302
  yield from self.sign_httpx_request(request)
@@ -36,6 +36,11 @@ class OtfClass(OtfItemBase):
36
36
  program_name: str | None = Field(None, alias="programName", exclude=True, repr=False)
37
37
  virtual_class: bool | None = Field(None, alias="virtualClass", exclude=True, repr=False)
38
38
 
39
+ @property
40
+ def coach_name(self) -> str:
41
+ """Shortcut to get the coach's name, to be compatible with new BookingV2Class"""
42
+ return self.coach.first_name or ""
43
+
39
44
  def __str__(self) -> str:
40
45
  starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
41
46
  return f"Class: {starts_at_str} {self.name} - {self.coach.first_name}"
@@ -90,6 +95,11 @@ class Booking(OtfItemBase):
90
95
  """Shortcut to get the class end time"""
91
96
  return self.otf_class.ends_at
92
97
 
98
+ @property
99
+ def id_value(self) -> str:
100
+ """Returns the booking_uuid, to be compatible with new BookingV2 model"""
101
+ return self.booking_uuid
102
+
93
103
  def __str__(self) -> str:
94
104
  starts_at_str = self.otf_class.starts_at.strftime("%a %b %d, %I:%M %p")
95
105
  class_name = self.otf_class.name
@@ -72,6 +72,16 @@ class BookingV2Class(OtfItemBase):
72
72
  )
73
73
  starts_at_utc: datetime | None = Field(None, alias="starts_at", exclude=True, repr=False)
74
74
 
75
+ @property
76
+ def coach_name(self) -> str:
77
+ """Shortcut to get the coach's name, to be compatible with old Booking OtfClass model"""
78
+ return self.coach or ""
79
+
80
+ @property
81
+ def ends_at(self) -> datetime:
82
+ """Emulates the end time of the class, to be compatible with old Booking OtfClass model"""
83
+ return get_end_time(self.starts_at, self.class_type)
84
+
75
85
  def __str__(self) -> str:
76
86
  starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
77
87
  return f"Class: {starts_at_str} {self.name} - {self.coach}"
@@ -160,7 +170,17 @@ class BookingV2(OtfItemBase):
160
170
  @property
161
171
  def ends_at(self) -> datetime:
162
172
  """Shortcut to get the class end time"""
163
- return get_end_time(self.otf_class.starts_at, self.otf_class.class_type)
173
+ return self.otf_class.ends_at
174
+
175
+ @property
176
+ def cancelled_date(self) -> datetime | None:
177
+ """Returns the canceled_at value in a backward-compatible way"""
178
+ return self.canceled_at
179
+
180
+ @property
181
+ def id_value(self) -> str:
182
+ """Returns the booking_id, to be compatible with old Booking model"""
183
+ return self.booking_id
164
184
 
165
185
  def __str__(self) -> str:
166
186
  starts_at_str = self.otf_class.starts_at.strftime("%a %b %d, %I:%M %p")
otf_api/utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import typing
3
- from datetime import date, datetime
3
+ from datetime import date, datetime, time
4
4
  from logging import getLogger
5
5
  from pathlib import Path
6
6
  from typing import Any
@@ -12,6 +12,8 @@ if typing.TYPE_CHECKING:
12
12
 
13
13
  LOGGER = getLogger(__name__)
14
14
 
15
+ MIN_TIME = datetime.min.time()
16
+
15
17
 
16
18
  def get_booking_uuid(booking_or_uuid: "str | models.Booking") -> str:
17
19
  from otf_api.models.bookings import Booking
@@ -58,7 +60,7 @@ def ensure_list(obj: list | Any | None) -> list:
58
60
  return obj
59
61
 
60
62
 
61
- def ensure_datetime(date_str: str | datetime | None) -> datetime | None:
63
+ def ensure_datetime(date_str: str | datetime | None, combine_with: time = MIN_TIME) -> datetime | None:
62
64
  if not date_str:
63
65
  return None
64
66
 
@@ -69,7 +71,7 @@ def ensure_datetime(date_str: str | datetime | None) -> datetime | None:
69
71
  return date_str
70
72
 
71
73
  if isinstance(date_str, date):
72
- return datetime.combine(date_str, datetime.min.time())
74
+ return datetime.combine(date_str, combine_with)
73
75
 
74
76
  raise ValueError(f"Expected str or datetime, got {type(date_str)}")
75
77
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otf-api
3
- Version: 0.11.0rc1
3
+ Version: 0.12.0
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,19 +1,19 @@
1
- otf_api/__init__.py,sha256=9EKuuZV9IGzs0IR8i13YCi-tO5qYQ9rFVKqvxyleKuo,209
2
- otf_api/api.py,sha256=XbPj4MR0spEoN_1rfh7TjiTI9j5HjSVgqX3bhQGtxns,67643
1
+ otf_api/__init__.py,sha256=QbShJl50bmebZLa_dNvs9PAoexI6pHGY1d_3endsC7M,205
2
+ otf_api/api.py,sha256=EtdFB-ZYQAP3b5Ghz-uNQIm_p6wr7Yf8fyJyL6iEBc0,67554
3
3
  otf_api/exceptions.py,sha256=GISekwF5dPt0Ol0WCU55kE5ODc5VxicNEEhmlguuE0U,1815
4
4
  otf_api/filters.py,sha256=fk2bFGi3srjS96qZlaDx-ARZRaj93NUTUdMJ01TX420,3702
5
5
  otf_api/logging.py,sha256=PRZpCaJ1F1Xya3L9Efkt3mKS5_QNr3sXjEUERSxYjvE,563
6
6
  otf_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- otf_api/utils.py,sha256=TATPgb5-t3nEAqo_iGY2p2PKzJA564alN1skb23OCMQ,4817
7
+ otf_api/utils.py,sha256=Yr_BKzQ28ARzHN5FvMkVIgZ1_3icZKaqFzs4SDv6ugw,4879
8
8
  otf_api/auth/__init__.py,sha256=PuhtiZ02GVP8zVSn1O-fhaYm7JM_tq5yUFATk_-8upk,132
9
- otf_api/auth/auth.py,sha256=69iZIGFn9l470PrUfLvwdMTIL0tHYJ3tvuKOxy7NSyU,13185
9
+ otf_api/auth/auth.py,sha256=5cPELC9Soif5knDDHm55ii1OMEPkJlGUphAdbOEmaRo,13278
10
10
  otf_api/auth/user.py,sha256=XlK3nbqJA4fF5UFmw2tt0eAle4QOQd6trnW72QrBsx4,2681
11
11
  otf_api/auth/utils.py,sha256=vc2pEyU-3yKdv0mR6r_zSHZyMw92j5UeIN_kzcb6TeE,2909
12
12
  otf_api/models/__init__.py,sha256=MSinaMQaBTGscL-YRKJ3axFiItQ1HoH62wC2xdaBMgk,1876
13
13
  otf_api/models/base.py,sha256=KJlIxl_sRj6f-g5vKYPw4yV6fGDk-fwZ93EO0JGPYMw,202
14
14
  otf_api/models/body_composition_list.py,sha256=jGdR-9ScvIOtULJNB99aYh2INk2ihoHAnTWtbQCIea4,12202
15
- otf_api/models/bookings.py,sha256=Lj-IHN1k-w4vF-5aIKbsiQ6Uq-I5_ELoPUKXPGkfbgM,4440
16
- otf_api/models/bookings_v2.py,sha256=9HyXGnDNDmIqmr4r0_3pR1mFtN_Lfp9nP-4bCiwe5QA,5865
15
+ otf_api/models/bookings.py,sha256=fnHZmN2F8fVSWS_tco3IwjO9SJbbxzuIs2WJcCC0gNs,4781
16
+ otf_api/models/bookings_v2.py,sha256=PFSlORNZF45TRvgLwewbVGoiOd3oDG1YGvvFMtXtbpo,6533
17
17
  otf_api/models/challenge_tracker_content.py,sha256=5Ucu1n4W15v1rzhoXNvAD9tCSg3JTUiR92HHiDAxRec,2597
18
18
  otf_api/models/challenge_tracker_detail.py,sha256=c2Ds7Kv2-VaPtxoXSUTI5zrmU1A1dcSaM1UIolwSVxU,4323
19
19
  otf_api/models/classes.py,sha256=aKV6LGEh0YiPxyOaoMD0gaQOSHqs69cYHhP9H_2p_jY,3051
@@ -31,8 +31,8 @@ otf_api/models/studio_detail.py,sha256=2gq0A27NOZGz_PTBvsB-dkzm01nYc9FHmx1NON6xp
31
31
  otf_api/models/studio_services.py,sha256=aGLQMQmjGVpI6YxzAl-mcp3Y9cHPXuH9dIqrl6E-78E,1665
32
32
  otf_api/models/telemetry.py,sha256=PQ_CbADW5-t-U2iEQJGugNy-c4rD0q76TfyIqeFnTho,3170
33
33
  otf_api/models/workout.py,sha256=P3xVTvcYrm_RdU6qi3Xm2BXTxxvhvF0dgoEcODY41AA,3678
34
- otf_api-0.11.0rc1.dist-info/licenses/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
35
- otf_api-0.11.0rc1.dist-info/METADATA,sha256=au0J0vMB2Edln-n_Swag6fh2Zneyw1nnle6jdTcJu9k,2148
36
- otf_api-0.11.0rc1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
37
- otf_api-0.11.0rc1.dist-info/top_level.txt,sha256=KAhYg1X2YG0LkTuVRhUV1I_AReNZUVNdEan7cp0pEE4,8
38
- otf_api-0.11.0rc1.dist-info/RECORD,,
34
+ otf_api-0.12.0.dist-info/licenses/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
35
+ otf_api-0.12.0.dist-info/METADATA,sha256=jmck88-0JUoy5Y8oezk2KnBJhJ63jxgwZX2xZu7OoKA,2145
36
+ otf_api-0.12.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
37
+ otf_api-0.12.0.dist-info/top_level.txt,sha256=KAhYg1X2YG0LkTuVRhUV1I_AReNZUVNdEan7cp0pEE4,8
38
+ otf_api-0.12.0.dist-info/RECORD,,