otf-api 0.11.0rc1__py3-none-any.whl → 0.11.1__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.11.1"
8
8
 
9
9
 
10
10
  __all__ = ["Otf", "OtfUser", "models"]
otf_api/api.py CHANGED
@@ -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."""
@@ -434,6 +439,9 @@ class Otf:
434
439
  end_date = pendulum.today().start_of("day").add(days=45)
435
440
  return self.get_bookings_new(start_date, end_date, exclude_canceled=False)
436
441
 
442
+ def _get_app_config_raw(self) -> dict[str, Any]:
443
+ return self._default_request("GET", "/member/app-configurations", headers={"SIGV4AUTH_REQUIRED": "true"})
444
+
437
445
  def get_bookings_new(
438
446
  self,
439
447
  start_dtme: datetime | str | None = None,
@@ -474,10 +482,13 @@ class Otf:
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.
833
+
834
+ Raises:
835
+ ValueError: If booking_id is None or empty string
836
+ BookingNotFoundError: If the booking does not exist.
837
+ """
808
838
 
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)
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)
812
842
 
813
- # booking_id = get_booking_id(booking)
843
+ booking_id = get_booking_id(booking)
814
844
 
815
- # if booking == booking_id: # ensure this booking exists by calling the booking endpoint
816
- # self.get_booking_new(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
817
847
 
818
- # self._cancel_booking_new_raw(booking_id)
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.
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otf-api
3
- Version: 0.11.0rc1
3
+ Version: 0.11.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,12 +1,12 @@
1
- otf_api/__init__.py,sha256=9EKuuZV9IGzs0IR8i13YCi-tO5qYQ9rFVKqvxyleKuo,209
2
- otf_api/api.py,sha256=XbPj4MR0spEoN_1rfh7TjiTI9j5HjSVgqX3bhQGtxns,67643
1
+ otf_api/__init__.py,sha256=VlLjNep7gdvPfnIDl1YbY8uY5eRM1vE4XJvNhI4mIB8,205
2
+ otf_api/api.py,sha256=95qIszuc2zjneEk-VhS9e-F5f2EnUs4lHGehL7rU2G4,67511
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
7
  otf_api/utils.py,sha256=TATPgb5-t3nEAqo_iGY2p2PKzJA564alN1skb23OCMQ,4817
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
@@ -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.11.1.dist-info/licenses/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
35
+ otf_api-0.11.1.dist-info/METADATA,sha256=BEppb5v_YlMUirZck7aKOtVkTemE7Uvantq2Gk8_00Y,2145
36
+ otf_api-0.11.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
37
+ otf_api-0.11.1.dist-info/top_level.txt,sha256=KAhYg1X2YG0LkTuVRhUV1I_AReNZUVNdEan7cp0pEE4,8
38
+ otf_api-0.11.1.dist-info/RECORD,,