otf-api 0.11.0rc1__tar.gz → 0.12.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.11.0rc1/src/otf_api.egg-info → otf_api-0.12.0}/PKG-INFO +1 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/pyproject.toml +1 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/__init__.py +1 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/api.py +112 -95
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/auth/auth.py +2 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/bookings.py +10 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/bookings_v2.py +21 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/utils.py +5 -3
- {otf_api-0.11.0rc1 → otf_api-0.12.0/src/otf_api.egg-info}/PKG-INFO +1 -1
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/LICENSE +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/README.md +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/setup.cfg +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/auth/__init__.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/auth/user.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/auth/utils.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/exceptions.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/filters.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/logging.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/__init__.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/base.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/body_composition_list.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/challenge_tracker_content.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/challenge_tracker_detail.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/classes.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/enums.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/lifetime_stats.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/member_detail.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/member_membership.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/member_purchases.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/mixins.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/notifications.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/out_of_studio_workout_history.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/performance_summary.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/ratings.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/studio_detail.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/studio_services.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/telemetry.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/models/workout.py +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api/py.typed +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api.egg-info/SOURCES.txt +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api.egg-info/dependency_links.txt +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api.egg-info/requires.txt +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/src/otf_api.egg-info/top_level.txt +0 -0
- {otf_api-0.11.0rc1 → otf_api-0.12.0}/tests/test_filters.py +0 -0
@@ -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
|
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."""
|
@@ -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,
|
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
|
-
|
440
|
-
|
441
|
-
|
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
|
-
|
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
|
471
|
+
include_canceled = not exclude_cancelled
|
464
472
|
|
465
|
-
|
466
|
-
|
473
|
+
end_date = ensure_datetime(end_date, time(23, 59, 59))
|
474
|
+
start_date = ensure_datetime(start_date)
|
467
475
|
|
468
|
-
|
469
|
-
|
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=
|
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
|
-
|
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
|
-
# 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
|
-
|
810
|
-
|
811
|
-
|
834
|
+
Raises:
|
835
|
+
ValueError: If booking_id is None or empty string
|
836
|
+
BookingNotFoundError: If the booking does not exist.
|
837
|
+
"""
|
812
838
|
|
813
|
-
|
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
|
-
|
816
|
-
# self.get_booking_new(booking_id)
|
843
|
+
booking_id = get_booking_id(booking)
|
817
844
|
|
818
|
-
|
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
|
-
|
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.
|
@@ -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,
|
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
|
-
|
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
|
|
@@ -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
|
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")
|
@@ -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,
|
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
|
|
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
|