otf-api 0.6.4__tar.gz → 0.7.1__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.
Files changed (45) hide show
  1. {otf_api-0.6.4 → otf_api-0.7.1}/PKG-INFO +3 -3
  2. {otf_api-0.6.4 → otf_api-0.7.1}/README.md +2 -0
  3. {otf_api-0.6.4 → otf_api-0.7.1}/pyproject.toml +1 -4
  4. otf_api-0.7.1/src/otf_api/__init__.py +7 -0
  5. {otf_api-0.6.4 → otf_api-0.7.1}/src/otf_api/api.py +76 -111
  6. {otf_api-0.6.4 → otf_api-0.7.1}/src/otf_api/auth.py +11 -10
  7. otf_api-0.7.1/src/otf_api/exceptions.py +18 -0
  8. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/__init__.py +6 -4
  9. otf_api-0.7.1/src/otf_api/models/base.py +7 -0
  10. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/body_composition_list.py +3 -14
  11. otf_api-0.7.1/src/otf_api/models/book_class.py +89 -0
  12. otf_api-0.7.1/src/otf_api/models/bookings.py +119 -0
  13. otf_api-0.7.1/src/otf_api/models/cancel_booking.py +49 -0
  14. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/challenge_tracker_detail.py +12 -13
  15. otf_api-0.7.1/src/otf_api/models/classes.py +80 -0
  16. otf_api-0.7.1/src/otf_api/models/enums.py +87 -0
  17. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/favorite_studios.py +17 -19
  18. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/lifetime_stats.py +0 -18
  19. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/member_detail.py +16 -19
  20. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/member_purchases.py +10 -10
  21. otf_api-0.7.1/src/otf_api/models/mixins.py +44 -0
  22. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/out_of_studio_workout_history.py +3 -3
  23. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/performance_summary_list.py +2 -5
  24. otf_api-0.7.1/src/otf_api/models/studio_detail.py +109 -0
  25. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/studio_services.py +2 -2
  26. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/telemetry.py +1 -1
  27. otf_api-0.6.4/src/otf_api/__init__.py +0 -15
  28. otf_api-0.6.4/src/otf_api/models/__init__.py +0 -66
  29. otf_api-0.6.4/src/otf_api/models/base.py +0 -22
  30. otf_api-0.6.4/src/otf_api/models/responses/book_class.py +0 -407
  31. otf_api-0.6.4/src/otf_api/models/responses/bookings.py +0 -160
  32. otf_api-0.6.4/src/otf_api/models/responses/cancel_booking.py +0 -95
  33. otf_api-0.6.4/src/otf_api/models/responses/classes.py +0 -148
  34. otf_api-0.6.4/src/otf_api/models/responses/enums.py +0 -23
  35. otf_api-0.6.4/src/otf_api/models/responses/studio_detail.py +0 -113
  36. {otf_api-0.6.4 → otf_api-0.7.1}/AUTHORS.md +0 -0
  37. {otf_api-0.6.4 → otf_api-0.7.1}/LICENSE +0 -0
  38. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/challenge_tracker_content.py +0 -0
  39. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/latest_agreement.py +0 -0
  40. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/member_membership.py +0 -0
  41. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/performance_summary_detail.py +0 -0
  42. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/telemetry_hr_history.py +0 -0
  43. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/telemetry_max_hr.py +0 -0
  44. {otf_api-0.6.4/src/otf_api/models/responses → otf_api-0.7.1/src/otf_api/models}/total_classes.py +0 -0
  45. {otf_api-0.6.4 → otf_api-0.7.1}/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.6.4
3
+ Version: 0.7.1
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  License: MIT
6
6
  Author: Jessica Smith
@@ -21,8 +21,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Requires-Dist: aiohttp (==3.10.*)
22
22
  Requires-Dist: humanize (>=4.9.0,<5.0.0)
23
23
  Requires-Dist: inflection (==0.5.*)
24
- Requires-Dist: loguru (==0.7.2)
25
- Requires-Dist: pendulum (>=3.0.0,<4.0.0)
26
24
  Requires-Dist: pint (==0.24.*)
27
25
  Requires-Dist: pycognito (==2024.5.1)
28
26
  Requires-Dist: pydantic (==2.7.3)
@@ -31,6 +29,8 @@ Description-Content-Type: text/markdown
31
29
 
32
30
  Simple API client for interacting with the OrangeTheory Fitness APIs.
33
31
 
32
+ Review the [documentation](https://otf-api.readthedocs.io/en/stable/).
33
+
34
34
 
35
35
  This library allows access to the OrangeTheory API to retrieve workouts and performance data, class schedules, studio information, and bookings.
36
36
 
@@ -1,5 +1,7 @@
1
1
  Simple API client for interacting with the OrangeTheory Fitness APIs.
2
2
 
3
+ Review the [documentation](https://otf-api.readthedocs.io/en/stable/).
4
+
3
5
 
4
6
  This library allows access to the OrangeTheory API to retrieve workouts and performance data, class schedules, studio information, and bookings.
5
7
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "otf-api"
3
- version = "0.6.4"
3
+ version = "0.7.1"
4
4
  description = "Python OrangeTheory Fitness API Client"
5
5
  authors = ["Jessica Smith <j.smith.git1@gmail.com>"]
6
6
  license = "MIT"
@@ -25,8 +25,6 @@ python = "^3.10"
25
25
  aiohttp = "3.10.*"
26
26
  humanize = "^4.9.0"
27
27
  inflection = "0.5.*"
28
- loguru = "0.7.2"
29
- pendulum = "^3.0.0"
30
28
  pint = "0.24.*"
31
29
  pycognito = "2024.5.1"
32
30
  pydantic = "2.7.3"
@@ -43,7 +41,6 @@ pytest = "8.2.2"
43
41
  pytest-asyncio = "0.23.7"
44
42
  pytest-cov = "5.0.0"
45
43
  pytest-loguru = "0.4.0"
46
- ruff = "0.4.9"
47
44
  tox = "4.15.1"
48
45
  twine = "5.1.1"
49
46
 
@@ -0,0 +1,7 @@
1
+ from .api import Otf
2
+ from .auth import OtfUser
3
+
4
+ __version__ = "0.7.1"
5
+
6
+
7
+ __all__ = ["Otf", "OtfUser"]
@@ -10,44 +10,9 @@ import requests
10
10
  from loguru import logger
11
11
  from yarl import URL
12
12
 
13
+ from otf_api import models
13
14
  from otf_api.auth import OtfUser
14
- from otf_api.models import (
15
- BodyCompositionList,
16
- BookClass,
17
- BookingList,
18
- BookingStatus,
19
- CancelBooking,
20
- ChallengeTrackerContent,
21
- ChallengeTrackerDetailList,
22
- ChallengeType,
23
- ClassType,
24
- DoW,
25
- EquipmentType,
26
- FavoriteStudioList,
27
- LatestAgreement,
28
- MemberDetail,
29
- MemberMembership,
30
- MemberPurchaseList,
31
- OtfClassList,
32
- OutOfStudioWorkoutHistoryList,
33
- Pagination,
34
- PerformanceSummaryDetail,
35
- PerformanceSummaryList,
36
- StatsResponse,
37
- StatsTime,
38
- StudioDetail,
39
- StudioDetailList,
40
- StudioServiceList,
41
- Telemetry,
42
- TelemetryHrHistory,
43
- TelemetryMaxHr,
44
- TotalClasses,
45
- )
46
-
47
-
48
- class AlreadyBookedError(Exception):
49
- pass
50
-
15
+ from otf_api.exceptions import AlreadyBookedError
51
16
 
52
17
  if typing.TYPE_CHECKING:
53
18
  from loguru import Logger
@@ -92,7 +57,7 @@ class Otf:
92
57
  user (OtfUser, optional): A user object. Default is None.
93
58
  """
94
59
 
95
- self.member: MemberDetail
60
+ self.member: models.MemberDetail
96
61
  self.home_studio_uuid: str
97
62
 
98
63
  if user:
@@ -119,7 +84,7 @@ class Otf:
119
84
  self.member = self._get_member_details_sync()
120
85
  self.home_studio_uuid = self.member.home_studio.studio_uuid
121
86
 
122
- def _get_member_details_sync(self) -> MemberDetail:
87
+ def _get_member_details_sync(self):
123
88
  """Get the member details synchronously.
124
89
 
125
90
  This is used to get the member details when the API is first initialized, to let use initialize
@@ -130,10 +95,10 @@ class Otf:
130
95
  """
131
96
  url = f"https://{API_BASE_URL}/member/members/{self._member_id}"
132
97
  resp = requests.get(url, headers=self.headers)
133
- return MemberDetail(**resp.json()["data"])
98
+ return models.MemberDetail(**resp.json()["data"])
134
99
 
135
100
  @property
136
- def headers(self) -> dict[str, str]:
101
+ def headers(self):
137
102
  """Get the headers for the API request."""
138
103
 
139
104
  # check the token before making a request in case it has expired
@@ -146,7 +111,7 @@ class Otf:
146
111
  }
147
112
 
148
113
  @property
149
- def session(self) -> aiohttp.ClientSession:
114
+ def session(self):
150
115
  """Get the aiohttp session."""
151
116
  if not getattr(self, "_session", None):
152
117
  self._session = aiohttp.ClientSession(headers=self.headers)
@@ -224,7 +189,7 @@ class Otf:
224
189
  """Perform an API request to the performance summary API."""
225
190
  return await self._do(method, API_IO_BASE_URL, url, params, headers)
226
191
 
227
- async def get_body_composition_list(self) -> BodyCompositionList:
192
+ async def get_body_composition_list(self):
228
193
  """Get the member's body composition list.
229
194
 
230
195
  Returns:
@@ -232,7 +197,7 @@ class Otf:
232
197
  """
233
198
  data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition")
234
199
 
235
- return BodyCompositionList(data=data["data"])
200
+ return models.BodyCompositionList(data=data["data"])
236
201
 
237
202
  async def get_classes(
238
203
  self,
@@ -241,11 +206,11 @@ class Otf:
241
206
  start_date: str | None = None,
242
207
  end_date: str | None = None,
243
208
  limit: int | None = None,
244
- class_type: ClassType | list[ClassType] | None = None,
209
+ class_type: models.ClassType | list[models.ClassType] | None = None,
245
210
  exclude_cancelled: bool = False,
246
- day_of_week: list[DoW] | None = None,
211
+ day_of_week: list[models.DoW] | None = None,
247
212
  start_time: list[str] | None = None,
248
- ) -> OtfClassList:
213
+ ):
249
214
  """Get the classes for the user.
250
215
 
251
216
  Returns a list of classes that are available for the user, based on the studio UUIDs provided. If no studio
@@ -278,7 +243,7 @@ class Otf:
278
243
  params = {"studio_ids": studio_uuids}
279
244
 
280
245
  classes_resp = await self._classes_request("GET", path, params=params)
281
- classes_list = OtfClassList(classes=classes_resp["items"])
246
+ classes_list = models.OtfClassList(classes=classes_resp["items"])
282
247
 
283
248
  if start_date:
284
249
  start_dtme = datetime.strptime(start_date, "%Y-%m-%d") # noqa
@@ -319,7 +284,7 @@ class Otf:
319
284
 
320
285
  classes_list.classes = list(filter(lambda c: not c.canceled, classes_list.classes))
321
286
 
322
- booking_resp = await self.get_bookings(start_date, end_date, status=BookingStatus.Booked)
287
+ booking_resp = await self.get_bookings(start_date, end_date, status=models.BookingStatus.Booked)
323
288
  booked_classes = {b.otf_class.class_uuid for b in booking_resp.bookings}
324
289
 
325
290
  for otf_class in classes_list.classes:
@@ -327,7 +292,7 @@ class Otf:
327
292
 
328
293
  return classes_list
329
294
 
330
- async def get_total_classes(self) -> TotalClasses:
295
+ async def get_total_classes(self):
331
296
  """Get the member's total classes. This is a simple object reflecting the total number of classes attended,
332
297
  both in-studio and OT Live.
333
298
 
@@ -335,9 +300,9 @@ class Otf:
335
300
  TotalClasses: The member's total classes.
336
301
  """
337
302
  data = await self._default_request("GET", "/mobile/v1/members/classes/summary")
338
- return TotalClasses(**data["data"])
303
+ return models.TotalClasses(**data["data"])
339
304
 
340
- async def book_class(self, class_uuid: str) -> BookClass | typing.Any:
305
+ async def book_class(self, class_uuid: str):
341
306
  """Book a class by class_uuid.
342
307
 
343
308
  Args:
@@ -362,10 +327,10 @@ class Otf:
362
327
  raise AlreadyBookedError(f"Class {class_uuid} is already booked.")
363
328
  raise Exception(f"Error booking class {class_uuid}: {json.dumps(resp)}")
364
329
 
365
- data = BookClass(**resp["data"])
330
+ data = models.BookClass(**resp["data"])
366
331
  return data
367
332
 
368
- async def cancel_booking(self, booking_uuid: str) -> CancelBooking:
333
+ async def cancel_booking(self, booking_uuid: str):
369
334
  """Cancel a class by booking_uuid.
370
335
 
371
336
  Args:
@@ -379,17 +344,17 @@ class Otf:
379
344
  resp = await self._default_request(
380
345
  "DELETE", f"/member/members/{self._member_id}/bookings/{booking_uuid}", params=params
381
346
  )
382
- return CancelBooking(**resp["data"])
347
+ return models.CancelBooking(**resp["data"])
383
348
 
384
349
  async def get_bookings(
385
350
  self,
386
351
  start_date: date | str | None = None,
387
352
  end_date: date | str | None = None,
388
- status: BookingStatus | None = None,
353
+ status: models.BookingStatus | None = None,
389
354
  limit: int | None = None,
390
355
  exclude_cancelled: bool = True,
391
356
  exclude_checkedin: bool = True,
392
- ) -> BookingList:
357
+ ):
393
358
  """Get the member's bookings.
394
359
 
395
360
  Args:
@@ -426,7 +391,7 @@ class Otf:
426
391
  used. I'm not sure if this is a bug or if the API is supposed to work this way.
427
392
  """
428
393
 
429
- if exclude_cancelled and status == BookingStatus.Cancelled:
394
+ if exclude_cancelled and status == models.BookingStatus.Cancelled:
430
395
  logger.warning(
431
396
  "Cannot exclude cancelled bookings when status is Cancelled. Setting exclude_cancelled to False."
432
397
  )
@@ -446,7 +411,7 @@ class Otf:
446
411
 
447
412
  bookings = res["data"][:limit] if limit else res["data"]
448
413
 
449
- data = BookingList(bookings=bookings)
414
+ data = models.BookingList(bookings=bookings)
450
415
  data.bookings = sorted(data.bookings, key=lambda x: x.otf_class.starts_at_local)
451
416
 
452
417
  for booking in data.bookings:
@@ -458,14 +423,14 @@ class Otf:
458
423
  booking.is_home_studio = False
459
424
 
460
425
  if exclude_cancelled:
461
- data.bookings = [b for b in data.bookings if b.status != BookingStatus.Cancelled]
426
+ data.bookings = [b for b in data.bookings if b.status != models.BookingStatus.Cancelled]
462
427
 
463
428
  if exclude_checkedin:
464
- data.bookings = [b for b in data.bookings if b.status != BookingStatus.CheckedIn]
429
+ data.bookings = [b for b in data.bookings if b.status != models.BookingStatus.CheckedIn]
465
430
 
466
431
  return data
467
432
 
468
- async def _get_bookings_old(self, status: BookingStatus | None = None) -> BookingList:
433
+ async def _get_bookings_old(self, status: models.BookingStatus | None = None):
469
434
  """Get the member's bookings.
470
435
 
471
436
  Args:
@@ -497,10 +462,10 @@ class Otf:
497
462
  """
498
463
 
499
464
  if status and status not in [
500
- BookingStatus.Cancelled,
501
- BookingStatus.Booked,
502
- BookingStatus.CheckedIn,
503
- BookingStatus.Waitlisted,
465
+ models.BookingStatus.Cancelled,
466
+ models.BookingStatus.Booked,
467
+ models.BookingStatus.CheckedIn,
468
+ models.BookingStatus.Waitlisted,
504
469
  ]:
505
470
  raise ValueError(
506
471
  "Invalid status provided. Only Cancelled, Booked, CheckedIn, Waitlisted, and None are supported."
@@ -512,20 +477,23 @@ class Otf:
512
477
 
513
478
  res = await self._default_request("GET", f"/member/members/{self._member_id}/bookings", params=params)
514
479
 
515
- return BookingList(bookings=res["data"])
480
+ return models.BookingList(bookings=res["data"])
516
481
 
517
- async def get_challenge_tracker_content(self) -> ChallengeTrackerContent:
482
+ async def get_challenge_tracker_content(self):
518
483
  """Get the member's challenge tracker content.
519
484
 
520
485
  Returns:
521
486
  ChallengeTrackerContent: The member's challenge tracker content.
522
487
  """
523
488
  data = await self._default_request("GET", f"/challenges/v3.1/member/{self._member_id}")
524
- return ChallengeTrackerContent(**data["Dto"])
489
+ return models.ChallengeTrackerContent(**data["Dto"])
525
490
 
526
491
  async def get_challenge_tracker_detail(
527
- self, equipment_id: EquipmentType, challenge_type_id: ChallengeType, challenge_sub_type_id: int = 0
528
- ) -> ChallengeTrackerDetailList:
492
+ self,
493
+ equipment_id: models.EquipmentType,
494
+ challenge_type_id: models.ChallengeType,
495
+ challenge_sub_type_id: int = 0,
496
+ ):
529
497
  """Get the member's challenge tracker details.
530
498
 
531
499
  Args:
@@ -549,9 +517,9 @@ class Otf:
549
517
 
550
518
  data = await self._default_request("GET", f"/challenges/v3/member/{self._member_id}/benchmarks", params=params)
551
519
 
552
- return ChallengeTrackerDetailList(details=data["Dto"])
520
+ return models.ChallengeTrackerDetailList(details=data["Dto"])
553
521
 
554
- async def get_challenge_tracker_participation(self, challenge_type_id: ChallengeType) -> typing.Any:
522
+ async def get_challenge_tracker_participation(self, challenge_type_id: models.ChallengeType):
555
523
  """Get the member's participation in a challenge.
556
524
 
557
525
  Args:
@@ -575,7 +543,7 @@ class Otf:
575
543
 
576
544
  async def get_member_detail(
577
545
  self, include_addresses: bool = True, include_class_summary: bool = True, include_credit_card: bool = False
578
- ) -> MemberDetail:
546
+ ):
579
547
  """Get the member details.
580
548
 
581
549
  Args:
@@ -611,9 +579,9 @@ class Otf:
611
579
  params = {"include": ",".join(include)} if include else None
612
580
 
613
581
  data = await self._default_request("GET", f"/member/members/{self._member_id}", params=params)
614
- return MemberDetail(**data["data"])
582
+ return models.MemberDetail(**data["data"])
615
583
 
616
- async def get_member_membership(self) -> MemberMembership:
584
+ async def get_member_membership(self):
617
585
  """Get the member's membership details.
618
586
 
619
587
  Returns:
@@ -621,18 +589,18 @@ class Otf:
621
589
  """
622
590
 
623
591
  data = await self._default_request("GET", f"/member/members/{self._member_id}/memberships")
624
- return MemberMembership(**data["data"])
592
+ return models.MemberMembership(**data["data"])
625
593
 
626
- async def get_member_purchases(self) -> MemberPurchaseList:
594
+ async def get_member_purchases(self):
627
595
  """Get the member's purchases, including monthly subscriptions and class packs.
628
596
 
629
597
  Returns:
630
598
  MemberPurchaseList: The member's purchases.
631
599
  """
632
600
  data = await self._default_request("GET", f"/member/members/{self._member_id}/purchases")
633
- return MemberPurchaseList(data=data["data"])
601
+ return models.MemberPurchaseList(data=data["data"])
634
602
 
635
- async def get_member_lifetime_stats(self, select_time: StatsTime = StatsTime.AllTime) -> StatsResponse:
603
+ async def get_member_lifetime_stats(self, select_time: models.StatsTime = models.StatsTime.AllTime):
636
604
  """Get the member's lifetime stats.
637
605
 
638
606
  Args:
@@ -649,10 +617,9 @@ class Otf:
649
617
 
650
618
  data = await self._default_request("GET", f"/performance/v2/{self._member_id}/over-time/{select_time.value}")
651
619
 
652
- stats = StatsResponse(**data["data"])
653
- return stats
620
+ return models.StatsResponse(**data["data"])
654
621
 
655
- async def get_out_of_studio_workout_history(self) -> OutOfStudioWorkoutHistoryList:
622
+ async def get_out_of_studio_workout_history(self):
656
623
  """Get the member's out of studio workout history.
657
624
 
658
625
  Returns:
@@ -660,9 +627,9 @@ class Otf:
660
627
  """
661
628
  data = await self._default_request("GET", f"/member/members/{self._member_id}/out-of-studio-workout")
662
629
 
663
- return OutOfStudioWorkoutHistoryList(data=data["data"])
630
+ return models.OutOfStudioWorkoutHistoryList(data=data["data"])
664
631
 
665
- async def get_favorite_studios(self) -> FavoriteStudioList:
632
+ async def get_favorite_studios(self):
666
633
  """Get the member's favorite studios.
667
634
 
668
635
  Returns:
@@ -670,9 +637,9 @@ class Otf:
670
637
  """
671
638
  data = await self._default_request("GET", f"/member/members/{self._member_id}/favorite-studios")
672
639
 
673
- return FavoriteStudioList(studios=data["data"])
640
+ return models.FavoriteStudioList(studios=data["data"])
674
641
 
675
- async def get_latest_agreement(self) -> LatestAgreement:
642
+ async def get_latest_agreement(self):
676
643
  """Get the latest agreement for the member.
677
644
 
678
645
  Returns:
@@ -684,9 +651,9 @@ class Otf:
684
651
  in general. The agreement ID is hardcoded in the endpoint, so it will always return the same agreement.
685
652
  """
686
653
  data = await self._default_request("GET", "/member/agreements/9d98fb27-0f00-4598-ad08-5b1655a59af6")
687
- return LatestAgreement(**data["data"])
654
+ return models.LatestAgreement(**data["data"])
688
655
 
689
- async def get_studio_services(self, studio_uuid: str | None = None) -> StudioServiceList:
656
+ async def get_studio_services(self, studio_uuid: str | None = None):
690
657
  """Get the services available at a specific studio. If no studio UUID is provided, the member's home studio
691
658
  will be used.
692
659
 
@@ -699,9 +666,9 @@ class Otf:
699
666
  """
700
667
  studio_uuid = studio_uuid or self.home_studio_uuid
701
668
  data = await self._default_request("GET", f"/member/studios/{studio_uuid}/services")
702
- return StudioServiceList(data=data["data"])
669
+ return models.StudioServiceList(data=data["data"])
703
670
 
704
- async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummaryList:
671
+ async def get_performance_summaries(self, limit: int = 30):
705
672
  """Get a list of performance summaries for the authenticated user.
706
673
 
707
674
  Args:
@@ -719,10 +686,9 @@ class Otf:
719
686
  path = "/v1/performance-summaries"
720
687
  params = {"limit": limit}
721
688
  res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers, params=params)
722
- retval = PerformanceSummaryList(summaries=res["items"])
723
- return retval
689
+ return models.PerformanceSummaryList(summaries=res["items"])
724
690
 
725
- async def get_performance_summary(self, performance_summary_id: str) -> PerformanceSummaryDetail:
691
+ async def get_performance_summary(self, performance_summary_id: str):
726
692
  """Get a detailed performance summary for a given workout.
727
693
 
728
694
  Args:
@@ -734,10 +700,9 @@ class Otf:
734
700
 
735
701
  path = f"/v1/performance-summaries/{performance_summary_id}"
736
702
  res = await self._performance_summary_request("GET", path, headers=self._perf_api_headers)
737
- retval = PerformanceSummaryDetail(**res)
738
- return retval
703
+ return models.PerformanceSummaryDetail(**res)
739
704
 
740
- async def get_studio_detail(self, studio_uuid: str | None = None) -> StudioDetail:
705
+ async def get_studio_detail(self, studio_uuid: str | None = None):
741
706
  """Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
742
707
  user's home studio.
743
708
 
@@ -754,7 +719,7 @@ class Otf:
754
719
  params = {"include": "locations"}
755
720
 
756
721
  res = await self._default_request("GET", path, params=params)
757
- return StudioDetail(**res["data"])
722
+ return models.StudioDetail(**res["data"])
758
723
 
759
724
  async def search_studios_by_geo(
760
725
  self,
@@ -763,7 +728,7 @@ class Otf:
763
728
  distance: float = 50,
764
729
  page_index: int = 1,
765
730
  page_size: int = 50,
766
- ) -> StudioDetailList:
731
+ ):
767
732
  """Search for studios by geographic location.
768
733
 
769
734
  Args:
@@ -806,21 +771,21 @@ class Otf:
806
771
  "distance": distance,
807
772
  }
808
773
 
809
- all_results: list[StudioDetail] = []
774
+ all_results: list[models.StudioDetail] = []
810
775
 
811
776
  while True:
812
777
  res = await self._default_request("GET", path, params=params)
813
- pagination = Pagination(**res["data"].pop("pagination"))
814
- all_results.extend([StudioDetail(**studio) for studio in res["data"]["studios"]])
778
+ pagination = models.Pagination(**res["data"].pop("pagination"))
779
+ all_results.extend([models.StudioDetail(**studio) for studio in res["data"]["studios"]])
815
780
 
816
781
  if len(all_results) == pagination.total_count:
817
782
  break
818
783
 
819
784
  params["pageIndex"] += 1
820
785
 
821
- return StudioDetailList(studios=all_results)
786
+ return models.StudioDetailList(studios=all_results)
822
787
 
823
- async def get_hr_history(self) -> TelemetryHrHistory:
788
+ async def get_hr_history(self):
824
789
  """Get the heartrate history for the user.
825
790
 
826
791
  Returns a list of history items that contain the max heartrate, start/end bpm for each zone,
@@ -834,9 +799,9 @@ class Otf:
834
799
 
835
800
  params = {"memberUuid": self._member_id}
836
801
  res = await self._telemetry_request("GET", path, params=params)
837
- return TelemetryHrHistory(**res)
802
+ return models.TelemetryHrHistory(**res)
838
803
 
839
- async def get_max_hr(self) -> TelemetryMaxHr:
804
+ async def get_max_hr(self):
840
805
  """Get the max heartrate for the user.
841
806
 
842
807
  Returns a simple object that has the member_uuid and the max_hr.
@@ -849,9 +814,9 @@ class Otf:
849
814
  params = {"memberUuid": self._member_id}
850
815
 
851
816
  res = await self._telemetry_request("GET", path, params=params)
852
- return TelemetryMaxHr(**res)
817
+ return models.TelemetryMaxHr(**res)
853
818
 
854
- async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> Telemetry:
819
+ async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120):
855
820
  """Get the telemetry for a performance summary.
856
821
 
857
822
  This returns an object that contains the max heartrate, start/end bpm for each zone,
@@ -869,11 +834,11 @@ class Otf:
869
834
 
870
835
  params = {"classHistoryUuid": performance_summary_id, "maxDataPoints": max_data_points}
871
836
  res = await self._telemetry_request("GET", path, params=params)
872
- return Telemetry(**res)
837
+ return models.Telemetry(**res)
873
838
 
874
839
  # the below do not return any data for me, so I can't test them
875
840
 
876
- async def _get_member_services(self, active_only: bool = True) -> typing.Any:
841
+ async def _get_member_services(self, active_only: bool = True):
877
842
  """Get the member's services.
878
843
 
879
844
  Args:
@@ -888,7 +853,7 @@ class Otf:
888
853
  )
889
854
  return data
890
855
 
891
- async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> typing.Any:
856
+ async def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None):
892
857
  """Get data from the member's aspire wearable.
893
858
 
894
859
  Note: I don't have an aspire wearable, so I can't test this.
@@ -1,9 +1,9 @@
1
1
  import typing
2
+ from datetime import datetime, timedelta
3
+ from logging import getLogger
2
4
  from typing import Any
3
5
 
4
6
  import jwt
5
- import pendulum
6
- from loguru import logger
7
7
  from pycognito import AWSSRP, Cognito, MFAChallengeException
8
8
  from pycognito.exceptions import TokenVerificationException
9
9
  from pydantic import Field
@@ -15,6 +15,7 @@ if typing.TYPE_CHECKING:
15
15
  from boto3.session import Session
16
16
  from botocore.config import Config
17
17
 
18
+ LOGGER = getLogger(__name__)
18
19
  CLIENT_ID = "65knvqta6p37efc2l3eh26pl5o" # from otlive
19
20
  USER_POOL_ID = "us-east-1_dYDxUeyL1"
20
21
 
@@ -64,12 +65,12 @@ class OtfCognito(Cognito):
64
65
  def device_key(self, value: str | None):
65
66
  if not value:
66
67
  if self._device_key:
67
- logger.info("Clearing device key")
68
+ LOGGER.debug("Clearing device key")
68
69
  self._device_key = value
69
70
  return
70
71
 
71
72
  redacted_value = value[:4] + "*" * (len(value) - 8) + value[-4:]
72
- logger.info(f"Setting device key: {redacted_value}")
73
+ LOGGER.debug(f"Setting device key: {redacted_value}")
73
74
  self._device_key = value
74
75
 
75
76
  def _set_tokens(self, tokens: dict[str, Any]):
@@ -116,7 +117,7 @@ class OtfCognito(Cognito):
116
117
  try:
117
118
  self.renew_access_token()
118
119
  except TokenVerificationException:
119
- logger.error("Failed to renew access token. Confirming device.")
120
+ LOGGER.error("Failed to renew access token. Confirming device.")
120
121
  self.device_key = None
121
122
  aws.confirm_device(tokens)
122
123
 
@@ -129,11 +130,11 @@ class OtfCognito(Cognito):
129
130
  """
130
131
  if not self.access_token:
131
132
  raise AttributeError("Access Token Required to Check Token")
132
- now = pendulum.now()
133
+ now = datetime.now() # noqa
133
134
  dec_access_token = jwt.decode(self.access_token, options={"verify_signature": False})
134
135
 
135
- exp = pendulum.DateTime.fromtimestamp(dec_access_token["exp"])
136
- if now > exp.subtract(minutes=15):
136
+ exp = datetime.fromtimestamp(dec_access_token["exp"]) # noqa
137
+ if now > exp - timedelta(minutes=15):
137
138
  expired = True
138
139
  if renew:
139
140
  self.renew_access_token()
@@ -147,7 +148,7 @@ class OtfCognito(Cognito):
147
148
  self._add_secret_hash(auth_params, "SECRET_HASH")
148
149
 
149
150
  if self.device_key:
150
- logger.info("Using device key for refresh token")
151
+ LOGGER.debug("Using device key for refresh token")
151
152
  auth_params["DEVICE_KEY"] = self.device_key
152
153
 
153
154
  refresh_response = self.client.initiate_auth(
@@ -311,5 +312,5 @@ class OtfUser(OtfItemBase):
311
312
  }
312
313
 
313
314
  @property
314
- def device_key(self) -> str:
315
+ def device_key(self):
315
316
  return self.cognito.device_key
@@ -0,0 +1,18 @@
1
+ class BookingError(Exception):
2
+ booking_uuid: str | None
3
+
4
+ def __init__(self, message: str, booking_uuid: str | None = None):
5
+ super().__init__(message)
6
+ self.booking_uuid = booking_uuid
7
+
8
+
9
+ class AlreadyBookedError(BookingError): ...
10
+
11
+
12
+ class BookingAlreadyCancelledError(BookingError): ...
13
+
14
+
15
+ class OutsideSchedulingWindowError(Exception): ...
16
+
17
+
18
+ class BookingNotFoundError(Exception): ...