otf-api 0.12.0__py3-none-any.whl → 0.13.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.
Files changed (74) hide show
  1. otf_api/__init__.py +35 -3
  2. otf_api/api/__init__.py +3 -0
  3. otf_api/api/_compat.py +77 -0
  4. otf_api/api/api.py +80 -0
  5. otf_api/api/bookings/__init__.py +3 -0
  6. otf_api/api/bookings/booking_api.py +541 -0
  7. otf_api/api/bookings/booking_client.py +112 -0
  8. otf_api/api/client.py +203 -0
  9. otf_api/api/members/__init__.py +3 -0
  10. otf_api/api/members/member_api.py +187 -0
  11. otf_api/api/members/member_client.py +112 -0
  12. otf_api/api/studios/__init__.py +3 -0
  13. otf_api/api/studios/studio_api.py +173 -0
  14. otf_api/api/studios/studio_client.py +120 -0
  15. otf_api/api/utils.py +307 -0
  16. otf_api/api/workouts/__init__.py +3 -0
  17. otf_api/api/workouts/workout_api.py +333 -0
  18. otf_api/api/workouts/workout_client.py +140 -0
  19. otf_api/auth/__init__.py +1 -1
  20. otf_api/auth/auth.py +155 -89
  21. otf_api/auth/user.py +5 -17
  22. otf_api/auth/utils.py +27 -2
  23. otf_api/cache.py +132 -0
  24. otf_api/exceptions.py +18 -6
  25. otf_api/models/__init__.py +25 -21
  26. otf_api/models/bookings/__init__.py +23 -0
  27. otf_api/models/bookings/bookings.py +134 -0
  28. otf_api/models/{bookings_v2.py → bookings/bookings_v2.py} +72 -31
  29. otf_api/models/bookings/classes.py +124 -0
  30. otf_api/models/{enums.py → bookings/enums.py} +7 -81
  31. otf_api/{filters.py → models/bookings/filters.py} +39 -11
  32. otf_api/models/{ratings.py → bookings/ratings.py} +2 -6
  33. otf_api/models/members/__init__.py +5 -0
  34. otf_api/models/members/member_detail.py +149 -0
  35. otf_api/models/members/member_membership.py +26 -0
  36. otf_api/models/members/member_purchases.py +29 -0
  37. otf_api/models/members/notifications.py +17 -0
  38. otf_api/models/mixins.py +48 -1
  39. otf_api/models/studios/__init__.py +5 -0
  40. otf_api/models/studios/enums.py +11 -0
  41. otf_api/models/studios/studio_detail.py +93 -0
  42. otf_api/models/studios/studio_services.py +36 -0
  43. otf_api/models/workouts/__init__.py +31 -0
  44. otf_api/models/{body_composition_list.py → workouts/body_composition_list.py} +140 -71
  45. otf_api/models/workouts/challenge_tracker_content.py +50 -0
  46. otf_api/models/workouts/challenge_tracker_detail.py +99 -0
  47. otf_api/models/workouts/enums.py +70 -0
  48. otf_api/models/workouts/lifetime_stats.py +96 -0
  49. otf_api/models/workouts/out_of_studio_workout_history.py +32 -0
  50. otf_api/models/{performance_summary.py → workouts/performance_summary.py} +19 -5
  51. otf_api/models/workouts/telemetry.py +88 -0
  52. otf_api/models/{workout.py → workouts/workout.py} +34 -20
  53. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/METADATA +4 -2
  54. otf_api-0.13.0.dist-info/RECORD +59 -0
  55. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/WHEEL +1 -1
  56. otf_api/api.py +0 -1682
  57. otf_api/logging.py +0 -19
  58. otf_api/models/bookings.py +0 -109
  59. otf_api/models/challenge_tracker_content.py +0 -59
  60. otf_api/models/challenge_tracker_detail.py +0 -88
  61. otf_api/models/classes.py +0 -70
  62. otf_api/models/lifetime_stats.py +0 -78
  63. otf_api/models/member_detail.py +0 -121
  64. otf_api/models/member_membership.py +0 -26
  65. otf_api/models/member_purchases.py +0 -29
  66. otf_api/models/notifications.py +0 -17
  67. otf_api/models/out_of_studio_workout_history.py +0 -32
  68. otf_api/models/studio_detail.py +0 -71
  69. otf_api/models/studio_services.py +0 -36
  70. otf_api/models/telemetry.py +0 -84
  71. otf_api/utils.py +0 -164
  72. otf_api-0.12.0.dist-info/RECORD +0 -38
  73. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/licenses/LICENSE +0 -0
  74. {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,333 @@
1
+ import typing
2
+ import warnings
3
+ from datetime import date, datetime
4
+ from logging import getLogger
5
+ from typing import Any, Literal
6
+
7
+ import pendulum
8
+
9
+ from otf_api import exceptions as exc
10
+ from otf_api import models
11
+ from otf_api.api import utils
12
+ from otf_api.api.client import OtfClient
13
+
14
+ from .workout_client import WorkoutClient
15
+
16
+ if typing.TYPE_CHECKING:
17
+ from otf_api import Otf
18
+
19
+ LOGGER = getLogger(__name__)
20
+
21
+
22
+ class WorkoutApi:
23
+ def __init__(self, otf: "Otf", otf_client: OtfClient):
24
+ """Initialize the Workout API client.
25
+
26
+ Args:
27
+ otf (Otf): The OTF API client.
28
+ otf_client (OtfClient): The OTF client to use for requests.
29
+ """
30
+ self.otf = otf
31
+ self.client = WorkoutClient(otf_client)
32
+
33
+ def get_body_composition_list(self) -> list[models.BodyCompositionData]:
34
+ """Get the member's body composition list.
35
+
36
+ Returns:
37
+ list[BodyCompositionData]: The member's body composition list.
38
+ """
39
+ data = self.client.get_body_composition_list()
40
+ return [models.BodyCompositionData(**item) for item in data]
41
+
42
+ def get_challenge_tracker(self) -> models.ChallengeTracker:
43
+ """Get the member's challenge tracker content.
44
+
45
+ Returns:
46
+ ChallengeTracker: The member's challenge tracker content.
47
+ """
48
+ data = self.client.get_challenge_tracker()
49
+ return models.ChallengeTracker(**data["Dto"])
50
+
51
+ def get_benchmarks(
52
+ self,
53
+ challenge_category_id: int = 0,
54
+ equipment_id: models.EquipmentType | Literal[0] = 0,
55
+ challenge_subcategory_id: int = 0,
56
+ ) -> list[models.FitnessBenchmark]:
57
+ """Get the member's challenge tracker participation details.
58
+
59
+ Args:
60
+ challenge_category_id (int): The challenge type ID.
61
+ equipment_id (EquipmentType | Literal[0]): The equipment ID, default is 0 - this doesn't seem\
62
+ to be have any impact on the results.
63
+ challenge_subcategory_id (int): The challenge sub type ID. Default is 0 - this doesn't seem\
64
+ to be have any impact on the results.
65
+
66
+ Returns:
67
+ list[FitnessBenchmark]: The member's challenge tracker details.
68
+ """
69
+ data = self.client.get_benchmarks(int(challenge_category_id), int(equipment_id), challenge_subcategory_id)
70
+ return [models.FitnessBenchmark(**item) for item in data]
71
+
72
+ def get_benchmarks_by_equipment(self, equipment_id: models.EquipmentType) -> list[models.FitnessBenchmark]:
73
+ """Get the member's challenge tracker participation details by equipment.
74
+
75
+ Args:
76
+ equipment_id (EquipmentType): The equipment type ID.
77
+
78
+ Returns:
79
+ list[FitnessBenchmark]: The member's challenge tracker details.
80
+ """
81
+ benchmarks = self.get_benchmarks(equipment_id=equipment_id)
82
+
83
+ benchmarks = [b for b in benchmarks if b.equipment_id == equipment_id]
84
+
85
+ return benchmarks
86
+
87
+ def get_benchmarks_by_challenge_category(self, challenge_category_id: int) -> list[models.FitnessBenchmark]:
88
+ """Get the member's challenge tracker participation details by challenge.
89
+
90
+ Args:
91
+ challenge_category_id (int): The challenge type ID.
92
+
93
+ Returns:
94
+ list[FitnessBenchmark]: The member's challenge tracker details.
95
+ """
96
+ benchmarks = self.get_benchmarks(challenge_category_id=challenge_category_id)
97
+
98
+ benchmarks = [b for b in benchmarks if b.challenge_category_id == challenge_category_id]
99
+
100
+ return benchmarks
101
+
102
+ def get_challenge_tracker_detail(self, challenge_category_id: int) -> models.FitnessBenchmark:
103
+ """Get details about a challenge.
104
+
105
+ This endpoint does not (usually) return member participation, but rather details about the challenge itself.
106
+
107
+ Args:
108
+ challenge_category_id (int): The challenge type ID.
109
+
110
+ Returns:
111
+ FitnessBenchmark: Details about the challenge.
112
+ """
113
+ data = self.client.get_challenge_tracker_detail(int(challenge_category_id))
114
+
115
+ if len(data) > 1:
116
+ LOGGER.warning("Multiple challenge participations found, returning the first one.")
117
+
118
+ if len(data) == 0:
119
+ raise exc.ResourceNotFoundError(f"Challenge {challenge_category_id} not found")
120
+
121
+ return models.FitnessBenchmark(**data[0])
122
+
123
+ def get_performance_summary(self, performance_summary_id: str) -> models.PerformanceSummary:
124
+ """Get the details for a performance summary. Generally should not be called directly. This.
125
+
126
+ Args:
127
+ performance_summary_id (str): The performance summary ID.
128
+
129
+ Returns:
130
+ dict[str, Any]: The performance summary details.
131
+ """
132
+ warnings.warn(
133
+ "`This endpoint does not return all data, consider using `get_workouts` instead.",
134
+ DeprecationWarning,
135
+ stacklevel=2,
136
+ )
137
+
138
+ resp = self.client.get_performance_summary(performance_summary_id)
139
+ return models.PerformanceSummary(**resp)
140
+
141
+ def get_hr_history(self) -> list[models.TelemetryHistoryItem]:
142
+ """Get the heartrate history for the user.
143
+
144
+ Returns a list of history items that contain the max heartrate, start/end bpm for each zone,
145
+ the change from the previous, the change bucket, and the assigned at time.
146
+
147
+ Returns:
148
+ list[HistoryItem]: The heartrate history for the user.
149
+
150
+ """
151
+ resp = self.client.get_hr_history_raw()
152
+ return [models.TelemetryHistoryItem(**item) for item in resp]
153
+
154
+ def get_telemetry(self, performance_summary_id: str, max_data_points: int = 150) -> models.Telemetry:
155
+ """Get the telemetry for a performance summary.
156
+
157
+ This returns an object that contains the max heartrate, start/end bpm for each zone,
158
+ and a list of telemetry items that contain the heartrate, splat points, calories, and timestamp.
159
+
160
+ Args:
161
+ performance_summary_id (str): The performance summary id.
162
+ max_data_points (int): The max data points to use for the telemetry. Default is 150, to match the app.
163
+
164
+ Returns:
165
+ TelemetryItem: The telemetry for the class history.
166
+ """
167
+ res = self.client.get_telemetry(performance_summary_id, max_data_points)
168
+ return models.Telemetry(**res)
169
+
170
+ def get_member_lifetime_stats_in_studio(
171
+ self, select_time: models.StatsTime = models.StatsTime.AllTime
172
+ ) -> models.InStudioStatsData:
173
+ """Get the member's lifetime stats in studio.
174
+
175
+ Args:
176
+ select_time (StatsTime): The time period to get stats for. Default is StatsTime.AllTime.
177
+
178
+ Returns:
179
+ InStudioStatsData: The member's lifetime stats in studio.
180
+ """
181
+ data = self.client.get_member_lifetime_stats(select_time.value)
182
+
183
+ stats = models.StatsResponse(**data)
184
+
185
+ return stats.in_studio.get_by_time(select_time)
186
+
187
+ def get_member_lifetime_stats_out_of_studio(
188
+ self, select_time: models.StatsTime = models.StatsTime.AllTime
189
+ ) -> models.OutStudioStatsData:
190
+ """Get the member's lifetime stats out of studio.
191
+
192
+ Args:
193
+ select_time (StatsTime): The time period to get stats for. Default is StatsTime.AllTime.
194
+
195
+ Returns:
196
+ OutStudioStatsData: The member's lifetime stats out of studio.
197
+ """
198
+ data = self.client.get_member_lifetime_stats(select_time.value)
199
+
200
+ stats = models.StatsResponse(**data)
201
+
202
+ return stats.out_studio.get_by_time(select_time)
203
+
204
+ def get_out_of_studio_workout_history(self) -> list[models.OutOfStudioWorkoutHistory]:
205
+ """Get the member's out of studio workout history.
206
+
207
+ Returns:
208
+ list[OutOfStudioWorkoutHistory]: The member's out of studio workout history.
209
+ """
210
+ data = self.client.get_out_of_studio_workout_history()
211
+
212
+ return [models.OutOfStudioWorkoutHistory(**workout) for workout in data]
213
+
214
+ def get_workout_from_booking(self, booking: str | models.BookingV2) -> models.Workout:
215
+ """Get a workout for a specific booking.
216
+
217
+ Args:
218
+ booking (str | Booking): The booking ID or BookingV2 object to get the workout for.
219
+
220
+ Returns:
221
+ Workout: The member's workout.
222
+
223
+ Raises:
224
+ BookingNotFoundError: If the booking does not exist.
225
+ ResourceNotFoundError: If the workout does not exist.
226
+ TypeError: If the booking is an old Booking model, as these do not have the necessary fields.
227
+ """
228
+ if isinstance(booking, models.Booking):
229
+ raise TypeError("This method cannot be used with the old Booking model")
230
+
231
+ booking_id = utils.get_booking_id(booking)
232
+
233
+ booking = self.otf.bookings.get_booking_new(booking_id)
234
+
235
+ if not booking.workout or not booking.workout.performance_summary_id:
236
+ raise exc.ResourceNotFoundError(f"Workout for booking {booking_id} not found.")
237
+
238
+ perf_summary = self.client.get_performance_summary(booking.workout.performance_summary_id)
239
+ telemetry = self.get_telemetry(booking.workout.performance_summary_id)
240
+ workout = models.Workout.create(**perf_summary, v2_booking=booking, telemetry=telemetry, api=self)
241
+
242
+ return workout
243
+
244
+ def get_workouts(
245
+ self, start_date: date | str | None = None, end_date: date | str | None = None
246
+ ) -> list[models.Workout]:
247
+ """Get the member's workouts, using the new bookings endpoint and the performance summary endpoint.
248
+
249
+ Args:
250
+ start_date (date | str | None): The start date for the workouts. If None, defaults to 30 days ago.
251
+ end_date (date | str | None): The end date for the workouts. If None, defaults to today.
252
+
253
+ Returns:
254
+ list[Workout]: The member's workouts.
255
+ """
256
+ start_date = utils.ensure_date(start_date) or pendulum.today().subtract(days=30).date()
257
+ end_date = utils.ensure_date(end_date) or datetime.today().date()
258
+
259
+ start_dtme = pendulum.datetime(start_date.year, start_date.month, start_date.day, 0, 0, 0)
260
+ end_dtme = pendulum.datetime(end_date.year, end_date.month, end_date.day, 23, 59, 59)
261
+
262
+ bookings = self.otf.bookings.get_bookings_new(
263
+ start_dtme, end_dtme, exclude_cancelled=True, remove_duplicates=True
264
+ )
265
+ bookings_dict = {b.workout.id: b for b in bookings if b.workout}
266
+
267
+ perf_summaries_dict = self.client.get_perf_summaries_threaded(list(bookings_dict.keys()))
268
+ telemetry_dict = self.client.get_telemetry_threaded(list(perf_summaries_dict.keys()))
269
+ perf_summary_to_class_uuid_map = self.client.get_perf_summary_to_class_uuid_mapping()
270
+
271
+ workouts: list[models.Workout] = []
272
+ for perf_id, perf_summary in perf_summaries_dict.items():
273
+ workout = models.Workout.create(
274
+ **perf_summary,
275
+ v2_booking=bookings_dict[perf_id],
276
+ telemetry=telemetry_dict.get(perf_id),
277
+ class_uuid=perf_summary_to_class_uuid_map.get(perf_id),
278
+ api=self,
279
+ )
280
+ workouts.append(workout)
281
+
282
+ return workouts
283
+
284
+ def rate_class_from_workout(
285
+ self,
286
+ workout: models.Workout,
287
+ class_rating: Literal[0, 1, 2, 3],
288
+ coach_rating: Literal[0, 1, 2, 3],
289
+ ) -> models.Workout:
290
+ """Rate a class and coach.
291
+
292
+ The class rating must be 0, 1, 2, or 3. 0 is the same as dismissing the prompt to rate the class/coach. 1 - 3\
293
+ is a range from bad to good.
294
+
295
+ Args:
296
+ workout (Workout): The workout to rate.
297
+ class_rating (int): The class rating. Must be 0, 1, 2, or 3.
298
+ coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
299
+
300
+ Returns:
301
+ Workout: The updated workout with the new ratings.
302
+
303
+ Raises:
304
+ AlreadyRatedError: If the performance summary is already rated.
305
+ ClassNotRatableError: If the performance summary is not rateable.
306
+ """
307
+ if not workout.ratable or not workout.class_uuid:
308
+ raise exc.ClassNotRatableError(f"Workout {workout.performance_summary_id} is not rateable.")
309
+
310
+ if workout.class_rating is not None or workout.coach_rating is not None:
311
+ raise exc.AlreadyRatedError(f"Workout {workout.performance_summary_id} already rated.")
312
+
313
+ self.otf.bookings.rate_class(workout.class_uuid, workout.performance_summary_id, class_rating, coach_rating)
314
+
315
+ return self.get_workout_from_booking(workout.booking_id)
316
+
317
+ # the below do not return any data for me, so I can't test them
318
+
319
+ def _get_aspire_data(self, datetime: str | None = None, unit: str | None = None) -> Any: # noqa: ANN401
320
+ """Get data from the member's aspire wearable.
321
+
322
+ Args:
323
+ datetime (str | None): The date and time to get data for. Default is None.
324
+ unit (str | None): The measurement unit. Default is None.
325
+
326
+ Returns:
327
+ Any: The member's aspire data.
328
+
329
+ Note:
330
+ I don't have an aspire wearable, so I can't test this.
331
+ """
332
+ data = self.client.get_aspire_data(datetime, unit)
333
+ return data
@@ -0,0 +1,140 @@
1
+ from concurrent.futures import ThreadPoolExecutor
2
+ from functools import partial
3
+ from typing import Any
4
+
5
+ from otf_api.api.client import API_IO_BASE_URL, API_TELEMETRY_BASE_URL, CACHE, OtfClient
6
+
7
+
8
+ class WorkoutClient:
9
+ """Client for retrieving workout and performance data from the OTF API.
10
+
11
+ This class provides methods to access telemetry data, performance summaries, and workout history.
12
+ """
13
+
14
+ def __init__(self, client: OtfClient):
15
+ self.client = client
16
+ self.user = client.user
17
+ self.member_uuid = client.member_uuid
18
+
19
+ def telemetry_request(
20
+ self, method: str, path: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None
21
+ ) -> Any: # noqa: ANN401
22
+ """Perform an API request to the Telemetry API."""
23
+ return self.client.do(method, API_TELEMETRY_BASE_URL, path, params, headers=headers)
24
+
25
+ def performance_summary_request(
26
+ self, method: str, path: str, params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None
27
+ ) -> Any: # noqa: ANN401
28
+ """Perform an API request to the performance summary API."""
29
+ perf_api_headers = {"koji-member-id": self.member_uuid, "koji-member-email": self.user.email_address}
30
+ headers = perf_api_headers | (headers or {})
31
+ return self.client.do(method, API_IO_BASE_URL, path, params, headers=headers)
32
+
33
+ def get_member_lifetime_stats(self, select_time: str) -> dict:
34
+ """Retrieve raw lifetime stats data."""
35
+ return self.client.default_request("GET", f"/performance/v2/{self.member_uuid}/over-time/{select_time}")["data"]
36
+
37
+ def get_out_of_studio_workout_history(self) -> dict:
38
+ """Retrieve raw out-of-studio workout history data."""
39
+ return self.client.default_request("GET", f"/member/members/{self.member_uuid}/out-of-studio-workout")["data"]
40
+
41
+ def get_performance_summaries(self, limit: int | None = None) -> dict:
42
+ """Retrieve raw performance summaries data."""
43
+ params = {"limit": limit} if limit else {}
44
+ return self.performance_summary_request("GET", "/v1/performance-summaries", params=params)
45
+
46
+ @CACHE.memoize(expire=600, tag="performance_summary", ignore=(0,))
47
+ def get_performance_summary(self, performance_summary_id: str) -> dict:
48
+ """Retrieve raw performance summary data."""
49
+ return self.performance_summary_request("GET", f"/v1/performance-summaries/{performance_summary_id}")
50
+
51
+ def get_hr_history_raw(self) -> dict:
52
+ """Retrieve raw heart rate history."""
53
+ return self.telemetry_request("GET", "/v1/physVars/maxHr/history", params={"memberUuid": self.member_uuid})[
54
+ "history"
55
+ ]
56
+
57
+ @CACHE.memoize(expire=600, tag="telemetry", ignore=(0,))
58
+ def get_telemetry(self, performance_summary_id: str, max_data_points: int = 150) -> dict:
59
+ """Retrieve raw telemetry data."""
60
+ return self.telemetry_request(
61
+ "GET",
62
+ "/v1/performance/summary",
63
+ params={"classHistoryUuid": performance_summary_id, "maxDataPoints": max_data_points},
64
+ )
65
+
66
+ def get_body_composition_list(self) -> dict:
67
+ """Retrieve raw body composition list."""
68
+ return self.client.default_request("GET", f"/member/members/{self.user.cognito_id}/body-composition")["data"]
69
+
70
+ def get_challenge_tracker(self) -> dict:
71
+ """Retrieve raw challenge tracker data."""
72
+ return self.client.default_request("GET", f"/challenges/v3.1/member/{self.member_uuid}")
73
+
74
+ def get_benchmarks(self, challenge_category_id: int, equipment_id: int, challenge_subcategory_id: int) -> dict:
75
+ """Retrieve raw fitness benchmark data."""
76
+ return self.client.default_request(
77
+ "GET",
78
+ f"/challenges/v3/member/{self.member_uuid}/benchmarks",
79
+ params={
80
+ "equipmentId": equipment_id,
81
+ "challengeTypeId": challenge_category_id,
82
+ "challengeSubTypeId": challenge_subcategory_id,
83
+ },
84
+ )["Dto"]
85
+
86
+ def get_challenge_tracker_detail(self, challenge_category_id: int) -> dict:
87
+ """Retrieve raw challenge tracker detail data."""
88
+ return self.client.default_request(
89
+ "GET",
90
+ f"/challenges/v1/member/{self.member_uuid}/participation",
91
+ params={"challengeTypeId": challenge_category_id},
92
+ )["Dto"]
93
+
94
+ def get_perf_summary_to_class_uuid_mapping(self) -> dict[str, str | None]:
95
+ """Get a mapping of performance summary IDs to class UUIDs. These will be used when rating a class.
96
+
97
+ Returns:
98
+ dict[str, str | None]: A dictionary mapping performance summary IDs to class UUIDs.
99
+ """
100
+ perf_summaries = self.get_performance_summaries()["items"]
101
+ return {item["id"]: item["class"].get("ot_base_class_uuid") for item in perf_summaries}
102
+
103
+ def get_perf_summaries_threaded(self, performance_summary_ids: list[str]) -> dict[str, dict[str, Any]]:
104
+ """Get performance summaries in a ThreadPoolExecutor, to speed up the process.
105
+
106
+ Args:
107
+ performance_summary_ids (list[str]): The performance summary IDs to get.
108
+
109
+ Returns:
110
+ dict[str, dict[str, Any]]: A dictionary of performance summaries, keyed by performance summary ID.
111
+ """
112
+ with ThreadPoolExecutor(max_workers=10) as pool:
113
+ perf_summaries = pool.map(self.get_performance_summary, performance_summary_ids)
114
+
115
+ perf_summaries_dict = {perf_summary["id"]: perf_summary for perf_summary in perf_summaries}
116
+ return perf_summaries_dict
117
+
118
+ def get_telemetry_threaded(
119
+ self, performance_summary_ids: list[str], max_data_points: int = 150
120
+ ) -> dict[str, dict[str, Any]]:
121
+ """Get telemetry in a ThreadPoolExecutor, to speed up the process.
122
+
123
+ Args:
124
+ performance_summary_ids (list[str]): The performance summary IDs to get.
125
+ max_data_points (int): The max data points to use for the telemetry. Default is 150.
126
+
127
+ Returns:
128
+ dict[str, Telemetry]: A dictionary of telemetry, keyed by performance summary ID.
129
+ """
130
+ partial_fn = partial(self.get_telemetry, max_data_points=max_data_points)
131
+ with ThreadPoolExecutor(max_workers=10) as pool:
132
+ telemetry = pool.map(partial_fn, performance_summary_ids)
133
+ telemetry_dict = {perf_summary["classHistoryUuid"]: perf_summary for perf_summary in telemetry}
134
+ return telemetry_dict
135
+
136
+ def get_aspire_data(self, datetime: str | None, unit: str | None) -> dict:
137
+ """Retrieve raw aspire wearable data."""
138
+ return self.client.default_request(
139
+ "GET", f"/member/wearables/{self.member_uuid}/wearable-daily", params={"datetime": datetime, "unit": unit}
140
+ )
otf_api/auth/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .auth import HttpxCognitoAuth
2
2
  from .user import OtfUser
3
3
 
4
- __all__ = ["OTF_AUTH_TYPE", "HttpxCognitoAuth", "OtfAuth", "OtfUser"]
4
+ __all__ = ["HttpxCognitoAuth", "OtfUser"]