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
@@ -1,8 +1,9 @@
|
|
1
1
|
from datetime import date, datetime, time
|
2
2
|
|
3
|
+
import pendulum
|
3
4
|
from pydantic import BaseModel, field_validator
|
4
5
|
|
5
|
-
from otf_api.models import ClassType, DoW, OtfClass
|
6
|
+
from otf_api.models.bookings import ClassType, DoW, OtfClass
|
6
7
|
|
7
8
|
|
8
9
|
class ClassFilter(BaseModel):
|
@@ -28,11 +29,11 @@ class ClassFilter(BaseModel):
|
|
28
29
|
start_time (list[time]): Filter classes by start time.
|
29
30
|
"""
|
30
31
|
|
31
|
-
start_date: date | None = None
|
32
|
-
end_date: date | None = None
|
33
|
-
class_type: list[ClassType] | None = None
|
34
|
-
day_of_week: list[DoW] | None = None
|
35
|
-
start_time: list[time] | None = None
|
32
|
+
start_date: date | str | None = None
|
33
|
+
end_date: date | str | None = None
|
34
|
+
class_type: list[ClassType] | ClassType | None = None
|
35
|
+
day_of_week: list[DoW] | DoW | None = None
|
36
|
+
start_time: list[time] | time | None = None
|
36
37
|
|
37
38
|
def filter_classes(self, classes: list[OtfClass]) -> list[OtfClass]:
|
38
39
|
"""Filters a list of classes based on the filter arguments.
|
@@ -51,9 +52,11 @@ class ClassFilter(BaseModel):
|
|
51
52
|
self.end_date = self.end_date.date()
|
52
53
|
|
53
54
|
if self.start_date:
|
55
|
+
assert isinstance(self.start_date, date), "start_date must be a date object"
|
54
56
|
classes = [c for c in classes if c.starts_at.date() >= self.start_date]
|
55
57
|
|
56
58
|
if self.end_date:
|
59
|
+
assert isinstance(self.end_date, date), "end_date must be a date object"
|
57
60
|
classes = [c for c in classes if c.starts_at.date() <= self.end_date]
|
58
61
|
|
59
62
|
if self.class_type:
|
@@ -63,35 +66,60 @@ class ClassFilter(BaseModel):
|
|
63
66
|
classes = [c for c in classes if c.day_of_week in self.day_of_week]
|
64
67
|
|
65
68
|
if self.start_time:
|
69
|
+
assert isinstance(self.start_time, list), "start_time must be a list of time objects"
|
66
70
|
classes = [c for c in classes if c.starts_at.time() in self.start_time]
|
67
71
|
|
68
72
|
return classes
|
69
73
|
|
70
74
|
@field_validator("class_type", "day_of_week", "start_time", mode="before")
|
71
75
|
@classmethod
|
72
|
-
def _single_item_to_list(cls, v):
|
76
|
+
def _single_item_to_list(cls, v: str | list | None) -> list | None:
|
73
77
|
if v and not isinstance(v, list):
|
74
78
|
return [v]
|
79
|
+
|
80
|
+
if not v:
|
81
|
+
return None
|
82
|
+
|
75
83
|
return v
|
76
84
|
|
77
85
|
@field_validator("day_of_week", mode="before")
|
78
86
|
@classmethod
|
79
|
-
def _day_of_week_str_to_enum(cls, v):
|
87
|
+
def _day_of_week_str_to_enum(cls, v: str | list[str] | None) -> list[DoW] | None:
|
80
88
|
if v and isinstance(v, str):
|
81
89
|
return [DoW(v.title())]
|
82
90
|
|
83
91
|
if v and isinstance(v, list) and not all(isinstance(i, DoW) for i in v):
|
84
92
|
return [DoW(i.title()) for i in v]
|
85
93
|
|
86
|
-
|
94
|
+
if not v:
|
95
|
+
return None
|
96
|
+
|
97
|
+
return v # type: ignore
|
87
98
|
|
88
99
|
@field_validator("class_type", mode="before")
|
89
100
|
@classmethod
|
90
|
-
def _class_type_str_to_enum(cls, v):
|
101
|
+
def _class_type_str_to_enum(cls, v: str | list[str] | None) -> list[ClassType] | None:
|
91
102
|
if v and isinstance(v, str):
|
92
103
|
return [ClassType.get_case_insensitive(v)]
|
93
104
|
|
94
105
|
if v and isinstance(v, list) and not all(isinstance(i, ClassType) for i in v):
|
95
106
|
return [ClassType.get_case_insensitive(i) for i in v]
|
96
107
|
|
97
|
-
|
108
|
+
if not v:
|
109
|
+
return None
|
110
|
+
|
111
|
+
return v # type: ignore
|
112
|
+
|
113
|
+
@field_validator("start_date", "end_date", mode="before")
|
114
|
+
@classmethod
|
115
|
+
def _date_string_to_date(cls, v: str | date | None) -> date | None:
|
116
|
+
if v is None:
|
117
|
+
return None
|
118
|
+
if isinstance(v, date):
|
119
|
+
return v
|
120
|
+
|
121
|
+
try:
|
122
|
+
value = pendulum.parse(v)
|
123
|
+
return value.date() if isinstance(value, datetime) else value # type: ignore
|
124
|
+
except (ValueError, TypeError):
|
125
|
+
raise ValueError(f"Invalid date format: {v}. Expected format is YYYY-MM-DD or a date object.")
|
@@ -9,9 +9,7 @@ CLASS_RATING_MAP = {0: 0, 1: 19, 2: 20, 3: 21}
|
|
9
9
|
|
10
10
|
|
11
11
|
def get_class_rating_value(class_rating: int) -> int:
|
12
|
-
"""
|
13
|
-
Convert the class rating from the old values to the new values.
|
14
|
-
"""
|
12
|
+
"""Convert the class rating from the old values to the new values."""
|
15
13
|
if class_rating not in CLASS_RATING_MAP:
|
16
14
|
raise ValueError(f"Invalid class rating {class_rating}")
|
17
15
|
|
@@ -19,9 +17,7 @@ def get_class_rating_value(class_rating: int) -> int:
|
|
19
17
|
|
20
18
|
|
21
19
|
def get_coach_rating_value(coach_rating: int) -> int:
|
22
|
-
"""
|
23
|
-
Convert the coach rating from the old values to the new values.
|
24
|
-
"""
|
20
|
+
"""Convert the coach rating from the old values to the new values."""
|
25
21
|
if coach_rating not in COACH_RATING_MAP:
|
26
22
|
raise ValueError(f"Invalid coach rating {coach_rating}")
|
27
23
|
|
@@ -0,0 +1,149 @@
|
|
1
|
+
from datetime import date, datetime
|
2
|
+
|
3
|
+
from pydantic import Field, field_validator
|
4
|
+
|
5
|
+
from otf_api.models.base import OtfItemBase
|
6
|
+
from otf_api.models.mixins import AddressMixin, ApiMixin
|
7
|
+
from otf_api.models.studios.studio_detail import StudioDetail
|
8
|
+
|
9
|
+
|
10
|
+
class Address(AddressMixin, OtfItemBase):
|
11
|
+
member_address_uuid: str | None = Field(None, validation_alias="memberAddressUUId", exclude=True, repr=False)
|
12
|
+
type: str | None = None
|
13
|
+
|
14
|
+
|
15
|
+
class MemberProfile(OtfItemBase):
|
16
|
+
unit_of_measure: str | None = Field(None, validation_alias="unitOfMeasure")
|
17
|
+
max_hr_type: str | None = Field(None, validation_alias="maxHrType")
|
18
|
+
manual_max_hr: int | None = Field(None, validation_alias="manualMaxHr")
|
19
|
+
formula_max_hr: int | None = Field(None, validation_alias="formulaMaxHr")
|
20
|
+
automated_hr: int | None = Field(None, validation_alias="automatedHr")
|
21
|
+
|
22
|
+
member_profile_uuid: str | None = Field(None, validation_alias="memberProfileUUId", exclude=True, repr=False)
|
23
|
+
member_optin_flow_type_id: int | None = Field(
|
24
|
+
None, validation_alias="memberOptinFlowTypeId", exclude=True, repr=False
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
class MemberClassSummary(OtfItemBase):
|
29
|
+
total_classes_booked: int | None = Field(None, validation_alias="totalClassesBooked")
|
30
|
+
total_classes_attended: int | None = Field(None, validation_alias="totalClassesAttended")
|
31
|
+
total_intro_classes: int | None = Field(None, validation_alias="totalIntro")
|
32
|
+
total_ot_live_classes_booked: int | None = Field(None, validation_alias="totalOTLiveClassesBooked")
|
33
|
+
total_ot_live_classes_attended: int | None = Field(None, validation_alias="totalOTLiveClassesAttended")
|
34
|
+
total_classes_used_hrm: int | None = Field(None, validation_alias="totalClassesUsedHRM")
|
35
|
+
total_studios_visited: int | None = Field(None, validation_alias="totalStudiosVisited")
|
36
|
+
first_visit_date: date | None = Field(None, validation_alias="firstVisitDate")
|
37
|
+
last_class_visited_date: date | None = Field(None, validation_alias="lastClassVisitedDate")
|
38
|
+
last_class_booked_date: date | None = Field(None, validation_alias="lastClassBookedDate")
|
39
|
+
|
40
|
+
last_class_studio_visited: int | None = Field(
|
41
|
+
None, validation_alias="lastClassStudioVisited", exclude=True, repr=False
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
class MemberDetail(ApiMixin, OtfItemBase):
|
46
|
+
member_uuid: str = Field(..., validation_alias="memberUUId")
|
47
|
+
cognito_id: str = Field(
|
48
|
+
...,
|
49
|
+
validation_alias="cognitoId",
|
50
|
+
exclude=True,
|
51
|
+
repr=False,
|
52
|
+
description="Cognito user ID, not necessary for end users. Also on OtfUser object.",
|
53
|
+
)
|
54
|
+
|
55
|
+
home_studio: StudioDetail
|
56
|
+
profile: MemberProfile = Field(..., validation_alias="memberProfile")
|
57
|
+
class_summary: MemberClassSummary | None = Field(None, validation_alias="memberClassSummary")
|
58
|
+
addresses: list[Address] | None = Field(default_factory=list)
|
59
|
+
|
60
|
+
studio_display_name: str | None = Field(
|
61
|
+
None,
|
62
|
+
validation_alias="userName",
|
63
|
+
description="The value that is displayed on tread/rower tablets and OTBeat screens",
|
64
|
+
)
|
65
|
+
first_name: str | None = Field(None, validation_alias="firstName")
|
66
|
+
last_name: str | None = Field(None, validation_alias="lastName")
|
67
|
+
email: str | None = Field(None, validation_alias="email")
|
68
|
+
phone_number: str | None = Field(None, validation_alias="phoneNumber")
|
69
|
+
birth_day: date | None = Field(None, validation_alias="birthDay")
|
70
|
+
gender: str | None = Field(None, validation_alias="gender")
|
71
|
+
locale: str | None = Field(None, validation_alias="locale")
|
72
|
+
weight: int | None = Field(None, validation_alias="weight")
|
73
|
+
weight_units: str | None = Field(None, validation_alias="weightMeasure")
|
74
|
+
height: int | None = Field(None, validation_alias="height")
|
75
|
+
height_units: str | None = Field(None, validation_alias="heightMeasure")
|
76
|
+
|
77
|
+
# unused fields - leaving these in for now in case someone finds a purpose for them
|
78
|
+
# but they will potentially (likely?) be removed in the future
|
79
|
+
|
80
|
+
# mbo fields
|
81
|
+
mbo_id: str | None = Field(None, validation_alias="mboId", exclude=True, repr=False, description="MindBody attr")
|
82
|
+
mbo_status: str | None = Field(
|
83
|
+
None, validation_alias="mboStatus", exclude=True, repr=False, description="MindBody attr"
|
84
|
+
)
|
85
|
+
mbo_studio_id: int | None = Field(
|
86
|
+
None, validation_alias="mboStudioId", exclude=True, repr=False, description="MindBody attr"
|
87
|
+
)
|
88
|
+
mbo_unique_id: int | None = Field(
|
89
|
+
None, validation_alias="mboUniqueId", exclude=True, repr=False, description="MindBody attr"
|
90
|
+
)
|
91
|
+
|
92
|
+
# ids
|
93
|
+
created_by: str | None = Field(None, validation_alias="createdBy", exclude=True, repr=False)
|
94
|
+
home_studio_id: int | None = Field(
|
95
|
+
None, validation_alias="homeStudioId", exclude=True, repr=False, description="Not used by API"
|
96
|
+
)
|
97
|
+
member_id: int | None = Field(
|
98
|
+
None, validation_alias="memberId", exclude=True, repr=False, description="Not used by API"
|
99
|
+
)
|
100
|
+
otf_acs_id: str | None = Field(None, validation_alias="otfAcsId", exclude=True, repr=False)
|
101
|
+
updated_by: str | None = Field(None, validation_alias="updatedBy", exclude=True, repr=False)
|
102
|
+
|
103
|
+
# unused address/member detail fields
|
104
|
+
created_date: datetime | None = Field(None, validation_alias="createdDate", exclude=True, repr=False)
|
105
|
+
updated_date: datetime | None = Field(None, validation_alias="updatedDate", exclude=True, repr=False)
|
106
|
+
|
107
|
+
address_line1: str | None = Field(None, validation_alias="addressLine1", exclude=True, repr=False)
|
108
|
+
address_line2: str | None = Field(None, validation_alias="addressLine2", exclude=True, repr=False)
|
109
|
+
alternate_emails: None = Field(None, validation_alias="alternateEmails", exclude=True, repr=False)
|
110
|
+
cc_last4: str | None = Field(None, validation_alias="ccLast4", exclude=True, repr=False)
|
111
|
+
cc_type: str | None = Field(None, validation_alias="ccType", exclude=True, repr=False)
|
112
|
+
city: str | None = Field(None, exclude=True, repr=False)
|
113
|
+
home_phone: str | None = Field(None, validation_alias="homePhone", exclude=True, repr=False)
|
114
|
+
intro_neccessary: bool | None = Field(None, validation_alias="introNeccessary", exclude=True, repr=False)
|
115
|
+
is_deleted: bool | None = Field(None, validation_alias="isDeleted", exclude=True, repr=False)
|
116
|
+
is_member_verified: bool | None = Field(None, validation_alias="isMemberVerified", exclude=True, repr=False)
|
117
|
+
lead_prospect: bool | None = Field(None, validation_alias="leadProspect", exclude=True, repr=False)
|
118
|
+
max_hr: int | None = Field(
|
119
|
+
None, validation_alias="maxHr", exclude=True, repr=False, description="Also found in member_profile"
|
120
|
+
)
|
121
|
+
online_signup: None = Field(None, validation_alias="onlineSignup", exclude=True, repr=False)
|
122
|
+
phone_type: None = Field(None, validation_alias="phoneType", exclude=True, repr=False)
|
123
|
+
postal_code: str | None = Field(None, validation_alias="postalCode", exclude=True, repr=False)
|
124
|
+
state: str | None = Field(None, exclude=True, repr=False)
|
125
|
+
work_phone: str | None = Field(None, validation_alias="workPhone", exclude=True, repr=False)
|
126
|
+
year_imported: int | None = Field(None, validation_alias="yearImported", exclude=True, repr=False)
|
127
|
+
|
128
|
+
@field_validator("birth_day")
|
129
|
+
@classmethod
|
130
|
+
def validate_birth_day(cls, value: date | str | None, **_kwargs) -> date | None:
|
131
|
+
"""Convert birth_day to a date object if it is in the format of YYYY-MM-DD."""
|
132
|
+
if value is None:
|
133
|
+
return value
|
134
|
+
if not isinstance(value, date):
|
135
|
+
return datetime.strptime(value, "%Y-%m-%d").date()
|
136
|
+
return value
|
137
|
+
|
138
|
+
def update_name(self, first_name: str | None = None, last_name: str | None = None) -> None:
|
139
|
+
"""Update the name of the member.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
first_name (str | None): The new first name of the member.
|
143
|
+
last_name (str | None): The new last name of the member.
|
144
|
+
"""
|
145
|
+
self.raise_if_api_not_set()
|
146
|
+
|
147
|
+
updated_member = self._api.members.update_member_name(first_name, last_name)
|
148
|
+
self.first_name = updated_member.first_name
|
149
|
+
self.last_name = updated_member.last_name
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from otf_api.models.base import OtfItemBase
|
6
|
+
|
7
|
+
|
8
|
+
class MemberMembership(OtfItemBase):
|
9
|
+
payment_date: datetime | None = Field(None, validation_alias="paymentDate")
|
10
|
+
active_date: datetime | None = Field(None, validation_alias="activeDate")
|
11
|
+
expiration_date: datetime | None = Field(None, validation_alias="expirationDate")
|
12
|
+
current: bool | None = None
|
13
|
+
count: int | None = None
|
14
|
+
remaining: int | None = None
|
15
|
+
name: str | None = None
|
16
|
+
updated_date: datetime | None = Field(None, validation_alias="updatedDate")
|
17
|
+
created_date: datetime | None = Field(None, validation_alias="createdDate")
|
18
|
+
is_deleted: bool | None = Field(None, validation_alias="isDeleted")
|
19
|
+
|
20
|
+
member_membership_id: int | None = Field(None, validation_alias="memberMembershipId", exclude=True, repr=False)
|
21
|
+
member_membership_uuid: str | None = Field(None, validation_alias="memberMembershipUUId", exclude=True, repr=False)
|
22
|
+
membership_id: int | None = Field(None, validation_alias="membershipId", exclude=True, repr=False)
|
23
|
+
member_id: int | None = Field(None, validation_alias="memberId", exclude=True, repr=False)
|
24
|
+
mbo_description_id: str | None = Field(None, validation_alias="mboDescriptionId", exclude=True, repr=False)
|
25
|
+
created_by: str | None = Field(None, validation_alias="createdBy", exclude=True, repr=False)
|
26
|
+
updated_by: str | None = Field(None, validation_alias="updatedBy", exclude=True, repr=False)
|
@@ -0,0 +1,29 @@
|
|
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.studios import StudioDetail
|
7
|
+
|
8
|
+
|
9
|
+
class MemberPurchase(OtfItemBase):
|
10
|
+
purchase_uuid: str = Field(..., validation_alias="memberPurchaseUUId")
|
11
|
+
name: str | None = None
|
12
|
+
price: str | None = None
|
13
|
+
purchase_date_time: datetime | None = Field(None, validation_alias="memberPurchaseDateTime")
|
14
|
+
purchase_type: str | None = Field(None, validation_alias="memberPurchaseType")
|
15
|
+
status: str | None = None
|
16
|
+
quantity: int | None = None
|
17
|
+
studio: StudioDetail = Field(..., exclude=True, repr=False)
|
18
|
+
|
19
|
+
member_fee_id: int | None = Field(None, validation_alias="memberFeeId", exclude=True, repr=False)
|
20
|
+
member_id: int | None = Field(..., validation_alias="memberId", exclude=True, repr=False)
|
21
|
+
member_membership_id: int | None = Field(None, validation_alias="memberMembershipId", exclude=True, repr=False)
|
22
|
+
member_purchase_id: int | None = Field(..., validation_alias="memberPurchaseId", exclude=True, repr=False)
|
23
|
+
member_service_id: int | None = Field(None, validation_alias="memberServiceId", exclude=True, repr=False)
|
24
|
+
pos_contract_id: int | None = Field(None, validation_alias="posContractId", exclude=True, repr=False)
|
25
|
+
pos_description_id: int | None = Field(None, validation_alias="posDescriptionId", exclude=True, repr=False)
|
26
|
+
pos_pmt_ref_no: int | None = Field(None, validation_alias="posPmtRefNo", exclude=True, repr=False)
|
27
|
+
pos_product_id: int | None = Field(..., validation_alias="posProductId", exclude=True, repr=False)
|
28
|
+
pos_sale_id: int | None = Field(..., validation_alias="posSaleId", exclude=True, repr=False)
|
29
|
+
studio_id: int | None = Field(..., validation_alias="studioId", exclude=True, repr=False)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from pydantic import Field
|
2
|
+
|
3
|
+
from otf_api.models.base import OtfItemBase
|
4
|
+
|
5
|
+
|
6
|
+
class SmsNotificationSettings(OtfItemBase):
|
7
|
+
is_promotional_sms_opt_in: bool | None = Field(None, validation_alias="isPromotionalSmsOptIn")
|
8
|
+
is_transactional_sms_opt_in: bool | None = Field(None, validation_alias="isTransactionalSmsOptIn")
|
9
|
+
is_promotional_phone_opt_in: bool | None = Field(None, validation_alias="isPromotionalPhoneOptIn")
|
10
|
+
is_transactional_phone_opt_in: bool | None = Field(None, validation_alias="isTransactionalPhoneOptIn")
|
11
|
+
|
12
|
+
|
13
|
+
class EmailNotificationSettings(OtfItemBase):
|
14
|
+
is_system_email_opt_in: bool | None = Field(None, validation_alias="isSystemEmailOptIn")
|
15
|
+
is_promotional_email_opt_in: bool | None = Field(None, validation_alias="isPromotionalEmailOptIn")
|
16
|
+
is_transactional_email_opt_in: bool | None = Field(None, validation_alias="isTransactionalEmailOptIn")
|
17
|
+
email: str | None = None
|
otf_api/models/mixins.py
CHANGED
@@ -1,13 +1,58 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import Any
|
3
|
+
|
1
4
|
from pydantic import AliasChoices, Field, field_validator, model_validator
|
2
5
|
|
6
|
+
if typing.TYPE_CHECKING:
|
7
|
+
from otf_api.api.api import Otf
|
8
|
+
|
9
|
+
|
10
|
+
class ApiMixin:
|
11
|
+
"""Mixin for models that require an API instance to be set.
|
12
|
+
|
13
|
+
This allows us to create model methods such as `cancel`, `book`, etc., that require an API instance to function.
|
14
|
+
The API instance is set using the `set_api` method, and it can be accessed via the `_api` attribute.
|
15
|
+
If the API instance is not set, calling methods that require it will raise a ValueError.
|
16
|
+
"""
|
17
|
+
|
18
|
+
_api: "Otf" = None # type: ignore[assignment]
|
19
|
+
|
20
|
+
def set_api(self, api: "Otf") -> None:
|
21
|
+
"""Set the API instance for this model."""
|
22
|
+
self._api = api
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def create(cls, **kwargs) -> typing.Self:
|
26
|
+
"""Creates a new instance of the model with the given keyword arguments."""
|
27
|
+
api = kwargs.pop("api", None)
|
28
|
+
instance = cls(**kwargs)
|
29
|
+
if api is not None:
|
30
|
+
instance.set_api(api)
|
31
|
+
return instance
|
32
|
+
|
33
|
+
def raise_if_api_not_set(self) -> None:
|
34
|
+
"""Raises an error if the API instance is not set."""
|
35
|
+
if self._api is None:
|
36
|
+
raise ValueError("API instance is not set. Use set_api() to set it before calling this method.")
|
37
|
+
|
3
38
|
|
4
39
|
class PhoneLongitudeLatitudeMixin:
|
40
|
+
"""Mixin for models that require phone number, latitude, and longitude fields.
|
41
|
+
|
42
|
+
This mixin exists to make it easier to handle the various names these fields can have in different APIs.
|
43
|
+
"""
|
44
|
+
|
5
45
|
phone_number: str | None = Field(None, validation_alias=AliasChoices("phone", "phoneNumber"))
|
6
46
|
latitude: float | None = Field(None, validation_alias=AliasChoices("latitude"))
|
7
47
|
longitude: float | None = Field(None, validation_alias=AliasChoices("longitude"))
|
8
48
|
|
9
49
|
|
10
50
|
class AddressMixin:
|
51
|
+
"""Mixin for models that require address fields.
|
52
|
+
|
53
|
+
This mixin exists to make it easier to handle the various names these fields can have in different APIs.
|
54
|
+
"""
|
55
|
+
|
11
56
|
address_line1: str | None = Field(
|
12
57
|
None, validation_alias=AliasChoices("line1", "address1", "address", "physicalAddress")
|
13
58
|
)
|
@@ -27,7 +72,8 @@ class AddressMixin:
|
|
27
72
|
|
28
73
|
@model_validator(mode="before")
|
29
74
|
@classmethod
|
30
|
-
def
|
75
|
+
def validate_model(cls, values: Any) -> Any: # noqa: ANN401
|
76
|
+
"""Validates address fields and country format, handling specific cases."""
|
31
77
|
if set(values.keys()) == set(
|
32
78
|
["phone", "latitude", "longitude", "address1", "address2", "city", "state", "postalCode"]
|
33
79
|
):
|
@@ -44,6 +90,7 @@ class AddressMixin:
|
|
44
90
|
@field_validator("address_line1", "address_line2", "city", "postal_code", "state", "country")
|
45
91
|
@classmethod
|
46
92
|
def clean_strings(cls, value: str | None, **_kwargs) -> str | None:
|
93
|
+
"""Clean strings by stripping whitespace and returning None if empty."""
|
47
94
|
if value is None:
|
48
95
|
return value
|
49
96
|
value = value.strip()
|
@@ -0,0 +1,93 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from pydantic import AliasChoices, AliasPath, Field
|
4
|
+
|
5
|
+
from otf_api.models.base import OtfItemBase
|
6
|
+
from otf_api.models.mixins import AddressMixin, ApiMixin
|
7
|
+
|
8
|
+
from .enums import StudioStatus
|
9
|
+
|
10
|
+
|
11
|
+
class StudioLocation(AddressMixin, OtfItemBase):
|
12
|
+
phone_number: str | None = Field(None, validation_alias=AliasChoices("phone", "phoneNumber"))
|
13
|
+
latitude: float | None = Field(None, validation_alias=AliasChoices("latitude"))
|
14
|
+
longitude: float | None = Field(None, validation_alias=AliasChoices("longitude"))
|
15
|
+
|
16
|
+
physical_region: str | None = Field(None, validation_alias="physicalRegion", exclude=True, repr=False)
|
17
|
+
physical_country_id: int | None = Field(None, validation_alias="physicalCountryId", exclude=True, repr=False)
|
18
|
+
|
19
|
+
|
20
|
+
class StudioDetail(ApiMixin, OtfItemBase):
|
21
|
+
studio_uuid: str = Field(..., validation_alias="studioUUId", description="The OTF studio UUID")
|
22
|
+
|
23
|
+
contact_email: str | None = Field(None, validation_alias="contactEmail")
|
24
|
+
distance: float | None = Field(
|
25
|
+
None,
|
26
|
+
description="Distance from latitude and longitude provided to `search_studios_by_geo` method,\
|
27
|
+
NULL if that method was not used",
|
28
|
+
exclude=True,
|
29
|
+
repr=False,
|
30
|
+
)
|
31
|
+
location: StudioLocation = Field(..., validation_alias="studioLocation", default_factory=StudioLocation) # type: ignore
|
32
|
+
name: str | None = Field(None, validation_alias="studioName")
|
33
|
+
status: StudioStatus | None = Field(
|
34
|
+
None, validation_alias="studioStatus", description="Active, Temporarily Closed, Coming Soon"
|
35
|
+
)
|
36
|
+
time_zone: str | None = Field(None, validation_alias="timeZone")
|
37
|
+
|
38
|
+
# flags
|
39
|
+
accepts_ach: bool | None = Field(None, validation_alias="acceptsAch", exclude=True, repr=False)
|
40
|
+
accepts_american_express: bool | None = Field(
|
41
|
+
None, validation_alias="acceptsAmericanExpress", exclude=True, repr=False
|
42
|
+
)
|
43
|
+
accepts_discover: bool | None = Field(None, validation_alias="acceptsDiscover", exclude=True, repr=False)
|
44
|
+
accepts_visa_master_card: bool | None = Field(
|
45
|
+
None, validation_alias="acceptsVisaMasterCard", exclude=True, repr=False
|
46
|
+
)
|
47
|
+
allows_cr_waitlist: bool | None = Field(None, validation_alias="allowsCrWaitlist", exclude=True, repr=False)
|
48
|
+
allows_dashboard_access: bool | None = Field(
|
49
|
+
None, validation_alias="allowsDashboardAccess", exclude=True, repr=False
|
50
|
+
)
|
51
|
+
is_crm: bool | None = Field(None, validation_alias=AliasPath("studioProfiles", "isCrm"), exclude=True, repr=False)
|
52
|
+
is_integrated: bool | None = Field(
|
53
|
+
None, validation_alias="isIntegrated", exclude=True, repr=False, description="Always 'True'"
|
54
|
+
)
|
55
|
+
is_mobile: bool | None = Field(None, validation_alias="isMobile", exclude=True, repr=False)
|
56
|
+
is_otbeat: bool | None = Field(None, validation_alias="isOtbeat", exclude=True, repr=False)
|
57
|
+
is_web: bool | None = Field(None, validation_alias=AliasPath("studioProfiles", "isWeb"), exclude=True, repr=False)
|
58
|
+
sms_package_enabled: bool | None = Field(None, validation_alias="smsPackageEnabled", exclude=True, repr=False)
|
59
|
+
|
60
|
+
# misc
|
61
|
+
studio_id: int | None = Field(
|
62
|
+
None, validation_alias="studioId", description="Not used by API", exclude=True, repr=False
|
63
|
+
)
|
64
|
+
mbo_studio_id: int | None = Field(
|
65
|
+
None, validation_alias="mboStudioId", exclude=True, repr=False, description="MindBody attr"
|
66
|
+
)
|
67
|
+
open_date: datetime | None = Field(None, validation_alias="openDate", exclude=True, repr=False)
|
68
|
+
pricing_level: str | None = Field(
|
69
|
+
None, validation_alias="pricingLevel", exclude=True, repr=False, description="Pro, Legacy, Accelerate, or empty"
|
70
|
+
)
|
71
|
+
re_open_date: datetime | None = Field(None, validation_alias="reOpenDate", exclude=True, repr=False)
|
72
|
+
studio_number: str | None = Field(None, validation_alias="studioNumber", exclude=True, repr=False)
|
73
|
+
studio_physical_location_id: int | None = Field(
|
74
|
+
None, validation_alias="studioPhysicalLocationId", exclude=True, repr=False
|
75
|
+
)
|
76
|
+
studio_token: str | None = Field(None, validation_alias="studioToken", exclude=True, repr=False)
|
77
|
+
studio_type_id: int | None = Field(None, validation_alias="studioTypeId", exclude=True, repr=False)
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def create_empty_model(cls, studio_uuid: str) -> "StudioDetail":
|
81
|
+
"""Create an empty model with the given studio_uuid."""
|
82
|
+
# pylance doesn't know that the rest of the fields default to None, so we use type: ignore
|
83
|
+
return StudioDetail(studioUUId=studio_uuid, studioName="Studio Not Found", studioStatus="Unknown") # type: ignore
|
84
|
+
|
85
|
+
def add_to_favorites(self) -> None:
|
86
|
+
"""Adds the studio to the user's favorites."""
|
87
|
+
self.raise_if_api_not_set()
|
88
|
+
self._api.studios.add_favorite_studio(self.studio_uuid)
|
89
|
+
|
90
|
+
def remove_from_favorites(self) -> None:
|
91
|
+
"""Removes the studio from the user's favorites."""
|
92
|
+
self.raise_if_api_not_set()
|
93
|
+
self._api.studios.remove_favorite_studio(self.studio_uuid)
|
@@ -0,0 +1,36 @@
|
|
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.studios import StudioDetail
|
7
|
+
|
8
|
+
|
9
|
+
class StudioService(OtfItemBase):
|
10
|
+
studio: StudioDetail = Field(..., exclude=True, repr=False)
|
11
|
+
service_uuid: str = Field(..., validation_alias="serviceUUId")
|
12
|
+
name: str | None = None
|
13
|
+
price: str | None = None
|
14
|
+
qty: int | None = None
|
15
|
+
online_price: str | None = Field(None, validation_alias="onlinePrice")
|
16
|
+
tax_rate: str | None = Field(None, validation_alias="taxRate")
|
17
|
+
current: bool | None = None
|
18
|
+
is_deleted: bool | None = Field(None, validation_alias="isDeleted")
|
19
|
+
created_date: datetime | None = Field(None, validation_alias="createdDate")
|
20
|
+
updated_date: datetime | None = Field(None, validation_alias="updatedDate")
|
21
|
+
|
22
|
+
# unused fields
|
23
|
+
|
24
|
+
# ids
|
25
|
+
mbo_program_id: int | None = Field(None, validation_alias="mboProgramId", exclude=True, repr=False)
|
26
|
+
mbo_description_id: str | None = Field(None, validation_alias="mboDescriptionId", exclude=True, repr=False)
|
27
|
+
mbo_product_id: int | None = Field(None, validation_alias="mboProductId", exclude=True, repr=False)
|
28
|
+
service_id: int | None = Field(None, validation_alias="serviceId", exclude=True, repr=False)
|
29
|
+
studio_id: int | None = Field(None, validation_alias="studioId", exclude=True, repr=False)
|
30
|
+
created_by: str | None = Field(None, validation_alias="createdBy", exclude=True, repr=False)
|
31
|
+
updated_by: str | None = Field(None, validation_alias="updatedBy", exclude=True, repr=False)
|
32
|
+
|
33
|
+
# flags
|
34
|
+
is_web: bool | None = Field(None, validation_alias="isWeb", exclude=True, repr=False)
|
35
|
+
is_crm: bool | None = Field(None, validation_alias="isCrm", exclude=True, repr=False)
|
36
|
+
is_mobile: bool | None = Field(None, validation_alias="isMobile", exclude=True, repr=False)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from .body_composition_list import BodyCompositionData
|
2
|
+
from .challenge_tracker_content import ChallengeTracker
|
3
|
+
from .challenge_tracker_detail import FitnessBenchmark
|
4
|
+
from .enums import ChallengeCategory, EquipmentType, StatsTime
|
5
|
+
from .lifetime_stats import InStudioStatsData, OutStudioStatsData, StatsResponse, TimeStats
|
6
|
+
from .out_of_studio_workout_history import OutOfStudioWorkoutHistory
|
7
|
+
from .performance_summary import HeartRate, PerformanceSummary, Rower, Treadmill, ZoneTimeMinutes
|
8
|
+
from .telemetry import Telemetry, TelemetryHistoryItem
|
9
|
+
from .workout import Workout
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"BodyCompositionData",
|
13
|
+
"ChallengeCategory",
|
14
|
+
"ChallengeTracker",
|
15
|
+
"EquipmentType",
|
16
|
+
"FitnessBenchmark",
|
17
|
+
"HeartRate",
|
18
|
+
"InStudioStatsData",
|
19
|
+
"OutOfStudioWorkoutHistory",
|
20
|
+
"OutStudioStatsData",
|
21
|
+
"PerformanceSummary",
|
22
|
+
"Rower",
|
23
|
+
"StatsResponse",
|
24
|
+
"StatsTime",
|
25
|
+
"Telemetry",
|
26
|
+
"TelemetryHistoryItem",
|
27
|
+
"TimeStats",
|
28
|
+
"Treadmill",
|
29
|
+
"Workout",
|
30
|
+
"ZoneTimeMinutes",
|
31
|
+
]
|