otf-api 0.9.4__py3-none-any.whl → 0.10.0__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 +365 -204
- otf_api/models/__init__.py +13 -7
- otf_api/models/enums.py +18 -4
- otf_api/models/performance_summary.py +168 -0
- otf_api/models/studio_detail.py +2 -0
- {otf_api-0.9.4.dist-info → otf_api-0.10.0.dist-info}/METADATA +3 -1
- {otf_api-0.9.4.dist-info → otf_api-0.10.0.dist-info}/RECORD +10 -11
- otf_api/models/performance_summary_detail.py +0 -89
- otf_api/models/performance_summary_list.py +0 -50
- {otf_api-0.9.4.dist-info → otf_api-0.10.0.dist-info}/LICENSE +0 -0
- {otf_api-0.9.4.dist-info → otf_api-0.10.0.dist-info}/WHEEL +0 -0
otf_api/api.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
import atexit
|
2
2
|
import contextlib
|
3
3
|
import functools
|
4
|
+
import warnings
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
6
|
+
from copy import deepcopy
|
4
7
|
from datetime import date, datetime, timedelta
|
5
8
|
from json import JSONDecodeError
|
6
9
|
from logging import getLogger
|
@@ -8,11 +11,14 @@ from typing import Any, Literal
|
|
8
11
|
|
9
12
|
import attrs
|
10
13
|
import httpx
|
14
|
+
from cachetools import TTLCache, cached
|
15
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
11
16
|
from yarl import URL
|
12
17
|
|
13
18
|
from otf_api import exceptions as exc
|
14
19
|
from otf_api import filters, models
|
15
20
|
from otf_api.auth import OtfUser
|
21
|
+
from otf_api.models.enums import HISTORICAL_BOOKING_STATUSES
|
16
22
|
from otf_api.utils import ensure_date, ensure_list, get_booking_uuid, get_class_uuid
|
17
23
|
|
18
24
|
API_BASE_URL = "api.orangetheory.co"
|
@@ -58,6 +64,11 @@ class Otf:
|
|
58
64
|
# Combine immutable attributes into a single hash value
|
59
65
|
return hash(self.member_uuid)
|
60
66
|
|
67
|
+
@retry(
|
68
|
+
retry=retry_if_exception_type(exc.OtfRequestError),
|
69
|
+
stop=stop_after_attempt(3),
|
70
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
71
|
+
)
|
61
72
|
def _do(
|
62
73
|
self,
|
63
74
|
method: str,
|
@@ -87,13 +98,20 @@ class Otf:
|
|
87
98
|
LOGGER.exception(f"Response: {response.text}")
|
88
99
|
raise
|
89
100
|
except httpx.HTTPStatusError as e:
|
101
|
+
if e.response.status_code == 404:
|
102
|
+
raise exc.ResourceNotFoundError("Resource not found")
|
103
|
+
|
104
|
+
if e.response.status_code == 403:
|
105
|
+
raise
|
106
|
+
|
90
107
|
raise exc.OtfRequestError("Error making request", e, response=response, request=request)
|
91
108
|
except Exception as e:
|
92
109
|
LOGGER.exception(f"Error making request: {e}")
|
93
110
|
raise
|
94
111
|
|
95
112
|
if not response.text:
|
96
|
-
|
113
|
+
# insanely enough, at least one endpoint (get perf summary) returns None without error instead of 404
|
114
|
+
raise exc.OtfRequestError("Empty response", None, response=response, request=request)
|
97
115
|
|
98
116
|
try:
|
99
117
|
resp = response.json()
|
@@ -129,6 +147,201 @@ class Otf:
|
|
129
147
|
perf_api_headers = {"koji-member-id": self.member_uuid, "koji-member-email": self.user.email_address}
|
130
148
|
return self._do(method, API_IO_BASE_URL, url, params, perf_api_headers)
|
131
149
|
|
150
|
+
def _get_classes_raw(self, studio_uuids: list[str] | None) -> dict:
|
151
|
+
"""Retrieve raw class data."""
|
152
|
+
return self._classes_request("GET", "/v1/classes", params={"studio_ids": studio_uuids})
|
153
|
+
|
154
|
+
def _get_booking_raw(self, booking_uuid: str) -> dict:
|
155
|
+
"""Retrieve raw booking data."""
|
156
|
+
return self._default_request("GET", f"/member/members/{self.member_uuid}/bookings/{booking_uuid}")
|
157
|
+
|
158
|
+
def _get_bookings_raw(self, start_date: str | None, end_date: str | None, status: str | list[str] | None) -> dict:
|
159
|
+
"""Retrieve raw bookings data."""
|
160
|
+
|
161
|
+
if isinstance(status, list):
|
162
|
+
status = ",".join(status)
|
163
|
+
|
164
|
+
return self._default_request(
|
165
|
+
"GET",
|
166
|
+
f"/member/members/{self.member_uuid}/bookings",
|
167
|
+
params={"startDate": start_date, "endDate": end_date, "statuses": status},
|
168
|
+
)
|
169
|
+
|
170
|
+
def _get_member_detail_raw(self) -> dict:
|
171
|
+
"""Retrieve raw member details."""
|
172
|
+
return self._default_request(
|
173
|
+
"GET", f"/member/members/{self.member_uuid}", params={"include": "memberAddresses,memberClassSummary"}
|
174
|
+
)
|
175
|
+
|
176
|
+
def _get_member_membership_raw(self) -> dict:
|
177
|
+
"""Retrieve raw member membership details."""
|
178
|
+
return self._default_request("GET", f"/member/members/{self.member_uuid}/memberships")
|
179
|
+
|
180
|
+
def _get_performance_summaries_raw(self) -> dict:
|
181
|
+
"""Retrieve raw performance summaries data."""
|
182
|
+
return self._performance_summary_request("GET", "/v1/performance-summaries")
|
183
|
+
|
184
|
+
def _get_performance_summary_raw(self, performance_summary_id: str) -> dict:
|
185
|
+
"""Retrieve raw performance summary data."""
|
186
|
+
return self._performance_summary_request("GET", f"/v1/performance-summaries/{performance_summary_id}")
|
187
|
+
|
188
|
+
def _get_hr_history_raw(self) -> dict:
|
189
|
+
"""Retrieve raw heart rate history."""
|
190
|
+
return self._telemetry_request("GET", "/v1/physVars/maxHr/history", params={"memberUuid": self.member_uuid})
|
191
|
+
|
192
|
+
def _get_telemetry_raw(self, performance_summary_id: str, max_data_points: int) -> dict:
|
193
|
+
"""Retrieve raw telemetry data."""
|
194
|
+
return self._telemetry_request(
|
195
|
+
"GET",
|
196
|
+
"/v1/performance/summary",
|
197
|
+
params={"classHistoryUuid": performance_summary_id, "maxDataPoints": max_data_points},
|
198
|
+
)
|
199
|
+
|
200
|
+
def _get_studio_detail_raw(self, studio_uuid: str) -> dict:
|
201
|
+
"""Retrieve raw studio details."""
|
202
|
+
return self._default_request("GET", f"/mobile/v1/studios/{studio_uuid}")
|
203
|
+
|
204
|
+
def _get_studios_by_geo_raw(
|
205
|
+
self, latitude: float | None, longitude: float | None, distance: int, page_index: int, page_size: int
|
206
|
+
) -> dict:
|
207
|
+
"""Retrieve raw studios by geo data."""
|
208
|
+
return self._default_request(
|
209
|
+
"GET",
|
210
|
+
"/mobile/v1/studios",
|
211
|
+
params={
|
212
|
+
"latitude": latitude,
|
213
|
+
"longitude": longitude,
|
214
|
+
"distance": distance,
|
215
|
+
"pageIndex": page_index,
|
216
|
+
"pageSize": page_size,
|
217
|
+
},
|
218
|
+
)
|
219
|
+
|
220
|
+
def _get_body_composition_list_raw(self) -> dict:
|
221
|
+
"""Retrieve raw body composition list."""
|
222
|
+
return self._default_request("GET", f"/member/members/{self.user.cognito_id}/body-composition")
|
223
|
+
|
224
|
+
def _get_challenge_tracker_raw(self) -> dict:
|
225
|
+
"""Retrieve raw challenge tracker data."""
|
226
|
+
return self._default_request("GET", f"/challenges/v3.1/member/{self.member_uuid}")
|
227
|
+
|
228
|
+
def _get_benchmarks_raw(self, challenge_category_id: int, equipment_id: int, challenge_subcategory_id: int) -> dict:
|
229
|
+
"""Retrieve raw fitness benchmark data."""
|
230
|
+
return self._default_request(
|
231
|
+
"GET",
|
232
|
+
f"/challenges/v3/member/{self.member_uuid}/benchmarks",
|
233
|
+
params={
|
234
|
+
"equipmentId": equipment_id,
|
235
|
+
"challengeTypeId": challenge_category_id,
|
236
|
+
"challengeSubTypeId": challenge_subcategory_id,
|
237
|
+
},
|
238
|
+
)
|
239
|
+
|
240
|
+
def _get_sms_notification_settings_raw(self) -> dict:
|
241
|
+
"""Retrieve raw SMS notification settings."""
|
242
|
+
return self._default_request("GET", url="/sms/v1/preferences", params={"phoneNumber": self.member.phone_number})
|
243
|
+
|
244
|
+
def _get_email_notification_settings_raw(self) -> dict:
|
245
|
+
"""Retrieve raw email notification settings."""
|
246
|
+
return self._default_request("GET", url="/otfmailing/v2/preferences", params={"email": self.member.email})
|
247
|
+
|
248
|
+
def _get_member_lifetime_stats_raw(self, select_time: str) -> dict:
|
249
|
+
"""Retrieve raw lifetime stats data."""
|
250
|
+
return self._default_request("GET", f"/performance/v2/{self.member_uuid}/over-time/{select_time}")
|
251
|
+
|
252
|
+
def _get_member_services_raw(self, active_only: bool) -> dict:
|
253
|
+
"""Retrieve raw member services data."""
|
254
|
+
return self._default_request(
|
255
|
+
"GET", f"/member/members/{self.member_uuid}/services", params={"activeOnly": str(active_only).lower()}
|
256
|
+
)
|
257
|
+
|
258
|
+
def _get_aspire_data_raw(self, datetime: str | None, unit: str | None) -> dict:
|
259
|
+
"""Retrieve raw aspire wearable data."""
|
260
|
+
return self._default_request(
|
261
|
+
"GET", f"/member/wearables/{self.member_uuid}/wearable-daily", params={"datetime": datetime, "unit": unit}
|
262
|
+
)
|
263
|
+
|
264
|
+
def _get_member_purchases_raw(self) -> dict:
|
265
|
+
"""Retrieve raw member purchases data."""
|
266
|
+
return self._default_request("GET", f"/member/members/{self.member_uuid}/purchases")
|
267
|
+
|
268
|
+
def _get_favorite_studios_raw(self) -> dict:
|
269
|
+
"""Retrieve raw favorite studios data."""
|
270
|
+
return self._default_request("GET", f"/member/members/{self.member_uuid}/favorite-studios")
|
271
|
+
|
272
|
+
def _get_studio_services_raw(self, studio_uuid: str) -> dict:
|
273
|
+
"""Retrieve raw studio services data."""
|
274
|
+
return self._default_request("GET", f"/member/studios/{studio_uuid}/services")
|
275
|
+
|
276
|
+
def _get_out_of_studio_workout_history_raw(self) -> dict:
|
277
|
+
"""Retrieve raw out-of-studio workout history data."""
|
278
|
+
return self._default_request("GET", f"/member/members/{self.member_uuid}/out-of-studio-workout")
|
279
|
+
|
280
|
+
def _add_favorite_studio_raw(self, studio_uuids: list[str]) -> dict:
|
281
|
+
"""Retrieve raw response from adding a studio to favorite studios."""
|
282
|
+
return self._default_request("POST", "/mobile/v1/members/favorite-studios", json={"studioUUIds": studio_uuids})
|
283
|
+
|
284
|
+
def _remove_favorite_studio_raw(self, studio_uuids: list[str]) -> dict:
|
285
|
+
"""Retrieve raw response from removing a studio from favorite studios."""
|
286
|
+
return self._default_request(
|
287
|
+
"DELETE", "/mobile/v1/members/favorite-studios", json={"studioUUIds": studio_uuids}
|
288
|
+
)
|
289
|
+
|
290
|
+
def _get_challenge_tracker_detail_raw(self, challenge_category_id: int) -> dict:
|
291
|
+
"""Retrieve raw challenge tracker detail data."""
|
292
|
+
return self._default_request(
|
293
|
+
"GET",
|
294
|
+
f"/challenges/v1/member/{self.member_uuid}/participation",
|
295
|
+
params={"challengeTypeId": challenge_category_id},
|
296
|
+
)
|
297
|
+
|
298
|
+
def _update_sms_notification_settings_raw(self, promotional_enabled: bool, transactional_enabled: bool) -> dict:
|
299
|
+
"""Retrieve raw response from updating SMS notification settings."""
|
300
|
+
return self._default_request(
|
301
|
+
"POST",
|
302
|
+
"/sms/v1/preferences",
|
303
|
+
json={
|
304
|
+
"promosms": promotional_enabled,
|
305
|
+
"source": "OTF",
|
306
|
+
"transactionalsms": transactional_enabled,
|
307
|
+
"phoneNumber": self.member.phone_number,
|
308
|
+
},
|
309
|
+
)
|
310
|
+
|
311
|
+
def _update_email_notification_settings_raw(self, promotional_enabled: bool, transactional_enabled: bool) -> dict:
|
312
|
+
"""Retrieve raw response from updating email notification settings."""
|
313
|
+
return self._default_request(
|
314
|
+
"POST",
|
315
|
+
"/otfmailing/v2/preferences",
|
316
|
+
json={
|
317
|
+
"promotionalEmail": promotional_enabled,
|
318
|
+
"source": "OTF",
|
319
|
+
"transactionalEmail": transactional_enabled,
|
320
|
+
"email": self.member.email,
|
321
|
+
},
|
322
|
+
)
|
323
|
+
|
324
|
+
def _rate_class_raw(self, class_uuid: str, class_history_uuid: str, class_rating: int, coach_rating: int) -> dict:
|
325
|
+
"""Retrieve raw response from rating a class and coach."""
|
326
|
+
return self._default_request(
|
327
|
+
"POST",
|
328
|
+
"/mobile/v1/members/classes/ratings",
|
329
|
+
json={
|
330
|
+
"classUUId": class_uuid,
|
331
|
+
"otBeatClassHistoryUUId": class_history_uuid,
|
332
|
+
"classRating": class_rating,
|
333
|
+
"coachRating": coach_rating,
|
334
|
+
},
|
335
|
+
)
|
336
|
+
|
337
|
+
def _update_member_name_raw(self, first_name: str, last_name: str) -> dict:
|
338
|
+
"""Retrieve raw response from updating member name."""
|
339
|
+
return self._default_request(
|
340
|
+
"PUT",
|
341
|
+
f"/member/members/{self.member_uuid}",
|
342
|
+
json={"firstName": first_name, "lastName": last_name},
|
343
|
+
)
|
344
|
+
|
132
345
|
def get_classes(
|
133
346
|
self,
|
134
347
|
start_date: date | None = None,
|
@@ -204,7 +417,7 @@ class Otf:
|
|
204
417
|
else:
|
205
418
|
studio_uuids.append(self.home_studio_uuid)
|
206
419
|
|
207
|
-
classes_resp = self.
|
420
|
+
classes_resp = self._get_classes_raw(studio_uuids)
|
208
421
|
|
209
422
|
studio_dict = {s: self.get_studio_detail(s) for s in studio_uuids}
|
210
423
|
classes: list[models.OtfClass] = []
|
@@ -289,7 +502,7 @@ class Otf:
|
|
289
502
|
if not booking_uuid:
|
290
503
|
raise ValueError("booking_uuid is required")
|
291
504
|
|
292
|
-
data = self.
|
505
|
+
data = self._get_booking_raw(booking_uuid)
|
293
506
|
return models.Booking(**data["data"])
|
294
507
|
|
295
508
|
def get_booking_from_class(self, otf_class: str | models.OtfClass) -> models.Booking:
|
@@ -437,7 +650,7 @@ class Otf:
|
|
437
650
|
self,
|
438
651
|
start_date: date | str | None = None,
|
439
652
|
end_date: date | str | None = None,
|
440
|
-
status: models.BookingStatus | None = None,
|
653
|
+
status: models.BookingStatus | list[models.BookingStatus] | None = None,
|
441
654
|
exclude_cancelled: bool = True,
|
442
655
|
exclude_checkedin: bool = True,
|
443
656
|
) -> list[models.Booking]:
|
@@ -446,8 +659,7 @@ class Otf:
|
|
446
659
|
Args:
|
447
660
|
start_date (date | str | None): The start date for the bookings. Default is None.
|
448
661
|
end_date (date | str | None): The end date for the bookings. Default is None.
|
449
|
-
status (BookingStatus | None): The status
|
450
|
-
all statuses. Only a single status can be provided.
|
662
|
+
status (BookingStatus | list[BookingStatus] | None): The status(es) to filter by. Default is None.
|
451
663
|
exclude_cancelled (bool): Whether to exclude cancelled bookings. Default is True.
|
452
664
|
exclude_checkedin (bool): Whether to exclude checked-in bookings. Default is True.
|
453
665
|
|
@@ -467,12 +679,6 @@ class Otf:
|
|
467
679
|
---
|
468
680
|
If dates are provided, the endpoint will return bookings where the class date is within the provided
|
469
681
|
date range. If no dates are provided, it will go back 45 days and forward about 30 days.
|
470
|
-
|
471
|
-
Developer Notes:
|
472
|
-
---
|
473
|
-
Looking at the code in the app, it appears that this endpoint accepts multiple statuses. Indeed,
|
474
|
-
it does not throw an error if you include a list of statuses. However, only the last status in the list is
|
475
|
-
used. I'm not sure if this is a bug or if the API is supposed to work this way.
|
476
682
|
"""
|
477
683
|
|
478
684
|
if exclude_cancelled and status == models.BookingStatus.Cancelled:
|
@@ -487,11 +693,16 @@ class Otf:
|
|
487
693
|
if isinstance(end_date, date):
|
488
694
|
end_date = end_date.isoformat()
|
489
695
|
|
490
|
-
|
491
|
-
|
492
|
-
|
696
|
+
if isinstance(status, list):
|
697
|
+
status_value = ",".join(status)
|
698
|
+
elif isinstance(status, models.BookingStatus):
|
699
|
+
status_value = status.value
|
700
|
+
elif isinstance(status, str):
|
701
|
+
status_value = status
|
702
|
+
else:
|
703
|
+
status_value = None
|
493
704
|
|
494
|
-
resp = self.
|
705
|
+
resp = self._get_bookings_raw(start_date, end_date, status_value)["data"]
|
495
706
|
|
496
707
|
# add studio details for each booking, instead of using the different studio model returned by this endpoint
|
497
708
|
studio_uuids = {b["class"]["studio"]["studioUUId"] for b in resp}
|
@@ -512,55 +723,25 @@ class Otf:
|
|
512
723
|
|
513
724
|
return bookings
|
514
725
|
|
515
|
-
def
|
516
|
-
"""Get the member's bookings.
|
517
|
-
|
518
|
-
Args:
|
519
|
-
status (BookingStatus | None): The status of the bookings to get. Default is None, which includes
|
520
|
-
all statuses. Only a single status can be provided.
|
726
|
+
def get_historical_bookings(self) -> list[models.Booking]:
|
727
|
+
"""Get the member's historical bookings. This will go back 45 days and return all bookings
|
728
|
+
for that time period.
|
521
729
|
|
522
730
|
Returns:
|
523
|
-
list[Booking]: The member's bookings.
|
524
|
-
|
525
|
-
Raises:
|
526
|
-
ValueError: If an unaccepted status is provided.
|
527
|
-
|
528
|
-
Notes:
|
529
|
-
---
|
530
|
-
This one is called with the param named 'status'. Dates cannot be provided, because if the endpoint
|
531
|
-
receives a date, it will return as if the param name was 'statuses'.
|
532
|
-
|
533
|
-
Note: This seems to only work for Cancelled, Booked, CheckedIn, and Waitlisted statuses. If you provide
|
534
|
-
a different status, it will return all bookings, not filtered by status. The results in this scenario do
|
535
|
-
not line up with the `get_bookings` with no status provided, as that returns fewer records. Likely the
|
536
|
-
filtered dates are different on the backend.
|
537
|
-
|
538
|
-
My guess: the endpoint called with dates and 'statuses' is a "v2" kind of thing, where they upgraded without
|
539
|
-
changing the version of the api. Calling it with no dates and a singular (limited) status is probably v1.
|
540
|
-
|
541
|
-
I'm leaving this in here for reference, but marking it private. I just don't want to have to puzzle over
|
542
|
-
this again if I remove it and forget about it.
|
543
|
-
|
731
|
+
list[Booking]: The member's historical bookings.
|
544
732
|
"""
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
status_value = status.value if status else None
|
557
|
-
|
558
|
-
res = self._default_request(
|
559
|
-
"GET", f"/member/members/{self.member_uuid}/bookings", params={"status": status_value}
|
733
|
+
# api goes back 45 days but we'll go back 47 to be safe
|
734
|
+
start_date = datetime.today().date() - timedelta(days=47)
|
735
|
+
end_date = datetime.today().date()
|
736
|
+
|
737
|
+
return self.get_bookings(
|
738
|
+
start_date=start_date,
|
739
|
+
end_date=end_date,
|
740
|
+
status=HISTORICAL_BOOKING_STATUSES,
|
741
|
+
exclude_cancelled=False,
|
742
|
+
exclude_checkedin=False,
|
560
743
|
)
|
561
744
|
|
562
|
-
return [models.Booking(**b) for b in res["data"]]
|
563
|
-
|
564
745
|
def get_member_detail(self) -> models.MemberDetail:
|
565
746
|
"""Get the member details.
|
566
747
|
|
@@ -568,9 +749,7 @@ class Otf:
|
|
568
749
|
MemberDetail: The member details.
|
569
750
|
"""
|
570
751
|
|
571
|
-
|
572
|
-
|
573
|
-
resp = self._default_request("GET", f"/member/members/{self.member_uuid}", params=params)
|
752
|
+
resp = self._get_member_detail_raw()
|
574
753
|
data = resp["data"]
|
575
754
|
|
576
755
|
# use standard StudioDetail model instead of the one returned by this endpoint
|
@@ -586,7 +765,7 @@ class Otf:
|
|
586
765
|
MemberMembership: The member's membership details.
|
587
766
|
"""
|
588
767
|
|
589
|
-
data = self.
|
768
|
+
data = self._get_member_membership_raw()
|
590
769
|
return models.MemberMembership(**data["data"])
|
591
770
|
|
592
771
|
def get_member_purchases(self) -> list[models.MemberPurchase]:
|
@@ -595,9 +774,7 @@ class Otf:
|
|
595
774
|
Returns:
|
596
775
|
list[MemberPurchase]: The member's purchases.
|
597
776
|
"""
|
598
|
-
|
599
|
-
|
600
|
-
purchases = data["data"]
|
777
|
+
purchases = self._get_member_purchases_raw()["data"]
|
601
778
|
|
602
779
|
for p in purchases:
|
603
780
|
p["studio"] = self.get_studio_detail(p["studio"]["studioUUId"])
|
@@ -621,7 +798,7 @@ class Otf:
|
|
621
798
|
Any: The member's lifetime stats.
|
622
799
|
"""
|
623
800
|
|
624
|
-
data = self.
|
801
|
+
data = self._get_member_lifetime_stats_raw(select_time.value)
|
625
802
|
|
626
803
|
stats = models.StatsResponse(**data["data"])
|
627
804
|
|
@@ -665,7 +842,7 @@ class Otf:
|
|
665
842
|
Returns:
|
666
843
|
list[OutOfStudioWorkoutHistory]: The member's out of studio workout history.
|
667
844
|
"""
|
668
|
-
data = self.
|
845
|
+
data = self._get_out_of_studio_workout_history_raw()
|
669
846
|
|
670
847
|
return [models.OutOfStudioWorkoutHistory(**workout) for workout in data["data"]]
|
671
848
|
|
@@ -675,7 +852,7 @@ class Otf:
|
|
675
852
|
Returns:
|
676
853
|
list[StudioDetail]: The member's favorite studios.
|
677
854
|
"""
|
678
|
-
data = self.
|
855
|
+
data = self._get_favorite_studios_raw()
|
679
856
|
studio_uuids = [studio["studioUUId"] for studio in data["data"]]
|
680
857
|
return [self.get_studio_detail(studio_uuid) for studio_uuid in studio_uuids]
|
681
858
|
|
@@ -694,8 +871,7 @@ class Otf:
|
|
694
871
|
if not studio_uuids:
|
695
872
|
raise ValueError("studio_uuids is required")
|
696
873
|
|
697
|
-
|
698
|
-
resp = self._default_request("POST", "/mobile/v1/members/favorite-studios", json=body)
|
874
|
+
resp = self._add_favorite_studio_raw(studio_uuids)
|
699
875
|
|
700
876
|
new_faves = resp.get("data", {}).get("studios", [])
|
701
877
|
|
@@ -716,8 +892,9 @@ class Otf:
|
|
716
892
|
if not studio_uuids:
|
717
893
|
raise ValueError("studio_uuids is required")
|
718
894
|
|
719
|
-
|
720
|
-
|
895
|
+
# keeping the convention of regular/raw methods even though this method doesn't return anything
|
896
|
+
# in case that changes in the future
|
897
|
+
self._remove_favorite_studio_raw(studio_uuids)
|
721
898
|
|
722
899
|
def get_studio_services(self, studio_uuid: str | None = None) -> list[models.StudioService]:
|
723
900
|
"""Get the services available at a specific studio. If no studio UUID is provided, the member's home studio
|
@@ -730,14 +907,14 @@ class Otf:
|
|
730
907
|
list[StudioService]: The services available at the studio.
|
731
908
|
"""
|
732
909
|
studio_uuid = studio_uuid or self.home_studio_uuid
|
733
|
-
data = self.
|
910
|
+
data = self._get_studio_services_raw(studio_uuid)
|
734
911
|
|
735
912
|
for d in data["data"]:
|
736
913
|
d["studio"] = self.get_studio_detail(studio_uuid)
|
737
914
|
|
738
915
|
return [models.StudioService(**d) for d in data["data"]]
|
739
916
|
|
740
|
-
@
|
917
|
+
@cached(cache=TTLCache(maxsize=1024, ttl=600))
|
741
918
|
def get_studio_detail(self, studio_uuid: str | None = None) -> models.StudioDetail:
|
742
919
|
"""Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
|
743
920
|
user's home studio.
|
@@ -749,10 +926,7 @@ class Otf:
|
|
749
926
|
StudioDetail: Detailed information about the studio.
|
750
927
|
"""
|
751
928
|
studio_uuid = studio_uuid or self.home_studio_uuid
|
752
|
-
|
753
|
-
path = f"/mobile/v1/studios/{studio_uuid}"
|
754
|
-
|
755
|
-
res = self._default_request("GET", path)
|
929
|
+
res = self._get_studio_detail_raw(studio_uuid)
|
756
930
|
|
757
931
|
return models.StudioDetail(**res["data"])
|
758
932
|
|
@@ -804,18 +978,25 @@ class Otf:
|
|
804
978
|
Returns:
|
805
979
|
list[models.StudioDetail]: List of studios matching the search criteria.
|
806
980
|
"""
|
807
|
-
path = "/mobile/v1/studios"
|
808
|
-
|
809
981
|
distance = min(distance, 250) # max distance is 250 miles
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
982
|
+
page_size = 100
|
983
|
+
page_index = 1
|
984
|
+
LOGGER.debug(
|
985
|
+
"Starting studio search",
|
986
|
+
extra={
|
987
|
+
"latitude": latitude,
|
988
|
+
"longitude": longitude,
|
989
|
+
"distance": distance,
|
990
|
+
"page_index": page_index,
|
991
|
+
"page_size": page_size,
|
992
|
+
},
|
993
|
+
)
|
814
994
|
|
815
995
|
all_results: dict[str, dict[str, Any]] = {}
|
816
996
|
|
817
997
|
while True:
|
818
|
-
res = self.
|
998
|
+
res = self._get_studios_by_geo_raw(latitude, longitude, distance, page_index, page_size)
|
999
|
+
|
819
1000
|
studios = res["data"].get("studios", [])
|
820
1001
|
total_count = res["data"].get("pagination", {}).get("totalCount", 0)
|
821
1002
|
|
@@ -823,7 +1004,7 @@ class Otf:
|
|
823
1004
|
if len(all_results) >= total_count or not studios:
|
824
1005
|
break
|
825
1006
|
|
826
|
-
|
1007
|
+
page_index += 1
|
827
1008
|
|
828
1009
|
LOGGER.info("Studio search completed, fetched %d of %d studios", len(all_results), total_count, stacklevel=2)
|
829
1010
|
|
@@ -835,7 +1016,7 @@ class Otf:
|
|
835
1016
|
Returns:
|
836
1017
|
list[BodyCompositionData]: The member's body composition list.
|
837
1018
|
"""
|
838
|
-
data = self.
|
1019
|
+
data = self._get_body_composition_list_raw()
|
839
1020
|
return [models.BodyCompositionData(**item) for item in data["data"]]
|
840
1021
|
|
841
1022
|
def get_challenge_tracker(self) -> models.ChallengeTracker:
|
@@ -844,7 +1025,7 @@ class Otf:
|
|
844
1025
|
Returns:
|
845
1026
|
ChallengeTracker: The member's challenge tracker content.
|
846
1027
|
"""
|
847
|
-
data = self.
|
1028
|
+
data = self._get_challenge_tracker_raw()
|
848
1029
|
return models.ChallengeTracker(**data["Dto"])
|
849
1030
|
|
850
1031
|
def get_benchmarks(
|
@@ -865,13 +1046,7 @@ class Otf:
|
|
865
1046
|
Returns:
|
866
1047
|
list[FitnessBenchmark]: The member's challenge tracker details.
|
867
1048
|
"""
|
868
|
-
|
869
|
-
"equipmentId": int(equipment_id),
|
870
|
-
"challengeTypeId": int(challenge_category_id),
|
871
|
-
"challengeSubTypeId": challenge_subcategory_id,
|
872
|
-
}
|
873
|
-
|
874
|
-
data = self._default_request("GET", f"/challenges/v3/member/{self.member_uuid}/benchmarks", params=params)
|
1049
|
+
data = self._get_benchmarks_raw(int(challenge_category_id), int(equipment_id), challenge_subcategory_id)
|
875
1050
|
return [models.FitnessBenchmark(**item) for item in data["Dto"]]
|
876
1051
|
|
877
1052
|
def get_benchmarks_by_equipment(self, equipment_id: models.EquipmentType) -> list[models.FitnessBenchmark]:
|
@@ -917,11 +1092,7 @@ class Otf:
|
|
917
1092
|
FitnessBenchmark: Details about the challenge.
|
918
1093
|
"""
|
919
1094
|
|
920
|
-
data = self.
|
921
|
-
"GET",
|
922
|
-
f"/challenges/v1/member/{self.member_uuid}/participation",
|
923
|
-
params={"challengeTypeId": int(challenge_category_id)},
|
924
|
-
)
|
1095
|
+
data = self._get_challenge_tracker_detail_raw(int(challenge_category_id))
|
925
1096
|
|
926
1097
|
if len(data["Dto"]) > 1:
|
927
1098
|
LOGGER.warning("Multiple challenge participations found, returning the first one.")
|
@@ -931,15 +1102,49 @@ class Otf:
|
|
931
1102
|
|
932
1103
|
return models.FitnessBenchmark(**data["Dto"][0])
|
933
1104
|
|
934
|
-
|
935
|
-
|
1105
|
+
@cached(cache=TTLCache(maxsize=1024, ttl=600))
|
1106
|
+
def get_performance_summaries_dict(self) -> dict[str, models.PerformanceSummary]:
|
1107
|
+
"""Get a dictionary of performance summaries for the authenticated user.
|
1108
|
+
|
1109
|
+
Returns:
|
1110
|
+
dict[str, PerformanceSummary]: A dictionary of performance summaries, keyed by class history UUID.
|
1111
|
+
|
1112
|
+
Developer Notes:
|
1113
|
+
---
|
1114
|
+
In the app, this is referred to as 'getInStudioWorkoutHistory'.
|
1115
|
+
|
1116
|
+
"""
|
1117
|
+
|
1118
|
+
items = self._get_performance_summaries_raw()["items"]
|
1119
|
+
|
1120
|
+
distinct_studio_ids = set([rec["class"]["studio"]["id"] for rec in items])
|
1121
|
+
perf_summary_ids = set([rec["id"] for rec in items])
|
1122
|
+
|
1123
|
+
with ThreadPoolExecutor() as pool:
|
1124
|
+
studio_futures = {s: pool.submit(self.get_studio_detail, s) for s in distinct_studio_ids}
|
1125
|
+
perf_summary_futures = {s: pool.submit(self._get_performancy_summary_detail, s) for s in perf_summary_ids}
|
1126
|
+
|
1127
|
+
studio_dict = {k: v.result() for k, v in studio_futures.items()}
|
1128
|
+
# deepcopy these so that mutating them in PerformanceSummary doesn't affect the cache
|
1129
|
+
perf_summary_dict = {k: deepcopy(v.result()) for k, v in perf_summary_futures.items()}
|
1130
|
+
|
1131
|
+
for item in items:
|
1132
|
+
item["class"]["studio"] = studio_dict[item["class"]["studio"]["id"]]
|
1133
|
+
item["detail"] = perf_summary_dict[item["id"]]
|
1134
|
+
|
1135
|
+
entries = [models.PerformanceSummary(**item) for item in items]
|
1136
|
+
entries_dict = {entry.class_history_uuid: entry for entry in entries}
|
1137
|
+
|
1138
|
+
return entries_dict
|
1139
|
+
|
1140
|
+
def get_performance_summaries(self, limit: int | None = None) -> list[models.PerformanceSummary]:
|
1141
|
+
"""Get a list of all performance summaries for the authenticated user.
|
936
1142
|
|
937
1143
|
Args:
|
938
|
-
limit (int): The maximum number of
|
939
|
-
only_include_rateable (bool): Whether to only include rateable performance summaries. Defaults to True.
|
1144
|
+
limit (int | None): The maximum number of entries to return. Default is None. Deprecated.
|
940
1145
|
|
941
1146
|
Returns:
|
942
|
-
list[
|
1147
|
+
list[PerformanceSummary]: A list of performance summaries.
|
943
1148
|
|
944
1149
|
Developer Notes:
|
945
1150
|
---
|
@@ -947,28 +1152,48 @@ class Otf:
|
|
947
1152
|
|
948
1153
|
"""
|
949
1154
|
|
950
|
-
|
951
|
-
|
1155
|
+
if limit:
|
1156
|
+
warnings.warn("Limit is deprecated and will be removed in a future version.", DeprecationWarning)
|
952
1157
|
|
953
|
-
|
1158
|
+
records = list(self.get_performance_summaries_dict().values())
|
954
1159
|
|
955
|
-
|
956
|
-
|
1160
|
+
sorted_records = sorted(records, key=lambda x: x.otf_class.starts_at, reverse=True)
|
1161
|
+
|
1162
|
+
return sorted_records
|
1163
|
+
|
1164
|
+
def get_performance_summary(self, performance_summary_id: str) -> models.PerformanceSummary:
|
1165
|
+
"""Get performance summary for a given workout.
|
957
1166
|
|
958
1167
|
Args:
|
959
1168
|
performance_summary_id (str): The ID of the performance summary to retrieve.
|
960
1169
|
|
961
1170
|
Returns:
|
962
|
-
|
1171
|
+
PerformanceSummary: The performance summary.
|
963
1172
|
"""
|
964
1173
|
|
965
|
-
|
966
|
-
res = self._performance_summary_request("GET", path)
|
1174
|
+
perf_summary = self.get_performance_summaries_dict().get(performance_summary_id)
|
967
1175
|
|
968
|
-
if
|
1176
|
+
if perf_summary is None:
|
969
1177
|
raise exc.ResourceNotFoundError(f"Performance summary {performance_summary_id} not found")
|
970
1178
|
|
971
|
-
return
|
1179
|
+
return perf_summary
|
1180
|
+
|
1181
|
+
@functools.lru_cache(maxsize=1024)
|
1182
|
+
def _get_performancy_summary_detail(self, performance_summary_id: str) -> dict[str, Any]:
|
1183
|
+
"""Get the details for a performance summary.
|
1184
|
+
|
1185
|
+
Args:
|
1186
|
+
performance_summary_id (str): The performance summary ID.
|
1187
|
+
|
1188
|
+
Returns:
|
1189
|
+
dict[str, Any]: The performance summary details.
|
1190
|
+
|
1191
|
+
Developer Notes:
|
1192
|
+
---
|
1193
|
+
This is mostly here to cache the results of the raw method.
|
1194
|
+
"""
|
1195
|
+
|
1196
|
+
return self._get_performance_summary_raw(performance_summary_id)
|
972
1197
|
|
973
1198
|
def get_hr_history(self) -> list[models.TelemetryHistoryItem]:
|
974
1199
|
"""Get the heartrate history for the user.
|
@@ -980,10 +1205,7 @@ class Otf:
|
|
980
1205
|
list[HistoryItem]: The heartrate history for the user.
|
981
1206
|
|
982
1207
|
"""
|
983
|
-
|
984
|
-
|
985
|
-
params = {"memberUuid": self.member_uuid}
|
986
|
-
resp = self._telemetry_request("GET", path, params=params)
|
1208
|
+
resp = self._get_hr_history_raw()
|
987
1209
|
return [models.TelemetryHistoryItem(**item) for item in resp["history"]]
|
988
1210
|
|
989
1211
|
def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> models.Telemetry:
|
@@ -998,12 +1220,9 @@ class Otf:
|
|
998
1220
|
|
999
1221
|
Returns:
|
1000
1222
|
TelemetryItem: The telemetry for the class history.
|
1001
|
-
|
1002
1223
|
"""
|
1003
|
-
path = "/v1/performance/summary"
|
1004
1224
|
|
1005
|
-
|
1006
|
-
res = self._telemetry_request("GET", path, params=params)
|
1225
|
+
res = self._get_telemetry_raw(performance_summary_id, max_data_points)
|
1007
1226
|
return models.Telemetry(**res)
|
1008
1227
|
|
1009
1228
|
def get_sms_notification_settings(self) -> models.SmsNotificationSettings:
|
@@ -1012,7 +1231,7 @@ class Otf:
|
|
1012
1231
|
Returns:
|
1013
1232
|
SmsNotificationSettings: The member's SMS notification settings.
|
1014
1233
|
"""
|
1015
|
-
res = self.
|
1234
|
+
res = self._get_sms_notification_settings_raw()
|
1016
1235
|
|
1017
1236
|
return models.SmsNotificationSettings(**res["data"])
|
1018
1237
|
|
@@ -1047,7 +1266,6 @@ class Otf:
|
|
1047
1266
|
}
|
1048
1267
|
```
|
1049
1268
|
"""
|
1050
|
-
url = "/sms/v1/preferences"
|
1051
1269
|
|
1052
1270
|
current_settings = self.get_sms_notification_settings()
|
1053
1271
|
|
@@ -1058,14 +1276,7 @@ class Otf:
|
|
1058
1276
|
transactional_enabled if transactional_enabled is not None else current_settings.is_transactional_sms_opt_in
|
1059
1277
|
)
|
1060
1278
|
|
1061
|
-
|
1062
|
-
"promosms": promotional_enabled,
|
1063
|
-
"source": "OTF",
|
1064
|
-
"transactionalsms": transactional_enabled,
|
1065
|
-
"phoneNumber": self.member.phone_number,
|
1066
|
-
}
|
1067
|
-
|
1068
|
-
self._default_request("POST", url, json=body)
|
1279
|
+
self._update_sms_notification_settings_raw(promotional_enabled, transactional_enabled)
|
1069
1280
|
|
1070
1281
|
# the response returns nothing useful, so we just query the settings again
|
1071
1282
|
new_settings = self.get_sms_notification_settings()
|
@@ -1077,7 +1288,7 @@ class Otf:
|
|
1077
1288
|
Returns:
|
1078
1289
|
EmailNotificationSettings: The member's email notification settings.
|
1079
1290
|
"""
|
1080
|
-
res = self.
|
1291
|
+
res = self._get_email_notification_settings_raw()
|
1081
1292
|
|
1082
1293
|
return models.EmailNotificationSettings(**res["data"])
|
1083
1294
|
|
@@ -1104,14 +1315,7 @@ class Otf:
|
|
1104
1315
|
else current_settings.is_transactional_email_opt_in
|
1105
1316
|
)
|
1106
1317
|
|
1107
|
-
|
1108
|
-
"promotionalEmail": promotional_enabled,
|
1109
|
-
"source": "OTF",
|
1110
|
-
"transactionalEmail": transactional_enabled,
|
1111
|
-
"email": self.member.email,
|
1112
|
-
}
|
1113
|
-
|
1114
|
-
self._default_request("POST", "/otfmailing/v2/preferences", json=body)
|
1318
|
+
self._update_email_notification_settings_raw(promotional_enabled, transactional_enabled)
|
1115
1319
|
|
1116
1320
|
# the response returns nothing useful, so we just query the settings again
|
1117
1321
|
new_settings = self.get_email_notification_settings()
|
@@ -1139,10 +1343,7 @@ class Otf:
|
|
1139
1343
|
LOGGER.warning("No changes to names, nothing to update.")
|
1140
1344
|
return self.member
|
1141
1345
|
|
1142
|
-
|
1143
|
-
body = {"firstName": first_name, "lastName": last_name}
|
1144
|
-
|
1145
|
-
res = self._default_request("PUT", path, json=body)
|
1346
|
+
res = self._update_member_name_raw(first_name, last_name)
|
1146
1347
|
|
1147
1348
|
return models.MemberDetail(**res["data"])
|
1148
1349
|
|
@@ -1152,7 +1353,7 @@ class Otf:
|
|
1152
1353
|
class_history_uuid: str,
|
1153
1354
|
class_rating: Literal[0, 1, 2, 3],
|
1154
1355
|
coach_rating: Literal[0, 1, 2, 3],
|
1155
|
-
) -> models.
|
1356
|
+
) -> models.PerformanceSummary:
|
1156
1357
|
"""Rate a class and coach. A simpler method is provided in `rate_class_from_performance_summary`.
|
1157
1358
|
|
1158
1359
|
|
@@ -1167,7 +1368,7 @@ class Otf:
|
|
1167
1368
|
coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
|
1168
1369
|
|
1169
1370
|
Returns:
|
1170
|
-
|
1371
|
+
PerformanceSummary: The updated performance summary.
|
1171
1372
|
"""
|
1172
1373
|
|
1173
1374
|
# com/orangetheoryfitness/fragment/rating/RateStatus.java
|
@@ -1188,80 +1389,45 @@ class Otf:
|
|
1188
1389
|
body_class_rating = CLASS_RATING_MAP[class_rating]
|
1189
1390
|
body_coach_rating = COACH_RATING_MAP[coach_rating]
|
1190
1391
|
|
1191
|
-
body = {
|
1192
|
-
"classUUId": class_uuid,
|
1193
|
-
"otBeatClassHistoryUUId": class_history_uuid,
|
1194
|
-
"classRating": body_class_rating,
|
1195
|
-
"coachRating": body_coach_rating,
|
1196
|
-
}
|
1197
|
-
|
1198
1392
|
try:
|
1199
|
-
self.
|
1393
|
+
self._rate_class_raw(class_uuid, class_history_uuid, body_class_rating, body_coach_rating)
|
1200
1394
|
except exc.OtfRequestError as e:
|
1201
1395
|
if e.response.status_code == 403:
|
1202
1396
|
raise exc.AlreadyRatedError(f"Performance summary {class_history_uuid} is already rated.") from None
|
1203
1397
|
raise
|
1204
1398
|
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
the class UUID, which is required to rate the class. It will also be used to return an updated performance
|
1212
|
-
summary entry after rating a class.
|
1399
|
+
# we have to clear the cache after rating a class, otherwise we will get back the same data
|
1400
|
+
# showing it not rated. that would be incorrect, confusing, and would cause errors if the user
|
1401
|
+
# then attempted to rate the class again.
|
1402
|
+
# we could attempt to only refresh the one record but with these endpoints that's not simple
|
1403
|
+
# NOTE: the individual perf summary endpoint does not have rating data, so it's cache is not cleared
|
1404
|
+
self.get_performance_summaries_dict.cache_clear()
|
1213
1405
|
|
1214
|
-
|
1215
|
-
class_history_uuid (str): The performance summary ID.
|
1216
|
-
|
1217
|
-
Returns:
|
1218
|
-
PerformanceSummaryEntry: The performance summary entry.
|
1219
|
-
|
1220
|
-
Raises:
|
1221
|
-
ResourceNotFoundError: If the performance summary is not found.
|
1222
|
-
"""
|
1223
|
-
|
1224
|
-
# try going in as small of increments as possible, assuming that the rating request
|
1225
|
-
# will be for a recent class
|
1226
|
-
for limit in [5, 20, 60, 100]:
|
1227
|
-
summaries = self.get_performance_summaries(limit)
|
1228
|
-
summary = next((s for s in summaries if s.class_history_uuid == class_history_uuid), None)
|
1229
|
-
|
1230
|
-
if summary:
|
1231
|
-
return summary
|
1232
|
-
|
1233
|
-
raise exc.ResourceNotFoundError(f"Performance summary {class_history_uuid} not found.")
|
1406
|
+
return self.get_performance_summary(class_history_uuid)
|
1234
1407
|
|
1235
1408
|
def rate_class_from_performance_summary(
|
1236
1409
|
self,
|
1237
|
-
perf_summary: models.
|
1410
|
+
perf_summary: models.PerformanceSummary,
|
1238
1411
|
class_rating: Literal[0, 1, 2, 3],
|
1239
1412
|
coach_rating: Literal[0, 1, 2, 3],
|
1240
|
-
) -> models.
|
1413
|
+
) -> models.PerformanceSummary:
|
1241
1414
|
"""Rate a class and coach. The class rating must be 0, 1, 2, or 3. 0 is the same as dismissing the prompt to
|
1242
1415
|
rate the class/coach. 1 - 3 is a range from bad to good.
|
1243
1416
|
|
1244
1417
|
Args:
|
1245
|
-
perf_summary (
|
1418
|
+
perf_summary (PerformanceSummary): The performance summary to rate.
|
1246
1419
|
class_rating (int): The class rating. Must be 0, 1, 2, or 3.
|
1247
1420
|
coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
|
1248
1421
|
|
1249
1422
|
Returns:
|
1250
|
-
|
1423
|
+
PerformanceSummary: The updated performance summary.
|
1251
1424
|
|
1252
1425
|
Raises:
|
1253
|
-
ValueError: If `perf_summary` is not a PerformanceSummaryEntry.
|
1254
1426
|
AlreadyRatedError: If the performance summary is already rated.
|
1255
1427
|
ClassNotRatableError: If the performance summary is not rateable.
|
1256
1428
|
ValueError: If the performance summary does not have an associated class.
|
1257
1429
|
"""
|
1258
1430
|
|
1259
|
-
if isinstance(perf_summary, models.PerformanceSummaryDetail):
|
1260
|
-
perf_summary = self._get_performance_summary_entry_from_id(perf_summary.class_history_uuid)
|
1261
|
-
|
1262
|
-
if not isinstance(perf_summary, models.PerformanceSummaryEntry):
|
1263
|
-
raise ValueError(f"`perf_summary` must be a PerformanceSummaryEntry, got {type(perf_summary)}")
|
1264
|
-
|
1265
1431
|
if perf_summary.is_rated:
|
1266
1432
|
raise exc.AlreadyRatedError(f"Performance summary {perf_summary.class_history_uuid} is already rated.")
|
1267
1433
|
|
@@ -1288,10 +1454,7 @@ class Otf:
|
|
1288
1454
|
Returns:
|
1289
1455
|
Any: The member's services.
|
1290
1456
|
"""
|
1291
|
-
|
1292
|
-
data = self._default_request(
|
1293
|
-
"GET", f"/member/members/{self.member_uuid}/services", params={"activeOnly": active_only_str}
|
1294
|
-
)
|
1457
|
+
data = self._get_member_services_raw(active_only)
|
1295
1458
|
return data
|
1296
1459
|
|
1297
1460
|
def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> Any:
|
@@ -1306,7 +1469,5 @@ class Otf:
|
|
1306
1469
|
Returns:
|
1307
1470
|
Any: The member's aspire data.
|
1308
1471
|
"""
|
1309
|
-
|
1310
|
-
|
1311
|
-
data = self._default_request("GET", f"/member/wearables/{self.member_uuid}/wearable-daily", params=params)
|
1472
|
+
data = self._get_aspire_data_raw(datetime, unit)
|
1312
1473
|
return data
|