otf-api 0.8.2__py3-none-any.whl → 0.9.1__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 (43) hide show
  1. otf_api/__init__.py +7 -4
  2. otf_api/api.py +699 -480
  3. otf_api/auth/__init__.py +4 -0
  4. otf_api/auth/auth.py +234 -0
  5. otf_api/auth/user.py +66 -0
  6. otf_api/auth/utils.py +129 -0
  7. otf_api/exceptions.py +38 -5
  8. otf_api/filters.py +97 -0
  9. otf_api/logging.py +19 -0
  10. otf_api/models/__init__.py +27 -38
  11. otf_api/models/body_composition_list.py +47 -50
  12. otf_api/models/bookings.py +63 -87
  13. otf_api/models/challenge_tracker_content.py +42 -21
  14. otf_api/models/challenge_tracker_detail.py +68 -48
  15. otf_api/models/classes.py +53 -62
  16. otf_api/models/enums.py +108 -30
  17. otf_api/models/lifetime_stats.py +59 -45
  18. otf_api/models/member_detail.py +95 -115
  19. otf_api/models/member_membership.py +18 -17
  20. otf_api/models/member_purchases.py +21 -127
  21. otf_api/models/mixins.py +37 -33
  22. otf_api/models/notifications.py +17 -0
  23. otf_api/models/out_of_studio_workout_history.py +22 -31
  24. otf_api/models/performance_summary_detail.py +47 -42
  25. otf_api/models/performance_summary_list.py +19 -37
  26. otf_api/models/studio_detail.py +51 -98
  27. otf_api/models/studio_services.py +27 -48
  28. otf_api/models/telemetry.py +14 -5
  29. otf_api/utils.py +134 -0
  30. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/METADATA +21 -10
  31. otf_api-0.9.1.dist-info/RECORD +35 -0
  32. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/WHEEL +1 -1
  33. otf_api/auth.py +0 -316
  34. otf_api/models/book_class.py +0 -89
  35. otf_api/models/cancel_booking.py +0 -49
  36. otf_api/models/favorite_studios.py +0 -106
  37. otf_api/models/latest_agreement.py +0 -21
  38. otf_api/models/telemetry_hr_history.py +0 -34
  39. otf_api/models/telemetry_max_hr.py +0 -13
  40. otf_api/models/total_classes.py +0 -8
  41. otf_api-0.8.2.dist-info/AUTHORS.md +0 -9
  42. otf_api-0.8.2.dist-info/RECORD +0 -36
  43. {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/LICENSE +0 -0
@@ -1,59 +1,48 @@
1
- from .body_composition_list import BodyCompositionList
2
- from .book_class import BookClass
3
- from .bookings import Booking, BookingList
4
- from .cancel_booking import CancelBooking
5
- from .challenge_tracker_content import ChallengeTrackerContent
6
- from .challenge_tracker_detail import ChallengeTrackerDetailList
7
- from .classes import OtfClass, OtfClassList
8
- from .enums import BookingStatus, ChallengeType, ClassType, DoW, EquipmentType, StatsTime, StudioStatus
9
- from .favorite_studios import FavoriteStudioList
10
- from .latest_agreement import LatestAgreement
11
- from .lifetime_stats import StatsResponse
1
+ from .body_composition_list import BodyCompositionData
2
+ from .bookings import Booking
3
+ from .challenge_tracker_content import ChallengeTracker
4
+ from .challenge_tracker_detail import FitnessBenchmark
5
+ from .classes import OtfClass
6
+ from .enums import BookingStatus, ChallengeCategory, ClassType, DoW, EquipmentType, StatsTime, StudioStatus
7
+ from .lifetime_stats import StatsResponse, TimeStats
12
8
  from .member_detail import MemberDetail
13
9
  from .member_membership import MemberMembership
14
- from .member_purchases import MemberPurchaseList
15
- from .out_of_studio_workout_history import OutOfStudioWorkoutHistoryList
10
+ from .member_purchases import MemberPurchase
11
+ from .notifications import EmailNotificationSettings, SmsNotificationSettings
12
+ from .out_of_studio_workout_history import OutOfStudioWorkoutHistory
16
13
  from .performance_summary_detail import PerformanceSummaryDetail
17
- from .performance_summary_list import PerformanceSummaryList
18
- from .studio_detail import Pagination, StudioDetail, StudioDetailList
19
- from .studio_services import StudioServiceList
20
- from .telemetry import Telemetry
21
- from .telemetry_hr_history import TelemetryHrHistory
22
- from .telemetry_max_hr import TelemetryMaxHr
23
- from .total_classes import TotalClasses
14
+ from .performance_summary_list import PerformanceSummaryEntry
15
+ from .studio_detail import StudioDetail
16
+ from .studio_services import StudioService
17
+ from .telemetry import Telemetry, TelemetryHistoryItem
24
18
 
25
19
  __all__ = [
26
- "BodyCompositionList",
27
- "BookClass",
20
+ "BodyCompositionData",
28
21
  "Booking",
29
- "BookingList",
30
22
  "BookingStatus",
31
- "CancelBooking",
32
- "ChallengeTrackerContent",
33
- "ChallengeTrackerDetailList",
34
- "ChallengeType",
23
+ "ChallengeCategory",
24
+ "ChallengeParticipation",
25
+ "ChallengeTracker",
35
26
  "ClassType",
36
27
  "DoW",
28
+ "EmailNotificationSettings",
37
29
  "EquipmentType",
38
- "FavoriteStudioList",
30
+ "FitnessBenchmark",
39
31
  "LatestAgreement",
40
32
  "MemberDetail",
41
33
  "MemberMembership",
42
- "MemberPurchaseList",
43
- "OtfClassList",
34
+ "MemberPurchase",
44
35
  "OtfClass",
45
- "OutOfStudioWorkoutHistoryList",
46
- "Pagination",
36
+ "OutOfStudioWorkoutHistory",
47
37
  "PerformanceSummaryDetail",
48
- "PerformanceSummaryList",
38
+ "PerformanceSummaryEntry",
39
+ "SmsNotificationSettings",
49
40
  "StatsResponse",
50
41
  "StatsTime",
51
42
  "StudioDetail",
52
- "StudioDetailList",
53
- "StudioServiceList",
43
+ "StudioService",
54
44
  "StudioStatus",
55
45
  "Telemetry",
56
- "TelemetryHrHistory",
57
- "TelemetryMaxHr",
58
- "TotalClasses",
46
+ "TelemetryHistoryItem",
47
+ "TimeStats",
59
48
  ]
@@ -1,27 +1,28 @@
1
- import inspect
2
1
  from datetime import datetime
3
- from enum import Enum
2
+ from enum import StrEnum
3
+ from typing import Literal
4
4
 
5
5
  import pint
6
- from pydantic import BaseModel, Field, field_validator
6
+ from pydantic import Field, field_validator
7
7
 
8
8
  from otf_api.models.base import OtfItemBase
9
9
 
10
10
  ureg = pint.UnitRegistry()
11
11
 
12
+
12
13
  DEFAULT_WEIGHT_DIVIDERS = [55.0, 70.0, 85.0, 100.0, 115.0, 130.0, 145.0, 160.0, 175.0, 190.0, 205.0]
13
14
  DEFAULT_SKELETAL_MUSCLE_MASS_DIVIDERS = [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0]
14
15
  DEFAULT_BODY_FAT_MASS_DIVIDERS = [40.0, 60.0, 80.0, 100.0, 160.0, 220.0, 280.0, 340.0, 400.0, 460.0, 520.0]
15
16
 
16
17
 
17
- class AverageType(str, Enum):
18
+ class AverageType(StrEnum):
18
19
  BELOW_AVERAGE = "BELOW_AVERAGE"
19
20
  AVERAGE = "AVERAGE"
20
21
  ABOVE_AVERAGE = "ABOVE_AVERAGE"
21
22
  MINIMUM = "MINIMUM" # unused
22
23
 
23
24
 
24
- class BodyFatPercentIndicator(str, Enum):
25
+ class BodyFatPercentIndicator(StrEnum):
25
26
  NO_INDICATOR = "NO_INDICATOR"
26
27
  MINIMUM_BODY_FAT = "MINIMUM_BODY_FAT" # unused
27
28
  LOW_BODY_FAT = "LOW_BODY_FAT" # unused
@@ -31,11 +32,6 @@ class BodyFatPercentIndicator(str, Enum):
31
32
  OBESE_BODY_FAT = "OBESE_BODY_FAT" # unused
32
33
 
33
34
 
34
- class Gender(str, Enum):
35
- MALE = "M"
36
- FEMALE = "F"
37
-
38
-
39
35
  def get_percent_body_fat_descriptor(
40
36
  percent_body_fat: float, body_fat_percent_dividers: list[float]
41
37
  ) -> BodyFatPercentIndicator:
@@ -61,8 +57,8 @@ def get_relative_descriptor(in_body_value: float, in_body_dividers: list[float])
61
57
  return AverageType.ABOVE_AVERAGE
62
58
 
63
59
 
64
- def get_body_fat_percent_dividers(age: int, gender: Gender) -> list[float]:
65
- if gender == Gender.MALE:
60
+ def get_body_fat_percent_dividers(age: int, gender: Literal["M", "F"]) -> list[float]:
61
+ if gender == "M":
66
62
  return get_body_fat_percent_dividers_male(age)
67
63
 
68
64
  return get_body_fat_percent_dividers_female(age)
@@ -166,13 +162,15 @@ class ExtraCellularWaterOverTotalBodyWater(OtfItemBase):
166
162
 
167
163
 
168
164
  class BodyCompositionData(OtfItemBase):
165
+ # NOTE: weight is hardcoded to be pounds here, regardless of the unit shown in the member details
166
+
169
167
  member_uuid: str = Field(..., alias="memberUUId")
170
- member_id: str = Field(..., alias="memberId")
168
+ member_id: str | int = Field(..., alias="memberId")
171
169
  scan_result_uuid: str = Field(..., alias="scanResultUUId")
172
- inbody_id: str = Field(..., alias="id", exclude=True, description="InBody ID, same as email address")
170
+ inbody_id: str = Field(..., alias="id", exclude=True, repr=False, description="InBody ID, same as email address")
173
171
  email: str
174
172
  height: str = Field(..., description="Height in cm")
175
- gender: Gender
173
+ gender: Literal["M", "F"]
176
174
  age: int
177
175
  scan_datetime: datetime = Field(..., alias="testDatetime")
178
176
  provided_weight: float = Field(
@@ -193,44 +191,47 @@ class BodyCompositionData(OtfItemBase):
193
191
  in_body_type: str = Field(..., alias="inBodyType")
194
192
 
195
193
  # excluded because they are only useful for end result of calculations
196
- body_fat_mass_dividers: list[float] = Field(..., alias="bfmGraphScale", exclude=True)
197
- body_fat_mass_plot_point: float = Field(..., alias="pfatnew", exclude=True)
198
- skeletal_muscle_mass_dividers: list[float] = Field(..., alias="smmGraphScale", exclude=True)
199
- skeletal_muscle_mass_plot_point: float = Field(..., alias="psmm", exclude=True)
200
- weight_dividers: list[float] = Field(..., alias="wtGraphScale", exclude=True)
201
- weight_plot_point: float = Field(..., alias="pwt", exclude=True)
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)
202
200
 
203
201
  # excluded due to 0 values
204
- body_fat_mass_details: BodyFatMass = Field(..., exclude=True)
205
- body_fat_mass_percent_details: BodyFatMassPercent = Field(..., exclude=True)
206
- total_body_weight_details: TotalBodyWeight = Field(..., exclude=True)
207
- intra_cellular_water_details: IntraCellularWater = Field(..., exclude=True)
208
- extra_cellular_water_details: ExtraCellularWater = Field(..., exclude=True)
209
- extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = Field(..., exclude=True)
210
- visceral_fat_level: float = Field(..., alias="vfl", exclude=True)
211
- visceral_fat_area: float = Field(..., alias="vfa", exclude=True)
212
- body_comp_measurement: float = Field(..., alias="bcm", exclude=True)
213
- total_body_weight_over_lean_body_mass: float = Field(..., alias="tbwOverLBM", exclude=True)
214
- intracellular_water: float = Field(..., alias="icw", exclude=True)
215
- extracellular_water: float = Field(..., alias="ecw", exclude=True)
216
- lean_body_mass_control: float = Field(..., alias="lbmControl", exclude=True)
202
+ body_fat_mass_details: BodyFatMass = Field(..., exclude=True, repr=False)
203
+ body_fat_mass_percent_details: BodyFatMassPercent = Field(..., exclude=True, repr=False)
204
+ total_body_weight_details: TotalBodyWeight = Field(..., exclude=True, repr=False)
205
+ intra_cellular_water_details: IntraCellularWater = Field(..., exclude=True, repr=False)
206
+ extra_cellular_water_details: ExtraCellularWater = Field(..., exclude=True, repr=False)
207
+ extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = Field(
208
+ ..., exclude=True, repr=False
209
+ )
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)
217
217
 
218
218
  def __init__(self, **data):
219
- # populate child models
220
- child_model_dict = {
221
- k: v.annotation
222
- for k, v in self.model_fields.items()
223
- if inspect.isclass(v.annotation) and issubclass(v.annotation, BaseModel)
219
+ # Convert the nested dictionaries to the appropriate classes
220
+ attr_to_class_map = {
221
+ "lean_body_mass_details": LeanBodyMass,
222
+ "lean_body_mass_percent_details": LeanBodyMassPercent,
223
+ "body_fat_mass_details": BodyFatMass,
224
+ "body_fat_mass_percent_details": BodyFatMassPercent,
225
+ "total_body_weight_details": TotalBodyWeight,
226
+ "intra_cellular_water_details": IntraCellularWater,
227
+ "extra_cellular_water_details": ExtraCellularWater,
228
+ "extra_cellular_water_over_total_body_water_details": ExtraCellularWaterOverTotalBodyWater,
224
229
  }
225
- for k, v in child_model_dict.items():
226
- data[k] = v(**data)
227
230
 
228
- super().__init__(**data)
231
+ for attr, cls in attr_to_class_map.items():
232
+ data[attr] = cls(**data)
229
233
 
230
- @field_validator("member_id", mode="before")
231
- @classmethod
232
- def int_to_str(cls, v: int):
233
- return str(v)
234
+ super().__init__(**data)
234
235
 
235
236
  @field_validator("skeletal_muscle_mass_dividers", "weight_dividers", "body_fat_mass_dividers", mode="before")
236
237
  @classmethod
@@ -289,7 +290,3 @@ class BodyCompositionData(OtfItemBase):
289
290
  return get_percent_body_fat_descriptor(
290
291
  self.percent_body_fat, get_body_fat_percent_dividers(self.age, self.gender)
291
292
  )
292
-
293
-
294
- class BodyCompositionList(OtfItemBase):
295
- data: list[BodyCompositionData]
@@ -1,119 +1,95 @@
1
- from collections.abc import Hashable
2
1
  from datetime import datetime
3
- from typing import Any
4
2
 
5
3
  from pydantic import Field
6
4
 
7
5
  from otf_api.models.base import OtfItemBase
8
- from otf_api.models.enums import BookingStatus, StudioStatus
9
- from otf_api.models.mixins import OtfClassTimeMixin
10
-
11
-
12
- class Location(OtfItemBase):
13
- address_one: str | None = Field(None, alias="address1")
14
- address_two: str | None = Field(alias="address2")
15
- city: str | None = None
16
- country: str | None = None
17
- distance: float | None = None
18
- location_name: str | None = Field(None, alias="locationName")
19
- latitude: float | None = Field(None, alias="latitude")
20
- longitude: float | None = Field(None, alias="longitude")
21
- phone_number: str | None = Field(None, alias="phone")
22
- postal_code: str | None = Field(None, alias="postalCode")
23
- state: str | None = None
6
+ from otf_api.models.enums import BookingStatus
7
+ from otf_api.models.studio_detail import StudioDetail
24
8
 
25
9
 
26
10
  class Coach(OtfItemBase):
27
11
  coach_uuid: str = Field(alias="coachUUId")
28
- name: str
29
12
  first_name: str | None = Field(None, alias="firstName")
30
13
  last_name: str | None = Field(None, alias="lastName")
31
- image_url: str | None = Field(None, alias="imageUrl", exclude=True)
32
- profile_picture_url: str | None = Field(None, alias="profilePictureUrl", exclude=True)
33
-
34
-
35
- class StudioLocation(OtfItemBase):
36
- latitude: float | None = Field(None, alias="latitude")
37
- longitude: float | None = Field(None, alias="longitude")
38
- phone_number: str | None = Field(None, alias="phoneNumber")
39
- physical_city: str | None = Field(None, alias="physicalCity")
40
- physical_address: str | None = Field(None, alias="physicalAddress")
41
- physical_address2: str | None = Field(None, alias="physicalAddress2")
42
- physical_state: str | None = Field(None, alias="physicalState")
43
- physical_postal_code: str | None = Field(None, alias="physicalPostalCode")
44
- physical_region: str | None = Field(None, alias="physicalRegion", exclude=True)
45
- physical_country_id: int | None = Field(None, alias="physicalCountryId", exclude=True)
46
- physical_country: str | None = Field(None, alias="physicalCountry")
47
- country: dict[Hashable, Any] | None = Field(None, alias="country", exclude=True)
48
-
49
-
50
- class Studio(OtfItemBase):
51
- studio_uuid: str = Field(alias="studioUUId")
52
- studio_name: str = Field(alias="studioName")
53
- studio_id: int = Field(alias="studioId")
54
- description: str | None = None
55
- contact_email: str | None = Field(None, alias="contactEmail", exclude=True)
56
- status: StudioStatus | None = None
57
- logo_url: str | None = Field(None, alias="logoUrl", exclude=True)
58
- time_zone: str = Field(alias="timeZone")
59
- mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True)
60
- allows_cr_waitlist: bool | None = Field(None, alias="allowsCRWaitlist")
61
- cr_waitlist_flag_last_updated: datetime | None = Field(None, alias="crWaitlistFlagLastUpdated", exclude=True)
62
- studio_location: StudioLocation | None = Field(None, alias="studioLocation", exclude=True)
63
-
64
-
65
- class OtfClass(OtfItemBase, OtfClassTimeMixin):
14
+
15
+ # unused fields
16
+ name: str = Field(exclude=True, repr=False)
17
+
18
+ @property
19
+ def full_name(self) -> str:
20
+ return f"{self.first_name} {self.last_name}"
21
+
22
+
23
+ class OtfClass(OtfItemBase):
66
24
  class_uuid: str = Field(alias="classUUId")
67
25
  name: str
68
- description: str | None = Field(None, exclude=True)
69
- starts_at_local: datetime = Field(alias="startDateTime")
70
- ends_at_local: datetime = Field(alias="endDateTime")
26
+ starts_at: datetime = Field(alias="startDateTime", description="Start time in local timezone")
27
+ ends_at: datetime = Field(alias="endDateTime", description="End time in local timezone")
71
28
  is_available: bool = Field(alias="isAvailable")
72
29
  is_cancelled: bool = Field(alias="isCancelled")
73
- program_name: str | None = Field(None, alias="programName")
74
- coach_id: int | None = Field(None, alias="coachId")
75
- studio: Studio
30
+ studio: StudioDetail
76
31
  coach: Coach
77
- location: Location | None = None
78
- virtual_class: bool | None = Field(None, alias="virtualClass")
79
32
 
80
-
81
- class Member(OtfItemBase):
82
- member_uuid: str = Field(alias="memberUUId")
83
- first_name: str = Field(alias="firstName")
84
- last_name: str = Field(alias="lastName")
85
- email: str | None = None
86
- phone_number: str | None = Field(None, alias="phoneNumber")
87
- gender: str | None = None
88
- cc_last_4: str | None = Field(None, alias="ccLast4", exclude=True)
33
+ # unused fields
34
+ coach_id: int | None = Field(None, alias="coachId", exclude=True, repr=False, description="Not used by API")
35
+ description: str | None = Field(None, exclude=True, repr=False)
36
+ program_name: str | None = Field(None, alias="programName", exclude=True, repr=False)
37
+ virtual_class: bool | None = Field(None, alias="virtualClass", exclude=True, repr=False)
89
38
 
90
39
 
91
40
  class Booking(OtfItemBase):
92
- class_booking_id: int = Field(alias="classBookingId")
93
- class_booking_uuid: str = Field(alias="classBookingUUId", description="ID used to cancel the booking")
94
- studio_id: int = Field(alias="studioId")
95
- class_id: int = Field(alias="classId")
41
+ booking_uuid: str = Field(alias="classBookingUUId", description="ID used to cancel the booking")
96
42
  is_intro: bool = Field(alias="isIntro")
97
- member_id: int = Field(alias="memberId")
98
- mbo_member_id: str | None = Field(None, alias="mboMemberId", exclude=True)
99
- mbo_class_id: int | None = Field(None, alias="mboClassId", exclude=True)
100
- mbo_visit_id: int | None = Field(None, alias="mboVisitId", exclude=True)
101
- mbo_waitlist_entry_id: int | None = Field(None, alias="mboWaitlistEntryId", exclude=True)
102
- mbo_sync_message: str | None = Field(None, alias="mboSyncMessage", exclude=True)
103
43
  status: BookingStatus
104
44
  booked_date: datetime | None = Field(None, alias="bookedDate")
105
45
  checked_in_date: datetime | None = Field(None, alias="checkedInDate")
106
46
  cancelled_date: datetime | None = Field(None, alias="cancelledDate")
107
- created_by: str = Field(alias="createdBy", exclude=True)
108
47
  created_date: datetime = Field(alias="createdDate")
109
- updated_by: str = Field(alias="updatedBy", exclude=True)
110
48
  updated_date: datetime = Field(alias="updatedDate")
111
49
  is_deleted: bool = Field(alias="isDeleted")
112
- member: Member | None = Field(None, exclude=True)
113
50
  waitlist_position: int | None = Field(None, alias="waitlistPosition")
114
51
  otf_class: OtfClass = Field(alias="class")
115
52
  is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio")
116
53
 
117
-
118
- class BookingList(OtfItemBase):
119
- bookings: list[Booking]
54
+ # unused fields
55
+ class_booking_id: int = Field(alias="classBookingId", exclude=True, repr=False, description="Not used by API")
56
+ class_id: int = Field(alias="classId", exclude=True, repr=False, description="Not used by API")
57
+ created_by: str = Field(alias="createdBy", exclude=True, repr=False)
58
+ mbo_class_id: int | None = Field(None, alias="mboClassId", exclude=True, repr=False, description="MindBody attr")
59
+ mbo_member_id: str | None = Field(None, alias="mboMemberId", exclude=True, repr=False, description="MindBody attr")
60
+ mbo_sync_message: str | None = Field(
61
+ None, alias="mboSyncMessage", exclude=True, repr=False, description="MindBody attr"
62
+ )
63
+ mbo_visit_id: int | None = Field(None, alias="mboVisitId", exclude=True, repr=False, description="MindBody attr")
64
+ mbo_waitlist_entry_id: int | None = Field(None, alias="mboWaitlistEntryId", exclude=True, repr=False)
65
+ member_id: int = Field(alias="memberId", exclude=True, repr=False, description="Not used by API")
66
+ studio_id: int = Field(alias="studioId", exclude=True, repr=False, description="Not used by API")
67
+ updated_by: str = Field(alias="updatedBy", exclude=True, repr=False)
68
+
69
+ @property
70
+ def studio_uuid(self) -> str:
71
+ """Shortcut to get the studio UUID"""
72
+ return self.otf_class.studio.studio_uuid
73
+
74
+ @property
75
+ def class_uuid(self) -> str:
76
+ """Shortcut to get the class UUID"""
77
+ return self.otf_class.class_uuid
78
+
79
+ @property
80
+ def starts_at(self) -> datetime:
81
+ """Shortcut to get the class start time"""
82
+ return self.otf_class.starts_at
83
+
84
+ @property
85
+ def ends_at(self) -> datetime:
86
+ """Shortcut to get the class end time"""
87
+ return self.otf_class.ends_at
88
+
89
+ def __str__(self) -> str:
90
+ starts_at_str = self.otf_class.starts_at.strftime("%a %b %d, %I:%M %p")
91
+ class_name = self.otf_class.name
92
+ coach_name = self.otf_class.coach.name
93
+ booked_str = self.status.value
94
+
95
+ return f"Booking: {starts_at_str} {class_name} - {coach_name} ({booked_str})"
@@ -1,38 +1,59 @@
1
+ """
2
+ These models represent the data returned by the challenge tracker endpoint.
3
+
4
+ I believe these are used by the app to populate the list of challenges, programs,
5
+ and benchmarks that a member can compete in.
6
+
7
+ The actual data for *your* participation in these challenges is returned by a different
8
+ endpoint.
9
+ """
10
+
1
11
  from pydantic import Field
2
12
 
3
13
  from otf_api.models.base import OtfItemBase
14
+ from otf_api.models.enums import ChallengeCategory, EquipmentType
4
15
 
5
16
 
6
17
  class Year(OtfItemBase):
7
- year: str = Field(..., alias="Year")
8
- is_participated: bool = Field(..., alias="IsParticipated")
9
- in_progress: bool = Field(..., alias="InProgress")
18
+ year: int | None = Field(None, alias="Year")
19
+ is_participated: bool | None = Field(None, alias="IsParticipated")
20
+ in_progress: bool | None = Field(None, alias="InProgress")
10
21
 
11
22
 
12
23
  class Program(OtfItemBase):
13
- challenge_category_id: int = Field(..., alias="ChallengeCategoryId")
14
- challenge_sub_category_id: int = Field(..., alias="ChallengeSubCategoryId")
15
- challenge_name: str = Field(..., alias="ChallengeName")
16
- years: list[Year] = Field(..., alias="Years")
17
- logo_url: str = Field(..., alias="LogoUrl")
24
+ """A program represents multi-day/week challenges that members can participate in."""
25
+
26
+ # NOTE: These ones do seem to match the ChallengeType enums in the OTF app.
27
+ # Leaving them as int for now though in case older data or other user's
28
+ # data doesn't match up.
29
+ challenge_category_id: int | None = Field(None, alias="ChallengeCategoryId")
30
+ challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId")
31
+ challenge_name: str | None = Field(None, alias="ChallengeName")
32
+ years: list[Year] = Field(default_factory=list, alias="Years")
18
33
 
19
34
 
20
35
  class Challenge(OtfItemBase):
21
- challenge_category_id: int = Field(..., alias="ChallengeCategoryId")
22
- challenge_sub_category_id: int = Field(..., alias="ChallengeSubCategoryId")
23
- challenge_name: str = Field(..., alias="ChallengeName")
24
- years: list[Year] = Field(..., alias="Years")
25
- logo_url: str = Field(..., alias="LogoUrl")
36
+ """A challenge represents a single day or event that members can participate in."""
37
+
38
+ # NOTE: The challenge category/subcategory ids here do not seem to be at
39
+ # all related to the ChallengeType enums or the few SubCategory enums I've
40
+ # been able to puzzle out. I haven't been able to link them to any code
41
+ # in the OTF app. Due to that, they are being excluded from the model for now.
42
+ challenge_category_id: ChallengeCategory | None = Field(None, alias="ChallengeCategoryId")
43
+ challenge_sub_category_id: int | None = Field(None, alias="ChallengeSubCategoryId")
44
+ challenge_name: str | None = Field(None, alias="ChallengeName")
45
+ years: list[Year] = Field(default_factory=list, alias="Years")
26
46
 
27
47
 
28
48
  class Benchmark(OtfItemBase):
29
- equipment_id: int = Field(..., alias="EquipmentId")
30
- equipment_name: str = Field(..., alias="EquipmentName")
31
- years: list[Year] = Field(..., alias="Years")
32
- logo_url: str = Field(..., alias="LogoUrl")
49
+ """A benchmark represents a specific workout that members can participate in."""
50
+
51
+ equipment_id: EquipmentType | None = Field(None, alias="EquipmentId")
52
+ equipment_name: str | None = Field(None, alias="EquipmentName")
53
+ years: list[Year] = Field(default_factory=list, alias="Years")
33
54
 
34
55
 
35
- class ChallengeTrackerContent(OtfItemBase):
36
- programs: list[Program] = Field(..., alias="Programs")
37
- challenges: list[Challenge] = Field(..., alias="Challenges")
38
- benchmarks: list[Benchmark] = Field(..., alias="Benchmarks")
56
+ class ChallengeTracker(OtfItemBase):
57
+ programs: list[Program] = Field(default_factory=list, alias="Programs")
58
+ challenges: list[Challenge] = Field(default_factory=list, alias="Challenges")
59
+ benchmarks: list[Benchmark] = Field(default_factory=list, alias="Benchmarks")