otf-api 0.8.2__py3-none-any.whl → 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- otf_api/__init__.py +7 -4
- otf_api/api.py +699 -480
- otf_api/auth/__init__.py +4 -0
- otf_api/auth/auth.py +234 -0
- otf_api/auth/user.py +66 -0
- otf_api/auth/utils.py +129 -0
- otf_api/exceptions.py +38 -5
- otf_api/filters.py +97 -0
- otf_api/logging.py +19 -0
- otf_api/models/__init__.py +27 -38
- otf_api/models/body_composition_list.py +47 -50
- otf_api/models/bookings.py +63 -87
- otf_api/models/challenge_tracker_content.py +42 -21
- otf_api/models/challenge_tracker_detail.py +68 -48
- otf_api/models/classes.py +53 -62
- otf_api/models/enums.py +108 -30
- otf_api/models/lifetime_stats.py +59 -45
- otf_api/models/member_detail.py +95 -115
- otf_api/models/member_membership.py +18 -17
- otf_api/models/member_purchases.py +21 -127
- otf_api/models/mixins.py +37 -33
- otf_api/models/notifications.py +17 -0
- otf_api/models/out_of_studio_workout_history.py +22 -31
- otf_api/models/performance_summary_detail.py +47 -42
- otf_api/models/performance_summary_list.py +19 -37
- otf_api/models/studio_detail.py +51 -98
- otf_api/models/studio_services.py +27 -48
- otf_api/models/telemetry.py +14 -5
- otf_api/utils.py +134 -0
- {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/METADATA +21 -10
- otf_api-0.9.1.dist-info/RECORD +35 -0
- {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/WHEEL +1 -1
- otf_api/auth.py +0 -316
- otf_api/models/book_class.py +0 -89
- otf_api/models/cancel_booking.py +0 -49
- otf_api/models/favorite_studios.py +0 -106
- otf_api/models/latest_agreement.py +0 -21
- otf_api/models/telemetry_hr_history.py +0 -34
- otf_api/models/telemetry_max_hr.py +0 -13
- otf_api/models/total_classes.py +0 -8
- otf_api-0.8.2.dist-info/AUTHORS.md +0 -9
- otf_api-0.8.2.dist-info/RECORD +0 -36
- {otf_api-0.8.2.dist-info → otf_api-0.9.1.dist-info}/LICENSE +0 -0
@@ -1,4 +1,6 @@
|
|
1
|
-
from
|
1
|
+
from datetime import datetime, time
|
2
|
+
|
3
|
+
from pydantic import AliasPath, Field, field_validator
|
2
4
|
|
3
5
|
from otf_api.models.base import OtfItemBase
|
4
6
|
|
@@ -19,61 +21,64 @@ class HeartRate(OtfItemBase):
|
|
19
21
|
avg_hr_percent: int
|
20
22
|
|
21
23
|
|
22
|
-
class
|
23
|
-
display_value: float
|
24
|
+
class PerformanceMetric(OtfItemBase):
|
25
|
+
display_value: time | float
|
24
26
|
display_unit: str
|
25
27
|
metric_value: float
|
26
28
|
|
29
|
+
@field_validator("display_value", mode="before")
|
30
|
+
@classmethod
|
31
|
+
def convert_to_time_format(cls, value) -> time | float:
|
32
|
+
if not value:
|
33
|
+
return value
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
35
|
+
if isinstance(value, float | int):
|
36
|
+
return value
|
37
|
+
|
38
|
+
if isinstance(value, str) and ":" in value:
|
39
|
+
if value.count(":") == 1:
|
40
|
+
minutes, seconds = value.split(":")
|
41
|
+
return time(minute=int(minutes), second=int(seconds))
|
42
|
+
if value.count(":") == 2:
|
43
|
+
hours, minutes, seconds = value.split(":")
|
44
|
+
return time(hour=int(hours), minute=int(minutes), second=int(seconds))
|
45
|
+
|
46
|
+
return value
|
32
47
|
|
33
48
|
|
34
49
|
class BaseEquipment(OtfItemBase):
|
35
|
-
avg_pace:
|
36
|
-
avg_speed:
|
37
|
-
max_pace:
|
38
|
-
max_speed:
|
39
|
-
moving_time:
|
40
|
-
total_distance:
|
50
|
+
avg_pace: PerformanceMetric
|
51
|
+
avg_speed: PerformanceMetric
|
52
|
+
max_pace: PerformanceMetric
|
53
|
+
max_speed: PerformanceMetric
|
54
|
+
moving_time: PerformanceMetric
|
55
|
+
total_distance: PerformanceMetric
|
41
56
|
|
42
57
|
|
43
58
|
class Treadmill(BaseEquipment):
|
44
|
-
avg_incline:
|
45
|
-
elevation_gained:
|
46
|
-
max_incline:
|
59
|
+
avg_incline: PerformanceMetric
|
60
|
+
elevation_gained: PerformanceMetric
|
61
|
+
max_incline: PerformanceMetric
|
47
62
|
|
48
63
|
|
49
64
|
class Rower(BaseEquipment):
|
50
|
-
avg_cadence:
|
51
|
-
avg_power:
|
52
|
-
max_cadence:
|
53
|
-
|
54
|
-
|
55
|
-
class EquipmentData(OtfItemBase):
|
56
|
-
treadmill: Treadmill
|
57
|
-
rower: Rower
|
58
|
-
|
59
|
-
|
60
|
-
class Details(OtfItemBase):
|
61
|
-
calories_burned: int
|
62
|
-
splat_points: int
|
63
|
-
step_count: int
|
64
|
-
active_time_seconds: int
|
65
|
-
zone_time_minutes: ZoneTimeMinutes
|
66
|
-
heart_rate: HeartRate
|
67
|
-
equipment_data: EquipmentData
|
68
|
-
|
69
|
-
|
70
|
-
class Class(OtfItemBase):
|
71
|
-
starts_at_local: str
|
72
|
-
name: str
|
65
|
+
avg_cadence: PerformanceMetric
|
66
|
+
avg_power: PerformanceMetric
|
67
|
+
max_cadence: PerformanceMetric
|
73
68
|
|
74
69
|
|
75
70
|
class PerformanceSummaryDetail(OtfItemBase):
|
76
71
|
id: str
|
77
|
-
|
78
|
-
|
79
|
-
|
72
|
+
class_name: str | None = Field(None, alias=AliasPath("class", "name"))
|
73
|
+
class_starts_at: datetime | None = Field(None, alias=AliasPath("class", "starts_at_local"))
|
74
|
+
|
75
|
+
ratable: bool | None = None
|
76
|
+
calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
|
77
|
+
splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
|
78
|
+
step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
|
79
|
+
active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds"))
|
80
|
+
zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes"))
|
81
|
+
heart_rate: HeartRate | None = Field(None, alias=AliasPath("details", "heart_rate"))
|
82
|
+
|
83
|
+
rower_data: Rower | None = Field(None, alias=AliasPath("details", "equipment_data", "rower"))
|
84
|
+
treadmill_data: Treadmill | None = Field(None, alias=AliasPath("details", "equipment_data", "treadmill"))
|
@@ -1,4 +1,6 @@
|
|
1
|
-
from
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from pydantic import AliasPath, Field
|
2
4
|
|
3
5
|
from otf_api.models.base import OtfItemBase
|
4
6
|
|
@@ -11,32 +13,11 @@ class ZoneTimeMinutes(OtfItemBase):
|
|
11
13
|
red: int
|
12
14
|
|
13
15
|
|
14
|
-
class Details(OtfItemBase):
|
15
|
-
calories_burned: int
|
16
|
-
splat_points: int
|
17
|
-
step_count: int
|
18
|
-
active_time_seconds: int
|
19
|
-
zone_time_minutes: ZoneTimeMinutes
|
20
|
-
|
21
|
-
|
22
|
-
class Coach(OtfItemBase):
|
23
|
-
image_url: str | None = None
|
24
|
-
first_name: str
|
25
|
-
|
26
|
-
|
27
|
-
class Studio(OtfItemBase):
|
28
|
-
id: str
|
29
|
-
license_number: str
|
30
|
-
name: str
|
31
|
-
|
32
|
-
|
33
16
|
class Class(OtfItemBase):
|
34
|
-
|
35
|
-
|
17
|
+
class_uuid: str | None = Field(None, description="Only populated if class is ratable", alias="ot_base_class_uuid")
|
18
|
+
starts_at: datetime | None = Field(None, alias="starts_at_local")
|
36
19
|
name: str | None = None
|
37
20
|
type: str | None = None
|
38
|
-
coach: Coach
|
39
|
-
studio: Studio
|
40
21
|
|
41
22
|
|
42
23
|
class CoachRating(OtfItemBase):
|
@@ -51,18 +32,19 @@ class ClassRating(OtfItemBase):
|
|
51
32
|
value: int
|
52
33
|
|
53
34
|
|
54
|
-
class Ratings(OtfItemBase):
|
55
|
-
coach: CoachRating
|
56
|
-
otf_class: ClassRating = Field(..., alias="class")
|
57
|
-
|
58
|
-
|
59
35
|
class PerformanceSummaryEntry(OtfItemBase):
|
60
36
|
id: str = Field(..., alias="id")
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
37
|
+
calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
|
38
|
+
splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
|
39
|
+
step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
|
40
|
+
active_time_seconds: int | None = Field(None, alias=AliasPath("details", "active_time_seconds"))
|
41
|
+
zone_time_minutes: ZoneTimeMinutes | None = Field(None, alias=AliasPath("details", "zone_time_minutes"))
|
42
|
+
ratable: bool | None = None
|
43
|
+
otf_class: Class | None = Field(None, alias="class")
|
44
|
+
coach: str | None = Field(None, alias=AliasPath("class", "coach", "first_name"))
|
45
|
+
coach_rating: CoachRating | None = Field(None, alias=AliasPath("ratings", "coach"))
|
46
|
+
class_rating: ClassRating | None = Field(None, alias=AliasPath("ratings", "class"))
|
47
|
+
|
48
|
+
@property
|
49
|
+
def is_rated(self) -> bool:
|
50
|
+
return self.coach_rating is not None or self.class_rating is not None
|
otf_api/models/studio_detail.py
CHANGED
@@ -1,109 +1,62 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
|
3
|
-
from pydantic import Field
|
3
|
+
from pydantic import AliasChoices, AliasPath, Field
|
4
4
|
|
5
5
|
from otf_api.models.base import OtfItemBase
|
6
|
+
from otf_api.models.enums import StudioStatus
|
7
|
+
from otf_api.models.mixins import AddressMixin
|
6
8
|
|
7
9
|
|
8
|
-
class
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
currency_alphabetic_code: str | None = Field(None, alias="currencyAlphabeticCode")
|
10
|
+
class StudioLocation(AddressMixin):
|
11
|
+
phone_number: str | None = Field(None, alias=AliasChoices("phone", "phoneNumber"))
|
12
|
+
latitude: float = Field(..., alias=AliasChoices("latitude"))
|
13
|
+
longitude: float = Field(..., alias=AliasChoices("longitude"))
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
physical_address: str | None = Field(None, alias="physicalAddress")
|
17
|
-
physical_address2: str | None = Field(None, alias="physicalAddress2")
|
18
|
-
physical_city: str | None = Field(None, alias="physicalCity")
|
19
|
-
physical_state: str | None = Field(None, alias="physicalState")
|
20
|
-
physical_postal_code: str | None = Field(None, alias="physicalPostalCode")
|
21
|
-
physical_region: str | None = Field(None, alias="physicalRegion", exclude=True)
|
22
|
-
physical_country: str | None = Field(None, alias="physicalCountry", exclude=True)
|
23
|
-
country: Country | None = Field(None, exclude=True)
|
24
|
-
phone_number: str | None = Field(None, alias="phoneNumber")
|
25
|
-
latitude: float | None = Field(None, exclude=True)
|
26
|
-
longitude: float | None = Field(None, exclude=True)
|
27
|
-
|
28
|
-
|
29
|
-
class Language(OtfItemBase):
|
30
|
-
language_id: None = Field(None, alias="languageId")
|
31
|
-
language_code: None = Field(None, alias="languageCode")
|
32
|
-
language_name: None = Field(None, alias="languageName")
|
33
|
-
|
34
|
-
|
35
|
-
class StudioLocationLocalized(OtfItemBase):
|
36
|
-
language: Language | None = Field(None, exclude=True)
|
37
|
-
studio_name: str | None = Field(None, alias="studioName")
|
38
|
-
studio_address: str | None = Field(None, alias="studioAddress")
|
39
|
-
|
40
|
-
|
41
|
-
class StudioProfiles(OtfItemBase):
|
42
|
-
is_web: bool | None = Field(None, alias="isWeb")
|
43
|
-
intro_capacity: int | None = Field(None, alias="introCapacity")
|
44
|
-
is_crm: bool | None = Field(None, alias="isCrm")
|
45
|
-
|
46
|
-
|
47
|
-
class SocialMediaLink(OtfItemBase):
|
48
|
-
id: str
|
49
|
-
language_id: str | None = Field(None, alias="languageId")
|
50
|
-
name: str
|
51
|
-
value: str
|
15
|
+
physical_region: str | None = Field(None, alias="physicalRegion", exclude=True, repr=False)
|
16
|
+
physical_country_id: int | None = Field(None, alias="physicalCountryId", exclude=True, repr=False)
|
52
17
|
|
53
18
|
|
54
19
|
class StudioDetail(OtfItemBase):
|
55
|
-
|
56
|
-
studio_uuid: str = Field(..., alias="studioUUId")
|
57
|
-
studio_location_localized: StudioLocationLocalized | None = Field(
|
58
|
-
None, alias="studioLocationLocalized", exclude=True
|
59
|
-
)
|
60
|
-
studio_location: StudioLocation | None = Field(None, alias="studioLocation")
|
61
|
-
studio_name: str | None = Field(None, alias="studioName")
|
62
|
-
studio_number: str | None = Field(None, alias="studioNumber", exclude=True)
|
63
|
-
studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True)
|
64
|
-
studio_profiles: StudioProfiles | None = Field(None, alias="studioProfiles", exclude=True)
|
65
|
-
studio_status: str | None = Field(None, alias="studioStatus", exclude=True)
|
66
|
-
studio_token: str | None = Field(None, alias="studioToken", exclude=True)
|
67
|
-
studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True)
|
68
|
-
studio_version: str | None = Field(None, alias="studioVersion", exclude=True)
|
69
|
-
mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True)
|
70
|
-
accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True)
|
71
|
-
accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True)
|
72
|
-
accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True)
|
73
|
-
accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True)
|
74
|
-
allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True)
|
75
|
-
allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True)
|
76
|
-
area_id: int | None = Field(None, alias="areaId", exclude=True)
|
77
|
-
commission_percent: int | None = Field(None, alias="commissionPercent", exclude=True)
|
78
|
-
contact_email: str | None = Field(None, alias="contactEmail", exclude=True)
|
79
|
-
cr_waitlist_flag_last_updated: datetime | None = Field(None, alias="crWaitlistFlagLastUpdated", exclude=True)
|
80
|
-
description: str | None = Field(None, exclude=True)
|
81
|
-
distance: float | None = Field(None, exclude=True)
|
82
|
-
environment: str | None = Field(None, exclude=True)
|
83
|
-
is_integrated: bool | None = Field(None, alias="isIntegrated", exclude=True)
|
84
|
-
is_mobile: bool | None = Field(None, alias="isMobile", exclude=True)
|
85
|
-
is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True)
|
86
|
-
logo_url: str | None = Field(None, alias="logoUrl", exclude=True)
|
87
|
-
market_id: int | None = Field(None, alias="marketId", exclude=True)
|
88
|
-
marketing_fund_rate: int | None = Field(None, alias="marketingFundRate", exclude=True)
|
89
|
-
open_date: datetime | None = Field(None, alias="openDate", exclude=True)
|
90
|
-
pos_type_id: int | None = Field(None, alias="posTypeId", exclude=True)
|
91
|
-
pricing_level: str | None = Field(None, alias="pricingLevel", exclude=True)
|
92
|
-
re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True)
|
93
|
-
royalty_rate: int | None = Field(None, alias="royaltyRate", exclude=True)
|
94
|
-
sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True)
|
95
|
-
social_media_links: list[SocialMediaLink] | None = Field(None, alias="socialMediaLinks", exclude=True)
|
96
|
-
state_id: int | None = Field(None, alias="stateId", exclude=True)
|
97
|
-
tax_rate: str | None = Field(None, alias="taxRate", exclude=True)
|
98
|
-
time_zone: str | None = Field(None, alias="timeZone", exclude=True)
|
99
|
-
|
100
|
-
|
101
|
-
class Pagination(OtfItemBase):
|
102
|
-
page_index: int | None = Field(None, alias="pageIndex")
|
103
|
-
page_size: int | None = Field(None, alias="pageSize")
|
104
|
-
total_count: int | None = Field(None, alias="totalCount")
|
105
|
-
total_pages: int | None = Field(None, alias="totalPages")
|
106
|
-
|
20
|
+
studio_uuid: str = Field(..., alias="studioUUId", description="The OTF studio UUID")
|
107
21
|
|
108
|
-
|
109
|
-
|
22
|
+
contact_email: str | None = Field(None, alias="contactEmail")
|
23
|
+
distance: float | None = Field(
|
24
|
+
None,
|
25
|
+
description="Distance from latitude and longitude provided to `search_studios_by_geo` method,\
|
26
|
+
NULL if that method was not used",
|
27
|
+
)
|
28
|
+
location: StudioLocation = Field(..., alias="studioLocation")
|
29
|
+
name: str | None = Field(None, alias="studioName")
|
30
|
+
status: StudioStatus | None = Field(
|
31
|
+
None, alias="studioStatus", description="Active, Temporarily Closed, Coming Soon"
|
32
|
+
)
|
33
|
+
time_zone: str | None = Field(None, alias="timeZone")
|
34
|
+
|
35
|
+
# flags
|
36
|
+
accepts_ach: bool | None = Field(None, alias="acceptsAch", exclude=True, repr=False)
|
37
|
+
accepts_american_express: bool | None = Field(None, alias="acceptsAmericanExpress", exclude=True, repr=False)
|
38
|
+
accepts_discover: bool | None = Field(None, alias="acceptsDiscover", exclude=True, repr=False)
|
39
|
+
accepts_visa_master_card: bool | None = Field(None, alias="acceptsVisaMasterCard", exclude=True, repr=False)
|
40
|
+
allows_cr_waitlist: bool | None = Field(None, alias="allowsCrWaitlist", exclude=True, repr=False)
|
41
|
+
allows_dashboard_access: bool | None = Field(None, alias="allowsDashboardAccess", exclude=True, repr=False)
|
42
|
+
is_crm: bool | None = Field(None, alias=AliasPath("studioProfiles", "isCrm"), exclude=True, repr=False)
|
43
|
+
is_integrated: bool | None = Field(
|
44
|
+
None, alias="isIntegrated", exclude=True, repr=False, description="Always 'True'"
|
45
|
+
)
|
46
|
+
is_mobile: bool | None = Field(None, alias="isMobile", exclude=True, repr=False)
|
47
|
+
is_otbeat: bool | None = Field(None, alias="isOtbeat", exclude=True, repr=False)
|
48
|
+
is_web: bool | None = Field(None, alias=AliasPath("studioProfiles", "isWeb"), exclude=True, repr=False)
|
49
|
+
sms_package_enabled: bool | None = Field(None, alias="smsPackageEnabled", exclude=True, repr=False)
|
50
|
+
|
51
|
+
# misc
|
52
|
+
studio_id: int | None = Field(None, alias="studioId", description="Not used by API", exclude=True, repr=False)
|
53
|
+
mbo_studio_id: int | None = Field(None, alias="mboStudioId", exclude=True, repr=False, description="MindBody attr")
|
54
|
+
open_date: datetime | None = Field(None, alias="openDate", exclude=True, repr=False)
|
55
|
+
pricing_level: str | None = Field(
|
56
|
+
None, alias="pricingLevel", exclude=True, repr=False, description="Pro, Legacy, Accelerate, or empty"
|
57
|
+
)
|
58
|
+
re_open_date: datetime | None = Field(None, alias="reOpenDate", exclude=True, repr=False)
|
59
|
+
studio_number: str | None = Field(None, alias="studioNumber", exclude=True, repr=False)
|
60
|
+
studio_physical_location_id: int | None = Field(None, alias="studioPhysicalLocationId", exclude=True, repr=False)
|
61
|
+
studio_token: str | None = Field(None, alias="studioToken", exclude=True, repr=False)
|
62
|
+
studio_type_id: int | None = Field(None, alias="studioTypeId", exclude=True, repr=False)
|
@@ -3,55 +3,34 @@ from datetime import datetime
|
|
3
3
|
from pydantic import Field
|
4
4
|
|
5
5
|
from otf_api.models.base import OtfItemBase
|
6
|
-
|
7
|
-
|
8
|
-
class Currency(OtfItemBase):
|
9
|
-
currency_alphabetic_code: str = Field(..., alias="currencyAlphabeticCode")
|
10
|
-
|
11
|
-
|
12
|
-
class DefaultCurrency(OtfItemBase):
|
13
|
-
currency_id: int = Field(..., alias="currencyId")
|
14
|
-
currency: Currency
|
15
|
-
|
16
|
-
|
17
|
-
class Country(OtfItemBase):
|
18
|
-
country_currency_code: str = Field(..., alias="countryCurrencyCode")
|
19
|
-
default_currency: DefaultCurrency = Field(..., alias="defaultCurrency")
|
20
|
-
|
21
|
-
|
22
|
-
class StudioLocation(OtfItemBase):
|
23
|
-
studio_location_id: int = Field(..., alias="studioLocationId")
|
24
|
-
country: Country
|
25
|
-
|
26
|
-
|
27
|
-
class Studio(OtfItemBase):
|
28
|
-
studio_id: int = Field(..., alias="studioId")
|
29
|
-
studio_location: StudioLocation = Field(..., alias="studioLocation")
|
6
|
+
from otf_api.models.studio_detail import StudioDetail
|
30
7
|
|
31
8
|
|
32
9
|
class StudioService(OtfItemBase):
|
33
|
-
|
10
|
+
studio: StudioDetail = Field(..., exclude=True, repr=False)
|
34
11
|
service_uuid: str = Field(..., alias="serviceUUId")
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
12
|
+
name: str | None = None
|
13
|
+
price: str | None = None
|
14
|
+
qty: int | None = None
|
15
|
+
online_price: str | None = Field(None, alias="onlinePrice")
|
16
|
+
tax_rate: str | None = Field(None, alias="taxRate")
|
17
|
+
current: bool | None = None
|
18
|
+
is_deleted: bool | None = Field(None, alias="isDeleted")
|
19
|
+
created_date: datetime | None = Field(None, alias="createdDate")
|
20
|
+
updated_date: datetime | None = Field(None, alias="updatedDate")
|
21
|
+
|
22
|
+
# unused fields
|
23
|
+
|
24
|
+
# ids
|
25
|
+
mbo_program_id: int | None = Field(None, alias="mboProgramId", exclude=True, repr=False)
|
26
|
+
mbo_description_id: str | None = Field(None, alias="mboDescriptionId", exclude=True, repr=False)
|
27
|
+
mbo_product_id: int | None = Field(None, alias="mboProductId", exclude=True, repr=False)
|
28
|
+
service_id: int | None = Field(None, alias="serviceId", exclude=True, repr=False)
|
29
|
+
studio_id: int | None = Field(None, alias="studioId", exclude=True, repr=False)
|
30
|
+
created_by: str | None = Field(None, alias="createdBy", exclude=True, repr=False)
|
31
|
+
updated_by: str | None = Field(None, alias="updatedBy", exclude=True, repr=False)
|
32
|
+
|
33
|
+
# flags
|
34
|
+
is_web: bool | None = Field(None, alias="isWeb", exclude=True, repr=False)
|
35
|
+
is_crm: bool | None = Field(None, alias="isCrm", exclude=True, repr=False)
|
36
|
+
is_mobile: bool | None = Field(None, alias="isMobile", exclude=True, repr=False)
|
otf_api/models/telemetry.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from datetime import datetime, timedelta
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from pydantic import Field
|
4
|
+
from pydantic import AliasPath, Field
|
5
5
|
|
6
6
|
from otf_api.models.base import OtfItemBase
|
7
7
|
|
@@ -50,13 +50,22 @@ class TelemetryItem(OtfItemBase):
|
|
50
50
|
class Telemetry(OtfItemBase):
|
51
51
|
member_uuid: str = Field(..., alias="memberUuid")
|
52
52
|
class_history_uuid: str = Field(..., alias="classHistoryUuid")
|
53
|
-
class_start_time: datetime = Field(
|
54
|
-
max_hr: int = Field(
|
53
|
+
class_start_time: datetime | None = Field(None, alias="classStartTime")
|
54
|
+
max_hr: int | None = Field(None, alias="maxHr")
|
55
55
|
zones: Zones
|
56
|
-
window_size: int = Field(
|
57
|
-
telemetry: list[TelemetryItem]
|
56
|
+
window_size: int | None = Field(None, alias="windowSize")
|
57
|
+
telemetry: list[TelemetryItem] = Field(default_factory=list)
|
58
58
|
|
59
59
|
def __init__(self, **data: dict[str, Any]):
|
60
60
|
super().__init__(**data)
|
61
61
|
for telem in self.telemetry:
|
62
62
|
telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp)
|
63
|
+
|
64
|
+
|
65
|
+
class TelemetryHistoryItem(OtfItemBase):
|
66
|
+
max_hr_type: str | None = Field(None, alias=AliasPath("maxHr", "type"))
|
67
|
+
max_hr_value: int | None = Field(None, alias=AliasPath("maxHr", "value"))
|
68
|
+
zones: Zones | None = None
|
69
|
+
change_from_previous: int | None = Field(None, alias="changeFromPrevious")
|
70
|
+
change_bucket: str | None = Field(None, alias="changeBucket")
|
71
|
+
assigned_at: datetime | None = Field(None, alias="assignedAt")
|
otf_api/utils.py
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
import json
|
2
|
+
import typing
|
3
|
+
from datetime import date, datetime
|
4
|
+
from logging import getLogger
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import attrs
|
9
|
+
|
10
|
+
if typing.TYPE_CHECKING:
|
11
|
+
from otf_api.models.bookings import Booking
|
12
|
+
from otf_api.models.classes import OtfClass
|
13
|
+
|
14
|
+
LOGGER = getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
def get_booking_uuid(booking_or_uuid: "str | Booking") -> str:
|
18
|
+
from otf_api.models.bookings import Booking
|
19
|
+
|
20
|
+
if isinstance(booking_or_uuid, str):
|
21
|
+
return booking_or_uuid
|
22
|
+
|
23
|
+
if isinstance(booking_or_uuid, Booking):
|
24
|
+
return booking_or_uuid.booking_uuid
|
25
|
+
|
26
|
+
raise ValueError(f"Expected Booking or str, got {type(booking_or_uuid)}")
|
27
|
+
|
28
|
+
|
29
|
+
def get_class_uuid(class_or_uuid: "str | OtfClass") -> str:
|
30
|
+
from otf_api.models.classes import OtfClass
|
31
|
+
|
32
|
+
if isinstance(class_or_uuid, str):
|
33
|
+
return class_or_uuid
|
34
|
+
|
35
|
+
if isinstance(class_or_uuid, OtfClass):
|
36
|
+
return class_or_uuid.class_uuid
|
37
|
+
|
38
|
+
raise ValueError(f"Expected OtfClass or str, got {type(class_or_uuid)}")
|
39
|
+
|
40
|
+
|
41
|
+
def ensure_list(obj: list | Any | None) -> list:
|
42
|
+
if obj is None:
|
43
|
+
return []
|
44
|
+
if not isinstance(obj, list):
|
45
|
+
return [obj]
|
46
|
+
return obj
|
47
|
+
|
48
|
+
|
49
|
+
def ensure_date(date_str: str | date | None) -> date | None:
|
50
|
+
if not date_str:
|
51
|
+
return None
|
52
|
+
|
53
|
+
if isinstance(date_str, str):
|
54
|
+
return datetime.fromisoformat(date_str).date()
|
55
|
+
|
56
|
+
if isinstance(date_str, datetime):
|
57
|
+
return date_str.date()
|
58
|
+
|
59
|
+
return date_str
|
60
|
+
|
61
|
+
|
62
|
+
@attrs.define
|
63
|
+
class CacheableData:
|
64
|
+
"""Represents a cacheable data object, with methods to read and write to cache."""
|
65
|
+
|
66
|
+
name: str
|
67
|
+
cache_dir: Path
|
68
|
+
|
69
|
+
def __attrs_post_init__(self):
|
70
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
71
|
+
|
72
|
+
@property
|
73
|
+
def cache_path(self) -> Path:
|
74
|
+
"""The path to the cache file."""
|
75
|
+
return self.cache_dir.expanduser().joinpath(f"{self.name}_cache.json")
|
76
|
+
|
77
|
+
def get_cached_data(self, keys: list[str] | None = None) -> dict[str, str]:
|
78
|
+
"""Reads the cache file and returns the data if it exists and is valid.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
dict[str, str]: The cached data, or an empty dictionary if the cache is invalid or missing.
|
82
|
+
"""
|
83
|
+
LOGGER.debug(f"Loading {self.name} from cache ({self.cache_path})")
|
84
|
+
try:
|
85
|
+
if not self.cache_path.exists():
|
86
|
+
return {}
|
87
|
+
|
88
|
+
if self.cache_path.stat().st_size == 0:
|
89
|
+
return {}
|
90
|
+
|
91
|
+
data: dict[str, str] = json.loads(self.cache_path.read_text())
|
92
|
+
if not keys:
|
93
|
+
return data
|
94
|
+
|
95
|
+
if set(data.keys()).issuperset(set(keys)):
|
96
|
+
return {k: v for k, v in data.items() if k in keys}
|
97
|
+
raise ValueError(f"Data must contain all keys: {keys}")
|
98
|
+
except Exception:
|
99
|
+
LOGGER.exception(f"Failed to read {self.cache_path.name}")
|
100
|
+
return {}
|
101
|
+
|
102
|
+
def write_to_cache(self, data: dict[str, str]) -> None:
|
103
|
+
"""Writes the data to the cache file."""
|
104
|
+
LOGGER.debug(f"Writing {self.name} to cache ({self.cache_path})")
|
105
|
+
|
106
|
+
# double check everything exists
|
107
|
+
if not self.cache_path.parent.exists():
|
108
|
+
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
|
109
|
+
|
110
|
+
if not self.cache_path.exists():
|
111
|
+
self.cache_path.touch()
|
112
|
+
|
113
|
+
existing_data = self.get_cached_data()
|
114
|
+
data = {**existing_data, **data}
|
115
|
+
|
116
|
+
self.cache_path.write_text(json.dumps(data, indent=4, default=str))
|
117
|
+
|
118
|
+
def clear_cache(self, keys: list[str] | None = None) -> None:
|
119
|
+
"""Deletes the cache file if it exists."""
|
120
|
+
if not self.cache_path.exists():
|
121
|
+
return
|
122
|
+
|
123
|
+
if not keys:
|
124
|
+
self.cache_path.unlink()
|
125
|
+
LOGGER.debug(f"{self.name} cache deleted")
|
126
|
+
return
|
127
|
+
|
128
|
+
assert isinstance(keys, list), "Keys must be a list"
|
129
|
+
|
130
|
+
data = self.get_cached_data()
|
131
|
+
for key in keys:
|
132
|
+
data.pop(key, None)
|
133
|
+
|
134
|
+
self.write_to_cache(data)
|
@@ -1,17 +1,16 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: otf-api
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.1
|
4
4
|
Summary: Python OrangeTheory Fitness API Client
|
5
5
|
License: MIT
|
6
6
|
Author: Jessica Smith
|
7
7
|
Author-email: j.smith.git1@gmail.com
|
8
|
-
Requires-Python: >=3.
|
8
|
+
Requires-Python: >=3.11,<4.0
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
10
10
|
Classifier: Intended Audience :: Developers
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
12
12
|
Classifier: Operating System :: OS Independent
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
17
16
|
Classifier: Programming Language :: Python :: 3.13
|
@@ -19,12 +18,15 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
18
|
Classifier: Topic :: Software Development :: Libraries
|
20
19
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
21
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
22
|
-
Requires-Dist:
|
21
|
+
Requires-Dist: attrs (>=24.3.0,<25.0.0)
|
22
|
+
Requires-Dist: httpx (>=0.27.2)
|
23
23
|
Requires-Dist: humanize (>=4.9.0,<5.0.0)
|
24
24
|
Requires-Dist: inflection (==0.5.*)
|
25
|
+
Requires-Dist: mypy-boto3-cognito-idp (>=1.35.93,<2.0.0)
|
25
26
|
Requires-Dist: pint (==0.24.*)
|
26
27
|
Requires-Dist: pycognito (==2024.5.1)
|
27
|
-
Requires-Dist: pydantic (
|
28
|
+
Requires-Dist: pydantic (>=2.7.3)
|
29
|
+
Requires-Dist: yarl (>=1.18.3,<2.0.0)
|
28
30
|
Project-URL: Documentation, https://otf-api.readthedocs.io/en/stable/
|
29
31
|
Description-Content-Type: text/markdown
|
30
32
|
|
@@ -42,11 +44,20 @@ pip install otf-api
|
|
42
44
|
|
43
45
|
## Overview
|
44
46
|
|
45
|
-
To use the API, you need to create an instance of the `Otf` class
|
47
|
+
To use the API, you need to create an instance of the `Otf` class. This will authenticate you with the API and allow you to make requests. When the `Otf` object is created it automatically grabs your member details and home studio, to simplify the process of making requests.
|
46
48
|
|
49
|
+
You can either pass an `OtfUser` object to the `OtfClass` or you can pass nothing and allow it to prompt you for your username and password.
|
47
50
|
|
48
|
-
|
51
|
+
You can also export environment variables `OTF_EMAIL` and `OTF_PASSWORD` to get these from the environment.
|
49
52
|
|
50
|
-
|
51
|
-
|
53
|
+
```python
|
54
|
+
from otf_api import Otf, OtfUser
|
55
|
+
|
56
|
+
otf = Otf()
|
57
|
+
|
58
|
+
# OR
|
59
|
+
|
60
|
+
otf = Otf(user=OtfUser(<email_address>,<password>))
|
61
|
+
|
62
|
+
```
|
52
63
|
|