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.
Files changed (38) hide show
  1. otf_api/__init__.py +70 -0
  2. otf_api/__version__.py +1 -0
  3. otf_api/api.py +143 -0
  4. otf_api/classes_api.py +44 -0
  5. otf_api/member_api.py +380 -0
  6. otf_api/models/__init__.py +63 -0
  7. otf_api/models/auth.py +141 -0
  8. otf_api/models/base.py +7 -0
  9. otf_api/models/responses/__init__.py +60 -0
  10. otf_api/models/responses/bookings.py +130 -0
  11. otf_api/models/responses/challenge_tracker_content.py +38 -0
  12. otf_api/models/responses/challenge_tracker_detail.py +68 -0
  13. otf_api/models/responses/classes.py +57 -0
  14. otf_api/models/responses/enums.py +87 -0
  15. otf_api/models/responses/favorite_studios.py +106 -0
  16. otf_api/models/responses/latest_agreement.py +21 -0
  17. otf_api/models/responses/member_detail.py +134 -0
  18. otf_api/models/responses/member_membership.py +25 -0
  19. otf_api/models/responses/member_purchases.py +135 -0
  20. otf_api/models/responses/out_of_studio_workout_history.py +41 -0
  21. otf_api/models/responses/performance_summary_detail.py +77 -0
  22. otf_api/models/responses/performance_summary_list.py +67 -0
  23. otf_api/models/responses/studio_detail.py +111 -0
  24. otf_api/models/responses/studio_services.py +57 -0
  25. otf_api/models/responses/telemetry.py +53 -0
  26. otf_api/models/responses/telemetry_hr_history.py +34 -0
  27. otf_api/models/responses/telemetry_max_hr.py +13 -0
  28. otf_api/models/responses/total_classes.py +8 -0
  29. otf_api/models/responses/workouts.py +78 -0
  30. otf_api/performance_api.py +54 -0
  31. otf_api/py.typed +0 -0
  32. otf_api/studios_api.py +96 -0
  33. otf_api/telemetry_api.py +95 -0
  34. otf_api-0.2.0.dist-info/AUTHORS.md +9 -0
  35. otf_api-0.2.0.dist-info/LICENSE +21 -0
  36. otf_api-0.2.0.dist-info/METADATA +28 -0
  37. otf_api-0.2.0.dist-info/RECORD +38 -0
  38. 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)
@@ -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,9 @@
1
+ # Credits
2
+
3
+ ## Development Lead
4
+
5
+ * Jessica Smith <j.smith.git1@gmail.com>
6
+
7
+ ## Contributors
8
+
9
+ None yet. Why not be the first?
@@ -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
+