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.
- otf_api/__init__.py +35 -3
- otf_api/api/__init__.py +3 -0
- otf_api/api/_compat.py +77 -0
- otf_api/api/api.py +80 -0
- otf_api/api/bookings/__init__.py +3 -0
- otf_api/api/bookings/booking_api.py +541 -0
- otf_api/api/bookings/booking_client.py +112 -0
- otf_api/api/client.py +203 -0
- otf_api/api/members/__init__.py +3 -0
- otf_api/api/members/member_api.py +187 -0
- otf_api/api/members/member_client.py +112 -0
- otf_api/api/studios/__init__.py +3 -0
- otf_api/api/studios/studio_api.py +173 -0
- otf_api/api/studios/studio_client.py +120 -0
- otf_api/api/utils.py +307 -0
- otf_api/api/workouts/__init__.py +3 -0
- otf_api/api/workouts/workout_api.py +333 -0
- otf_api/api/workouts/workout_client.py +140 -0
- otf_api/auth/__init__.py +1 -1
- otf_api/auth/auth.py +155 -89
- otf_api/auth/user.py +5 -17
- otf_api/auth/utils.py +27 -2
- otf_api/cache.py +132 -0
- otf_api/exceptions.py +18 -6
- otf_api/models/__init__.py +25 -21
- otf_api/models/bookings/__init__.py +23 -0
- otf_api/models/bookings/bookings.py +134 -0
- otf_api/models/{bookings_v2.py → bookings/bookings_v2.py} +72 -31
- otf_api/models/bookings/classes.py +124 -0
- otf_api/models/{enums.py → bookings/enums.py} +7 -81
- otf_api/{filters.py → models/bookings/filters.py} +39 -11
- otf_api/models/{ratings.py → bookings/ratings.py} +2 -6
- otf_api/models/members/__init__.py +5 -0
- otf_api/models/members/member_detail.py +149 -0
- otf_api/models/members/member_membership.py +26 -0
- otf_api/models/members/member_purchases.py +29 -0
- otf_api/models/members/notifications.py +17 -0
- otf_api/models/mixins.py +48 -1
- otf_api/models/studios/__init__.py +5 -0
- otf_api/models/studios/enums.py +11 -0
- otf_api/models/studios/studio_detail.py +93 -0
- otf_api/models/studios/studio_services.py +36 -0
- otf_api/models/workouts/__init__.py +31 -0
- otf_api/models/{body_composition_list.py → workouts/body_composition_list.py} +140 -71
- otf_api/models/workouts/challenge_tracker_content.py +50 -0
- otf_api/models/workouts/challenge_tracker_detail.py +99 -0
- otf_api/models/workouts/enums.py +70 -0
- otf_api/models/workouts/lifetime_stats.py +96 -0
- otf_api/models/workouts/out_of_studio_workout_history.py +32 -0
- otf_api/models/{performance_summary.py → workouts/performance_summary.py} +19 -5
- otf_api/models/workouts/telemetry.py +88 -0
- otf_api/models/{workout.py → workouts/workout.py} +34 -20
- {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/METADATA +4 -2
- otf_api-0.13.0.dist-info/RECORD +59 -0
- {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/WHEEL +1 -1
- otf_api/api.py +0 -1682
- otf_api/logging.py +0 -19
- otf_api/models/bookings.py +0 -109
- otf_api/models/challenge_tracker_content.py +0 -59
- otf_api/models/challenge_tracker_detail.py +0 -88
- otf_api/models/classes.py +0 -70
- otf_api/models/lifetime_stats.py +0 -78
- otf_api/models/member_detail.py +0 -121
- otf_api/models/member_membership.py +0 -26
- otf_api/models/member_purchases.py +0 -29
- otf_api/models/notifications.py +0 -17
- otf_api/models/out_of_studio_workout_history.py +0 -32
- otf_api/models/studio_detail.py +0 -71
- otf_api/models/studio_services.py +0 -36
- otf_api/models/telemetry.py +0 -84
- otf_api/utils.py +0 -164
- otf_api-0.12.0.dist-info/RECORD +0 -38
- {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {otf_api-0.12.0.dist-info → otf_api-0.13.0.dist-info}/top_level.txt +0 -0
otf_api/models/__init__.py
CHANGED
@@ -1,31 +1,35 @@
|
|
1
|
-
from .
|
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
|
-
|
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
|
-
|
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.
|
9
|
-
|
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(
|
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(
|
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(
|
60
|
+
class_type: ClassType = Field(validation_alias="type")
|
63
61
|
starts_at: datetime = Field(
|
64
|
-
|
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,
|
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,
|
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(...,
|
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
|
-
...,
|
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(...,
|
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(...,
|
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
|
134
|
-
|
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
|
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) ->
|
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
|
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
|