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 +1 -1
- otf_api/api.py +97 -80
- otf_api/auth/auth.py +2 -1
- {otf_api-0.11.0rc1.dist-info → otf_api-0.11.1.dist-info}/METADATA +1 -1
- {otf_api-0.11.0rc1.dist-info → otf_api-0.11.1.dist-info}/RECORD +8 -8
- {otf_api-0.11.0rc1.dist-info → otf_api-0.11.1.dist-info}/WHEEL +0 -0
- {otf_api-0.11.0rc1.dist-info → otf_api-0.11.1.dist-info}/licenses/LICENSE +0 -0
- {otf_api-0.11.0rc1.dist-info → otf_api-0.11.1.dist-info}/top_level.txt +0 -0
otf_api/__init__.py
CHANGED
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
|
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
|
-
|
127
|
-
|
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,
|
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]
|
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}"
|
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
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
783
|
-
|
784
|
-
|
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
|
-
|
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
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
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
|
-
|
810
|
-
|
811
|
-
|
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
|
-
|
843
|
+
booking_id = get_booking_id(booking)
|
814
844
|
|
815
|
-
|
816
|
-
|
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
|
-
|
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
|
-
|
1487
|
-
|
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
|
-
|
1490
|
-
|
1522
|
+
Returns:
|
1523
|
+
Workout: The member's workout.
|
1491
1524
|
|
1492
|
-
|
1493
|
-
|
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
|
-
|
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
|
-
|
1502
|
-
|
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
|
-
|
1506
|
-
|
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
|
-
|
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
|
-
|
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,12 +1,12 @@
|
|
1
|
-
otf_api/__init__.py,sha256=
|
2
|
-
otf_api/api.py,sha256=
|
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=
|
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.
|
35
|
-
otf_api-0.11.
|
36
|
-
otf_api-0.11.
|
37
|
-
otf_api-0.11.
|
38
|
-
otf_api-0.11.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|