otf-api 0.10.0__tar.gz → 0.10.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 (34) hide show
  1. {otf_api-0.10.0 → otf_api-0.10.1}/PKG-INFO +1 -1
  2. {otf_api-0.10.0 → otf_api-0.10.1}/pyproject.toml +1 -1
  3. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/__init__.py +1 -1
  4. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/api.py +46 -26
  5. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/enums.py +1 -0
  6. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/performance_summary.py +2 -1
  7. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/studio_detail.py +3 -3
  8. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/telemetry.py +4 -1
  9. {otf_api-0.10.0 → otf_api-0.10.1}/LICENSE +0 -0
  10. {otf_api-0.10.0 → otf_api-0.10.1}/README.md +0 -0
  11. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/auth/__init__.py +0 -0
  12. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/auth/auth.py +0 -0
  13. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/auth/user.py +0 -0
  14. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/auth/utils.py +0 -0
  15. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/exceptions.py +0 -0
  16. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/filters.py +0 -0
  17. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/logging.py +0 -0
  18. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/__init__.py +0 -0
  19. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/base.py +0 -0
  20. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/body_composition_list.py +0 -0
  21. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/bookings.py +0 -0
  22. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/challenge_tracker_content.py +0 -0
  23. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/challenge_tracker_detail.py +0 -0
  24. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/classes.py +0 -0
  25. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/lifetime_stats.py +0 -0
  26. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/member_detail.py +0 -0
  27. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/member_membership.py +0 -0
  28. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/member_purchases.py +0 -0
  29. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/mixins.py +0 -0
  30. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/notifications.py +0 -0
  31. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/out_of_studio_workout_history.py +0 -0
  32. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/models/studio_services.py +0 -0
  33. {otf_api-0.10.0 → otf_api-0.10.1}/src/otf_api/py.typed +0 -0
  34. {otf_api-0.10.0 → otf_api-0.10.1}/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.1
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.1"
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.1"
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
@@ -177,9 +176,10 @@ class Otf:
177
176
  """Retrieve raw member membership details."""
178
177
  return self._default_request("GET", f"/member/members/{self.member_uuid}/memberships")
179
178
 
180
- def _get_performance_summaries_raw(self) -> dict:
179
+ def _get_performance_summaries_raw(self, limit: int | None = None) -> dict:
181
180
  """Retrieve raw performance summaries data."""
182
- return self._performance_summary_request("GET", "/v1/performance-summaries")
181
+ params = {"limit": limit} if limit else {}
182
+ return self._performance_summary_request("GET", "/v1/performance-summaries", params=params)
183
183
 
184
184
  def _get_performance_summary_raw(self, performance_summary_id: str) -> dict:
185
185
  """Retrieve raw performance summary data."""
@@ -321,14 +321,16 @@ class Otf:
321
321
  },
322
322
  )
323
323
 
324
- def _rate_class_raw(self, class_uuid: str, class_history_uuid: str, class_rating: int, coach_rating: int) -> dict:
324
+ def _rate_class_raw(
325
+ self, class_uuid: str, performance_summary_id: str, class_rating: int, coach_rating: int
326
+ ) -> dict:
325
327
  """Retrieve raw response from rating a class and coach."""
326
328
  return self._default_request(
327
329
  "POST",
328
330
  "/mobile/v1/members/classes/ratings",
329
331
  json={
330
332
  "classUUId": class_uuid,
331
- "otBeatClassHistoryUUId": class_history_uuid,
333
+ "otBeatClassHistoryUUId": performance_summary_id,
332
334
  "classRating": class_rating,
333
335
  "coachRating": coach_rating,
334
336
  },
@@ -919,6 +921,8 @@ class Otf:
919
921
  """Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
920
922
  user's home studio.
921
923
 
924
+ If the studio is not found, it will return a StudioDetail object with default values.
925
+
922
926
  Args:
923
927
  studio_uuid (str, optional): The studio UUID to get detailed information about.
924
928
 
@@ -926,7 +930,11 @@ class Otf:
926
930
  StudioDetail: Detailed information about the studio.
927
931
  """
928
932
  studio_uuid = studio_uuid or self.home_studio_uuid
929
- res = self._get_studio_detail_raw(studio_uuid)
933
+
934
+ try:
935
+ res = self._get_studio_detail_raw(studio_uuid)
936
+ except exc.ResourceNotFoundError:
937
+ return models.StudioDetail(studioUUId=studio_uuid, studioName="Studio Not Found", studioStatus="Unknown")
930
938
 
931
939
  return models.StudioDetail(**res["data"])
932
940
 
@@ -1103,9 +1111,12 @@ class Otf:
1103
1111
  return models.FitnessBenchmark(**data["Dto"][0])
1104
1112
 
1105
1113
  @cached(cache=TTLCache(maxsize=1024, ttl=600))
1106
- def get_performance_summaries_dict(self) -> dict[str, models.PerformanceSummary]:
1114
+ def get_performance_summaries_dict(self, limit: int | None = None) -> dict[str, models.PerformanceSummary]:
1107
1115
  """Get a dictionary of performance summaries for the authenticated user.
1108
1116
 
1117
+ Args:
1118
+ limit (int | None): The maximum number of entries to return. Default is None.
1119
+
1109
1120
  Returns:
1110
1121
  dict[str, PerformanceSummary]: A dictionary of performance summaries, keyed by class history UUID.
1111
1122
 
@@ -1115,7 +1126,7 @@ class Otf:
1115
1126
 
1116
1127
  """
1117
1128
 
1118
- items = self._get_performance_summaries_raw()["items"]
1129
+ items = self._get_performance_summaries_raw(limit=limit)["items"]
1119
1130
 
1120
1131
  distinct_studio_ids = set([rec["class"]["studio"]["id"] for rec in items])
1121
1132
  perf_summary_ids = set([rec["id"] for rec in items])
@@ -1133,7 +1144,7 @@ class Otf:
1133
1144
  item["detail"] = perf_summary_dict[item["id"]]
1134
1145
 
1135
1146
  entries = [models.PerformanceSummary(**item) for item in items]
1136
- entries_dict = {entry.class_history_uuid: entry for entry in entries}
1147
+ entries_dict = {entry.performance_summary_id: entry for entry in entries}
1137
1148
 
1138
1149
  return entries_dict
1139
1150
 
@@ -1141,7 +1152,7 @@ class Otf:
1141
1152
  """Get a list of all performance summaries for the authenticated user.
1142
1153
 
1143
1154
  Args:
1144
- limit (int | None): The maximum number of entries to return. Default is None. Deprecated.
1155
+ limit (int | None): The maximum number of entries to return. Default is None.
1145
1156
 
1146
1157
  Returns:
1147
1158
  list[PerformanceSummary]: A list of performance summaries.
@@ -1152,26 +1163,33 @@ class Otf:
1152
1163
 
1153
1164
  """
1154
1165
 
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())
1166
+ records = list(self.get_performance_summaries_dict(limit=limit).values())
1159
1167
 
1160
1168
  sorted_records = sorted(records, key=lambda x: x.otf_class.starts_at, reverse=True)
1161
1169
 
1162
1170
  return sorted_records
1163
1171
 
1164
- def get_performance_summary(self, performance_summary_id: str) -> models.PerformanceSummary:
1172
+ def get_performance_summary(
1173
+ self, performance_summary_id: str, limit: int | None = None
1174
+ ) -> models.PerformanceSummary:
1165
1175
  """Get performance summary for a given workout.
1166
1176
 
1177
+ Note: Due to the way the OTF API is set up, we have to call both the list and the get endpoints. By
1178
+ default this will call the list endpoint with no limit, in order to ensure that the performance summary
1179
+ is returned if it exists. This could result in a lot of requests, so you also have the option to provide
1180
+ a limit to only fetch a certain number of performance summaries.
1181
+
1167
1182
  Args:
1168
1183
  performance_summary_id (str): The ID of the performance summary to retrieve.
1169
1184
 
1170
1185
  Returns:
1171
1186
  PerformanceSummary: The performance summary.
1187
+
1188
+ Raises:
1189
+ ResourceNotFoundError: If the performance_summary_id is not in the list of performance summaries.
1172
1190
  """
1173
1191
 
1174
- perf_summary = self.get_performance_summaries_dict().get(performance_summary_id)
1192
+ perf_summary = self.get_performance_summaries_dict(limit=limit).get(performance_summary_id)
1175
1193
 
1176
1194
  if perf_summary is None:
1177
1195
  raise exc.ResourceNotFoundError(f"Performance summary {performance_summary_id} not found")
@@ -1180,7 +1198,7 @@ class Otf:
1180
1198
 
1181
1199
  @functools.lru_cache(maxsize=1024)
1182
1200
  def _get_performancy_summary_detail(self, performance_summary_id: str) -> dict[str, Any]:
1183
- """Get the details for a performance summary.
1201
+ """Get the details for a performance summary. Generally should not be called directly. This
1184
1202
 
1185
1203
  Args:
1186
1204
  performance_summary_id (str): The performance summary ID.
@@ -1350,7 +1368,7 @@ class Otf:
1350
1368
  def _rate_class(
1351
1369
  self,
1352
1370
  class_uuid: str,
1353
- class_history_uuid: str,
1371
+ performance_summary_id: str,
1354
1372
  class_rating: Literal[0, 1, 2, 3],
1355
1373
  coach_rating: Literal[0, 1, 2, 3],
1356
1374
  ) -> models.PerformanceSummary:
@@ -1363,7 +1381,7 @@ class Otf:
1363
1381
 
1364
1382
  Args:
1365
1383
  class_uuid (str): The class UUID.
1366
- class_history_uuid (str): The performance summary ID.
1384
+ performance_summary_id (str): The performance summary ID.
1367
1385
  class_rating (int): The class rating. Must be 0, 1, 2, or 3.
1368
1386
  coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
1369
1387
 
@@ -1390,10 +1408,10 @@ class Otf:
1390
1408
  body_coach_rating = COACH_RATING_MAP[coach_rating]
1391
1409
 
1392
1410
  try:
1393
- self._rate_class_raw(class_uuid, class_history_uuid, body_class_rating, body_coach_rating)
1411
+ self._rate_class_raw(class_uuid, performance_summary_id, body_class_rating, body_coach_rating)
1394
1412
  except exc.OtfRequestError as e:
1395
1413
  if e.response.status_code == 403:
1396
- raise exc.AlreadyRatedError(f"Performance summary {class_history_uuid} is already rated.") from None
1414
+ raise exc.AlreadyRatedError(f"Performance summary {performance_summary_id} is already rated.") from None
1397
1415
  raise
1398
1416
 
1399
1417
  # we have to clear the cache after rating a class, otherwise we will get back the same data
@@ -1403,7 +1421,7 @@ class Otf:
1403
1421
  # NOTE: the individual perf summary endpoint does not have rating data, so it's cache is not cleared
1404
1422
  self.get_performance_summaries_dict.cache_clear()
1405
1423
 
1406
- return self.get_performance_summary(class_history_uuid)
1424
+ return self.get_performance_summary(performance_summary_id)
1407
1425
 
1408
1426
  def rate_class_from_performance_summary(
1409
1427
  self,
@@ -1429,18 +1447,20 @@ class Otf:
1429
1447
  """
1430
1448
 
1431
1449
  if perf_summary.is_rated:
1432
- raise exc.AlreadyRatedError(f"Performance summary {perf_summary.class_history_uuid} is already rated.")
1450
+ raise exc.AlreadyRatedError(f"Performance summary {perf_summary.performance_summary_id} is already rated.")
1433
1451
 
1434
1452
  if not perf_summary.ratable:
1435
- raise exc.ClassNotRatableError(f"Performance summary {perf_summary.class_history_uuid} is not rateable.")
1453
+ raise exc.ClassNotRatableError(
1454
+ f"Performance summary {perf_summary.performance_summary_id} is not rateable."
1455
+ )
1436
1456
 
1437
1457
  if not perf_summary.otf_class or not perf_summary.otf_class.class_uuid:
1438
1458
  raise ValueError(
1439
- f"Performance summary {perf_summary.class_history_uuid} does not have an associated class."
1459
+ f"Performance summary {perf_summary.performance_summary_id} does not have an associated class."
1440
1460
  )
1441
1461
 
1442
1462
  return self._rate_class(
1443
- perf_summary.otf_class.class_uuid, perf_summary.class_history_uuid, class_rating, coach_rating
1463
+ perf_summary.otf_class.class_uuid, perf_summary.performance_summary_id, class_rating, coach_rating
1444
1464
  )
1445
1465
 
1446
1466
  # the below do not return any data for me, so I can't test them
@@ -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