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
@@ -1,31 +1,35 @@
1
- from .body_composition_list import BodyCompositionData
2
- from .bookings import Booking
3
- from .bookings_v2 import BookingV2, BookingV2Class
4
- from .challenge_tracker_content import ChallengeTracker
5
- from .challenge_tracker_detail import FitnessBenchmark
6
- from .classes import OtfClass
7
- from .enums import (
1
+ from .bookings import (
8
2
  HISTORICAL_BOOKING_STATUSES,
3
+ Booking,
9
4
  BookingStatus,
10
- ChallengeCategory,
5
+ BookingV2,
6
+ BookingV2Class,
11
7
  ClassType,
12
8
  DoW,
9
+ OtfClass,
10
+ get_class_rating_value,
11
+ get_coach_rating_value,
12
+ )
13
+ from .members import MemberDetail, MemberMembership, MemberPurchase
14
+ from .members.notifications import EmailNotificationSettings, SmsNotificationSettings
15
+ from .studios import StudioDetail, StudioService, StudioStatus
16
+ from .workouts import (
17
+ BodyCompositionData,
18
+ ChallengeCategory,
19
+ ChallengeTracker,
13
20
  EquipmentType,
21
+ FitnessBenchmark,
22
+ InStudioStatsData,
23
+ OutOfStudioWorkoutHistory,
24
+ OutStudioStatsData,
25
+ PerformanceSummary,
26
+ StatsResponse,
14
27
  StatsTime,
15
- StudioStatus,
28
+ Telemetry,
29
+ TelemetryHistoryItem,
30
+ TimeStats,
31
+ Workout,
16
32
  )
17
- from .lifetime_stats import InStudioStatsData, OutStudioStatsData, StatsResponse, TimeStats
18
- from .member_detail import MemberDetail
19
- from .member_membership import MemberMembership
20
- from .member_purchases import MemberPurchase
21
- from .notifications import EmailNotificationSettings, SmsNotificationSettings
22
- from .out_of_studio_workout_history import OutOfStudioWorkoutHistory
23
- from .performance_summary import PerformanceSummary
24
- from .ratings import get_class_rating_value, get_coach_rating_value
25
- from .studio_detail import StudioDetail
26
- from .studio_services import StudioService
27
- from .telemetry import Telemetry, TelemetryHistoryItem
28
- from .workout import Workout
29
33
 
30
34
  __all__ = [
31
35
  "HISTORICAL_BOOKING_STATUSES",
@@ -0,0 +1,23 @@
1
+ from .bookings import Booking
2
+ from .bookings_v2 import BookingV2, BookingV2Class, BookingV2Studio, BookingV2Workout, Rating
3
+ from .classes import OtfClass
4
+ from .enums import HISTORICAL_BOOKING_STATUSES, BookingStatus, ClassType, DoW
5
+ from .filters import ClassFilter
6
+ from .ratings import get_class_rating_value, get_coach_rating_value
7
+
8
+ __all__ = [
9
+ "HISTORICAL_BOOKING_STATUSES",
10
+ "Booking",
11
+ "BookingStatus",
12
+ "BookingV2",
13
+ "BookingV2Class",
14
+ "BookingV2Studio",
15
+ "BookingV2Workout",
16
+ "ClassFilter",
17
+ "ClassType",
18
+ "DoW",
19
+ "OtfClass",
20
+ "Rating",
21
+ "get_class_rating_value",
22
+ "get_coach_rating_value",
23
+ ]
@@ -0,0 +1,134 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import Field
4
+
5
+ from otf_api.models.base import OtfItemBase
6
+ from otf_api.models.mixins import ApiMixin
7
+ from otf_api.models.studios import StudioDetail
8
+
9
+ from .enums import BookingStatus
10
+
11
+
12
+ class Coach(OtfItemBase):
13
+ coach_uuid: str = Field(validation_alias="coachUUId")
14
+ first_name: str | None = Field(None, validation_alias="firstName")
15
+ last_name: str | None = Field(None, validation_alias="lastName")
16
+
17
+ # unused fields
18
+ name: str = Field(exclude=True, repr=False)
19
+
20
+ @property
21
+ def full_name(self) -> str:
22
+ """Returns the full name of the coach."""
23
+ return f"{self.first_name} {self.last_name}"
24
+
25
+
26
+ class OtfClass(OtfItemBase):
27
+ class_uuid: str = Field(validation_alias="classUUId")
28
+ name: str
29
+ starts_at: datetime = Field(validation_alias="startDateTime", description="Start time in local timezone")
30
+ ends_at: datetime = Field(validation_alias="endDateTime", description="End time in local timezone")
31
+ is_available: bool = Field(validation_alias="isAvailable")
32
+ is_cancelled: bool = Field(validation_alias="isCancelled")
33
+ studio: StudioDetail
34
+ coach: Coach
35
+
36
+ # unused fields
37
+ coach_id: int | None = Field(
38
+ None, validation_alias="coachId", exclude=True, repr=False, description="Not used by API"
39
+ )
40
+ description: str | None = Field(None, exclude=True, repr=False)
41
+ program_name: str | None = Field(None, validation_alias="programName", exclude=True, repr=False)
42
+ virtual_class: bool | None = Field(None, validation_alias="virtualClass", exclude=True, repr=False)
43
+
44
+ @property
45
+ def coach_name(self) -> str:
46
+ """Shortcut to get the coach's name, to be compatible with new BookingV2Class."""
47
+ return self.coach.first_name or ""
48
+
49
+ def __str__(self) -> str:
50
+ """Returns a string representation of the class."""
51
+ starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
52
+ return f"Class: {starts_at_str} {self.name} - {self.coach.first_name}"
53
+
54
+
55
+ class Booking(ApiMixin, OtfItemBase):
56
+ booking_uuid: str = Field(validation_alias="classBookingUUId", description="ID used to cancel the booking")
57
+ is_intro: bool = Field(validation_alias="isIntro")
58
+ status: BookingStatus
59
+ booked_date: datetime | None = Field(None, validation_alias="bookedDate")
60
+ checked_in_date: datetime | None = Field(None, validation_alias="checkedInDate")
61
+ cancelled_date: datetime | None = Field(None, validation_alias="cancelledDate")
62
+ created_date: datetime = Field(validation_alias="createdDate")
63
+ updated_date: datetime = Field(validation_alias="updatedDate")
64
+ is_deleted: bool = Field(validation_alias="isDeleted")
65
+ waitlist_position: int | None = Field(None, validation_alias="waitlistPosition")
66
+ otf_class: OtfClass = Field(validation_alias="class")
67
+ is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio")
68
+
69
+ # unused fields
70
+ class_booking_id: int = Field(
71
+ validation_alias="classBookingId", exclude=True, repr=False, description="Not used by API"
72
+ )
73
+ class_id: int = Field(validation_alias="classId", exclude=True, repr=False, description="Not used by API")
74
+ created_by: str = Field(validation_alias="createdBy", exclude=True, repr=False)
75
+ mbo_class_id: int | None = Field(
76
+ None, validation_alias="mboClassId", exclude=True, repr=False, description="MindBody attr"
77
+ )
78
+ mbo_member_id: str | None = Field(
79
+ None, validation_alias="mboMemberId", exclude=True, repr=False, description="MindBody attr"
80
+ )
81
+ mbo_sync_message: str | None = Field(
82
+ None, validation_alias="mboSyncMessage", exclude=True, repr=False, description="MindBody attr"
83
+ )
84
+ mbo_visit_id: int | None = Field(
85
+ None, validation_alias="mboVisitId", exclude=True, repr=False, description="MindBody attr"
86
+ )
87
+ mbo_waitlist_entry_id: int | None = Field(None, validation_alias="mboWaitlistEntryId", exclude=True, repr=False)
88
+ member_id: int = Field(validation_alias="memberId", exclude=True, repr=False, description="Not used by API")
89
+ studio_id: int = Field(validation_alias="studioId", exclude=True, repr=False, description="Not used by API")
90
+ updated_by: str = Field(validation_alias="updatedBy", exclude=True, repr=False)
91
+
92
+ @property
93
+ def studio_uuid(self) -> str:
94
+ """Shortcut to get the studio UUID."""
95
+ return self.otf_class.studio.studio_uuid
96
+
97
+ @property
98
+ def class_uuid(self) -> str:
99
+ """Shortcut to get the class UUID."""
100
+ return self.otf_class.class_uuid
101
+
102
+ @property
103
+ def starts_at(self) -> datetime:
104
+ """Shortcut to get the class start time."""
105
+ return self.otf_class.starts_at
106
+
107
+ @property
108
+ def ends_at(self) -> datetime:
109
+ """Shortcut to get the class end time."""
110
+ return self.otf_class.ends_at
111
+
112
+ @property
113
+ def id_value(self) -> str:
114
+ """Returns the booking_uuid, to be compatible with new BookingV2 model."""
115
+ return self.booking_uuid
116
+
117
+ def __str__(self) -> str:
118
+ """Returns a string representation of the booking."""
119
+ starts_at_str = self.otf_class.starts_at.strftime("%a %b %d, %I:%M %p")
120
+ class_name = self.otf_class.name
121
+ coach_name = self.otf_class.coach.name
122
+ booked_str = self.status.value
123
+
124
+ return f"Booking: {starts_at_str} {class_name} - {coach_name} ({booked_str})"
125
+
126
+ def cancel(self) -> None:
127
+ """Cancels the booking by calling the proper API method.
128
+
129
+ Raises:
130
+ ValueError: If the API instance is not set.
131
+ """
132
+ self.raise_if_api_not_set()
133
+
134
+ self._api.bookings.cancel_booking(self)
@@ -5,9 +5,10 @@ import pendulum
5
5
  from pydantic import AliasPath, Field
6
6
 
7
7
  from otf_api.models.base import OtfItemBase
8
- from otf_api.models.enums import BookingStatus, ClassType
9
- from otf_api.models.mixins import AddressMixin, PhoneLongitudeLatitudeMixin
10
- from otf_api.models.performance_summary import ZoneTimeMinutes
8
+ from otf_api.models.mixins import AddressMixin, ApiMixin, PhoneLongitudeLatitudeMixin
9
+
10
+ # from otf_api.models.performance_summary import ZoneTimeMinutes
11
+ from .enums import BookingStatus, ClassType
11
12
 
12
13
  LOGGER = getLogger(__name__)
13
14
 
@@ -16,10 +17,7 @@ class Address(AddressMixin, OtfItemBase): ...
16
17
 
17
18
 
18
19
  def get_end_time(start_time: datetime, class_type: ClassType) -> datetime:
19
- """
20
- Get the end time of a class based on the start time and class type.
21
- """
22
-
20
+ """Get the end time of a class based on the start time and class type."""
23
21
  start_time = pendulum.instance(start_time)
24
22
 
25
23
  match class_type:
@@ -46,7 +44,7 @@ class Rating(OtfItemBase):
46
44
 
47
45
 
48
46
  class BookingV2Studio(PhoneLongitudeLatitudeMixin, OtfItemBase):
49
- studio_uuid: str = Field(alias="id")
47
+ studio_uuid: str = Field(validation_alias="id")
50
48
  name: str | None = None
51
49
  time_zone: str | None = None
52
50
  email: str | None = None
@@ -56,53 +54,85 @@ class BookingV2Studio(PhoneLongitudeLatitudeMixin, OtfItemBase):
56
54
  mbo_studio_id: str | None = Field(None, description="MindBody attr", repr=False, exclude=True)
57
55
 
58
56
 
59
- class BookingV2Class(OtfItemBase):
60
- class_id: str = Field(alias="id", description="Matches the `class_id` attribute of the OtfClass model")
57
+ class BookingV2Class(ApiMixin, OtfItemBase):
58
+ class_id: str = Field(validation_alias="id", description="Matches the `class_id` attribute of the OtfClass model")
61
59
  name: str
62
- class_type: ClassType = Field(alias="type")
60
+ class_type: ClassType = Field(validation_alias="type")
63
61
  starts_at: datetime = Field(
64
- alias="starts_at_local",
62
+ validation_alias="starts_at_local",
65
63
  description="The start time of the class. Reflects local time, but the object does not have a timezone.",
66
64
  )
67
65
  studio: BookingV2Studio | None = None
68
66
  coach: str | None = Field(None, validation_alias=AliasPath("coach", "first_name"))
69
67
 
70
68
  class_uuid: str | None = Field(
71
- None, alias="ot_base_class_uuid", description="Only present when class is ratable", exclude=True, repr=False
69
+ None,
70
+ validation_alias="ot_base_class_uuid",
71
+ description="Only present when class is ratable",
72
+ exclude=True,
73
+ repr=False,
72
74
  )
73
- starts_at_utc: datetime | None = Field(None, alias="starts_at", exclude=True, repr=False)
75
+ starts_at_utc: datetime | None = Field(None, validation_alias="starts_at", exclude=True, repr=False)
74
76
 
75
77
  @property
76
78
  def coach_name(self) -> str:
77
- """Shortcut to get the coach's name, to be compatible with old Booking OtfClass model"""
79
+ """Shortcut to get the coach's name, to be compatible with old Booking OtfClass model."""
78
80
  return self.coach or ""
79
81
 
80
82
  @property
81
83
  def ends_at(self) -> datetime:
82
- """Emulates the end time of the class, to be compatible with old Booking OtfClass model"""
84
+ """Emulates the end time of the class, to be compatible with old Booking OtfClass model."""
83
85
  return get_end_time(self.starts_at, self.class_type)
84
86
 
85
87
  def __str__(self) -> str:
88
+ """Returns a string representation of the class."""
86
89
  starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
87
90
  return f"Class: {starts_at_str} {self.name} - {self.coach}"
88
91
 
92
+ def get_booking(self) -> "BookingV2":
93
+ """Returns a BookingV2 instance for this class.
94
+
95
+ Raises:
96
+ BookingNotFoundError: If the booking does not exist.
97
+ ValueError: If class_uuid is None or empty string or if the API instance is not set.
98
+ """
99
+ self.raise_if_api_not_set()
100
+
101
+ if not self.class_uuid:
102
+ raise ValueError("class_uuid is required to get the booking")
103
+
104
+ return self._api.bookings.get_booking_from_class_new(self)
105
+
106
+ def cancel_booking(self) -> None:
107
+ """Cancels the booking by calling the proper API method.
108
+
109
+ Raises:
110
+ BookingNotFoundError: If the booking does not exist.
111
+ ValueError: If class_uuid is None or empty string or if the API instance is not set.
112
+ """
113
+ self.raise_if_api_not_set()
114
+
115
+ self.get_booking().cancel()
116
+
89
117
 
90
118
  class BookingV2Workout(OtfItemBase):
91
119
  id: str
92
- performance_summary_id: str = Field(..., alias="id", description="Alias to id, to simplify the API")
120
+ performance_summary_id: str = Field(..., validation_alias="id", description="Alias to id, to simplify the API")
93
121
  calories_burned: int
94
122
  splat_points: int
95
123
  step_count: int
96
124
  active_time_seconds: int
97
- zone_time_minutes: ZoneTimeMinutes
125
+ # zone_time_minutes: ZoneTimeMinutes
98
126
 
99
127
 
100
- class BookingV2(OtfItemBase):
128
+ class BookingV2(ApiMixin, OtfItemBase):
101
129
  booking_id: str = Field(
102
- ..., alias="id", description="The booking ID used to cancel the booking - must be canceled through new endpoint"
130
+ ...,
131
+ validation_alias="id",
132
+ description="The booking ID used to cancel the booking - must be canceled through new endpoint",
103
133
  )
104
134
 
105
- member_uuid: str = Field(..., alias="member_id")
135
+ member_uuid: str = Field(..., validation_alias="member_id")
106
136
  service_name: str | None = Field(None, description="Represents tier of member")
107
137
 
108
138
  cross_regional: bool | None = None
@@ -113,7 +143,7 @@ class BookingV2(OtfItemBase):
113
143
  canceled_at: datetime | None = None
114
144
  ratable: bool
115
145
 
116
- otf_class: BookingV2Class = Field(..., alias="class")
146
+ otf_class: BookingV2Class = Field(..., validation_alias="class")
117
147
  workout: BookingV2Workout | None = None
118
148
  coach_rating: Rating | None = Field(None, validation_alias=AliasPath("ratings", "coach"))
119
149
  class_rating: Rating | None = Field(None, validation_alias=AliasPath("ratings", "class"))
@@ -130,13 +160,13 @@ class BookingV2(OtfItemBase):
130
160
  exclude=True,
131
161
  repr=False,
132
162
  )
133
- updated_at: datetime | None = Field(
134
- None, description="Date the booking was updated, not when the booking was made", exclude=True, repr=False
163
+ updated_at: datetime = Field(
164
+ ..., description="Date the booking was updated, not when the booking was made", exclude=True, repr=False
135
165
  )
136
166
 
137
167
  @property
138
168
  def status(self) -> BookingStatus:
139
- """Emulates the booking status from the old API, but with less specificity"""
169
+ """Emulates the booking status from the old API, but with less specificity."""
140
170
  if self.late_canceled:
141
171
  return BookingStatus.LateCancelled
142
172
 
@@ -150,42 +180,53 @@ class BookingV2(OtfItemBase):
150
180
 
151
181
  @property
152
182
  def studio_uuid(self) -> str:
153
- """Shortcut to get the studio UUID"""
183
+ """Shortcut to get the studio UUID."""
154
184
  if self.otf_class.studio is None:
155
185
  return ""
156
186
  return self.otf_class.studio.studio_uuid
157
187
 
158
188
  @property
159
189
  def class_uuid(self) -> str:
160
- """Shortcut to get the class UUID"""
190
+ """Shortcut to get the class UUID."""
161
191
  if self.otf_class.class_uuid is None:
162
192
  return ""
163
193
  return self.otf_class.class_uuid
164
194
 
165
195
  @property
166
196
  def starts_at(self) -> datetime:
167
- """Shortcut to get the class start time"""
197
+ """Shortcut to get the class start time."""
168
198
  return self.otf_class.starts_at
169
199
 
170
200
  @property
171
201
  def ends_at(self) -> datetime:
172
- """Shortcut to get the class end time"""
202
+ """Shortcut to get the class end time."""
173
203
  return self.otf_class.ends_at
174
204
 
175
205
  @property
176
206
  def cancelled_date(self) -> datetime | None:
177
- """Returns the canceled_at value in a backward-compatible way"""
207
+ """Returns the canceled_at value in a backward-compatible way."""
178
208
  return self.canceled_at
179
209
 
180
210
  @property
181
211
  def id_value(self) -> str:
182
- """Returns the booking_id, to be compatible with old Booking model"""
212
+ """Returns the booking_id, to be compatible with old Booking model."""
183
213
  return self.booking_id
184
214
 
185
215
  def __str__(self) -> str:
216
+ """Returns a string representation of the booking."""
186
217
  starts_at_str = self.otf_class.starts_at.strftime("%a %b %d, %I:%M %p")
187
218
  class_name = self.otf_class.name
188
219
  coach_name = self.otf_class.coach
189
220
  booked_str = self.status.value
190
221
 
191
222
  return f"Booking: {starts_at_str} {class_name} - {coach_name} ({booked_str})"
223
+
224
+ def cancel(self) -> None:
225
+ """Cancels the booking by calling the proper API method.
226
+
227
+ Raises:
228
+ ValueError: If the API instance is not set.
229
+ """
230
+ self.raise_if_api_not_set()
231
+
232
+ self._api.bookings.cancel_booking_new(self)
@@ -0,0 +1,124 @@
1
+ import typing
2
+ from datetime import datetime
3
+
4
+ from pydantic import AliasPath, Field
5
+
6
+ from otf_api import exceptions as exc
7
+ from otf_api.models.base import OtfItemBase
8
+ from otf_api.models.mixins import ApiMixin
9
+ from otf_api.models.studios import StudioDetail
10
+
11
+ from .enums import ClassType, DoW
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from otf_api.models.bookings import Booking, BookingV2
15
+
16
+
17
+ class OtfClass(ApiMixin, OtfItemBase):
18
+ class_uuid: str = Field(validation_alias="ot_base_class_uuid", description="The OTF class UUID")
19
+ class_id: str | None = Field(None, validation_alias="id", description="Matches new booking endpoint class id")
20
+
21
+ name: str | None = Field(None, description="The name of the class")
22
+ class_type: ClassType = Field(validation_alias="type")
23
+ coach: str | None = Field(None, validation_alias=AliasPath("coach", "first_name"))
24
+ ends_at: datetime = Field(
25
+ validation_alias="ends_at_local",
26
+ description="The end time of the class. Reflects local time, but the object does not have a timezone.",
27
+ )
28
+ starts_at: datetime = Field(
29
+ validation_alias="starts_at_local",
30
+ description="The start time of the class. Reflects local time, but the object does not have a timezone.",
31
+ )
32
+ studio: StudioDetail
33
+
34
+ # capacity/status fields
35
+ booking_capacity: int | None = None
36
+ full: bool | None = None
37
+ max_capacity: int | None = None
38
+ waitlist_available: bool | None = None
39
+ waitlist_size: int | None = Field(None, description="The number of people on the waitlist")
40
+ is_booked: bool | None = Field(None, description="Custom helper field to determine if class is already booked")
41
+ is_cancelled: bool | None = Field(None, validation_alias="canceled")
42
+ is_home_studio: bool | None = Field(None, description="Custom helper field to determine if at home studio")
43
+
44
+ created_at: datetime | None = Field(None, exclude=True, repr=False)
45
+ ends_at_utc: datetime | None = Field(None, validation_alias="ends_at", exclude=True, repr=False)
46
+ mbo_class_description_id: str | None = Field(None, exclude=True, repr=False, description="MindBody attr")
47
+ mbo_class_id: str | None = Field(None, exclude=True, repr=False, description="MindBody attr")
48
+ mbo_class_schedule_id: str | None = Field(None, exclude=True, repr=False, description="MindBody attr")
49
+ starts_at_utc: datetime | None = Field(None, validation_alias="starts_at", exclude=True, repr=False)
50
+ updated_at: datetime | None = Field(None, exclude=True, repr=False)
51
+
52
+ @property
53
+ def day_of_week(self) -> DoW:
54
+ """Returns the day of the week as an enum."""
55
+ dow = self.starts_at.strftime("%A")
56
+ return DoW(dow)
57
+
58
+ def __str__(self) -> str:
59
+ """Returns a string representation of the class."""
60
+ starts_at_str = self.starts_at.strftime("%a %b %d, %I:%M %p")
61
+ booked_str = ""
62
+ if self.is_booked:
63
+ booked_str = "Booked"
64
+ elif self.has_availability:
65
+ booked_str = "Available"
66
+ elif self.waitlist_available:
67
+ booked_str = "Waitlist Available"
68
+ else:
69
+ booked_str = "Full"
70
+ return f"Class: {starts_at_str} {self.name} - {self.coach} ({booked_str})"
71
+
72
+ @property
73
+ def has_availability(self) -> bool:
74
+ """Represents if the class has availability."""
75
+ return not self.full
76
+
77
+ @property
78
+ def day_of_week_enum(self) -> DoW:
79
+ """Returns the day of the week as an enum."""
80
+ dow = self.starts_at.strftime("%A").upper()
81
+ return DoW(dow)
82
+
83
+ def book_class(self) -> "Booking":
84
+ """Book a class by providing either the class_uuid or the OtfClass object.
85
+
86
+ Returns:
87
+ Booking: The booking.
88
+
89
+ Raises:
90
+ AlreadyBookedError: If the class is already booked.
91
+ OutsideSchedulingWindowError: If the class is outside the scheduling window.
92
+ ValueError: If class_uuid is None or empty string.
93
+ OtfException: If there is an error booking the class.
94
+ """
95
+ self.raise_if_api_not_set()
96
+ new_booking = self._api.bookings.book_class(self.class_uuid)
97
+ self.is_booked = True
98
+ return new_booking
99
+
100
+ def cancel_booking(self) -> None:
101
+ """Cancels the class booking.
102
+
103
+ Raises:
104
+ BookingNotFoundError: If the booking does not exist.
105
+ ValueError: If booking_uuid is None or empty string or the API is not set.
106
+ """
107
+ self.raise_if_api_not_set()
108
+ self.get_booking().cancel()
109
+
110
+ def get_booking(self) -> "Booking | BookingV2":
111
+ """Get the booking for this class.
112
+
113
+ Returns:
114
+ Booking | BookingV2: The booking associated with this class.
115
+
116
+ Raises:
117
+ BookingNotFoundError: If the booking does not exist.
118
+ ValueError: If the API is not set.
119
+ """
120
+ self.raise_if_api_not_set()
121
+ try:
122
+ return self._api.bookings.get_booking_from_class(self)
123
+ except exc.BookingNotFoundError:
124
+ return self._api.bookings.get_booking_from_class_new(self)
@@ -1,14 +1,5 @@
1
- from enum import IntEnum, StrEnum
2
-
3
-
4
- class StudioStatus(StrEnum):
5
- OTHER = "OTHER"
6
- ACTIVE = "Active"
7
- INACTIVE = "Inactive"
8
- COMING_SOON = "Coming Soon"
9
- TEMP_CLOSED = "Temporarily Closed"
10
- PERM_CLOSED = "Permanently Closed"
11
- UNKNOWN = "Unknown"
1
+ from enum import StrEnum
2
+ from typing import Self
12
3
 
13
4
 
14
5
  class BookingStatus(StrEnum):
@@ -98,84 +89,19 @@ class ClassType(StrEnum):
98
89
  TREAD_50 = "TREAD_50"
99
90
 
100
91
  @classmethod
101
- def get_case_insensitive(cls, value: str) -> str:
92
+ def get_case_insensitive(cls, value: str) -> "Self":
93
+ """Returns the actual value of the enum, regardless of case."""
102
94
  value = (value or "").strip()
103
95
  value = value.replace(" ", "_")
104
- lcase_to_actual = {item.value.lower(): item.value for item in cls}
96
+ lcase_to_actual = {item.value.lower(): item for item in cls}
105
97
  return lcase_to_actual[value.lower()]
106
98
 
107
99
  @staticmethod
108
100
  def get_standard_class_types() -> list["ClassType"]:
109
- """Returns 2G/3G/Tornado - 60/90 minute classes"""
101
+ """Returns 2G/3G/Tornado - 60/90 minute classes."""
110
102
  return [ClassType.ORANGE_60, ClassType.ORANGE_90]
111
103
 
112
104
  @staticmethod
113
105
  def get_tread_strength_class_types() -> list["ClassType"]:
114
- """Returns Tread/Strength 50 minute classes"""
106
+ """Returns Tread/Strength 50 minute classes."""
115
107
  return [ClassType.TREAD_50, ClassType.STRENGTH_50]
116
-
117
-
118
- class StatsTime(StrEnum):
119
- LastYear = "lastYear"
120
- ThisYear = "thisYear"
121
- LastMonth = "lastMonth"
122
- ThisMonth = "thisMonth"
123
- LastWeek = "lastWeek"
124
- ThisWeek = "thisWeek"
125
- AllTime = "allTime"
126
-
127
-
128
- class EquipmentType(IntEnum):
129
- Treadmill = 2
130
- Strider = 3
131
- Rower = 4
132
- Bike = 5
133
- WeightFloor = 6
134
- PowerWalker = 7
135
-
136
-
137
- class ChallengeCategory(IntEnum):
138
- Other = 0
139
- DriTri = 2
140
- Infinity = 3
141
- MarathonMonth = 5
142
- OrangeEverest = 9
143
- CatchMeIfYouCan = 10
144
- TwoHundredMeterRow = 15
145
- FiveHundredMeterRow = 16
146
- TwoThousandMeterRow = 17
147
- TwelveMinuteTreadmill = 18
148
- OneMileTreadmill = 19
149
- TenMinuteRow = 20
150
- HellWeek = 52
151
- Inferno = 55
152
- Mayhem = 58
153
- BackAtIt = 60
154
- FourteenMinuteRow = 61
155
- TwelveDaysOfFitness = 63
156
- TransformationChallenge = 64
157
- RemixInSix = 65
158
- Push = 66
159
- QuarterMileTreadmill = 69
160
- OneThousandMeterRow = 70
161
-
162
-
163
- class DriTriChallengeSubCategory(IntEnum):
164
- FullRun = 1
165
- SprintRun = 3
166
- Relay = 4
167
- StrengthRun = 1500
168
-
169
-
170
- class MarathonMonthChallengeSubCategory(IntEnum):
171
- Original = 1
172
- Full = 14
173
- Half = 15
174
- Ultra = 16
175
-
176
-
177
- # only Other, DriTri, and MarathonMonth have subcategories
178
-
179
- # BackAtIt and Transformation are multi-week challenges
180
-
181
- # RemixInSix, Mayhem, HellWeek, Push, and TwelveDaysOfFitness are multi-day challenges