otf-api 0.2.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 +70 -0
- otf_api/__version__.py +1 -0
- otf_api/api.py +143 -0
- otf_api/classes_api.py +44 -0
- otf_api/member_api.py +380 -0
- otf_api/models/__init__.py +63 -0
- otf_api/models/auth.py +141 -0
- otf_api/models/base.py +7 -0
- otf_api/models/responses/__init__.py +60 -0
- otf_api/models/responses/bookings.py +130 -0
- otf_api/models/responses/challenge_tracker_content.py +38 -0
- otf_api/models/responses/challenge_tracker_detail.py +68 -0
- otf_api/models/responses/classes.py +57 -0
- otf_api/models/responses/enums.py +87 -0
- otf_api/models/responses/favorite_studios.py +106 -0
- otf_api/models/responses/latest_agreement.py +21 -0
- otf_api/models/responses/member_detail.py +134 -0
- otf_api/models/responses/member_membership.py +25 -0
- otf_api/models/responses/member_purchases.py +135 -0
- otf_api/models/responses/out_of_studio_workout_history.py +41 -0
- otf_api/models/responses/performance_summary_detail.py +77 -0
- otf_api/models/responses/performance_summary_list.py +67 -0
- otf_api/models/responses/studio_detail.py +111 -0
- otf_api/models/responses/studio_services.py +57 -0
- otf_api/models/responses/telemetry.py +53 -0
- otf_api/models/responses/telemetry_hr_history.py +34 -0
- otf_api/models/responses/telemetry_max_hr.py +13 -0
- otf_api/models/responses/total_classes.py +8 -0
- otf_api/models/responses/workouts.py +78 -0
- otf_api/performance_api.py +54 -0
- otf_api/py.typed +0 -0
- otf_api/studios_api.py +96 -0
- otf_api/telemetry_api.py +95 -0
- otf_api-0.2.0.dist-info/AUTHORS.md +9 -0
- otf_api-0.2.0.dist-info/LICENSE +21 -0
- otf_api-0.2.0.dist-info/METADATA +28 -0
- otf_api-0.2.0.dist-info/RECORD +38 -0
- otf_api-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from otf_api.models.base import OtfBaseModel
|
6
|
+
|
7
|
+
|
8
|
+
class Country(OtfBaseModel):
|
9
|
+
country_id: int = Field(..., alias="countryId")
|
10
|
+
country_currency_code: str = Field(..., alias="countryCurrencyCode")
|
11
|
+
country_currency_name: str = Field(..., alias="countryCurrencyName")
|
12
|
+
currency_alphabetic_code: str = Field(..., alias="currencyAlphabeticCode")
|
13
|
+
|
14
|
+
|
15
|
+
class StudioLocation(OtfBaseModel):
|
16
|
+
physical_address: str = Field(..., alias="physicalAddress")
|
17
|
+
physical_address2: str | None = Field(..., alias="physicalAddress2")
|
18
|
+
physical_city: str = Field(..., alias="physicalCity")
|
19
|
+
physical_state: str = Field(..., alias="physicalState")
|
20
|
+
physical_postal_code: str = Field(..., alias="physicalPostalCode")
|
21
|
+
physical_region: str = Field(..., alias="physicalRegion")
|
22
|
+
physical_country: str = Field(..., alias="physicalCountry")
|
23
|
+
country: Country
|
24
|
+
phone_number: str = Field(..., alias="phoneNumber")
|
25
|
+
latitude: float
|
26
|
+
longitude: float
|
27
|
+
|
28
|
+
|
29
|
+
class Language(OtfBaseModel):
|
30
|
+
language_id: None = Field(..., alias="languageId")
|
31
|
+
language_code: None = Field(..., alias="languageCode")
|
32
|
+
language_name: None = Field(..., alias="languageName")
|
33
|
+
|
34
|
+
|
35
|
+
class StudioLocationLocalized(OtfBaseModel):
|
36
|
+
language: Language
|
37
|
+
studio_name: None = Field(..., alias="studioName")
|
38
|
+
studio_address: None = Field(..., alias="studioAddress")
|
39
|
+
|
40
|
+
|
41
|
+
class StudioProfiles(OtfBaseModel):
|
42
|
+
is_web: bool = Field(..., alias="isWeb")
|
43
|
+
intro_capacity: int = Field(..., alias="introCapacity")
|
44
|
+
is_crm: bool | None = Field(..., alias="isCrm")
|
45
|
+
|
46
|
+
|
47
|
+
class SocialMediaLink(OtfBaseModel):
|
48
|
+
id: str
|
49
|
+
language_id: str = Field(..., alias="languageId")
|
50
|
+
name: str
|
51
|
+
value: str
|
52
|
+
|
53
|
+
|
54
|
+
class StudioDetail(OtfBaseModel):
|
55
|
+
studio_id: int = Field(..., alias="studioId")
|
56
|
+
studio_uuid: str = Field(..., alias="studioUUId")
|
57
|
+
mbo_studio_id: int | None = Field(..., alias="mboStudioId")
|
58
|
+
studio_number: str = Field(..., alias="studioNumber")
|
59
|
+
studio_name: str = Field(..., alias="studioName")
|
60
|
+
studio_physical_location_id: int = Field(..., alias="studioPhysicalLocationId")
|
61
|
+
time_zone: str | None = Field(..., alias="timeZone")
|
62
|
+
contact_email: str | None = Field(..., alias="contactEmail")
|
63
|
+
studio_token: str = Field(..., alias="studioToken")
|
64
|
+
environment: str
|
65
|
+
pricing_level: str | None = Field(..., alias="pricingLevel")
|
66
|
+
tax_rate: str | None = Field(..., alias="taxRate")
|
67
|
+
accepts_visa_master_card: bool = Field(..., alias="acceptsVisaMasterCard")
|
68
|
+
accepts_american_express: bool = Field(..., alias="acceptsAmericanExpress")
|
69
|
+
accepts_discover: bool = Field(..., alias="acceptsDiscover")
|
70
|
+
accepts_ach: bool = Field(..., alias="acceptsAch")
|
71
|
+
is_integrated: bool = Field(..., alias="isIntegrated")
|
72
|
+
description: str | None = None
|
73
|
+
studio_version: str | None = Field(..., alias="studioVersion")
|
74
|
+
studio_status: str = Field(..., alias="studioStatus")
|
75
|
+
open_date: datetime | None = Field(..., alias="openDate")
|
76
|
+
re_open_date: datetime | None = Field(..., alias="reOpenDate")
|
77
|
+
studio_type_id: int | None = Field(..., alias="studioTypeId")
|
78
|
+
pos_type_id: int | None = Field(..., alias="posTypeId")
|
79
|
+
market_id: int | None = Field(..., alias="marketId")
|
80
|
+
area_id: int | None = Field(..., alias="areaId")
|
81
|
+
state_id: int | None = Field(..., alias="stateId")
|
82
|
+
logo_url: str | None = Field(..., alias="logoUrl")
|
83
|
+
page_color1: str | None = Field(..., alias="pageColor1")
|
84
|
+
page_color2: str | None = Field(..., alias="pageColor2")
|
85
|
+
page_color3: str | None = Field(..., alias="pageColor3")
|
86
|
+
page_color4: str | None = Field(..., alias="pageColor4")
|
87
|
+
sms_package_enabled: bool | None = Field(..., alias="smsPackageEnabled")
|
88
|
+
allows_dashboard_access: bool | None = Field(..., alias="allowsDashboardAccess")
|
89
|
+
allows_cr_waitlist: bool = Field(..., alias="allowsCrWaitlist")
|
90
|
+
cr_waitlist_flag_last_updated: datetime | None = Field(..., alias="crWaitlistFlagLastUpdated")
|
91
|
+
royalty_rate: int | None = Field(..., alias="royaltyRate")
|
92
|
+
marketing_fund_rate: int | None = Field(..., alias="marketingFundRate")
|
93
|
+
commission_percent: int | None = Field(..., alias="commissionPercent")
|
94
|
+
is_mobile: bool | None = Field(..., alias="isMobile")
|
95
|
+
is_otbeat: bool | None = Field(..., alias="isOtbeat")
|
96
|
+
distance: float | None = None
|
97
|
+
studio_location: StudioLocation = Field(..., alias="studioLocation")
|
98
|
+
studio_location_localized: StudioLocationLocalized = Field(..., alias="studioLocationLocalized")
|
99
|
+
studio_profiles: StudioProfiles = Field(..., alias="studioProfiles")
|
100
|
+
social_media_links: list[SocialMediaLink] = Field(..., alias="socialMediaLinks")
|
101
|
+
|
102
|
+
|
103
|
+
class Pagination(OtfBaseModel):
|
104
|
+
page_index: int = Field(..., alias="pageIndex")
|
105
|
+
page_size: int = Field(..., alias="pageSize")
|
106
|
+
total_count: int = Field(..., alias="totalCount")
|
107
|
+
total_pages: int = Field(..., alias="totalPages")
|
108
|
+
|
109
|
+
|
110
|
+
class StudioDetailList(OtfBaseModel):
|
111
|
+
studios: list[StudioDetail]
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from otf_api.models.base import OtfBaseModel
|
6
|
+
|
7
|
+
|
8
|
+
class Currency(OtfBaseModel):
|
9
|
+
currency_alphabetic_code: str = Field(..., alias="currencyAlphabeticCode")
|
10
|
+
|
11
|
+
|
12
|
+
class DefaultCurrency(OtfBaseModel):
|
13
|
+
currency_id: int = Field(..., alias="currencyId")
|
14
|
+
currency: Currency
|
15
|
+
|
16
|
+
|
17
|
+
class Country(OtfBaseModel):
|
18
|
+
country_currency_code: str = Field(..., alias="countryCurrencyCode")
|
19
|
+
default_currency: DefaultCurrency = Field(..., alias="defaultCurrency")
|
20
|
+
|
21
|
+
|
22
|
+
class StudioLocation(OtfBaseModel):
|
23
|
+
studio_location_id: int = Field(..., alias="studioLocationId")
|
24
|
+
country: Country
|
25
|
+
|
26
|
+
|
27
|
+
class Studio(OtfBaseModel):
|
28
|
+
studio_id: int = Field(..., alias="studioId")
|
29
|
+
studio_location: StudioLocation = Field(..., alias="studioLocation")
|
30
|
+
|
31
|
+
|
32
|
+
class StudioService(OtfBaseModel):
|
33
|
+
service_id: int = Field(..., alias="serviceId")
|
34
|
+
service_uuid: str = Field(..., alias="serviceUUId")
|
35
|
+
studio_id: int = Field(..., alias="studioId")
|
36
|
+
name: str
|
37
|
+
price: str
|
38
|
+
qty: int
|
39
|
+
mbo_program_id: int = Field(..., alias="mboProgramId")
|
40
|
+
mbo_description_id: str = Field(..., alias="mboDescriptionId")
|
41
|
+
mbo_product_id: int = Field(..., alias="mboProductId")
|
42
|
+
online_price: str = Field(..., alias="onlinePrice")
|
43
|
+
tax_rate: str = Field(..., alias="taxRate")
|
44
|
+
current: bool
|
45
|
+
is_web: bool = Field(..., alias="isWeb")
|
46
|
+
is_crm: bool = Field(..., alias="isCrm")
|
47
|
+
is_mobile: bool = Field(..., alias="isMobile")
|
48
|
+
created_by: str = Field(..., alias="createdBy")
|
49
|
+
created_date: datetime = Field(..., alias="createdDate")
|
50
|
+
updated_by: str = Field(..., alias="updatedBy")
|
51
|
+
updated_date: datetime = Field(..., alias="updatedDate")
|
52
|
+
is_deleted: bool = Field(..., alias="isDeleted")
|
53
|
+
studio: Studio
|
54
|
+
|
55
|
+
|
56
|
+
class StudioServiceList(OtfBaseModel):
|
57
|
+
data: list[StudioService]
|
@@ -0,0 +1,53 @@
|
|
1
|
+
from datetime import datetime, timedelta
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pydantic import Field
|
5
|
+
|
6
|
+
from otf_api.models.base import OtfBaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class Zone(OtfBaseModel):
|
10
|
+
start_bpm: int = Field(..., alias="startBpm")
|
11
|
+
end_bpm: int = Field(..., alias="endBpm")
|
12
|
+
|
13
|
+
|
14
|
+
class Zones(OtfBaseModel):
|
15
|
+
gray: Zone
|
16
|
+
blue: Zone
|
17
|
+
green: Zone
|
18
|
+
orange: Zone
|
19
|
+
red: Zone
|
20
|
+
|
21
|
+
|
22
|
+
class TreadData(OtfBaseModel):
|
23
|
+
tread_speed: float = Field(..., alias="treadSpeed")
|
24
|
+
tread_incline: float = Field(..., alias="treadIncline")
|
25
|
+
agg_tread_distance: int = Field(..., alias="aggTreadDistance")
|
26
|
+
|
27
|
+
|
28
|
+
class TelemetryItem(OtfBaseModel):
|
29
|
+
relative_timestamp: int = Field(..., alias="relativeTimestamp")
|
30
|
+
hr: int
|
31
|
+
agg_splats: int = Field(..., alias="aggSplats")
|
32
|
+
agg_calories: int = Field(..., alias="aggCalories")
|
33
|
+
timestamp: datetime | None = Field(
|
34
|
+
None,
|
35
|
+
init_var=False,
|
36
|
+
description="The timestamp of the telemetry item, calculated from the class start time and relative timestamp.",
|
37
|
+
)
|
38
|
+
tread_data: TreadData | None = Field(None, alias="treadData")
|
39
|
+
|
40
|
+
|
41
|
+
class Telemetry(OtfBaseModel):
|
42
|
+
member_uuid: str = Field(..., alias="memberUuid")
|
43
|
+
class_history_uuid: str = Field(..., alias="classHistoryUuid")
|
44
|
+
class_start_time: datetime = Field(..., alias="classStartTime")
|
45
|
+
max_hr: int = Field(..., alias="maxHr")
|
46
|
+
zones: Zones
|
47
|
+
window_size: int = Field(..., alias="windowSize")
|
48
|
+
telemetry: list[TelemetryItem]
|
49
|
+
|
50
|
+
def __init__(self, **data: dict[str, Any]):
|
51
|
+
super().__init__(**data)
|
52
|
+
for telem in self.telemetry:
|
53
|
+
telem.timestamp = self.class_start_time + timedelta(seconds=telem.relative_timestamp)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from pydantic import Field
|
2
|
+
|
3
|
+
from otf_api.models.base import OtfBaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class MaxHr(OtfBaseModel):
|
7
|
+
type: str
|
8
|
+
value: int
|
9
|
+
|
10
|
+
|
11
|
+
class Zone(OtfBaseModel):
|
12
|
+
start_bpm: int = Field(..., alias="startBpm")
|
13
|
+
end_bpm: int = Field(..., alias="endBpm")
|
14
|
+
|
15
|
+
|
16
|
+
class Zones(OtfBaseModel):
|
17
|
+
gray: Zone
|
18
|
+
blue: Zone
|
19
|
+
green: Zone
|
20
|
+
orange: Zone
|
21
|
+
red: Zone
|
22
|
+
|
23
|
+
|
24
|
+
class HistoryItem(OtfBaseModel):
|
25
|
+
max_hr: MaxHr = Field(..., alias="maxHr")
|
26
|
+
zones: Zones
|
27
|
+
change_from_previous: int = Field(..., alias="changeFromPrevious")
|
28
|
+
change_bucket: str = Field(..., alias="changeBucket")
|
29
|
+
assigned_at: str = Field(..., alias="assignedAt")
|
30
|
+
|
31
|
+
|
32
|
+
class TelemetryHrHistory(OtfBaseModel):
|
33
|
+
member_uuid: str = Field(..., alias="memberUuid")
|
34
|
+
history: list[HistoryItem]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from pydantic import Field
|
2
|
+
|
3
|
+
from otf_api.models.base import OtfBaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class MaxHr(OtfBaseModel):
|
7
|
+
type: str
|
8
|
+
value: int
|
9
|
+
|
10
|
+
|
11
|
+
class TelemetryMaxHr(OtfBaseModel):
|
12
|
+
member_uuid: str = Field(..., alias="memberUuid")
|
13
|
+
max_hr: MaxHr = Field(..., alias="maxHr")
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from pydantic import Field
|
2
|
+
|
3
|
+
from otf_api.models.base import OtfBaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class TotalClasses(OtfBaseModel):
|
7
|
+
total_in_studio_classes_attended: int = Field(..., alias="totalInStudioClassesAttended")
|
8
|
+
total_otlive_classes_attended: int = Field(..., alias="totalOtliveClassesAttended")
|
@@ -0,0 +1,78 @@
|
|
1
|
+
from ast import literal_eval
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from pydantic import Field, PrivateAttr
|
6
|
+
|
7
|
+
from otf_api.models.base import OtfBaseModel
|
8
|
+
|
9
|
+
|
10
|
+
class WorkoutType(OtfBaseModel):
|
11
|
+
id: int
|
12
|
+
display_name: str = Field(..., alias="displayName")
|
13
|
+
icon: str
|
14
|
+
|
15
|
+
|
16
|
+
class Workout(OtfBaseModel):
|
17
|
+
studio_number: str = Field(..., alias="studioNumber")
|
18
|
+
studio_name: str = Field(..., alias="studioName")
|
19
|
+
class_type: str = Field(..., alias="classType")
|
20
|
+
active_time: int = Field(..., alias="activeTime")
|
21
|
+
coach: str
|
22
|
+
member_uuid: str = Field(..., alias="memberUuId")
|
23
|
+
class_date: datetime = Field(..., alias="classDate")
|
24
|
+
total_calories: int = Field(..., alias="totalCalories")
|
25
|
+
avg_hr: int = Field(..., alias="avgHr")
|
26
|
+
max_hr: int = Field(..., alias="maxHr")
|
27
|
+
avg_percent_hr: int = Field(..., alias="avgPercentHr")
|
28
|
+
max_percent_hr: int = Field(..., alias="maxPercentHr")
|
29
|
+
total_splat_points: int = Field(..., alias="totalSplatPoints")
|
30
|
+
red_zone_time_second: int = Field(..., alias="redZoneTimeSecond")
|
31
|
+
orange_zone_time_second: int = Field(..., alias="orangeZoneTimeSecond")
|
32
|
+
green_zone_time_second: int = Field(..., alias="greenZoneTimeSecond")
|
33
|
+
blue_zone_time_second: int = Field(..., alias="blueZoneTimeSecond")
|
34
|
+
black_zone_time_second: int = Field(..., alias="blackZoneTimeSecond")
|
35
|
+
step_count: int = Field(..., alias="stepCount")
|
36
|
+
class_history_uuid: str = Field(..., alias="classHistoryUuId")
|
37
|
+
class_id: str = Field(..., alias="classId")
|
38
|
+
date_created: datetime = Field(..., alias="dateCreated")
|
39
|
+
date_updated: datetime = Field(..., alias="dateUpdated")
|
40
|
+
is_intro: bool = Field(..., alias="isIntro")
|
41
|
+
is_leader: bool = Field(..., alias="isLeader")
|
42
|
+
member_email: None = Field(..., alias="memberEmail")
|
43
|
+
member_name: str = Field(..., alias="memberName")
|
44
|
+
member_performance_id: str = Field(..., alias="memberPerformanceId")
|
45
|
+
minute_by_minute_hr: list[int] = Field(
|
46
|
+
...,
|
47
|
+
alias="minuteByMinuteHr",
|
48
|
+
description="HR data for each minute of the workout. It is returned as a string literal, so it needs to be "
|
49
|
+
"evaluated to a list. If can't be parsed, it will return an empty list.",
|
50
|
+
)
|
51
|
+
source: str
|
52
|
+
studio_account_uuid: str = Field(..., alias="studioAccountUuId")
|
53
|
+
version: str
|
54
|
+
workout_type: WorkoutType = Field(..., alias="workoutType")
|
55
|
+
_minute_by_minute_raw: str | None = PrivateAttr(None)
|
56
|
+
|
57
|
+
@property
|
58
|
+
def active_time_minutes(self) -> int:
|
59
|
+
"""Get the active time in minutes."""
|
60
|
+
return self.active_time // 60
|
61
|
+
|
62
|
+
def __init__(self, **data: Any):
|
63
|
+
if "minuteByMinuteHr" in data:
|
64
|
+
try:
|
65
|
+
data["minuteByMinuteHr"] = literal_eval(data["minuteByMinuteHr"])
|
66
|
+
except (ValueError, SyntaxError):
|
67
|
+
data["minuteByMinuteHr"] = []
|
68
|
+
|
69
|
+
super().__init__(**data)
|
70
|
+
self._minute_by_minute_raw = data.get("minuteByMinuteHr")
|
71
|
+
|
72
|
+
|
73
|
+
class WorkoutList(OtfBaseModel):
|
74
|
+
workouts: list[Workout]
|
75
|
+
|
76
|
+
@property
|
77
|
+
def by_class_history_uuid(self) -> dict[str, Workout]:
|
78
|
+
return {workout.class_history_uuid: workout for workout in self.workouts}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import typing
|
2
|
+
|
3
|
+
from otf_api.models.responses.performance_summary_detail import PerformanceSummaryDetail
|
4
|
+
from otf_api.models.responses.performance_summary_list import PerformanceSummaryList
|
5
|
+
|
6
|
+
if typing.TYPE_CHECKING:
|
7
|
+
from otf_api import Api
|
8
|
+
|
9
|
+
|
10
|
+
class PerformanceApi:
|
11
|
+
def __init__(self, api: "Api"):
|
12
|
+
self._api = api
|
13
|
+
self.logger = api.logger
|
14
|
+
|
15
|
+
# simplify access to member_id and member_uuid
|
16
|
+
self._member_id = self._api.user.member_id
|
17
|
+
self._member_uuid = self._api.user.member_uuid
|
18
|
+
self._headers = {"koji-member-id": self._member_id, "koji-member-email": self._api.user.id_claims_data.email}
|
19
|
+
|
20
|
+
async def get_performance_summaries(self, limit: int = 30) -> PerformanceSummaryList:
|
21
|
+
"""Get a list of performance summaries for the authenticated user.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
limit (int): The maximum number of performance summaries to return. Defaults to 30.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
PerformanceSummaryList: A list of performance summaries.
|
28
|
+
|
29
|
+
Developer Notes:
|
30
|
+
---
|
31
|
+
In the app, this is referred to as 'getInStudioWorkoutHistory'.
|
32
|
+
|
33
|
+
"""
|
34
|
+
|
35
|
+
path = "/v1/performance-summaries"
|
36
|
+
params = {"limit": limit}
|
37
|
+
res = await self._api._performance_summary_request("GET", path, headers=self._headers, params=params)
|
38
|
+
retval = PerformanceSummaryList(summaries=res["items"])
|
39
|
+
return retval
|
40
|
+
|
41
|
+
async def get_performance_summary(self, performance_summary_id: str) -> PerformanceSummaryDetail:
|
42
|
+
"""Get a detailed performance summary for a given workout.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
performance_summary_id (str): The ID of the performance summary to retrieve.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
PerformanceSummaryDetail: A detailed performance summary.
|
49
|
+
"""
|
50
|
+
|
51
|
+
path = f"/v1/performance-summaries/{performance_summary_id}"
|
52
|
+
res = await self._api._performance_summary_request("GET", path, headers=self._headers)
|
53
|
+
retval = PerformanceSummaryDetail(**res)
|
54
|
+
return retval
|
otf_api/py.typed
ADDED
File without changes
|
otf_api/studios_api.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
import typing
|
2
|
+
|
3
|
+
from otf_api.models.responses.studio_detail import Pagination, StudioDetail, StudioDetailList
|
4
|
+
|
5
|
+
if typing.TYPE_CHECKING:
|
6
|
+
from otf_api import Api
|
7
|
+
|
8
|
+
|
9
|
+
class StudiosApi:
|
10
|
+
def __init__(self, api: "Api"):
|
11
|
+
self._api = api
|
12
|
+
self.logger = api.logger
|
13
|
+
|
14
|
+
# simplify access to member_id and member_uuid
|
15
|
+
self._member_id = self._api.user.member_id
|
16
|
+
self._member_uuid = self._api.user.member_uuid
|
17
|
+
|
18
|
+
async def get_studio_detail(self, studio_uuid: str | None = None) -> StudioDetail:
|
19
|
+
"""Get detailed information about a specific studio. If no studio UUID is provided, it will default to the
|
20
|
+
user's home studio.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
studio_uuid (str): Studio UUID to get details for. Defaults to None, which will default to the user's home\
|
24
|
+
studio.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
StudioDetail: Detailed information about the studio.
|
28
|
+
"""
|
29
|
+
studio_uuid = studio_uuid or self._api.home_studio.studio_uuid
|
30
|
+
|
31
|
+
path = f"/mobile/v1/studios/{studio_uuid}"
|
32
|
+
params = {"include": "locations"}
|
33
|
+
|
34
|
+
res = await self._api._default_request("GET", path, params=params)
|
35
|
+
return StudioDetail(**res["data"])
|
36
|
+
|
37
|
+
async def search_studios_by_geo(
|
38
|
+
self,
|
39
|
+
latitude: float | None = None,
|
40
|
+
longitude: float | None = None,
|
41
|
+
distance: float = 50,
|
42
|
+
page_index: int = 1,
|
43
|
+
page_size: int = 50,
|
44
|
+
) -> StudioDetailList:
|
45
|
+
"""Search for studios by geographic location.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
latitude (float, optional): Latitude of the location to search around, if None uses home studio latitude.
|
49
|
+
longitude (float, optional): Longitude of the location to search around, if None uses home studio longitude.
|
50
|
+
distance (float, optional): Distance in miles to search around the location. Defaults to 50.
|
51
|
+
page_index (int, optional): Page index to start at. Defaults to 1.
|
52
|
+
page_size (int, optional): Number of results per page. Defaults to 50.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
StudioDetailList: List of studios that match the search criteria.
|
56
|
+
|
57
|
+
Notes:
|
58
|
+
---
|
59
|
+
There does not seem to be a limit to the number of results that can be requested total or per page, the
|
60
|
+
library enforces a limit of 50 results per page to avoid potential rate limiting issues.
|
61
|
+
|
62
|
+
"""
|
63
|
+
path = "/mobile/v1/studios"
|
64
|
+
|
65
|
+
latitude = latitude or self._api.home_studio.studio_location.latitude
|
66
|
+
longitude = longitude or self._api.home_studio.studio_location.longitude
|
67
|
+
|
68
|
+
if page_size > 50:
|
69
|
+
self.logger.warning("The API does not support more than 50 results per page, limiting to 50.")
|
70
|
+
page_size = 50
|
71
|
+
|
72
|
+
if page_index < 1:
|
73
|
+
self.logger.warning("Page index must be greater than 0, setting to 1.")
|
74
|
+
page_index = 1
|
75
|
+
|
76
|
+
params = {
|
77
|
+
"pageIndex": page_index,
|
78
|
+
"pageSize": page_size,
|
79
|
+
"latitude": latitude,
|
80
|
+
"longitude": longitude,
|
81
|
+
"distance": distance,
|
82
|
+
}
|
83
|
+
|
84
|
+
all_results: list[StudioDetail] = []
|
85
|
+
|
86
|
+
while True:
|
87
|
+
res = await self._api._default_request("GET", path, params=params)
|
88
|
+
pagination = Pagination(**res["data"].pop("pagination"))
|
89
|
+
all_results.extend([StudioDetail(**studio) for studio in res["data"]["studios"]])
|
90
|
+
|
91
|
+
if len(all_results) == pagination.total_count:
|
92
|
+
break
|
93
|
+
|
94
|
+
params["pageIndex"] += 1
|
95
|
+
|
96
|
+
return StudioDetailList(studios=all_results)
|
otf_api/telemetry_api.py
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
import typing
|
2
|
+
from math import ceil
|
3
|
+
|
4
|
+
from otf_api.models.responses.telemetry import Telemetry
|
5
|
+
from otf_api.models.responses.telemetry_hr_history import TelemetryHrHistory
|
6
|
+
from otf_api.models.responses.telemetry_max_hr import TelemetryMaxHr
|
7
|
+
|
8
|
+
if typing.TYPE_CHECKING:
|
9
|
+
from otf_api import Api
|
10
|
+
|
11
|
+
|
12
|
+
class TelemtryApi:
|
13
|
+
def __init__(self, api: "Api"):
|
14
|
+
self._api = api
|
15
|
+
self.logger = api.logger
|
16
|
+
|
17
|
+
# simplify access to member_id and member_uuid
|
18
|
+
self._member_id = self._api.user.member_id
|
19
|
+
self._member_uuid = self._api.user.member_uuid
|
20
|
+
|
21
|
+
async def get_hr_history(self) -> TelemetryHrHistory:
|
22
|
+
"""Get the heartrate history for the user.
|
23
|
+
|
24
|
+
Returns a list of history items that contain the max heartrate, start/end bpm for each zone,
|
25
|
+
the change from the previous, the change bucket, and the assigned at time.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
TelemetryHrHistory: The heartrate history for the user.
|
29
|
+
|
30
|
+
"""
|
31
|
+
path = "/v1/physVars/maxHr/history"
|
32
|
+
|
33
|
+
params = {"memberUuid": self._member_id}
|
34
|
+
res = await self._api._telemetry_request("GET", path, params=params)
|
35
|
+
return TelemetryHrHistory(**res)
|
36
|
+
|
37
|
+
async def get_max_hr(self) -> TelemetryMaxHr:
|
38
|
+
"""Get the max heartrate for the user.
|
39
|
+
|
40
|
+
Returns a simple object that has the member_uuid and the max_hr.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
TelemetryMaxHr: The max heartrate for the user.
|
44
|
+
"""
|
45
|
+
path = "/v1/physVars/maxHr"
|
46
|
+
|
47
|
+
params = {"memberUuid": self._member_id}
|
48
|
+
|
49
|
+
res = await self._api._telemetry_request("GET", path, params=params)
|
50
|
+
return TelemetryMaxHr(**res)
|
51
|
+
|
52
|
+
async def get_telemetry(self, class_history_uuid: str, max_data_points: int = 0) -> Telemetry:
|
53
|
+
"""Get the telemetry for a class history.
|
54
|
+
|
55
|
+
This returns an object that contains the max heartrate, start/end bpm for each zone,
|
56
|
+
and a list of telemetry items that contain the heartrate, splat points, calories, and timestamp.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
class_history_uuid (str): The class history UUID.
|
60
|
+
max_data_points (int): The max data points to use for the telemetry. Default is 0, which will attempt to\
|
61
|
+
get the max data points from the workout. If the workout is not found, it will default to 120 data points.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
TelemetryItem: The telemetry for the class history.
|
65
|
+
|
66
|
+
"""
|
67
|
+
path = "/v1/performance/summary"
|
68
|
+
|
69
|
+
max_data_points = max_data_points or await self._get_max_data_points(class_history_uuid)
|
70
|
+
|
71
|
+
params = {"classHistoryUuid": class_history_uuid, "maxDataPoints": max_data_points}
|
72
|
+
res = await self._api._telemetry_request("GET", path, params=params)
|
73
|
+
return Telemetry(**res)
|
74
|
+
|
75
|
+
async def _get_max_data_points(self, class_history_uuid: str) -> int:
|
76
|
+
"""Get the max data points to use for the telemetry.
|
77
|
+
|
78
|
+
Attempts to get the amount of active time for the workout from the OT Live API. If the workout is not found,
|
79
|
+
it will default to 120 data points. If it is found, it will calculate the amount of data points needed based on
|
80
|
+
the active time. This should amount to a data point per 30 seconds, roughly.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
class_history_uuid (str): The class history UUID.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
int: The max data points to use.
|
87
|
+
"""
|
88
|
+
workouts = await self._api.member_api.get_workouts()
|
89
|
+
workout = workouts.by_class_history_uuid.get(class_history_uuid)
|
90
|
+
max_data_points = 120 if workout is None else ceil(active_time_to_data_points(workout.active_time))
|
91
|
+
return max_data_points
|
92
|
+
|
93
|
+
|
94
|
+
def active_time_to_data_points(active_time: int) -> float:
|
95
|
+
return active_time / 60 * 2
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024, Jessica Smith
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: otf-api
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: Python OrangeTheory Fitness API Client
|
5
|
+
License: MIT
|
6
|
+
Author: Jessica Smith
|
7
|
+
Author-email: j.smith.git1@gmail.com
|
8
|
+
Requires-Python: >=3.10,<4.0
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
21
|
+
Requires-Dist: aiohttp (==3.9.5)
|
22
|
+
Requires-Dist: loguru (==0.7.2)
|
23
|
+
Requires-Dist: pycognito (==2024.5.1)
|
24
|
+
Requires-Dist: pydantic (==2.7.3)
|
25
|
+
Description-Content-Type: text/markdown
|
26
|
+
|
27
|
+
Simple API client for interacting with the OrangeTheory APIs. This project is in now way affiliated with OrangeTheory Fitness.
|
28
|
+
|