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
@@ -35,6 +35,15 @@ class BodyFatPercentIndicator(StrEnum):
35
35
  def get_percent_body_fat_descriptor(
36
36
  percent_body_fat: float, body_fat_percent_dividers: list[float]
37
37
  ) -> BodyFatPercentIndicator:
38
+ """Get the body fat percent descriptor based on the percent body fat and dividers.
39
+
40
+ Args:
41
+ percent_body_fat (float): The percent body fat to check
42
+ body_fat_percent_dividers (list[float]): The dividers for the percent body fat
43
+
44
+ Returns:
45
+ BodyFatPercentIndicator: The body fat percent descriptor
46
+ """
38
47
  if not percent_body_fat or not body_fat_percent_dividers[3]:
39
48
  return BodyFatPercentIndicator.NO_INDICATOR
40
49
 
@@ -48,6 +57,15 @@ def get_percent_body_fat_descriptor(
48
57
 
49
58
 
50
59
  def get_relative_descriptor(in_body_value: float, in_body_dividers: list[float]) -> AverageType:
60
+ """Get the relative descriptor for the InBody value.
61
+
62
+ Args:
63
+ in_body_value (float): The InBody value to check
64
+ in_body_dividers (list[float]): The dividers for the InBody value
65
+
66
+ Returns:
67
+ AverageType: The relative descriptor for the InBody value
68
+ """
51
69
  if in_body_value <= in_body_dividers[2]:
52
70
  return AverageType.BELOW_AVERAGE
53
71
 
@@ -58,6 +76,17 @@ def get_relative_descriptor(in_body_value: float, in_body_dividers: list[float])
58
76
 
59
77
 
60
78
  def get_body_fat_percent_dividers(age: int, gender: Literal["M", "F"]) -> list[float]:
79
+ """Get the body fat percent dividers based on age and gender.
80
+
81
+ Converted more or less directly from the Java code in the OTF app.
82
+
83
+ Args:
84
+ age (int): The age of the person
85
+ gender (Literal["M", "F"]): The gender from the member details
86
+
87
+ Returns:
88
+ list[float]: The body fat percent dividers
89
+ """
61
90
  if gender == "M":
62
91
  return get_body_fat_percent_dividers_male(age)
63
92
 
@@ -65,6 +94,16 @@ def get_body_fat_percent_dividers(age: int, gender: Literal["M", "F"]) -> list[f
65
94
 
66
95
 
67
96
  def get_body_fat_percent_dividers_male(age: int) -> list[float]:
97
+ """Get the body fat percent dividers for males based on age.
98
+
99
+ Converted more or less directly from the Java code in the OTF app.
100
+
101
+ Args:
102
+ age (int): The age of the person
103
+
104
+ Returns:
105
+ list[float]: The body fat percent dividers
106
+ """
68
107
  match age:
69
108
  case age if 0 <= age < 30:
70
109
  return [0.0, 13.1, 21.1, 100.0]
@@ -81,6 +120,16 @@ def get_body_fat_percent_dividers_male(age: int) -> list[float]:
81
120
 
82
121
 
83
122
  def get_body_fat_percent_dividers_female(age: int) -> list[float]:
123
+ """Get the body fat percent dividers for females based on age.
124
+
125
+ Converted more or less directly from the Java code in the OTF app.
126
+
127
+ Args:
128
+ age (int): The age of the person
129
+
130
+ Returns:
131
+ list[float]: The body fat percent dividers
132
+ """
84
133
  match age:
85
134
  case age if 0 <= age < 30:
86
135
  return [0.0, 19.1, 26.1, 100.0]
@@ -97,106 +146,110 @@ def get_body_fat_percent_dividers_female(age: int) -> list[float]:
97
146
 
98
147
 
99
148
  class LeanBodyMass(OtfItemBase):
100
- left_arm: float = Field(..., alias="lbmOfLeftArm")
101
- left_leg: float = Field(..., alias="lbmOfLeftLeg")
102
- right_arm: float = Field(..., alias="lbmOfRightArm")
103
- right_leg: float = Field(..., alias="lbmOfRightLeg")
104
- trunk: float = Field(..., alias="lbmOfTrunk")
149
+ left_arm: float = Field(..., validation_alias="lbmOfLeftArm")
150
+ left_leg: float = Field(..., validation_alias="lbmOfLeftLeg")
151
+ right_arm: float = Field(..., validation_alias="lbmOfRightArm")
152
+ right_leg: float = Field(..., validation_alias="lbmOfRightLeg")
153
+ trunk: float = Field(..., validation_alias="lbmOfTrunk")
105
154
 
106
155
 
107
156
  class LeanBodyMassPercent(OtfItemBase):
108
- left_arm: float = Field(..., alias="lbmPercentOfLeftArm")
109
- left_leg: float = Field(..., alias="lbmPercentOfLeftLeg")
110
- right_arm: float = Field(..., alias="lbmPercentOfRightArm")
111
- right_leg: float = Field(..., alias="lbmPercentOfRightLeg")
112
- trunk: float = Field(..., alias="lbmPercentOfTrunk")
157
+ left_arm: float = Field(..., validation_alias="lbmPercentOfLeftArm")
158
+ left_leg: float = Field(..., validation_alias="lbmPercentOfLeftLeg")
159
+ right_arm: float = Field(..., validation_alias="lbmPercentOfRightArm")
160
+ right_leg: float = Field(..., validation_alias="lbmPercentOfRightLeg")
161
+ trunk: float = Field(..., validation_alias="lbmPercentOfTrunk")
113
162
 
114
163
 
115
164
  class BodyFatMass(OtfItemBase):
116
- control: float = Field(..., alias="bfmControl")
117
- left_arm: float = Field(..., alias="bfmOfLeftArm")
118
- left_leg: float = Field(..., alias="bfmOfLeftLeg")
119
- right_arm: float = Field(..., alias="bfmOfRightArm")
120
- right_leg: float = Field(..., alias="bfmOfRightLeg")
121
- trunk: float = Field(..., alias="bfmOfTrunk")
165
+ control: float = Field(..., validation_alias="bfmControl")
166
+ left_arm: float = Field(..., validation_alias="bfmOfLeftArm")
167
+ left_leg: float = Field(..., validation_alias="bfmOfLeftLeg")
168
+ right_arm: float = Field(..., validation_alias="bfmOfRightArm")
169
+ right_leg: float = Field(..., validation_alias="bfmOfRightLeg")
170
+ trunk: float = Field(..., validation_alias="bfmOfTrunk")
122
171
 
123
172
 
124
173
  class BodyFatMassPercent(OtfItemBase):
125
- left_arm: float = Field(..., alias="bfmPercentOfLeftArm")
126
- left_leg: float = Field(..., alias="bfmPercentOfLeftLeg")
127
- right_arm: float = Field(..., alias="bfmPercentOfRightArm")
128
- right_leg: float = Field(..., alias="bfmPercentOfRightLeg")
129
- trunk: float = Field(..., alias="bfmPercentOfTrunk")
174
+ left_arm: float = Field(..., validation_alias="bfmPercentOfLeftArm")
175
+ left_leg: float = Field(..., validation_alias="bfmPercentOfLeftLeg")
176
+ right_arm: float = Field(..., validation_alias="bfmPercentOfRightArm")
177
+ right_leg: float = Field(..., validation_alias="bfmPercentOfRightLeg")
178
+ trunk: float = Field(..., validation_alias="bfmPercentOfTrunk")
130
179
 
131
180
 
132
181
  class TotalBodyWeight(OtfItemBase):
133
- right_arm: float = Field(..., alias="tbwOfRightArm")
134
- left_arm: float = Field(..., alias="tbwOfLeftArm")
135
- trunk: float = Field(..., alias="tbwOfTrunk")
136
- right_leg: float = Field(..., alias="tbwOfRightLeg")
137
- left_leg: float = Field(..., alias="tbwOfLeftLeg")
182
+ right_arm: float = Field(..., validation_alias="tbwOfRightArm")
183
+ left_arm: float = Field(..., validation_alias="tbwOfLeftArm")
184
+ trunk: float = Field(..., validation_alias="tbwOfTrunk")
185
+ right_leg: float = Field(..., validation_alias="tbwOfRightLeg")
186
+ left_leg: float = Field(..., validation_alias="tbwOfLeftLeg")
138
187
 
139
188
 
140
189
  class IntraCellularWater(OtfItemBase):
141
- right_arm: float = Field(..., alias="icwOfRightArm")
142
- left_arm: float = Field(..., alias="icwOfLeftArm")
143
- trunk: float = Field(..., alias="icwOfTrunk")
144
- right_leg: float = Field(..., alias="icwOfRightLeg")
145
- left_leg: float = Field(..., alias="icwOfLeftLeg")
190
+ right_arm: float = Field(..., validation_alias="icwOfRightArm")
191
+ left_arm: float = Field(..., validation_alias="icwOfLeftArm")
192
+ trunk: float = Field(..., validation_alias="icwOfTrunk")
193
+ right_leg: float = Field(..., validation_alias="icwOfRightLeg")
194
+ left_leg: float = Field(..., validation_alias="icwOfLeftLeg")
146
195
 
147
196
 
148
197
  class ExtraCellularWater(OtfItemBase):
149
- right_arm: float = Field(..., alias="ecwOfRightArm")
150
- left_arm: float = Field(..., alias="ecwOfLeftArm")
151
- trunk: float = Field(..., alias="ecwOfTrunk")
152
- right_leg: float = Field(..., alias="ecwOfRightLeg")
153
- left_leg: float = Field(..., alias="ecwOfLeftLeg")
198
+ right_arm: float = Field(..., validation_alias="ecwOfRightArm")
199
+ left_arm: float = Field(..., validation_alias="ecwOfLeftArm")
200
+ trunk: float = Field(..., validation_alias="ecwOfTrunk")
201
+ right_leg: float = Field(..., validation_alias="ecwOfRightLeg")
202
+ left_leg: float = Field(..., validation_alias="ecwOfLeftLeg")
154
203
 
155
204
 
156
205
  class ExtraCellularWaterOverTotalBodyWater(OtfItemBase):
157
- right_arm: float = Field(..., alias="ecwOverTBWOfRightArm")
158
- left_arm: float = Field(..., alias="ecwOverTBWOfLeftArm")
159
- trunk: float = Field(..., alias="ecwOverTBWOfTrunk")
160
- right_leg: float = Field(..., alias="ecwOverTBWOfRightLeg")
161
- left_leg: float = Field(..., alias="ecwOverTBWOfLeftLeg")
206
+ right_arm: float = Field(..., validation_alias="ecwOverTBWOfRightArm")
207
+ left_arm: float = Field(..., validation_alias="ecwOverTBWOfLeftArm")
208
+ trunk: float = Field(..., validation_alias="ecwOverTBWOfTrunk")
209
+ right_leg: float = Field(..., validation_alias="ecwOverTBWOfRightLeg")
210
+ left_leg: float = Field(..., validation_alias="ecwOverTBWOfLeftLeg")
162
211
 
163
212
 
164
213
  class BodyCompositionData(OtfItemBase):
165
214
  # NOTE: weight is hardcoded to be pounds here, regardless of the unit shown in the member details
166
215
 
167
- member_uuid: str = Field(..., alias="memberUUId")
168
- member_id: str | int = Field(..., alias="memberId")
169
- scan_result_uuid: str = Field(..., alias="scanResultUUId")
170
- inbody_id: str = Field(..., alias="id", exclude=True, repr=False, description="InBody ID, same as email address")
216
+ member_uuid: str = Field(..., validation_alias="memberUUId")
217
+ member_id: str | int = Field(..., validation_alias="memberId")
218
+ scan_result_uuid: str = Field(..., validation_alias="scanResultUUId")
219
+ inbody_id: str = Field(
220
+ ..., validation_alias="id", exclude=True, repr=False, description="InBody ID, same as email address"
221
+ )
171
222
  email: str
172
223
  height: str = Field(..., description="Height in cm")
173
224
  gender: Literal["M", "F"]
174
225
  age: int
175
- scan_datetime: datetime = Field(..., alias="testDatetime")
226
+ scan_datetime: datetime = Field(..., validation_alias="testDatetime")
176
227
  provided_weight: float = Field(
177
- ..., alias="weight", description="Weight in pounds, provided by member at time of scan"
228
+ ..., validation_alias="weight", description="Weight in pounds, provided by member at time of scan"
178
229
  )
179
230
 
180
231
  lean_body_mass_details: LeanBodyMass
181
232
  lean_body_mass_percent_details: LeanBodyMassPercent
182
233
 
183
- total_body_weight: float = Field(..., alias="tbw", description="Total body weight in pounds, based on scan results")
184
- dry_lean_mass: float = Field(..., alias="dlm")
185
- body_fat_mass: float = Field(..., alias="bfm")
186
- lean_body_mass: float = Field(..., alias="lbm")
187
- skeletal_muscle_mass: float = Field(..., alias="smm")
188
- body_mass_index: float = Field(..., alias="bmi")
189
- percent_body_fat: float = Field(..., alias="pbf")
190
- basal_metabolic_rate: float = Field(..., alias="bmr")
191
- in_body_type: str = Field(..., alias="inBodyType")
234
+ total_body_weight: float = Field(
235
+ ..., validation_alias="tbw", description="Total body weight in pounds, based on scan results"
236
+ )
237
+ dry_lean_mass: float = Field(..., validation_alias="dlm")
238
+ body_fat_mass: float = Field(..., validation_alias="bfm")
239
+ lean_body_mass: float = Field(..., validation_alias="lbm")
240
+ skeletal_muscle_mass: float = Field(..., validation_alias="smm")
241
+ body_mass_index: float = Field(..., validation_alias="bmi")
242
+ percent_body_fat: float = Field(..., validation_alias="pbf")
243
+ basal_metabolic_rate: float = Field(..., validation_alias="bmr")
244
+ in_body_type: str = Field(..., validation_alias="inBodyType")
192
245
 
193
246
  # excluded because they are only useful for end result of calculations
194
- body_fat_mass_dividers: list[float] = Field(..., alias="bfmGraphScale", exclude=True, repr=False)
195
- body_fat_mass_plot_point: float = Field(..., alias="pfatnew", exclude=True, repr=False)
196
- skeletal_muscle_mass_dividers: list[float] = Field(..., alias="smmGraphScale", exclude=True, repr=False)
197
- skeletal_muscle_mass_plot_point: float = Field(..., alias="psmm", exclude=True, repr=False)
198
- weight_dividers: list[float] = Field(..., alias="wtGraphScale", exclude=True, repr=False)
199
- weight_plot_point: float = Field(..., alias="pwt", exclude=True, repr=False)
247
+ body_fat_mass_dividers: list[float] = Field(..., validation_alias="bfmGraphScale", exclude=True, repr=False)
248
+ body_fat_mass_plot_point: float = Field(..., validation_alias="pfatnew", exclude=True, repr=False)
249
+ skeletal_muscle_mass_dividers: list[float] = Field(..., validation_alias="smmGraphScale", exclude=True, repr=False)
250
+ skeletal_muscle_mass_plot_point: float = Field(..., validation_alias="psmm", exclude=True, repr=False)
251
+ weight_dividers: list[float] = Field(..., validation_alias="wtGraphScale", exclude=True, repr=False)
252
+ weight_plot_point: float = Field(..., validation_alias="pwt", exclude=True, repr=False)
200
253
 
201
254
  # excluded due to 0 values
202
255
  body_fat_mass_details: BodyFatMass = Field(..., exclude=True, repr=False)
@@ -207,13 +260,13 @@ class BodyCompositionData(OtfItemBase):
207
260
  extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = Field(
208
261
  ..., exclude=True, repr=False
209
262
  )
210
- visceral_fat_level: float = Field(..., alias="vfl", exclude=True, repr=False)
211
- visceral_fat_area: float = Field(..., alias="vfa", exclude=True, repr=False)
212
- body_comp_measurement: float = Field(..., alias="bcm", exclude=True, repr=False)
213
- total_body_weight_over_lean_body_mass: float = Field(..., alias="tbwOverLBM", exclude=True, repr=False)
214
- intracellular_water: float = Field(..., alias="icw", exclude=True, repr=False)
215
- extracellular_water: float = Field(..., alias="ecw", exclude=True, repr=False)
216
- lean_body_mass_control: float = Field(..., alias="lbmControl", exclude=True, repr=False)
263
+ visceral_fat_level: float = Field(..., validation_alias="vfl", exclude=True, repr=False)
264
+ visceral_fat_area: float = Field(..., validation_alias="vfa", exclude=True, repr=False)
265
+ body_comp_measurement: float = Field(..., validation_alias="bcm", exclude=True, repr=False)
266
+ total_body_weight_over_lean_body_mass: float = Field(..., validation_alias="tbwOverLBM", exclude=True, repr=False)
267
+ intracellular_water: float = Field(..., validation_alias="icw", exclude=True, repr=False)
268
+ extracellular_water: float = Field(..., validation_alias="ecw", exclude=True, repr=False)
269
+ lean_body_mass_control: float = Field(..., validation_alias="lbmControl", exclude=True, repr=False)
217
270
 
218
271
  def __init__(self, **data):
219
272
  # Convert the nested dictionaries to the appropriate classes
@@ -235,12 +288,28 @@ class BodyCompositionData(OtfItemBase):
235
288
 
236
289
  @field_validator("skeletal_muscle_mass_dividers", "weight_dividers", "body_fat_mass_dividers", mode="before")
237
290
  @classmethod
238
- def convert_dividers_to_float_list(cls, v: str):
291
+ def convert_dividers_to_float_list(cls, v: str) -> list[float]:
292
+ """Convert the dividers from a string to a list of floats.
293
+
294
+ Args:
295
+ v (str): The dividers as a string, separated by semicolons.
296
+
297
+ Returns:
298
+ list[float]: The dividers as a list of floats.
299
+ """
239
300
  return [float(i) for i in v.split(";")]
240
301
 
241
302
  @field_validator("total_body_weight", mode="before")
242
303
  @classmethod
243
- def convert_body_weight_from_kg_to_pounds(cls, v: float):
304
+ def convert_body_weight_from_kg_to_pounds(cls, v: float) -> float:
305
+ """Convert the body weight from kg to pounds.
306
+
307
+ Args:
308
+ v (float): The body weight in kg.
309
+
310
+ Returns:
311
+ float: The body weight in pounds.
312
+ """
244
313
  return ureg.Quantity(v, ureg.kilogram).to(ureg.pound).magnitude
245
314
 
246
315
  @property
@@ -0,0 +1,50 @@
1
+ from pydantic import Field
2
+
3
+ from otf_api.models.base import OtfItemBase
4
+
5
+ from .enums import EquipmentType
6
+
7
+
8
+ class Year(OtfItemBase):
9
+ year: int | None = Field(None, validation_alias="Year")
10
+ is_participated: bool | None = Field(None, validation_alias="IsParticipated")
11
+ in_progress: bool | None = Field(None, validation_alias="InProgress")
12
+
13
+
14
+ class Program(OtfItemBase):
15
+ """A program represents multi-day/week challenges that members can participate in."""
16
+
17
+ # NOTE: These ones do seem to match the ChallengeType enums in the OTF app.
18
+ # Leaving them as int for now though in case older data or other user's
19
+ # data doesn't match up.
20
+ challenge_category_id: int | None = Field(None, validation_alias="ChallengeCategoryId")
21
+ challenge_sub_category_id: int | None = Field(None, validation_alias="ChallengeSubCategoryId")
22
+ challenge_name: str | None = Field(None, validation_alias="ChallengeName")
23
+ years: list[Year] = Field(default_factory=list, validation_alias="Years")
24
+
25
+
26
+ class Challenge(OtfItemBase):
27
+ """A challenge represents a single day or event that members can participate in."""
28
+
29
+ # NOTE: The challenge category/subcategory ids here do not seem to be at
30
+ # all related to the ChallengeType enums or the few SubCategory enums I've
31
+ # been able to puzzle out. I haven't been able to link them to any code
32
+ # in the OTF app. Due to that, they are being excluded from the model for now.
33
+ challenge_category_id: int | None = Field(None, validation_alias="ChallengeCategoryId")
34
+ challenge_sub_category_id: int | None = Field(None, validation_alias="ChallengeSubCategoryId")
35
+ challenge_name: str | None = Field(None, validation_alias="ChallengeName")
36
+ years: list[Year] = Field(default_factory=list, validation_alias="Years")
37
+
38
+
39
+ class Benchmark(OtfItemBase):
40
+ """A benchmark represents a specific workout that members can participate in."""
41
+
42
+ equipment_id: EquipmentType | None = Field(None, validation_alias="EquipmentId")
43
+ equipment_name: str | None = Field(None, validation_alias="EquipmentName")
44
+ years: list[Year] = Field(default_factory=list, validation_alias="Years")
45
+
46
+
47
+ class ChallengeTracker(OtfItemBase):
48
+ programs: list[Program] = Field(default_factory=list, validation_alias="Programs")
49
+ challenges: list[Challenge] = Field(default_factory=list, validation_alias="Challenges")
50
+ benchmarks: list[Benchmark] = Field(default_factory=list, validation_alias="Benchmarks")
@@ -0,0 +1,99 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pydantic import Field
5
+
6
+ from otf_api.models.base import OtfItemBase
7
+
8
+ from .enums import EquipmentType
9
+
10
+
11
+ class MetricEntry(OtfItemBase):
12
+ title: str | None = Field(None, validation_alias="Title")
13
+ equipment_id: EquipmentType | None = Field(None, validation_alias="EquipmentId")
14
+ entry_type: str | None = Field(None, validation_alias="EntryType")
15
+ metric_key: str | None = Field(None, validation_alias="MetricKey")
16
+ min_value: str | None = Field(None, validation_alias="MinValue")
17
+ max_value: str | None = Field(None, validation_alias="MaxValue")
18
+
19
+
20
+ class BenchmarkHistory(OtfItemBase):
21
+ studio_name: str | None = Field(None, validation_alias="StudioName")
22
+ equipment_id: EquipmentType | None = Field(None, validation_alias="EquipmentId")
23
+ class_time: datetime | None = Field(None, validation_alias="ClassTime")
24
+ challenge_sub_category_id: int | None = Field(None, validation_alias="ChallengeSubCategoryId")
25
+ weight_lbs: int | None = Field(None, validation_alias="WeightLBS")
26
+ class_name: str | None = Field(None, validation_alias="ClassName")
27
+ coach_name: str | None = Field(None, validation_alias="CoachName")
28
+ result: float | str | None = Field(None, validation_alias="Result")
29
+ workout_type_id: int | None = Field(None, validation_alias="WorkoutTypeId", exclude=True, repr=False)
30
+ workout_id: int | None = Field(None, validation_alias="WorkoutId", exclude=True, repr=False)
31
+ linked_challenges: list[Any] | None = Field(None, validation_alias="LinkedChallenges", exclude=True, repr=False)
32
+
33
+ date_created: datetime | None = Field(
34
+ None,
35
+ validation_alias="DateCreated",
36
+ exclude=True,
37
+ repr=False,
38
+ description="When the entry was created in database, not useful to users",
39
+ )
40
+ date_updated: datetime | None = Field(
41
+ None,
42
+ validation_alias="DateUpdated",
43
+ exclude=True,
44
+ repr=False,
45
+ description="When the entry was updated in database, not useful to users",
46
+ )
47
+ class_id: int | None = Field(
48
+ None, validation_alias="ClassId", exclude=True, repr=False, description="Not used by API"
49
+ )
50
+ substitute_id: int | None = Field(
51
+ None,
52
+ validation_alias="SubstituteId",
53
+ exclude=True,
54
+ repr=False,
55
+ description="Not used by API, also always seems to be 0",
56
+ )
57
+
58
+
59
+ class ChallengeHistory(OtfItemBase):
60
+ studio_name: str | None = Field(None, validation_alias="StudioName")
61
+ start_date: datetime | None = Field(None, validation_alias="StartDate")
62
+ end_date: datetime | None = Field(None, validation_alias="EndDate")
63
+ total_result: float | str | None = Field(None, validation_alias="TotalResult")
64
+ is_finished: bool | None = Field(None, validation_alias="IsFinished")
65
+ benchmark_histories: list[BenchmarkHistory] = Field(default_factory=list, validation_alias="BenchmarkHistories")
66
+
67
+ challenge_id: int | None = Field(
68
+ None, validation_alias="ChallengeId", exclude=True, repr=False, description="Not used by API"
69
+ )
70
+ studio_id: int | None = Field(
71
+ None, validation_alias="StudioId", exclude=True, repr=False, description="Not used by API"
72
+ )
73
+ challenge_objective: str | None = Field(
74
+ None, validation_alias="ChallengeObjective", exclude=True, repr=False, description="Always the string 'None'"
75
+ )
76
+
77
+
78
+ class Goal(OtfItemBase):
79
+ goal: int | None = Field(None, validation_alias="Goal")
80
+ goal_period: str | None = Field(None, validation_alias="GoalPeriod")
81
+ overall_goal: int | None = Field(None, validation_alias="OverallGoal")
82
+ overall_goal_period: str | None = Field(None, validation_alias="OverallGoalPeriod")
83
+ min_overall: int | None = Field(None, validation_alias="MinOverall")
84
+ min_overall_period: str | None = Field(None, validation_alias="MinOverallPeriod")
85
+
86
+
87
+ class FitnessBenchmark(OtfItemBase):
88
+ challenge_category_id: int | None = Field(None, validation_alias="ChallengeCategoryId")
89
+ challenge_sub_category_id: int | None = Field(None, validation_alias="ChallengeSubCategoryId")
90
+ equipment_id: EquipmentType | None = Field(None, validation_alias="EquipmentId")
91
+ equipment_name: str | None = Field(None, validation_alias="EquipmentName")
92
+ metric_entry: MetricEntry | None = Field(None, validation_alias="MetricEntry")
93
+ challenge_name: str | None = Field(None, validation_alias="ChallengeName")
94
+ best_record: float | str | None = Field(None, validation_alias="BestRecord")
95
+ last_record: float | str | None = Field(None, validation_alias="LastRecord")
96
+ previous_record: float | str | None = Field(None, validation_alias="PreviousRecord")
97
+ unit: str | None = Field(None, validation_alias="Unit")
98
+ goals: Goal | None = Field(None, validation_alias="Goals")
99
+ challenge_histories: list[ChallengeHistory] = Field(default_factory=list, validation_alias="ChallengeHistories")
@@ -0,0 +1,70 @@
1
+ from enum import IntEnum, StrEnum
2
+
3
+
4
+ class StatsTime(StrEnum):
5
+ """Enum representing the different time periods for workout statistics."""
6
+
7
+ LastYear = "lastYear"
8
+ ThisYear = "thisYear"
9
+ LastMonth = "lastMonth"
10
+ ThisMonth = "thisMonth"
11
+ LastWeek = "lastWeek"
12
+ ThisWeek = "thisWeek"
13
+ AllTime = "allTime"
14
+
15
+
16
+ class EquipmentType(IntEnum):
17
+ """Enum representing the type of equipment used in workouts."""
18
+
19
+ Treadmill = 2
20
+ Strider = 3
21
+ Rower = 4
22
+ Bike = 5
23
+ WeightFloor = 6
24
+ PowerWalker = 7
25
+
26
+
27
+ class ChallengeCategory(IntEnum):
28
+ """Enum representing the different challenge categories."""
29
+
30
+ Other = 0
31
+ DriTri = 2
32
+ Infinity = 3
33
+ MarathonMonth = 5
34
+ OrangeEverest = 9
35
+ CatchMeIfYouCan = 10
36
+ TwoHundredMeterRow = 15
37
+ FiveHundredMeterRow = 16
38
+ TwoThousandMeterRow = 17
39
+ TwelveMinuteTreadmill = 18
40
+ OneMileTreadmill = 19
41
+ TenMinuteRow = 20
42
+ HellWeek = 52
43
+ Inferno = 55
44
+ Mayhem = 58
45
+ BackAtIt = 60
46
+ FourteenMinuteRow = 61
47
+ TwelveDaysOfFitness = 63
48
+ TransformationChallenge = 64
49
+ RemixInSix = 65
50
+ Push = 66
51
+ QuarterMileTreadmill = 69
52
+ OneThousandMeterRow = 70
53
+
54
+
55
+ class DriTriChallengeSubCategory(IntEnum):
56
+ """Enum representing the subcategories of the DriTri challenge."""
57
+
58
+ FullRun = 1
59
+ SprintRun = 3
60
+ Relay = 4
61
+ StrengthRun = 1500
62
+
63
+
64
+ class MarathonMonthChallengeSubCategory(IntEnum):
65
+ """Enum representing the subcategories of the Marathon Month challenge."""
66
+
67
+ Original = 1
68
+ Full = 14
69
+ Half = 15
70
+ Ultra = 16
@@ -0,0 +1,96 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from pydantic import Field, field_serializer
4
+
5
+ from otf_api.models.base import OtfItemBase
6
+
7
+ from .enums import StatsTime
8
+
9
+ T = TypeVar("T", bound=OtfItemBase)
10
+
11
+
12
+ class OutStudioMixin:
13
+ walking_distance: float | None = Field(None, validation_alias="walkingDistance")
14
+ running_distance: float | None = Field(None, validation_alias="runningDistance")
15
+ cycling_distance: float | None = Field(None, validation_alias="cyclingDistance")
16
+
17
+ @field_serializer("walking_distance", "running_distance", "cycling_distance")
18
+ @staticmethod
19
+ def limit_floats(value: float | int | None) -> float | None:
20
+ """Limit the float values to 2 decimal places."""
21
+ if value is not None:
22
+ return round(value, 2)
23
+ return value
24
+
25
+
26
+ class InStudioMixin:
27
+ treadmill_distance: float | None = Field(None, validation_alias="treadmillDistance")
28
+ treadmill_elevation_gained: float | None = Field(None, validation_alias="treadmillElevationGained")
29
+ rower_distance: float | None = Field(None, validation_alias="rowerDistance")
30
+ rower_watt: float | None = Field(None, validation_alias="rowerWatt")
31
+
32
+ @field_serializer("treadmill_distance", "treadmill_elevation_gained", "rower_distance", "rower_watt")
33
+ @staticmethod
34
+ def limit_floats(value: float | int | None) -> float | None:
35
+ """Limit the float values to 2 decimal places."""
36
+ if value is not None:
37
+ return round(value, 2)
38
+ return value
39
+
40
+
41
+ class BaseStatsData(OtfItemBase):
42
+ calories: float | None = None
43
+ splat_point: float | None = Field(None, validation_alias="splatPoint")
44
+ total_black_zone: float | None = Field(None, validation_alias="totalBlackZone")
45
+ total_blue_zone: float | None = Field(None, validation_alias="totalBlueZone")
46
+ total_green_zone: float | None = Field(None, validation_alias="totalGreenZone")
47
+ total_orange_zone: float | None = Field(None, validation_alias="totalOrangeZone")
48
+ total_red_zone: float | None = Field(None, validation_alias="totalRedZone")
49
+ workout_duration: float | None = Field(None, validation_alias="workoutDuration")
50
+ step_count: float | None = Field(None, validation_alias="stepCount")
51
+
52
+
53
+ class InStudioStatsData(InStudioMixin, BaseStatsData):
54
+ pass
55
+
56
+
57
+ class OutStudioStatsData(OutStudioMixin, BaseStatsData):
58
+ pass
59
+
60
+
61
+ class AllStatsData(OutStudioMixin, InStudioMixin, BaseStatsData):
62
+ pass
63
+
64
+
65
+ class TimeStats(OtfItemBase, Generic[T]):
66
+ last_year: T = Field(..., validation_alias="lastYear")
67
+ this_year: T = Field(..., validation_alias="thisYear")
68
+ last_month: T = Field(..., validation_alias="lastMonth")
69
+ this_month: T = Field(..., validation_alias="thisMonth")
70
+ last_week: T = Field(..., validation_alias="lastWeek")
71
+ this_week: T = Field(..., validation_alias="thisWeek")
72
+ all_time: T = Field(..., validation_alias="allTime")
73
+
74
+ def get_by_time(self, stats_time: StatsTime) -> T:
75
+ """Get the stats data by time."""
76
+ match stats_time:
77
+ case StatsTime.LastYear:
78
+ return self.last_year
79
+ case StatsTime.ThisYear:
80
+ return self.this_year
81
+ case StatsTime.LastMonth:
82
+ return self.last_month
83
+ case StatsTime.ThisMonth:
84
+ return self.this_month
85
+ case StatsTime.LastWeek:
86
+ return self.last_week
87
+ case StatsTime.ThisWeek:
88
+ return self.this_week
89
+ case StatsTime.AllTime:
90
+ return self.all_time
91
+
92
+
93
+ class StatsResponse(OtfItemBase):
94
+ all_stats: TimeStats[AllStatsData] = Field(..., validation_alias="allStats")
95
+ in_studio: TimeStats[InStudioStatsData] = Field(..., validation_alias="inStudio")
96
+ out_studio: TimeStats[OutStudioStatsData] = Field(..., validation_alias="outStudio")
@@ -0,0 +1,32 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import AliasPath, Field
4
+
5
+ from otf_api.models.base import OtfItemBase
6
+
7
+
8
+ class OutOfStudioWorkoutHistory(OtfItemBase):
9
+ member_uuid: str = Field(..., validation_alias="memberUUId")
10
+ workout_uuid: str = Field(..., validation_alias="workoutUUId")
11
+
12
+ workout_date: datetime | None = Field(None, validation_alias="workoutDate")
13
+ start_time: datetime | None = Field(None, validation_alias="startTime")
14
+ end_time: datetime | None = Field(None, validation_alias="endTime")
15
+ duration: float | None = None
16
+ duration_unit: str | None = Field(None, validation_alias="durationUnit")
17
+ total_calories: int | None = Field(None, validation_alias="totalCalories")
18
+ hr_percent_max: int | None = Field(None, validation_alias="hrPercentMax")
19
+ distance_unit: str | None = Field(None, validation_alias="distanceUnit")
20
+ total_distance: float | None = Field(None, validation_alias="totalDistance")
21
+ splat_points: int | None = Field(None, validation_alias="splatPoints")
22
+ target_heart_rate: int | None = Field(None, validation_alias="targetHeartRate")
23
+ total_steps: int | None = Field(None, validation_alias="totalSteps")
24
+ has_detailed_data: bool | None = Field(None, validation_alias="hasDetailedData")
25
+ avg_heartrate: int | None = Field(None, validation_alias="avgHeartrate")
26
+ max_heartrate: int | None = Field(None, validation_alias="maxHeartrate")
27
+ workout_type: str | None = Field(None, validation_alias=AliasPath("workoutType", "displayName"))
28
+ red_zone_seconds: int | None = Field(None, validation_alias="redZoneSeconds")
29
+ orange_zone_seconds: int | None = Field(None, validation_alias="orangeZoneSeconds")
30
+ green_zone_seconds: int | None = Field(None, validation_alias="greenZoneSeconds")
31
+ blue_zone_seconds: int | None = Field(None, validation_alias="blueZoneSeconds")
32
+ grey_zone_seconds: int | None = Field(None, validation_alias="greyZoneSeconds")