otf-api 0.3.0__tar.gz → 0.5.0__tar.gz
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-0.3.0 → otf_api-0.5.0}/PKG-INFO +5 -6
- {otf_api-0.3.0 → otf_api-0.5.0}/README.md +1 -2
- {otf_api-0.3.0 → otf_api-0.5.0}/pyproject.toml +21 -22
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/__init__.py +4 -4
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/api.py +137 -132
- otf_api-0.5.0/src/otf_api/auth.py +315 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/cli/app.py +2 -7
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/cli/bookings.py +6 -6
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/__init__.py +27 -18
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/__init__.py +24 -18
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/performance_summary_list.py +2 -2
- otf_api-0.3.0/src/otf_api/models/auth.py +0 -147
- otf_api-0.3.0/src/otf_api/models/responses/workouts.py +0 -78
- {otf_api-0.3.0 → otf_api-0.5.0}/AUTHORS.md +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/LICENSE +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/cli/__init__.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/cli/_utilities.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/cli/prompts.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/base.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/body_composition_list.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/book_class.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/bookings.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/cancel_booking.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/challenge_tracker_content.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/challenge_tracker_detail.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/classes.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/enums.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/favorite_studios.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/latest_agreement.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/lifetime_stats.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/member_detail.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/member_membership.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/member_purchases.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/out_of_studio_workout_history.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/performance_summary_detail.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/studio_detail.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/studio_services.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/telemetry.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/telemetry_hr_history.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/telemetry_max_hr.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/models/responses/total_classes.py +0 -0
- {otf_api-0.3.0 → otf_api-0.5.0}/src/otf_api/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: otf-api
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Python OrangeTheory Fitness API Client
|
5
5
|
License: MIT
|
6
6
|
Author: Jessica Smith
|
@@ -22,13 +22,13 @@ Requires-Dist: aiohttp (==3.9.5)
|
|
22
22
|
Requires-Dist: humanize (>=4.9.0,<5.0.0)
|
23
23
|
Requires-Dist: inflection (==0.5.*)
|
24
24
|
Requires-Dist: loguru (==0.7.2)
|
25
|
-
Requires-Dist: pendulum
|
25
|
+
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
26
26
|
Requires-Dist: pint (==0.24.*)
|
27
27
|
Requires-Dist: pycognito (==2024.5.1)
|
28
28
|
Requires-Dist: pydantic (==2.7.3)
|
29
29
|
Requires-Dist: python-box (>=7.2.0,<8.0.0)
|
30
|
-
Requires-Dist: readchar
|
31
|
-
Requires-Dist: typer
|
30
|
+
Requires-Dist: readchar (>=4.1.0,<5.0.0)
|
31
|
+
Requires-Dist: typer (>=0.12.3,<0.13.0)
|
32
32
|
Project-URL: Documentation, https://otf-api.readthedocs.io/en/stable/
|
33
33
|
Description-Content-Type: text/markdown
|
34
34
|
|
@@ -44,12 +44,11 @@ pip install otf-api
|
|
44
44
|
|
45
45
|
## Overview
|
46
46
|
|
47
|
-
To use the API, you need to create an instance of the `
|
47
|
+
To use the API, you need to create an instance of the `Otf` class, providing your email address and password. 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.
|
48
48
|
|
49
49
|
|
50
50
|
See the [examples](./examples) for more information on how to use the API.
|
51
51
|
|
52
|
-
|
53
52
|
Disclaimer:
|
54
53
|
This project is in no way affiliated with OrangeTheory Fitness.
|
55
54
|
|
@@ -10,11 +10,10 @@ pip install otf-api
|
|
10
10
|
|
11
11
|
## Overview
|
12
12
|
|
13
|
-
To use the API, you need to create an instance of the `
|
13
|
+
To use the API, you need to create an instance of the `Otf` class, providing your email address and password. 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.
|
14
14
|
|
15
15
|
|
16
16
|
See the [examples](./examples) for more information on how to use the API.
|
17
17
|
|
18
|
-
|
19
18
|
Disclaimer:
|
20
19
|
This project is in no way affiliated with OrangeTheory Fitness.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "otf-api"
|
3
|
-
version = "0.
|
3
|
+
version = "0.5.0"
|
4
4
|
description = "Python OrangeTheory Fitness API Client"
|
5
5
|
authors = ["Jessica Smith <j.smith.git1@gmail.com>"]
|
6
6
|
license = "MIT"
|
@@ -23,36 +23,37 @@ classifiers = [
|
|
23
23
|
[tool.poetry.dependencies]
|
24
24
|
python = "^3.10"
|
25
25
|
aiohttp = "3.9.5"
|
26
|
-
loguru = "0.7.2"
|
27
|
-
pydantic = "2.7.3"
|
28
|
-
pycognito = "2024.5.1"
|
29
|
-
typer = { version = "^0.12.3", extras = ["cli"] }
|
30
|
-
pendulum = { version = "^3.0.0", extras = ["cli"] }
|
31
|
-
|
32
|
-
|
33
|
-
readchar = { version = "^4.1.0", extras = ["cli"] }
|
34
26
|
humanize = "^4.9.0"
|
35
|
-
python-box = "^7.2.0"
|
36
27
|
inflection = "0.5.*"
|
28
|
+
loguru = "0.7.2"
|
29
|
+
pendulum = "^3.0.0"
|
37
30
|
pint = "0.24.*"
|
31
|
+
pycognito = "2024.5.1"
|
32
|
+
pydantic = "2.7.3"
|
33
|
+
python-box = "^7.2.0"
|
34
|
+
readchar = "^4.1.0"
|
35
|
+
typer = "^0.12.3"
|
36
|
+
|
38
37
|
[tool.poetry.group.dev.dependencies]
|
38
|
+
aioresponses = "0.7.6"
|
39
|
+
black = "^24.4.2"
|
40
|
+
build = "1.2.1"
|
41
|
+
bump-my-version = "^0.23.0"
|
42
|
+
httpx = "^0.27.0"
|
43
|
+
mypy = "1.10.0"
|
44
|
+
pre-commit = "3.7.1"
|
39
45
|
pytest = "8.2.2"
|
40
|
-
pytest-loguru = "0.4.0"
|
41
46
|
pytest-asyncio = "0.23.7"
|
42
|
-
aioresponses = "0.7.6"
|
43
|
-
tox = "4.15.1"
|
44
47
|
pytest-cov = "5.0.0"
|
45
|
-
|
48
|
+
pytest-loguru = "0.4.0"
|
46
49
|
ruff = "0.4.9"
|
47
|
-
|
48
|
-
mypy = "1.10.0"
|
50
|
+
tox = "4.15.1"
|
49
51
|
twine = "5.1.0"
|
50
|
-
black = "^24.4.2"
|
51
|
-
httpx = "^0.27.0"
|
52
|
-
bump-my-version = "^0.23.0"
|
53
52
|
|
54
53
|
|
55
54
|
[tool.poetry.group.docs.dependencies]
|
55
|
+
griffe-fieldz = "0.1.2"
|
56
|
+
mike = "2.1.1"
|
56
57
|
mkdocs = "1.6.0"
|
57
58
|
mkdocs-autorefs = "1.0.1"
|
58
59
|
mkdocs-gen-files = "0.5.0"
|
@@ -64,10 +65,8 @@ mkdocs-material-extensions = "1.3.1"
|
|
64
65
|
mkdocs-section-index = "0.3.9"
|
65
66
|
mkdocstrings = "0.25.1"
|
66
67
|
mkdocstrings-python = "1.10.3"
|
67
|
-
griffe-fieldz = "0.1.2"
|
68
|
-
mike = "2.1.1"
|
69
|
-
setuptools = "^70.0.0"
|
70
68
|
pkginfo = "^1.11.1"
|
69
|
+
setuptools = "^70.0.0"
|
71
70
|
virtualenv = "^20.26.2"
|
72
71
|
|
73
72
|
|
@@ -3,13 +3,13 @@ import sys
|
|
3
3
|
|
4
4
|
from loguru import logger
|
5
5
|
|
6
|
-
from .api import
|
7
|
-
from .
|
6
|
+
from .api import Otf
|
7
|
+
from .auth import OtfUser
|
8
8
|
|
9
|
-
__version__ = "0.
|
9
|
+
__version__ = "0.5.0"
|
10
10
|
|
11
11
|
|
12
|
-
__all__ = ["
|
12
|
+
__all__ = ["Otf", "OtfUser"]
|
13
13
|
|
14
14
|
logger.remove()
|
15
15
|
logger.add(sink=sys.stdout, level=os.getenv("OTF_LOG_LEVEL", "INFO"))
|
@@ -3,42 +3,45 @@ import contextlib
|
|
3
3
|
import json
|
4
4
|
import typing
|
5
5
|
from datetime import date, datetime
|
6
|
-
from math import ceil
|
7
6
|
from typing import Any
|
8
7
|
|
9
8
|
import aiohttp
|
9
|
+
import requests
|
10
10
|
from loguru import logger
|
11
11
|
from yarl import URL
|
12
12
|
|
13
|
-
from otf_api.
|
14
|
-
from otf_api.models
|
15
|
-
|
16
|
-
|
17
|
-
from otf_api.models.responses.classes import ClassType, DoW, OtfClassList
|
18
|
-
from otf_api.models.responses.favorite_studios import FavoriteStudioList
|
19
|
-
from otf_api.models.responses.lifetime_stats import StatsResponse, StatsTime
|
20
|
-
from otf_api.models.responses.performance_summary_detail import PerformanceSummaryDetail
|
21
|
-
from otf_api.models.responses.performance_summary_list import PerformanceSummaryList
|
22
|
-
from otf_api.models.responses.studio_detail import Pagination, StudioDetail, StudioDetailList
|
23
|
-
from otf_api.models.responses.telemetry import Telemetry
|
24
|
-
from otf_api.models.responses.telemetry_hr_history import TelemetryHrHistory
|
25
|
-
from otf_api.models.responses.telemetry_max_hr import TelemetryMaxHr
|
26
|
-
|
27
|
-
from .models import (
|
13
|
+
from otf_api.auth import OtfUser
|
14
|
+
from otf_api.models import (
|
15
|
+
BodyCompositionList,
|
16
|
+
BookClass,
|
28
17
|
BookingList,
|
29
18
|
BookingStatus,
|
19
|
+
CancelBooking,
|
30
20
|
ChallengeTrackerContent,
|
31
21
|
ChallengeTrackerDetailList,
|
32
22
|
ChallengeType,
|
23
|
+
ClassType,
|
24
|
+
DoW,
|
33
25
|
EquipmentType,
|
26
|
+
FavoriteStudioList,
|
34
27
|
LatestAgreement,
|
35
28
|
MemberDetail,
|
36
29
|
MemberMembership,
|
37
30
|
MemberPurchaseList,
|
31
|
+
OtfClassList,
|
38
32
|
OutOfStudioWorkoutHistoryList,
|
33
|
+
Pagination,
|
34
|
+
PerformanceSummaryDetail,
|
35
|
+
PerformanceSummaryList,
|
36
|
+
StatsResponse,
|
37
|
+
StatsTime,
|
38
|
+
StudioDetail,
|
39
|
+
StudioDetailList,
|
39
40
|
StudioServiceList,
|
41
|
+
Telemetry,
|
42
|
+
TelemetryHrHistory,
|
43
|
+
TelemetryMaxHr,
|
40
44
|
TotalClasses,
|
41
|
-
WorkoutList,
|
42
45
|
)
|
43
46
|
|
44
47
|
|
@@ -56,40 +59,55 @@ API_TELEMETRY_BASE_URL = "api.yuzu.orangetheory.com"
|
|
56
59
|
REQUEST_HEADERS = {"Authorization": None, "Content-Type": "application/json", "Accept": "application/json"}
|
57
60
|
|
58
61
|
|
59
|
-
class
|
60
|
-
""
|
62
|
+
class Otf:
|
63
|
+
logger: "Logger" = logger
|
64
|
+
user: OtfUser
|
65
|
+
_session: aiohttp.ClientSession
|
61
66
|
|
62
|
-
|
67
|
+
def __init__(
|
68
|
+
self,
|
69
|
+
username: str | None = None,
|
70
|
+
password: str | None = None,
|
71
|
+
access_token: str | None = None,
|
72
|
+
id_token: str | None = None,
|
73
|
+
refresh_token: str | None = None,
|
74
|
+
device_key: str | None = None,
|
75
|
+
user: OtfUser | None = None,
|
76
|
+
):
|
77
|
+
"""Create a new Otf instance.
|
78
|
+
|
79
|
+
Authentication methods:
|
63
80
|
---
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
async def main():
|
69
|
-
otf = await Api.create("username", "password")
|
70
|
-
print(otf.member)
|
81
|
+
- Provide a username and password.
|
82
|
+
- Provide an access token and id token.
|
83
|
+
- Provide a user object.
|
71
84
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
85
|
+
Args:
|
86
|
+
username (str, optional): The username of the user. Default is None.
|
87
|
+
password (str, optional): The password of the user. Default is None.
|
88
|
+
access_token (str, optional): The access token. Default is None.
|
89
|
+
id_token (str, optional): The id token. Default is None.
|
90
|
+
refresh_token (str, optional): The refresh token. Default is None.
|
91
|
+
device_key (str, optional): The device key. Default is None.
|
92
|
+
user (OtfUser, optional): A user object. Default is None.
|
93
|
+
"""
|
80
94
|
|
81
|
-
def __init__(self, username: str, password: str):
|
82
95
|
self.member: MemberDetail
|
83
|
-
self.
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
96
|
+
self.home_studio_uuid: str
|
97
|
+
|
98
|
+
if user:
|
99
|
+
self.user = user
|
100
|
+
elif username and password or (access_token and id_token):
|
101
|
+
self.user = OtfUser(
|
102
|
+
username=username,
|
103
|
+
password=password,
|
104
|
+
access_token=access_token,
|
105
|
+
id_token=id_token,
|
106
|
+
refresh_token=refresh_token,
|
107
|
+
device_key=device_key,
|
108
|
+
)
|
109
|
+
else:
|
110
|
+
raise ValueError("No valid authentication method provided")
|
93
111
|
|
94
112
|
# simplify access to member_id and member_uuid
|
95
113
|
self._member_id = self.user.member_id
|
@@ -98,26 +116,48 @@ class Api:
|
|
98
116
|
"koji-member-id": self._member_id,
|
99
117
|
"koji-member-email": self.user.id_claims_data.email,
|
100
118
|
}
|
119
|
+
self.member = self._get_member_details_sync()
|
120
|
+
self.home_studio_uuid = self.member.home_studio.studio_uuid
|
101
121
|
|
102
|
-
|
103
|
-
|
104
|
-
"""Create a new API instance. The username and password are required arguments because even though
|
105
|
-
we cache the token, they expire so quickly that we usually end up needing to re-authenticate.
|
122
|
+
def _get_member_details_sync(self) -> MemberDetail:
|
123
|
+
"""Get the member details synchronously.
|
106
124
|
|
107
|
-
|
108
|
-
|
109
|
-
|
125
|
+
This is used to get the member details when the API is first initialized, to let use initialize
|
126
|
+
without needing to await the member details.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
MemberDetail: The member details.
|
110
130
|
"""
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
131
|
+
url = f"https://{API_BASE_URL}/member/members/{self._member_id}"
|
132
|
+
resp = requests.get(url, headers=self.headers)
|
133
|
+
return MemberDetail(**resp.json()["data"])
|
134
|
+
|
135
|
+
@property
|
136
|
+
def headers(self) -> dict[str, str]:
|
137
|
+
"""Get the headers for the API request."""
|
138
|
+
|
139
|
+
# check the token before making a request in case it has expired
|
140
|
+
|
141
|
+
self.user.cognito.check_token()
|
142
|
+
return {
|
143
|
+
"Authorization": f"Bearer {self.user.cognito.id_token}",
|
144
|
+
"Content-Type": "application/json",
|
145
|
+
"Accept": "application/json",
|
146
|
+
}
|
147
|
+
|
148
|
+
@property
|
149
|
+
def session(self) -> aiohttp.ClientSession:
|
150
|
+
"""Get the aiohttp session."""
|
151
|
+
if not getattr(self, "_session", None):
|
152
|
+
self._session = aiohttp.ClientSession(headers=self.headers)
|
153
|
+
|
154
|
+
return self._session
|
115
155
|
|
116
156
|
def __del__(self) -> None:
|
117
157
|
if not hasattr(self, "session"):
|
118
158
|
return
|
159
|
+
|
119
160
|
try:
|
120
|
-
loop = asyncio.get_event_loop()
|
121
161
|
asyncio.create_task(self._close_session()) # noqa
|
122
162
|
except RuntimeError:
|
123
163
|
loop = asyncio.new_event_loop()
|
@@ -145,6 +185,12 @@ class Api:
|
|
145
185
|
|
146
186
|
logger.debug(f"Making {method!r} request to {full_url}, params: {params}")
|
147
187
|
|
188
|
+
# ensure we have headers that contain the most up-to-date token
|
189
|
+
if not headers:
|
190
|
+
headers = self.headers
|
191
|
+
else:
|
192
|
+
headers.update(self.headers)
|
193
|
+
|
148
194
|
text = None
|
149
195
|
async with self.session.request(method, full_url, headers=headers, params=params, **kwargs) as response:
|
150
196
|
with contextlib.suppress(Exception):
|
@@ -155,10 +201,8 @@ class Api:
|
|
155
201
|
except aiohttp.ClientResponseError as e:
|
156
202
|
logger.exception(f"Error making request: {e}")
|
157
203
|
logger.exception(f"Response: {text}")
|
158
|
-
# raise
|
159
204
|
except Exception as e:
|
160
205
|
logger.exception(f"Error making request: {e}")
|
161
|
-
# raise
|
162
206
|
|
163
207
|
return await response.json()
|
164
208
|
|
@@ -180,32 +224,15 @@ class Api:
|
|
180
224
|
"""Perform an API request to the performance summary API."""
|
181
225
|
return await self._do(method, API_IO_BASE_URL, url, params, headers)
|
182
226
|
|
183
|
-
async def
|
184
|
-
"""Get the
|
227
|
+
async def get_body_composition_list(self) -> BodyCompositionList:
|
228
|
+
"""Get the member's body composition list.
|
185
229
|
|
186
230
|
Returns:
|
187
|
-
|
188
|
-
|
189
|
-
Info:
|
190
|
-
---
|
191
|
-
This returns data from the same api the [OT Live website](https://otlive.orangetheory.com/) uses.
|
192
|
-
It is quite a bit of data, and all workouts going back to ~2019. The data includes the class history
|
193
|
-
UUID, which can be used to get telemetry data for a specific workout.
|
231
|
+
Any: The member's body composition list.
|
194
232
|
"""
|
233
|
+
data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition")
|
195
234
|
|
196
|
-
|
197
|
-
|
198
|
-
return WorkoutList(workouts=res["data"])
|
199
|
-
|
200
|
-
async def get_total_classes(self) -> TotalClasses:
|
201
|
-
"""Get the member's total classes. This is a simple object reflecting the total number of classes attended,
|
202
|
-
both in-studio and OT Live.
|
203
|
-
|
204
|
-
Returns:
|
205
|
-
TotalClasses: The member's total classes.
|
206
|
-
"""
|
207
|
-
data = await self._default_request("GET", "/mobile/v1/members/classes/summary")
|
208
|
-
return TotalClasses(**data["data"])
|
235
|
+
return BodyCompositionList(data=data["data"])
|
209
236
|
|
210
237
|
async def get_classes(
|
211
238
|
self,
|
@@ -231,7 +258,7 @@ class Api:
|
|
231
258
|
start_date (str | None): The start date to get classes for, in the format "YYYY-MM-DD". Default is None.
|
232
259
|
end_date (str | None): The end date to get classes for, in the format "YYYY-MM-DD". Default is None.
|
233
260
|
limit (int | None): Limit the number of classes returned. Default is None.
|
234
|
-
class_type (ClassType | list[ClassType] | None): The class type to filter by. Default is None. Multiple
|
261
|
+
class_type (ClassType | list[ClassType] | None): The class type to filter by. Default is None. Multiple\
|
235
262
|
class types can be provided, if there are multiple there will be a call per class type.
|
236
263
|
exclude_cancelled (bool): Whether to exclude cancelled classes. Default is False.
|
237
264
|
day_of_week (list[DoW] | None): The days of the week to filter by. Default is None.
|
@@ -242,9 +269,9 @@ class Api:
|
|
242
269
|
"""
|
243
270
|
|
244
271
|
if not studio_uuids:
|
245
|
-
studio_uuids = [self.
|
246
|
-
elif include_home_studio and self.
|
247
|
-
studio_uuids.append(self.
|
272
|
+
studio_uuids = [self.home_studio_uuid]
|
273
|
+
elif include_home_studio and self.home_studio_uuid not in studio_uuids:
|
274
|
+
studio_uuids.append(self.home_studio_uuid)
|
248
275
|
|
249
276
|
path = "/v1/classes"
|
250
277
|
|
@@ -280,7 +307,7 @@ class Api:
|
|
280
307
|
classes_list.classes = [c for c in classes_list.classes if not c.canceled]
|
281
308
|
|
282
309
|
for otf_class in classes_list.classes:
|
283
|
-
otf_class.is_home_studio = otf_class.studio.id == self.
|
310
|
+
otf_class.is_home_studio = otf_class.studio.id == self.home_studio_uuid
|
284
311
|
|
285
312
|
if day_of_week:
|
286
313
|
classes_list.classes = [c for c in classes_list.classes if c.day_of_week_enum in day_of_week]
|
@@ -300,6 +327,16 @@ class Api:
|
|
300
327
|
|
301
328
|
return classes_list
|
302
329
|
|
330
|
+
async def get_total_classes(self) -> TotalClasses:
|
331
|
+
"""Get the member's total classes. This is a simple object reflecting the total number of classes attended,
|
332
|
+
both in-studio and OT Live.
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
TotalClasses: The member's total classes.
|
336
|
+
"""
|
337
|
+
data = await self._default_request("GET", "/mobile/v1/members/classes/summary")
|
338
|
+
return TotalClasses(**data["data"])
|
339
|
+
|
303
340
|
async def book_class(self, class_uuid: str) -> BookClass | typing.Any:
|
304
341
|
"""Book a class by class_uuid.
|
305
342
|
|
@@ -415,7 +452,7 @@ class Api:
|
|
415
452
|
for booking in data.bookings:
|
416
453
|
if not booking.otf_class:
|
417
454
|
continue
|
418
|
-
if booking.otf_class.studio.studio_uuid == self.
|
455
|
+
if booking.otf_class.studio.studio_uuid == self.home_studio_uuid:
|
419
456
|
booking.is_home_studio = True
|
420
457
|
else:
|
421
458
|
booking.is_home_studio = False
|
@@ -660,7 +697,7 @@ class Api:
|
|
660
697
|
Returns:
|
661
698
|
StudioServiceList: The services available at the studio.
|
662
699
|
"""
|
663
|
-
studio_uuid = studio_uuid or self.
|
700
|
+
studio_uuid = studio_uuid or self.home_studio_uuid
|
664
701
|
data = await self._default_request("GET", f"/member/studios/{studio_uuid}/services")
|
665
702
|
return StudioServiceList(data=data["data"])
|
666
703
|
|
@@ -711,7 +748,7 @@ class Api:
|
|
711
748
|
Returns:
|
712
749
|
StudioDetail: Detailed information about the studio.
|
713
750
|
"""
|
714
|
-
studio_uuid = studio_uuid or self.
|
751
|
+
studio_uuid = studio_uuid or self.home_studio_uuid
|
715
752
|
|
716
753
|
path = f"/mobile/v1/studios/{studio_uuid}"
|
717
754
|
params = {"include": "locations"}
|
@@ -747,8 +784,11 @@ class Api:
|
|
747
784
|
"""
|
748
785
|
path = "/mobile/v1/studios"
|
749
786
|
|
750
|
-
|
751
|
-
|
787
|
+
if not latitude and not longitude:
|
788
|
+
home_studio = await self.get_studio_detail()
|
789
|
+
|
790
|
+
latitude = home_studio.studio_location.latitude
|
791
|
+
longitude = home_studio.studio_location.longitude
|
752
792
|
|
753
793
|
if page_size > 50:
|
754
794
|
self.logger.warning("The API does not support more than 50 results per page, limiting to 50.")
|
@@ -811,16 +851,15 @@ class Api:
|
|
811
851
|
res = await self._telemetry_request("GET", path, params=params)
|
812
852
|
return TelemetryMaxHr(**res)
|
813
853
|
|
814
|
-
async def get_telemetry(self,
|
815
|
-
"""Get the telemetry for a
|
854
|
+
async def get_telemetry(self, performance_summary_id: str, max_data_points: int = 120) -> Telemetry:
|
855
|
+
"""Get the telemetry for a performance summary.
|
816
856
|
|
817
857
|
This returns an object that contains the max heartrate, start/end bpm for each zone,
|
818
858
|
and a list of telemetry items that contain the heartrate, splat points, calories, and timestamp.
|
819
859
|
|
820
860
|
Args:
|
821
|
-
|
822
|
-
max_data_points (int): The max data points to use for the telemetry. Default is
|
823
|
-
get the max data points from the workout. If the workout is not found, it will default to 120 data points.
|
861
|
+
performance_summary_id (str): The performance summary id.
|
862
|
+
max_data_points (int): The max data points to use for the telemetry. Default is 120.
|
824
863
|
|
825
864
|
Returns:
|
826
865
|
TelemetryItem: The telemetry for the class history.
|
@@ -828,30 +867,10 @@ class Api:
|
|
828
867
|
"""
|
829
868
|
path = "/v1/performance/summary"
|
830
869
|
|
831
|
-
|
832
|
-
|
833
|
-
params = {"classHistoryUuid": class_history_uuid, "maxDataPoints": max_data_points}
|
870
|
+
params = {"classHistoryUuid": performance_summary_id, "maxDataPoints": max_data_points}
|
834
871
|
res = await self._telemetry_request("GET", path, params=params)
|
835
872
|
return Telemetry(**res)
|
836
873
|
|
837
|
-
async def _get_max_data_points(self, class_history_uuid: str) -> int:
|
838
|
-
"""Get the max data points to use for the telemetry.
|
839
|
-
|
840
|
-
Attempts to get the amount of active time for the workout from the OT Live API. If the workout is not found,
|
841
|
-
it will default to 120 data points. If it is found, it will calculate the amount of data points needed based on
|
842
|
-
the active time. This should amount to a data point per 30 seconds, roughly.
|
843
|
-
|
844
|
-
Args:
|
845
|
-
class_history_uuid (str): The class history UUID.
|
846
|
-
|
847
|
-
Returns:
|
848
|
-
int: The max data points to use.
|
849
|
-
"""
|
850
|
-
workouts = await self.get_workouts()
|
851
|
-
workout = workouts.by_class_history_uuid.get(class_history_uuid)
|
852
|
-
max_data_points = 120 if workout is None else ceil(active_time_to_data_points(workout.active_time))
|
853
|
-
return max_data_points
|
854
|
-
|
855
874
|
# the below do not return any data for me, so I can't test them
|
856
875
|
|
857
876
|
async def _get_member_services(self, active_only: bool = True) -> typing.Any:
|
@@ -885,17 +904,3 @@ class Api:
|
|
885
904
|
|
886
905
|
data = self._default_request("GET", f"/member/wearables/{self._member_id}/wearable-daily", params=params)
|
887
906
|
return data
|
888
|
-
|
889
|
-
async def get_body_composition_list(self) -> BodyCompositionList:
|
890
|
-
"""Get the member's body composition list.
|
891
|
-
|
892
|
-
Returns:
|
893
|
-
Any: The member's body composition list.
|
894
|
-
"""
|
895
|
-
data = await self._default_request("GET", f"/member/members/{self._member_uuid}/body-composition")
|
896
|
-
|
897
|
-
return BodyCompositionList(data=data["data"])
|
898
|
-
|
899
|
-
|
900
|
-
def active_time_to_data_points(active_time: int) -> float:
|
901
|
-
return active_time / 60 * 2
|