otf-api 0.7.1__tar.gz → 0.8.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.7.1 → otf_api-0.8.0}/PKG-INFO +2 -1
- {otf_api-0.7.1 → otf_api-0.8.0}/pyproject.toml +1 -1
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/__init__.py +1 -1
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/api.py +270 -178
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/bookings.py +2 -2
- {otf_api-0.7.1 → otf_api-0.8.0}/AUTHORS.md +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/LICENSE +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/README.md +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/auth.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/exceptions.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/__init__.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/base.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/body_composition_list.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/book_class.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/cancel_booking.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/challenge_tracker_content.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/challenge_tracker_detail.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/classes.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/enums.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/favorite_studios.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/latest_agreement.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/lifetime_stats.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/member_detail.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/member_membership.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/member_purchases.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/mixins.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/out_of_studio_workout_history.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/performance_summary_detail.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/performance_summary_list.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/studio_detail.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/studio_services.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/telemetry.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/telemetry_hr_history.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/telemetry_max_hr.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/models/total_classes.py +0 -0
- {otf_api-0.7.1 → otf_api-0.8.0}/src/otf_api/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: otf-api
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.0
|
4
4
|
Summary: Python OrangeTheory Fitness API Client
|
5
5
|
License: MIT
|
6
6
|
Author: Jessica Smith
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.10
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
17
18
|
Classifier: Topic :: Internet :: WWW/HTTP
|
18
19
|
Classifier: Topic :: Software Development :: Libraries
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
@@ -1,22 +1,22 @@
|
|
1
1
|
import asyncio
|
2
2
|
import contextlib
|
3
3
|
import json
|
4
|
-
import
|
5
|
-
from
|
4
|
+
from datetime import date, datetime, timedelta
|
5
|
+
from logging import Logger, getLogger
|
6
6
|
from typing import Any
|
7
7
|
|
8
8
|
import aiohttp
|
9
9
|
import requests
|
10
|
-
from loguru import logger
|
11
10
|
from yarl import URL
|
12
11
|
|
13
12
|
from otf_api import models
|
14
13
|
from otf_api.auth import OtfUser
|
15
|
-
from otf_api.exceptions import
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
from otf_api.exceptions import (
|
15
|
+
AlreadyBookedError,
|
16
|
+
BookingAlreadyCancelledError,
|
17
|
+
BookingNotFoundError,
|
18
|
+
OutsideSchedulingWindowError,
|
19
|
+
)
|
20
20
|
|
21
21
|
API_BASE_URL = "api.orangetheory.co"
|
22
22
|
API_IO_BASE_URL = "api.orangetheory.io"
|
@@ -25,7 +25,7 @@ REQUEST_HEADERS = {"Authorization": None, "Content-Type": "application/json", "A
|
|
25
25
|
|
26
26
|
|
27
27
|
class Otf:
|
28
|
-
logger: "Logger" =
|
28
|
+
logger: "Logger" = getLogger(__file__)
|
29
29
|
user: OtfUser
|
30
30
|
_session: aiohttp.ClientSession
|
31
31
|
|
@@ -148,7 +148,7 @@ class Otf:
|
|
148
148
|
|
149
149
|
full_url = str(URL.build(scheme="https", host=base_url, path=url))
|
150
150
|
|
151
|
-
logger.debug(f"Making {method!r} request to {full_url}, params: {params}")
|
151
|
+
self.logger.debug(f"Making {method!r} request to {full_url}, params: {params}")
|
152
152
|
|
153
153
|
# ensure we have headers that contain the most up-to-date token
|
154
154
|
if not headers:
|
@@ -164,10 +164,10 @@ class Otf:
|
|
164
164
|
try:
|
165
165
|
response.raise_for_status()
|
166
166
|
except aiohttp.ClientResponseError as e:
|
167
|
-
logger.exception(f"Error making request: {e}")
|
168
|
-
logger.exception(f"Response: {text}")
|
167
|
+
self.logger.exception(f"Error making request: {e}")
|
168
|
+
self.logger.exception(f"Response: {text}")
|
169
169
|
except Exception as e:
|
170
|
-
logger.exception(f"Error making request: {e}")
|
170
|
+
self.logger.exception(f"Error making request: {e}")
|
171
171
|
|
172
172
|
return await response.json()
|
173
173
|
|
@@ -189,16 +189,6 @@ class Otf:
|
|
189
189
|
"""Perform an API request to the performance summary API."""
|
190
190
|
return await self._do(method, API_IO_BASE_URL, url, params, headers)
|
191
191
|
|
192
|
-
async def get_body_composition_list(self):
|
193
|
-
"""Get the member's body composition list.
|
194
|
-
|
195
|
-
Returns:
|
196
|
-
Any: The member's body composition list.
|
197
|
-
"""
|
198
|
-
data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition")
|
199
|
-
|
200
|
-
return models.BodyCompositionList(data=data["data"])
|
201
|
-
|
202
192
|
async def get_classes(
|
203
193
|
self,
|
204
194
|
studio_uuids: list[str] | None = None,
|
@@ -210,7 +200,8 @@ class Otf:
|
|
210
200
|
exclude_cancelled: bool = False,
|
211
201
|
day_of_week: list[models.DoW] | None = None,
|
212
202
|
start_time: list[str] | None = None,
|
213
|
-
|
203
|
+
exclude_unbookable: bool = True,
|
204
|
+
) -> models.OtfClassList:
|
214
205
|
"""Get the classes for the user.
|
215
206
|
|
216
207
|
Returns a list of classes that are available for the user, based on the studio UUIDs provided. If no studio
|
@@ -228,6 +219,8 @@ class Otf:
|
|
228
219
|
exclude_cancelled (bool): Whether to exclude cancelled classes. Default is False.
|
229
220
|
day_of_week (list[DoW] | None): The days of the week to filter by. Default is None.
|
230
221
|
start_time (list[str] | None): The start time to filter by. Default is None.
|
222
|
+
exclude_unbookable (bool): Whether to exclude classes that are outside the scheduling window. Default is\
|
223
|
+
True.
|
231
224
|
|
232
225
|
Returns:
|
233
226
|
OtfClassList: The classes for the user.
|
@@ -238,11 +231,7 @@ class Otf:
|
|
238
231
|
elif include_home_studio and self.home_studio_uuid not in studio_uuids:
|
239
232
|
studio_uuids.append(self.home_studio_uuid)
|
240
233
|
|
241
|
-
|
242
|
-
|
243
|
-
params = {"studio_ids": studio_uuids}
|
244
|
-
|
245
|
-
classes_resp = await self._classes_request("GET", path, params=params)
|
234
|
+
classes_resp = await self._classes_request("GET", "/v1/classes", params={"studio_ids": studio_uuids})
|
246
235
|
classes_list = models.OtfClassList(classes=classes_resp["items"])
|
247
236
|
|
248
237
|
if start_date:
|
@@ -284,6 +273,11 @@ class Otf:
|
|
284
273
|
|
285
274
|
classes_list.classes = list(filter(lambda c: not c.canceled, classes_list.classes))
|
286
275
|
|
276
|
+
if exclude_unbookable:
|
277
|
+
# this endpoint returns classes that the `book_class` endpoint will reject, this filters them out
|
278
|
+
max_date = datetime.today().date() + timedelta(days=29)
|
279
|
+
classes_list.classes = [c for c in classes_list.classes if c.starts_at_local.date() <= max_date]
|
280
|
+
|
287
281
|
booking_resp = await self.get_bookings(start_date, end_date, status=models.BookingStatus.Booked)
|
288
282
|
booked_classes = {b.otf_class.class_uuid for b in booking_resp.bookings}
|
289
283
|
|
@@ -292,31 +286,77 @@ class Otf:
|
|
292
286
|
|
293
287
|
return classes_list
|
294
288
|
|
295
|
-
async def
|
296
|
-
"""Get
|
297
|
-
|
289
|
+
async def get_booking(self, booking_uuid: str) -> models.Booking:
|
290
|
+
"""Get a specific booking by booking_uuid.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
booking_uuid (str): The booking UUID to get.
|
298
294
|
|
299
295
|
Returns:
|
300
|
-
|
296
|
+
BookingList: The booking.
|
297
|
+
|
298
|
+
Raises:
|
299
|
+
ValueError: If booking_uuid is None or empty string.
|
301
300
|
"""
|
302
|
-
|
303
|
-
|
301
|
+
if not booking_uuid:
|
302
|
+
raise ValueError("booking_uuid is required")
|
303
|
+
|
304
|
+
data = await self._default_request("GET", f"/member/members/{self._member_id}/bookings/{booking_uuid}")
|
305
|
+
return models.Booking(**data["data"])
|
304
306
|
|
305
|
-
async def
|
306
|
-
"""
|
307
|
+
async def get_booking_by_class(self, class_: str | models.OtfClass) -> models.Booking:
|
308
|
+
"""Get a specific booking by class_uuid or OtfClass object.
|
307
309
|
|
308
310
|
Args:
|
309
|
-
|
311
|
+
class_ (str | OtfClass): The class UUID or the OtfClass object to get the booking for.
|
310
312
|
|
311
313
|
Returns:
|
312
|
-
|
314
|
+
Booking: The booking.
|
315
|
+
|
316
|
+
Raises:
|
317
|
+
BookingNotFoundError: If the booking does not exist.
|
318
|
+
ValueError: If class_uuid is None or empty string.
|
313
319
|
"""
|
314
320
|
|
315
|
-
|
321
|
+
class_uuid = class_.ot_class_uuid if isinstance(class_, models.OtfClass) else class_
|
316
322
|
|
317
|
-
|
323
|
+
if not class_uuid:
|
324
|
+
raise ValueError("class_uuid is required")
|
325
|
+
|
326
|
+
all_bookings = await self.get_bookings(exclude_cancelled=False, exclude_checkedin=False)
|
327
|
+
|
328
|
+
for booking in all_bookings.bookings:
|
318
329
|
if booking.otf_class.class_uuid == class_uuid:
|
319
|
-
|
330
|
+
return booking
|
331
|
+
|
332
|
+
raise BookingNotFoundError(f"Booking for class {class_uuid} not found.")
|
333
|
+
|
334
|
+
async def book_class(self, class_: str | models.OtfClass) -> models.Booking:
|
335
|
+
"""Book a class by providing either the class_uuid or the OtfClass object.
|
336
|
+
|
337
|
+
Args:
|
338
|
+
class_ (str | OtfClass): The class UUID or the OtfClass object to book.
|
339
|
+
|
340
|
+
Returns:
|
341
|
+
Booking: The booking.
|
342
|
+
|
343
|
+
Raises:
|
344
|
+
AlreadyBookedError: If the class is already booked.
|
345
|
+
OutsideSchedulingWindowError: If the class is outside the scheduling window.
|
346
|
+
ValueError: If class_uuid is None or empty string.
|
347
|
+
Exception: If there is an error booking the class.
|
348
|
+
"""
|
349
|
+
|
350
|
+
class_uuid = class_.ot_class_uuid if isinstance(class_, models.OtfClass) else class_
|
351
|
+
if not class_uuid:
|
352
|
+
raise ValueError("class_uuid is required")
|
353
|
+
|
354
|
+
with contextlib.suppress(BookingNotFoundError):
|
355
|
+
existing_booking = await self.get_booking_by_class(class_uuid)
|
356
|
+
if existing_booking.status != models.BookingStatus.Cancelled:
|
357
|
+
raise AlreadyBookedError(
|
358
|
+
f"Class {class_uuid} is already booked.", booking_uuid=existing_booking.class_booking_uuid
|
359
|
+
)
|
320
360
|
|
321
361
|
body = {"classUUId": class_uuid, "confirmed": False, "waitlist": False}
|
322
362
|
|
@@ -325,25 +365,50 @@ class Otf:
|
|
325
365
|
if resp["code"] == "ERROR":
|
326
366
|
if resp["data"]["errorCode"] == "603":
|
327
367
|
raise AlreadyBookedError(f"Class {class_uuid} is already booked.")
|
368
|
+
if resp["data"]["errorCode"] == "602":
|
369
|
+
raise OutsideSchedulingWindowError(f"Class {class_uuid} is outside the scheduling window.")
|
370
|
+
|
328
371
|
raise Exception(f"Error booking class {class_uuid}: {json.dumps(resp)}")
|
329
372
|
|
330
|
-
|
331
|
-
|
373
|
+
# get the booking details - we will only use this to get the booking_uuid
|
374
|
+
book_class = models.BookClass(**resp["data"])
|
375
|
+
|
376
|
+
booking = await self.get_booking(book_class.booking_uuid)
|
332
377
|
|
333
|
-
|
334
|
-
|
378
|
+
return booking
|
379
|
+
|
380
|
+
async def cancel_booking(self, booking: str | models.Booking):
|
381
|
+
"""Cancel a booking by providing either the booking_uuid or the Booking object.
|
335
382
|
|
336
383
|
Args:
|
337
|
-
|
384
|
+
booking (str | Booking): The booking UUID or the Booking object to cancel.
|
338
385
|
|
339
386
|
Returns:
|
340
|
-
|
387
|
+
CancelBooking: The cancelled booking.
|
388
|
+
|
389
|
+
Raises:
|
390
|
+
ValueError: If booking_uuid is None or empty string
|
391
|
+
BookingNotFoundError: If the booking does not exist.
|
341
392
|
"""
|
393
|
+
booking_uuid = booking.class_booking_uuid if isinstance(booking, models.Booking) else booking
|
394
|
+
|
395
|
+
if not booking_uuid:
|
396
|
+
raise ValueError("booking_uuid is required")
|
397
|
+
|
398
|
+
try:
|
399
|
+
await self.get_booking(booking_uuid)
|
400
|
+
except Exception:
|
401
|
+
raise BookingNotFoundError(f"Booking {booking_uuid} does not exist.")
|
342
402
|
|
343
403
|
params = {"confirmed": "true"}
|
344
404
|
resp = await self._default_request(
|
345
405
|
"DELETE", f"/member/members/{self._member_id}/bookings/{booking_uuid}", params=params
|
346
406
|
)
|
407
|
+
if resp["code"] == "NOT_AUTHORIZED" and resp["message"].startswith("This class booking has"):
|
408
|
+
raise BookingAlreadyCancelledError(
|
409
|
+
f"Booking {booking_uuid} is already cancelled.", booking_uuid=booking_uuid
|
410
|
+
)
|
411
|
+
|
347
412
|
return models.CancelBooking(**resp["data"])
|
348
413
|
|
349
414
|
async def get_bookings(
|
@@ -392,7 +457,7 @@ class Otf:
|
|
392
457
|
"""
|
393
458
|
|
394
459
|
if exclude_cancelled and status == models.BookingStatus.Cancelled:
|
395
|
-
logger.warning(
|
460
|
+
self.logger.warning(
|
396
461
|
"Cannot exclude cancelled bookings when status is Cancelled. Setting exclude_cancelled to False."
|
397
462
|
)
|
398
463
|
exclude_cancelled = False
|
@@ -430,7 +495,7 @@ class Otf:
|
|
430
495
|
|
431
496
|
return data
|
432
497
|
|
433
|
-
async def _get_bookings_old(self, status: models.BookingStatus | None = None):
|
498
|
+
async def _get_bookings_old(self, status: models.BookingStatus | None = None) -> models.BookingList:
|
434
499
|
"""Get the member's bookings.
|
435
500
|
|
436
501
|
Args:
|
@@ -473,74 +538,12 @@ class Otf:
|
|
473
538
|
|
474
539
|
status_value = status.value if status else None
|
475
540
|
|
476
|
-
|
477
|
-
|
478
|
-
|
541
|
+
res = await self._default_request(
|
542
|
+
"GET", f"/member/members/{self._member_id}/bookings", params={"status": status_value}
|
543
|
+
)
|
479
544
|
|
480
545
|
return models.BookingList(bookings=res["data"])
|
481
546
|
|
482
|
-
async def get_challenge_tracker_content(self):
|
483
|
-
"""Get the member's challenge tracker content.
|
484
|
-
|
485
|
-
Returns:
|
486
|
-
ChallengeTrackerContent: The member's challenge tracker content.
|
487
|
-
"""
|
488
|
-
data = await self._default_request("GET", f"/challenges/v3.1/member/{self._member_id}")
|
489
|
-
return models.ChallengeTrackerContent(**data["Dto"])
|
490
|
-
|
491
|
-
async def get_challenge_tracker_detail(
|
492
|
-
self,
|
493
|
-
equipment_id: models.EquipmentType,
|
494
|
-
challenge_type_id: models.ChallengeType,
|
495
|
-
challenge_sub_type_id: int = 0,
|
496
|
-
):
|
497
|
-
"""Get the member's challenge tracker details.
|
498
|
-
|
499
|
-
Args:
|
500
|
-
equipment_id (EquipmentType): The equipment ID.
|
501
|
-
challenge_type_id (ChallengeType): The challenge type ID.
|
502
|
-
challenge_sub_type_id (int): The challenge sub type ID. Default is 0.
|
503
|
-
|
504
|
-
Returns:
|
505
|
-
ChallengeTrackerDetailList: The member's challenge tracker details.
|
506
|
-
|
507
|
-
Notes:
|
508
|
-
---
|
509
|
-
I'm not sure what the challenge_sub_type_id is supposed to be, so it defaults to 0.
|
510
|
-
|
511
|
-
"""
|
512
|
-
params = {
|
513
|
-
"equipmentId": equipment_id.value,
|
514
|
-
"challengeTypeId": challenge_type_id.value,
|
515
|
-
"challengeSubTypeId": challenge_sub_type_id,
|
516
|
-
}
|
517
|
-
|
518
|
-
data = await self._default_request("GET", f"/challenges/v3/member/{self._member_id}/benchmarks", params=params)
|
519
|
-
|
520
|
-
return models.ChallengeTrackerDetailList(details=data["Dto"])
|
521
|
-
|
522
|
-
async def get_challenge_tracker_participation(self, challenge_type_id: models.ChallengeType):
|
523
|
-
"""Get the member's participation in a challenge.
|
524
|
-
|
525
|
-
Args:
|
526
|
-
challenge_type_id (ChallengeType): The challenge type ID.
|
527
|
-
|
528
|
-
Returns:
|
529
|
-
Any: The member's participation in the challenge.
|
530
|
-
|
531
|
-
Notes:
|
532
|
-
---
|
533
|
-
I've never gotten this to return anything other than invalid response. I'm not sure if it's a bug
|
534
|
-
in my code or the API.
|
535
|
-
|
536
|
-
"""
|
537
|
-
params = {"challengeTypeId": challenge_type_id.value}
|
538
|
-
|
539
|
-
data = await self._default_request(
|
540
|
-
"GET", f"/challenges/v1/member/{self._member_id}/participation", params=params
|
541
|
-
)
|
542
|
-
return data
|
543
|
-
|
544
547
|
async def get_member_detail(
|
545
548
|
self, include_addresses: bool = True, include_class_summary: bool = True, include_credit_card: bool = False
|
546
549
|
):
|
@@ -581,7 +584,7 @@ class Otf:
|
|
581
584
|
data = await self._default_request("GET", f"/member/members/{self._member_id}", params=params)
|
582
585
|
return models.MemberDetail(**data["data"])
|
583
586
|
|
584
|
-
async def get_member_membership(self):
|
587
|
+
async def get_member_membership(self) -> models.MemberMembership:
|
585
588
|
"""Get the member's membership details.
|
586
589
|
|
587
590
|
Returns:
|
@@ -591,7 +594,7 @@ class Otf:
|
|
591
594
|
data = await self._default_request("GET", f"/member/members/{self._member_id}/memberships")
|
592
595
|
return models.MemberMembership(**data["data"])
|
593
596
|
|
594
|
-
async def get_member_purchases(self):
|
597
|
+
async def get_member_purchases(self) -> models.MemberPurchaseList:
|
595
598
|
"""Get the member's purchases, including monthly subscriptions and class packs.
|
596
599
|
|
597
600
|
Returns:
|
@@ -600,7 +603,9 @@ class Otf:
|
|
600
603
|
data = await self._default_request("GET", f"/member/members/{self._member_id}/purchases")
|
601
604
|
return models.MemberPurchaseList(data=data["data"])
|
602
605
|
|
603
|
-
async def get_member_lifetime_stats(
|
606
|
+
async def get_member_lifetime_stats(
|
607
|
+
self, select_time: models.StatsTime = models.StatsTime.AllTime
|
608
|
+
) -> models.StatsResponse:
|
604
609
|
"""Get the member's lifetime stats.
|
605
610
|
|
606
611
|
Args:
|
@@ -617,9 +622,24 @@ class Otf:
|
|
617
622
|
|
618
623
|
data = await self._default_request("GET", f"/performance/v2/{self._member_id}/over-time/{select_time.value}")
|
619
624
|
|
620
|
-
|
625
|
+
stats = models.StatsResponse(**data["data"])
|
626
|
+
return stats
|
627
|
+
|
628
|
+
async def get_latest_agreement(self) -> models.LatestAgreement:
|
629
|
+
"""Get the latest agreement for the member.
|
630
|
+
|
631
|
+
Returns:
|
632
|
+
LatestAgreement: The agreement.
|
633
|
+
|
634
|
+
Notes:
|
635
|
+
---
|
636
|
+
In this context, "latest" means the most recent agreement with a specific ID, not the most recent agreement
|
637
|
+
in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement.
|
638
|
+
"""
|
639
|
+
data = await self._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6")
|
640
|
+
return models.LatestAgreement(**data["data"])
|
621
641
|
|
622
|
-
async def get_out_of_studio_workout_history(self):
|
642
|
+
async def get_out_of_studio_workout_history(self) -> models.OutOfStudioWorkoutHistoryList:
|
623
643
|
"""Get the member's out of studio workout history.
|
624
644
|
|
625
645
|
Returns:
|
@@ -627,9 +647,9 @@ class Otf:
|
|
627
647
|
"""
|
628
648
|
data = await self._default_request("GET", f"/member/members/{self._member_id}/out-of-studio-workout")
|
629
649
|
|
630
|
-
return models.OutOfStudioWorkoutHistoryList(
|
650
|
+
return models.OutOfStudioWorkoutHistoryList(workouts=data["data"])
|
631
651
|
|
632
|
-
async def get_favorite_studios(self):
|
652
|
+
async def get_favorite_studios(self) -> models.FavoriteStudioList:
|
633
653
|
"""Get the member's favorite studios.
|
634
654
|
|
635
655
|
Returns:
|
@@ -639,21 +659,7 @@ class Otf:
|
|
639
659
|
|
640
660
|
return models.FavoriteStudioList(studios=data["data"])
|
641
661
|
|
642
|
-
async def
|
643
|
-
"""Get the latest agreement for the member.
|
644
|
-
|
645
|
-
Returns:
|
646
|
-
LatestAgreement: The agreement.
|
647
|
-
|
648
|
-
Notes:
|
649
|
-
---
|
650
|
-
In this context, "latest" means the most recent agreement with a specific ID, not the most recent agreement
|
651
|
-
in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement.
|
652
|
-
"""
|
653
|
-
data = await self._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6")
|
654
|
-
return models.LatestAgreement(**data["data"])
|
655
|
-
|
656
|
-
async def get_studio_services(self, studio_uuid: str | None = None):
|
662
|
+
async def get_studio_services(self, studio_uuid: str | None = None) -> models.StudioServiceList:
|
657
663
|
"""Get the services available at a specific studio. If no studio UUID is provided, the member's home studio
|
658
664
|
will be used.
|
659
665
|
|
@@ -668,41 +674,7 @@ class Otf:
|
|
668
674
|
data = await self._default_request("GET", f"/member/studios/{studio_uuid}/services")
|
669
675
|
return models.StudioServiceList(data=data["data"])
|
670
676
|
|
671
|
-
async def
|
672
|
-
"""Get a list of performance summaries for the authenticated user.
|
673
|
-
|
674
|
-
Args:
|
675
|
-
limit (int): The maximum number of performance summaries to return. Defaults to 30.
|
676
|
-
|
677
|
-
Returns:
|
678
|
-
PerformanceSummaryList: A list of performance summaries.
|
679
|
-
|
680
|
-
Developer Notes:
|
681
|
-
---
|
682
|
-
In the app, this is referred to as 'getInStudioWorkoutHistory'.
|
683
|
-
|
684
|
-
"""
|
685
|
-
|
686
|
-
path = "/v1/performance-summaries"
|
687
|
-
params = {"limit": limit}
|
688
|
-
res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers, params=params)
|
689
|
-
return models.PerformanceSummaryList(summaries=res["items"])
|
690
|
-
|
691
|
-
async def get_performance_summary(self, performance_summary_id: str):
|
692
|
-
"""Get a detailed performance summary for a given workout.
|
693
|
-
|
694
|
-
Args:
|
695
|
-
performance_summary_id (str): The ID of the performance summary to retrieve.
|
696
|
-
|
697
|
-
Returns:
|
698
|
-
PerformanceSummaryDetail: A detailed performance summary.
|
699
|
-
"""
|
700
|
-
|
701
|
-
path = f"/v1/performance-summaries/{performance_summary_id}"
|
702
|
-
res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers)
|
703
|
-
return models.PerformanceSummaryDetail(**res)
|
704
|
-
|
705
|
-
async def get_studio_detail(self, studio_uuid: str | None = None):
|
677
|
+
async def get_studio_detail(self, studio_uuid: str | None = None) -> models.StudioDetail:
|
706
678
|
"""Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
|
707
679
|
user's home studio.
|
708
680
|
|
@@ -728,7 +700,7 @@ class Otf:
|
|
728
700
|
distance: float = 50,
|
729
701
|
page_index: int = 1,
|
730
702
|
page_size: int = 50,
|
731
|
-
):
|
703
|
+
) -> models.StudioDetailList:
|
732
704
|
"""Search for studios by geographic location.
|
733
705
|
|
734
706
|
Args:
|
@@ -785,7 +757,127 @@ class Otf:
|
|
785
757
|
|
786
758
|
return models.StudioDetailList(studios=all_results)
|
787
759
|
|
788
|
-
async def
|
760
|
+
async def get_total_classes(self) -> models.TotalClasses:
|
761
|
+
"""Get the member's total classes. This is a simple object reflecting the total number of classes attended,
|
762
|
+
both in-studio and OT Live.
|
763
|
+
|
764
|
+
Returns:
|
765
|
+
TotalClasses: The member's total classes.
|
766
|
+
"""
|
767
|
+
data = await self._default_request("GET", "/mobile/v1/members/classes/summary")
|
768
|
+
return models.TotalClasses(**data["data"])
|
769
|
+
|
770
|
+
async def get_body_composition_list(self) -> models.BodyCompositionList:
|
771
|
+
"""Get the member's body composition list.
|
772
|
+
|
773
|
+
Returns:
|
774
|
+
Any: The member's body composition list.
|
775
|
+
"""
|
776
|
+
data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition")
|
777
|
+
|
778
|
+
return models.BodyCompositionList(data=data["data"])
|
779
|
+
|
780
|
+
async def get_challenge_tracker_content(self) -> models.ChallengeTrackerContent:
|
781
|
+
"""Get the member's challenge tracker content.
|
782
|
+
|
783
|
+
Returns:
|
784
|
+
ChallengeTrackerContent: The member's challenge tracker content.
|
785
|
+
"""
|
786
|
+
data = await self._default_request("GET", f"/challenges/v3.1/member/{self._member_id}")
|
787
|
+
return models.ChallengeTrackerContent(**data["Dto"])
|
788
|
+
|
789
|
+
async def get_challenge_tracker_detail(
|
790
|
+
self,
|
791
|
+
equipment_id: models.EquipmentType,
|
792
|
+
challenge_type_id: models.ChallengeType,
|
793
|
+
challenge_sub_type_id: int = 0,
|
794
|
+
):
|
795
|
+
"""Get the member's challenge tracker details.
|
796
|
+
|
797
|
+
Args:
|
798
|
+
equipment_id (EquipmentType): The equipment ID.
|
799
|
+
challenge_type_id (ChallengeType): The challenge type ID.
|
800
|
+
challenge_sub_type_id (int): The challenge sub type ID. Default is 0.
|
801
|
+
|
802
|
+
Returns:
|
803
|
+
ChallengeTrackerDetailList: The member's challenge tracker details.
|
804
|
+
|
805
|
+
Notes:
|
806
|
+
---
|
807
|
+
I'm not sure what the challenge_sub_type_id is supposed to be, so it defaults to 0.
|
808
|
+
|
809
|
+
"""
|
810
|
+
params = {
|
811
|
+
"equipmentId": equipment_id.value,
|
812
|
+
"challengeTypeId": challenge_type_id.value,
|
813
|
+
"challengeSubTypeId": challenge_sub_type_id,
|
814
|
+
}
|
815
|
+
|
816
|
+
data = await self._default_request("GET", f"/challenges/v3/member/{self._member_id}/benchmarks", params=params)
|
817
|
+
|
818
|
+
return models.ChallengeTrackerDetailList(details=data["Dto"])
|
819
|
+
|
820
|
+
async def get_challenge_tracker_participation(self, challenge_type_id: models.ChallengeType) -> Any:
|
821
|
+
"""Get the member's participation in a challenge.
|
822
|
+
|
823
|
+
Args:
|
824
|
+
challenge_type_id (ChallengeType): The challenge type ID.
|
825
|
+
|
826
|
+
Returns:
|
827
|
+
Any: The member's participation in the challenge.
|
828
|
+
|
829
|
+
Notes:
|
830
|
+
---
|
831
|
+
I've never gotten this to return anything other than invalid response. I'm not sure if it's a bug
|
832
|
+
in my code or the API.
|
833
|
+
|
834
|
+
"""
|
835
|
+
|
836
|
+
data = await self._default_request(
|
837
|
+
"GET",
|
838
|
+
f"/challenges/v1/member/{self._member_id}/participation",
|
839
|
+
params={"challengeTypeId": challenge_type_id.value},
|
840
|
+
)
|
841
|
+
return data
|
842
|
+
|
843
|
+
async def get_performance_summaries(self, limit: int = 30) -> models.PerformanceSummaryList:
|
844
|
+
"""Get a list of performance summaries for the authenticated user.
|
845
|
+
|
846
|
+
Args:
|
847
|
+
limit (int): The maximum number of performance summaries to return. Defaults to 30.
|
848
|
+
|
849
|
+
Returns:
|
850
|
+
PerformanceSummaryList: A list of performance summaries.
|
851
|
+
|
852
|
+
Developer Notes:
|
853
|
+
---
|
854
|
+
In the app, this is referred to as 'getInStudioWorkoutHistory'.
|
855
|
+
|
856
|
+
"""
|
857
|
+
|
858
|
+
res = await self._performance_summary_request(
|
859
|
+
"GET",
|
860
|
+
"/v1/performance-summaries",
|
861
|
+
headers=self._perf_api_headers,
|
862
|
+
params={"limit": limit},
|
863
|
+
)
|
864
|
+
return models.PerformanceSummaryList(summaries=res["items"])
|
865
|
+
|
866
|
+
async def get_performance_summary(self, performance_summary_id: str) -> models.PerformanceSummaryDetail:
|
867
|
+
"""Get a detailed performance summary for a given workout.
|
868
|
+
|
869
|
+
Args:
|
870
|
+
performance_summary_id (str): The ID of the performance summary to retrieve.
|
871
|
+
|
872
|
+
Returns:
|
873
|
+
PerformanceSummaryDetail: A detailed performance summary.
|
874
|
+
"""
|
875
|
+
|
876
|
+
path = f"/v1/performance-summaries/{performance_summary_id}"
|
877
|
+
res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers)
|
878
|
+
return models.PerformanceSummaryDetail(**res)
|
879
|
+
|
880
|
+
async def get_hr_history(self) -> models.TelemetryHrHistory:
|
789
881
|
"""Get the heartrate history for the user.
|
790
882
|
|
791
883
|
Returns a list of history items that contain the max heartrate, start/end bpm for each zone,
|
@@ -801,7 +893,7 @@ class Otf:
|
|
801
893
|
res = await self._telemetry_request("GET", path, params=params)
|
802
894
|
return models.TelemetryHrHistory(**res)
|
803
895
|
|
804
|
-
async def get_max_hr(self):
|
896
|
+
async def get_max_hr(self) -> models.TelemetryMaxHr:
|
805
897
|
"""Get the max heartrate for the user.
|
806
898
|
|
807
899
|
Returns a simple object that has the member_uuid and the max_hr.
|
@@ -816,7 +908,7 @@ class Otf:
|
|
816
908
|
res = await self._telemetry_request("GET", path, params=params)
|
817
909
|
return models.TelemetryMaxHr(**res)
|
818
910
|
|
819
|
-
async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120):
|
911
|
+
async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> models.Telemetry:
|
820
912
|
"""Get the telemetry for a performance summary.
|
821
913
|
|
822
914
|
This returns an object that contains the max heartrate, start/end bpm for each zone,
|
@@ -838,7 +930,7 @@ class Otf:
|
|
838
930
|
|
839
931
|
# the below do not return any data for me, so I can't test them
|
840
932
|
|
841
|
-
async def _get_member_services(self, active_only: bool = True):
|
933
|
+
async def _get_member_services(self, active_only: bool = True) -> Any:
|
842
934
|
"""Get the member's services.
|
843
935
|
|
844
936
|
Args:
|
@@ -853,7 +945,7 @@ class Otf:
|
|
853
945
|
)
|
854
946
|
return data
|
855
947
|
|
856
|
-
async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None):
|
948
|
+
async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> Any:
|
857
949
|
"""Get data from the member's aspire wearable.
|
858
950
|
|
859
951
|
Note: I don't have an aspire wearable, so I can't test this.
|
@@ -72,8 +72,8 @@ class OtfClass(OtfItemBase, OtfClassTimeMixin):
|
|
72
72
|
is_cancelled: bool = Field(alias="isCancelled")
|
73
73
|
program_name: str | None = Field(None, alias="programName")
|
74
74
|
coach_id: int | None = Field(None, alias="coachId")
|
75
|
-
studio: Studio
|
76
|
-
coach: Coach
|
75
|
+
studio: Studio
|
76
|
+
coach: Coach
|
77
77
|
location: Location | None = None
|
78
78
|
virtual_class: bool | None = Field(None, alias="virtualClass")
|
79
79
|
|
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
|