otf-api 0.10.0__tar.gz → 0.10.2__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 (34) hide show
  1. {otf_api-0.10.0 → otf_api-0.10.2}/PKG-INFO +1 -1
  2. {otf_api-0.10.0 → otf_api-0.10.2}/pyproject.toml +1 -1
  3. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/__init__.py +1 -1
  4. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/api.py +47 -26
  5. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/bookings.py +4 -0
  6. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/classes.py +3 -1
  7. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/enums.py +1 -0
  8. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/performance_summary.py +2 -1
  9. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/studio_detail.py +3 -3
  10. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/telemetry.py +4 -1
  11. {otf_api-0.10.0 → otf_api-0.10.2}/LICENSE +0 -0
  12. {otf_api-0.10.0 → otf_api-0.10.2}/README.md +0 -0
  13. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/auth/__init__.py +0 -0
  14. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/auth/auth.py +0 -0
  15. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/auth/user.py +0 -0
  16. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/auth/utils.py +0 -0
  17. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/exceptions.py +0 -0
  18. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/filters.py +0 -0
  19. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/logging.py +0 -0
  20. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/__init__.py +0 -0
  21. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/base.py +0 -0
  22. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/body_composition_list.py +0 -0
  23. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/challenge_tracker_content.py +0 -0
  24. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/challenge_tracker_detail.py +0 -0
  25. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/lifetime_stats.py +0 -0
  26. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/member_detail.py +0 -0
  27. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/member_membership.py +0 -0
  28. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/member_purchases.py +0 -0
  29. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/mixins.py +0 -0
  30. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/notifications.py +0 -0
  31. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/out_of_studio_workout_history.py +0 -0
  32. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/models/studio_services.py +0 -0
  33. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/py.typed +0 -0
  34. {otf_api-0.10.0 → otf_api-0.10.2}/src/otf_api/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: otf-api
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  License: MIT
6
6
  Author: Jessica Smith
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "otf-api"
3
- version = "0.10.0"
3
+ version = "0.10.2"
4
4
  description = "Python OrangeTheory Fitness API Client"
5
5
  authors = ["Jessica Smith <j.smith.git1@gmail.com>"]
6
6
  license = "MIT"
@@ -4,7 +4,7 @@ from otf_api.api import Otf
4
4
  from otf_api import models
5
5
  from otf_api.auth import OtfUser
6
6
 
7
- __version__ = "0.10.0"
7
+ __version__ = "0.10.2"
8
8
 
9
9
 
10
10
  __all__ = ["Otf", "OtfUser", "models"]
@@ -1,7 +1,6 @@
1
1
  import atexit
2
2
  import contextlib
3
3
  import functools
4
- import warnings
5
4
  from concurrent.futures import ThreadPoolExecutor
6
5
  from copy import deepcopy
7
6
  from datetime import date, datetime, timedelta
@@ -68,6 +67,7 @@ class Otf:
68
67
  retry=retry_if_exception_type(exc.OtfRequestError),
69
68
  stop=stop_after_attempt(3),
70
69
  wait=wait_exponential(multiplier=1, min=4, max=10),
70
+ reraise=True,
71
71
  )
72
72
  def _do(
73
73
  self,
@@ -177,9 +177,10 @@ class Otf:
177
177
  """Retrieve raw member membership details."""
178
178
  return self._default_request("GET", f"/member/members/{self.member_uuid}/memberships")
179
179
 
180
- def _get_performance_summaries_raw(self) -> dict:
180
+ def _get_performance_summaries_raw(self, limit: int | None = None) -> dict:
181
181
  """Retrieve raw performance summaries data."""
182
- return self._performance_summary_request("GET", "/v1/performance-summaries")
182
+ params = {"limit": limit} if limit else {}
183
+ return self._performance_summary_request("GET", "/v1/performance-summaries", params=params)
183
184
 
184
185
  def _get_performance_summary_raw(self, performance_summary_id: str) -> dict:
185
186
  """Retrieve raw performance summary data."""
@@ -321,14 +322,16 @@ class Otf:
321
322
  },
322
323
  )
323
324
 
324
- def _rate_class_raw(self, class_uuid: str, class_history_uuid: str, class_rating: int, coach_rating: int) -> dict:
325
+ def _rate_class_raw(
326
+ self, class_uuid: str, performance_summary_id: str, class_rating: int, coach_rating: int
327
+ ) -> dict:
325
328
  """Retrieve raw response from rating a class and coach."""
326
329
  return self._default_request(
327
330
  "POST",
328
331
  "/mobile/v1/members/classes/ratings",
329
332
  json={
330
333
  "classUUId": class_uuid,
331
- "otBeatClassHistoryUUId": class_history_uuid,
334
+ "otBeatClassHistoryUUId": performance_summary_id,
332
335
  "classRating": class_rating,
333
336
  "coachRating": coach_rating,
334
337
  },
@@ -919,6 +922,8 @@ class Otf:
919
922
  """Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
920
923
  user's home studio.
921
924
 
925
+ If the studio is not found, it will return a StudioDetail object with default values.
926
+
922
927
  Args:
923
928
  studio_uuid (str, optional): The studio UUID to get detailed information about.
924
929
 
@@ -926,7 +931,11 @@ class Otf:
926
931
  StudioDetail: Detailed information about the studio.
927
932
  """
928
933
  studio_uuid = studio_uuid or self.home_studio_uuid
929
- res = self._get_studio_detail_raw(studio_uuid)
934
+
935
+ try:
936
+ res = self._get_studio_detail_raw(studio_uuid)
937
+ except exc.ResourceNotFoundError:
938
+ return models.StudioDetail(studioUUId=studio_uuid, studioName="Studio Not Found", studioStatus="Unknown")
930
939
 
931
940
  return models.StudioDetail(**res["data"])
932
941
 
@@ -1103,9 +1112,12 @@ class Otf:
1103
1112
  return models.FitnessBenchmark(**data["Dto"][0])
1104
1113
 
1105
1114
  @cached(cache=TTLCache(maxsize=1024, ttl=600))
1106
- def get_performance_summaries_dict(self) -> dict[str, models.PerformanceSummary]:
1115
+ def get_performance_summaries_dict(self, limit: int | None = None) -> dict[str, models.PerformanceSummary]:
1107
1116
  """Get a dictionary of performance summaries for the authenticated user.
1108
1117
 
1118
+ Args:
1119
+ limit (int | None): The maximum number of entries to return. Default is None.
1120
+
1109
1121
  Returns:
1110
1122
  dict[str, PerformanceSummary]: A dictionary of performance summaries, keyed by class history UUID.
1111
1123
 
@@ -1115,7 +1127,7 @@ class Otf:
1115
1127
 
1116
1128
  """
1117
1129
 
1118
- items = self._get_performance_summaries_raw()["items"]
1130
+ items = self._get_performance_summaries_raw(limit=limit)["items"]
1119
1131
 
1120
1132
  distinct_studio_ids = set([rec["class"]["studio"]["id"] for rec in items])
1121
1133
  perf_summary_ids = set([rec["id"] for rec in items])
@@ -1133,7 +1145,7 @@ class Otf:
1133
1145
  item["detail"] = perf_summary_dict[item["id"]]
1134
1146
 
1135
1147
  entries = [models.PerformanceSummary(**item) for item in items]
1136
- entries_dict = {entry.class_history_uuid: entry for entry in entries}
1148
+ entries_dict = {entry.performance_summary_id: entry for entry in entries}
1137
1149
 
1138
1150
  return entries_dict
1139
1151
 
@@ -1141,7 +1153,7 @@ class Otf:
1141
1153
  """Get a list of all performance summaries for the authenticated user.
1142
1154
 
1143
1155
  Args:
1144
- limit (int | None): The maximum number of entries to return. Default is None. Deprecated.
1156
+ limit (int | None): The maximum number of entries to return. Default is None.
1145
1157
 
1146
1158
  Returns:
1147
1159
  list[PerformanceSummary]: A list of performance summaries.
@@ -1152,26 +1164,33 @@ class Otf:
1152
1164
 
1153
1165
  """
1154
1166
 
1155
- if limit:
1156
- warnings.warn("Limit is deprecated and will be removed in a future version.", DeprecationWarning)
1157
-
1158
- records = list(self.get_performance_summaries_dict().values())
1167
+ records = list(self.get_performance_summaries_dict(limit=limit).values())
1159
1168
 
1160
1169
  sorted_records = sorted(records, key=lambda x: x.otf_class.starts_at, reverse=True)
1161
1170
 
1162
1171
  return sorted_records
1163
1172
 
1164
- def get_performance_summary(self, performance_summary_id: str) -> models.PerformanceSummary:
1173
+ def get_performance_summary(
1174
+ self, performance_summary_id: str, limit: int | None = None
1175
+ ) -> models.PerformanceSummary:
1165
1176
  """Get performance summary for a given workout.
1166
1177
 
1178
+ Note: Due to the way the OTF API is set up, we have to call both the list and the get endpoints. By
1179
+ default this will call the list endpoint with no limit, in order to ensure that the performance summary
1180
+ is returned if it exists. This could result in a lot of requests, so you also have the option to provide
1181
+ a limit to only fetch a certain number of performance summaries.
1182
+
1167
1183
  Args:
1168
1184
  performance_summary_id (str): The ID of the performance summary to retrieve.
1169
1185
 
1170
1186
  Returns:
1171
1187
  PerformanceSummary: The performance summary.
1188
+
1189
+ Raises:
1190
+ ResourceNotFoundError: If the performance_summary_id is not in the list of performance summaries.
1172
1191
  """
1173
1192
 
1174
- perf_summary = self.get_performance_summaries_dict().get(performance_summary_id)
1193
+ perf_summary = self.get_performance_summaries_dict(limit=limit).get(performance_summary_id)
1175
1194
 
1176
1195
  if perf_summary is None:
1177
1196
  raise exc.ResourceNotFoundError(f"Performance summary {performance_summary_id} not found")
@@ -1180,7 +1199,7 @@ class Otf:
1180
1199
 
1181
1200
  @functools.lru_cache(maxsize=1024)
1182
1201
  def _get_performancy_summary_detail(self, performance_summary_id: str) -> dict[str, Any]:
1183
- """Get the details for a performance summary.
1202
+ """Get the details for a performance summary. Generally should not be called directly. This
1184
1203
 
1185
1204
  Args:
1186
1205
  performance_summary_id (str): The performance summary ID.
@@ -1350,7 +1369,7 @@ class Otf:
1350
1369
  def _rate_class(
1351
1370
  self,
1352
1371
  class_uuid: str,
1353
- class_history_uuid: str,
1372
+ performance_summary_id: str,
1354
1373
  class_rating: Literal[0, 1, 2, 3],
1355
1374
  coach_rating: Literal[0, 1, 2, 3],
1356
1375
  ) -> models.PerformanceSummary:
@@ -1363,7 +1382,7 @@ class Otf:
1363
1382
 
1364
1383
  Args:
1365
1384
  class_uuid (str): The class UUID.
1366
- class_history_uuid (str): The performance summary ID.
1385
+ performance_summary_id (str): The performance summary ID.
1367
1386
  class_rating (int): The class rating. Must be 0, 1, 2, or 3.
1368
1387
  coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
1369
1388
 
@@ -1390,10 +1409,10 @@ class Otf:
1390
1409
  body_coach_rating = COACH_RATING_MAP[coach_rating]
1391
1410
 
1392
1411
  try:
1393
- self._rate_class_raw(class_uuid, class_history_uuid, body_class_rating, body_coach_rating)
1412
+ self._rate_class_raw(class_uuid, performance_summary_id, body_class_rating, body_coach_rating)
1394
1413
  except exc.OtfRequestError as e:
1395
1414
  if e.response.status_code == 403:
1396
- raise exc.AlreadyRatedError(f"Performance summary {class_history_uuid} is already rated.") from None
1415
+ raise exc.AlreadyRatedError(f"Performance summary {performance_summary_id} is already rated.") from None
1397
1416
  raise
1398
1417
 
1399
1418
  # we have to clear the cache after rating a class, otherwise we will get back the same data
@@ -1403,7 +1422,7 @@ class Otf:
1403
1422
  # NOTE: the individual perf summary endpoint does not have rating data, so it's cache is not cleared
1404
1423
  self.get_performance_summaries_dict.cache_clear()
1405
1424
 
1406
- return self.get_performance_summary(class_history_uuid)
1425
+ return self.get_performance_summary(performance_summary_id)
1407
1426
 
1408
1427
  def rate_class_from_performance_summary(
1409
1428
  self,
@@ -1429,18 +1448,20 @@ class Otf:
1429
1448
  """
1430
1449
 
1431
1450
  if perf_summary.is_rated:
1432
- raise exc.AlreadyRatedError(f"Performance summary {perf_summary.class_history_uuid} is already rated.")
1451
+ raise exc.AlreadyRatedError(f"Performance summary {perf_summary.performance_summary_id} is already rated.")
1433
1452
 
1434
1453
  if not perf_summary.ratable:
1435
- raise exc.ClassNotRatableError(f"Performance summary {perf_summary.class_history_uuid} is not rateable.")
1454
+ raise exc.ClassNotRatableError(
1455
+ f"Performance summary {perf_summary.performance_summary_id} is not rateable."
1456
+ )
1436
1457
 
1437
1458
  if not perf_summary.otf_class or not perf_summary.otf_class.class_uuid:
1438
1459
  raise ValueError(
1439
- f"Performance summary {perf_summary.class_history_uuid} does not have an associated class."
1460
+ f"Performance summary {perf_summary.performance_summary_id} does not have an associated class."
1440
1461
  )
1441
1462
 
1442
1463
  return self._rate_class(
1443
- perf_summary.otf_class.class_uuid, perf_summary.class_history_uuid, class_rating, coach_rating
1464
+ perf_summary.otf_class.class_uuid, perf_summary.performance_summary_id, class_rating, coach_rating
1444
1465
  )
1445
1466
 
1446
1467
  # the below do not return any data for me, so I can't test them
@@ -36,6 +36,10 @@ 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
+ def __str__(self) -> str:
40
+ starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
41
+ return f"Class: {starts_at_str} {self.name} - {self.coach.first_name}"
42
+
39
43
 
40
44
  class Booking(OtfItemBase):
41
45
  booking_uuid: str = Field(alias="classBookingUUId", description="ID used to cancel the booking")
@@ -33,7 +33,9 @@ class OtfClass(OtfItemBase):
33
33
  is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio")
34
34
 
35
35
  # unused fields
36
- class_id: str | None = Field(None, alias="id", exclude=True, repr=False, description="Not used by API")
36
+ class_id: str | None = Field(
37
+ None, alias="id", exclude=True, repr=False, description="Matches new booking endpoint class id"
38
+ )
37
39
 
38
40
  created_at: datetime | None = Field(None, exclude=True, repr=False)
39
41
  ends_at_utc: datetime | None = Field(None, alias="ends_at", exclude=True, repr=False)
@@ -8,6 +8,7 @@ class StudioStatus(StrEnum):
8
8
  COMING_SOON = "Coming Soon"
9
9
  TEMP_CLOSED = "Temporarily Closed"
10
10
  PERM_CLOSED = "Permanently Closed"
11
+ UNKNOWN = "Unknown"
11
12
 
12
13
 
13
14
  class BookingStatus(StrEnum):
@@ -105,7 +105,8 @@ class PerformanceSummary(OtfItemBase):
105
105
 
106
106
  """
107
107
 
108
- class_history_uuid: str = Field(..., alias="id")
108
+ performance_summary_id: str = Field(..., alias="id", description="Unique identifier for this performance summary")
109
+ class_history_uuid: str = Field(..., alias="id", description="Same as performance_summary_id")
109
110
  ratable: bool | None = None
110
111
  otf_class: Class | None = Field(None, alias="class")
111
112
  coach: str | None = Field(None, alias=AliasPath("class", "coach", "first_name"))
@@ -9,8 +9,8 @@ from otf_api.models.mixins import AddressMixin
9
9
 
10
10
  class StudioLocation(AddressMixin):
11
11
  phone_number: str | None = Field(None, alias=AliasChoices("phone", "phoneNumber"))
12
- latitude: float = Field(..., alias=AliasChoices("latitude"))
13
- longitude: float = Field(..., alias=AliasChoices("longitude"))
12
+ latitude: float | None = Field(None, alias=AliasChoices("latitude"))
13
+ longitude: float | None = Field(None, alias=AliasChoices("longitude"))
14
14
 
15
15
  physical_region: str | None = Field(None, alias="physicalRegion", exclude=True, repr=False)
16
16
  physical_country_id: int | None = Field(None, alias="physicalCountryId", exclude=True, repr=False)
@@ -27,7 +27,7 @@ class StudioDetail(OtfItemBase):
27
27
  exclude=True,
28
28
  repr=False,
29
29
  )
30
- location: StudioLocation = Field(..., alias="studioLocation")
30
+ location: StudioLocation = Field(..., alias="studioLocation", default_factory=StudioLocation)
31
31
  name: str | None = Field(None, alias="studioName")
32
32
  status: StudioStatus | None = Field(
33
33
  None, alias="studioStatus", description="Active, Temporarily Closed, Coming Soon"
@@ -49,7 +49,10 @@ class TelemetryItem(OtfItemBase):
49
49
 
50
50
  class Telemetry(OtfItemBase):
51
51
  member_uuid: str = Field(..., alias="memberUuid")
52
- class_history_uuid: str = Field(..., alias="classHistoryUuid")
52
+ performance_summary_id: str = Field(
53
+ ..., alias="classHistoryUuid", description="The ID of the performance summary this telemetry item belongs to."
54
+ )
55
+ class_history_uuid: str = Field(..., alias="classHistoryUuid", description="The same as performance_summary_id.")
53
56
  class_start_time: datetime | None = Field(None, alias="classStartTime")
54
57
  max_hr: int | None = Field(None, alias="maxHr")
55
58
  zones: Zones
File without changes
File without changes
File without changes
File without changes