otf-api 0.10.0__py3-none-any.whl → 0.10.2__py3-none-any.whl
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/__init__.py +1 -1
- otf_api/api.py +47 -26
- otf_api/models/bookings.py +4 -0
- otf_api/models/classes.py +3 -1
- otf_api/models/enums.py +1 -0
- otf_api/models/performance_summary.py +2 -1
- otf_api/models/studio_detail.py +3 -3
- otf_api/models/telemetry.py +4 -1
- {otf_api-0.10.0.dist-info → otf_api-0.10.2.dist-info}/METADATA +1 -1
- {otf_api-0.10.0.dist-info → otf_api-0.10.2.dist-info}/RECORD +12 -12
- {otf_api-0.10.0.dist-info → otf_api-0.10.2.dist-info}/WHEEL +1 -1
- {otf_api-0.10.0.dist-info → otf_api-0.10.2.dist-info}/LICENSE +0 -0
otf_api/__init__.py
CHANGED
otf_api/api.py
CHANGED
@@ -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
|
-
|
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(
|
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":
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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,
|
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 {
|
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(
|
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.
|
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(
|
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.
|
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.
|
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
|
otf_api/models/bookings.py
CHANGED
@@ -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")
|
otf_api/models/classes.py
CHANGED
@@ -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(
|
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)
|
otf_api/models/enums.py
CHANGED
@@ -105,7 +105,8 @@ class PerformanceSummary(OtfItemBase):
|
|
105
105
|
|
106
106
|
"""
|
107
107
|
|
108
|
-
|
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"))
|
otf_api/models/studio_detail.py
CHANGED
@@ -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(
|
13
|
-
longitude: float = Field(
|
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"
|
otf_api/models/telemetry.py
CHANGED
@@ -49,7 +49,10 @@ class TelemetryItem(OtfItemBase):
|
|
49
49
|
|
50
50
|
class Telemetry(OtfItemBase):
|
51
51
|
member_uuid: str = Field(..., alias="memberUuid")
|
52
|
-
|
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
|
@@ -1,5 +1,5 @@
|
|
1
|
-
otf_api/__init__.py,sha256=
|
2
|
-
otf_api/api.py,sha256=
|
1
|
+
otf_api/__init__.py,sha256=B_mniIQTSJOh_sn2p57CXH1bN6lFiBpPuP8fqj9itQE,205
|
2
|
+
otf_api/api.py,sha256=OmEwG3Zrfq1OQ-WoiaYg1LzsAPx8nsy6ft5ZSWb2xiQ,60334
|
3
3
|
otf_api/auth/__init__.py,sha256=Pu6ugbEXhgjJEpYKr7K2CqEac53Wq9FRF3F16GLQ_ww,133
|
4
4
|
otf_api/auth/auth.py,sha256=m9qzUn0hdAo6AZHm2Mn054nLqUWPxEyP3y3DOBVkvYA,9010
|
5
5
|
otf_api/auth/user.py,sha256=HgggfneeVMnKFWSUMnlikfZyC__HE8vVaKSpBS4_sIg,2256
|
@@ -10,11 +10,11 @@ otf_api/logging.py,sha256=PRZpCaJ1F1Xya3L9Efkt3mKS5_QNr3sXjEUERSxYjvE,563
|
|
10
10
|
otf_api/models/__init__.py,sha256=Ak1Wph5Yn9Npu3m3vvG-n5CsMH6j7NCuzoOBd0b_OlI,1524
|
11
11
|
otf_api/models/base.py,sha256=KJlIxl_sRj6f-g5vKYPw4yV6fGDk-fwZ93EO0JGPYMw,202
|
12
12
|
otf_api/models/body_composition_list.py,sha256=jGdR-9ScvIOtULJNB99aYh2INk2ihoHAnTWtbQCIea4,12202
|
13
|
-
otf_api/models/bookings.py,sha256=
|
13
|
+
otf_api/models/bookings.py,sha256=Lj-IHN1k-w4vF-5aIKbsiQ6Uq-I5_ELoPUKXPGkfbgM,4440
|
14
14
|
otf_api/models/challenge_tracker_content.py,sha256=zukbda38DloiBHW9HUEKQnumGhgVqpjjbxo7G4bDIc0,2630
|
15
15
|
otf_api/models/challenge_tracker_detail.py,sha256=KDfLRDjr8quVhDPVj9x_tuLnkJPua5y8C3WtGIVzXZw,4356
|
16
|
-
otf_api/models/classes.py,sha256=
|
17
|
-
otf_api/models/enums.py,sha256=
|
16
|
+
otf_api/models/classes.py,sha256=TIbFL-haguBqYSSLUEBzPAdv2B-zHNUvcmxcTSu0o6c,3041
|
17
|
+
otf_api/models/enums.py,sha256=VyG3w-kcpDMTsHvX7s6xp-cI61mqvsmN_A76jsnmKj0,4604
|
18
18
|
otf_api/models/lifetime_stats.py,sha256=sQw0oaT5uSlCTZyMSEEhYJg7IdOzuxelTI3mzDBHNls,3055
|
19
19
|
otf_api/models/member_detail.py,sha256=CDDZg3Ow07U57yRqbm3i-BVha0cvCNOZ8QhN0pliU_A,6598
|
20
20
|
otf_api/models/member_membership.py,sha256=jZwHzwtVyMUr8dWGlFbMYj9qveCbiOblWW5szXDUFFo,1338
|
@@ -22,13 +22,13 @@ otf_api/models/member_purchases.py,sha256=Ne7ByEbGTqTJhuEyCgywWe8I3nc-D46qw09up7
|
|
22
22
|
otf_api/models/mixins.py,sha256=VR5EeweySHhzaiqqnCr853Cpe1uK97cxY0IFEdf5T8w,2262
|
23
23
|
otf_api/models/notifications.py,sha256=AkmIfiIiU6wox_7puyenbhCX10SFvBD5eBAovcurRgY,833
|
24
24
|
otf_api/models/out_of_studio_workout_history.py,sha256=El5i0K2Td_sMReyfUKP-Iv1L1WgRx0ijjjnHzYvlCeY,1703
|
25
|
-
otf_api/models/performance_summary.py,sha256=
|
26
|
-
otf_api/models/studio_detail.py,sha256=
|
25
|
+
otf_api/models/performance_summary.py,sha256=0pg1TTnYEk7HW6exA3Czclt1YMtYRBusgPQFWN_udGo,6066
|
26
|
+
otf_api/models/studio_detail.py,sha256=DZicrQWee8iexyK5I8UN1wBYevTB_1dciz2EKBhPids,3729
|
27
27
|
otf_api/models/studio_services.py,sha256=aGLQMQmjGVpI6YxzAl-mcp3Y9cHPXuH9dIqrl6E-78E,1665
|
28
|
-
otf_api/models/telemetry.py,sha256=
|
28
|
+
otf_api/models/telemetry.py,sha256=LuoQ-W7wWQ-trjQqfITQsUpkWL1X3TXdsHiDhyIDvUk,2748
|
29
29
|
otf_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
30
|
otf_api/utils.py,sha256=-9JBjRmoS6xpua_lHccJYjEX2tWp2-5QQrbbuOYMyIo,3945
|
31
|
-
otf_api-0.10.
|
32
|
-
otf_api-0.10.
|
33
|
-
otf_api-0.10.
|
34
|
-
otf_api-0.10.
|
31
|
+
otf_api-0.10.2.dist-info/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
|
32
|
+
otf_api-0.10.2.dist-info/METADATA,sha256=ZAojA_OhEwYb2vHLBDErKWzKgpOXmgbYvQ5TL1PHgt8,2375
|
33
|
+
otf_api-0.10.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
34
|
+
otf_api-0.10.2.dist-info/RECORD,,
|
File without changes
|