otf-api 0.3.0__py3-none-any.whl → 0.5.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 +4 -4
- otf_api/api.py +137 -132
- otf_api/auth.py +315 -0
- otf_api/cli/app.py +2 -7
- otf_api/cli/bookings.py +6 -6
- otf_api/models/__init__.py +27 -18
- otf_api/models/responses/__init__.py +24 -18
- otf_api/models/responses/performance_summary_list.py +2 -2
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/METADATA +5 -6
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/RECORD +14 -15
- otf_api/models/auth.py +0 -147
- otf_api/models/responses/workouts.py +0 -78
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/AUTHORS.md +0 -0
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/LICENSE +0 -0
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/WHEEL +0 -0
- {otf_api-0.3.0.dist-info → otf_api-0.5.0.dist-info}/entry_points.txt +0 -0
otf_api/auth.py
ADDED
@@ -0,0 +1,315 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
import jwt
|
5
|
+
import pendulum
|
6
|
+
from loguru import logger
|
7
|
+
from pycognito import AWSSRP, Cognito, MFAChallengeException
|
8
|
+
from pycognito.exceptions import TokenVerificationException
|
9
|
+
from pydantic import Field
|
10
|
+
from pydantic.config import ConfigDict
|
11
|
+
|
12
|
+
from otf_api.models.base import OtfItemBase
|
13
|
+
|
14
|
+
if typing.TYPE_CHECKING:
|
15
|
+
from boto3.session import Session
|
16
|
+
from botocore.config import Config
|
17
|
+
|
18
|
+
CLIENT_ID = "65knvqta6p37efc2l3eh26pl5o" # from otlive
|
19
|
+
USER_POOL_ID = "us-east-1_dYDxUeyL1"
|
20
|
+
|
21
|
+
|
22
|
+
class OtfCognito(Cognito):
|
23
|
+
_device_key: str | None = None
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
user_pool_id: str,
|
28
|
+
client_id: str,
|
29
|
+
user_pool_region: str | None = None,
|
30
|
+
username: str | None = None,
|
31
|
+
id_token: str | None = None,
|
32
|
+
refresh_token: str | None = None,
|
33
|
+
access_token: str | None = None,
|
34
|
+
client_secret: str | None = None,
|
35
|
+
access_key: str | None = None,
|
36
|
+
secret_key: str | None = None,
|
37
|
+
session: "Session|None" = None,
|
38
|
+
botocore_config: "Config|None" = None,
|
39
|
+
boto3_client_kwargs: dict[str, Any] | None = None,
|
40
|
+
device_key: str | None = None,
|
41
|
+
):
|
42
|
+
super().__init__(
|
43
|
+
user_pool_id,
|
44
|
+
client_id,
|
45
|
+
user_pool_region=user_pool_region,
|
46
|
+
username=username,
|
47
|
+
id_token=id_token,
|
48
|
+
refresh_token=refresh_token,
|
49
|
+
access_token=access_token,
|
50
|
+
client_secret=client_secret,
|
51
|
+
access_key=access_key,
|
52
|
+
secret_key=secret_key,
|
53
|
+
session=session,
|
54
|
+
botocore_config=botocore_config,
|
55
|
+
boto3_client_kwargs=boto3_client_kwargs,
|
56
|
+
)
|
57
|
+
self.device_key = device_key
|
58
|
+
|
59
|
+
@property
|
60
|
+
def device_key(self) -> str | None:
|
61
|
+
return self._device_key
|
62
|
+
|
63
|
+
@device_key.setter
|
64
|
+
def device_key(self, value: str | None):
|
65
|
+
if not value:
|
66
|
+
if self._device_key:
|
67
|
+
logger.info("Clearing device key")
|
68
|
+
self._device_key = value
|
69
|
+
return
|
70
|
+
|
71
|
+
redacted_value = value[:4] + "*" * (len(value) - 8) + value[-4:]
|
72
|
+
logger.info(f"Setting device key: {redacted_value}")
|
73
|
+
self._device_key = value
|
74
|
+
|
75
|
+
def _set_tokens(self, tokens: dict[str, Any]):
|
76
|
+
"""Set the tokens and device metadata from the response.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
tokens (dict): The response from the Cognito service.
|
80
|
+
"""
|
81
|
+
super()._set_tokens(tokens)
|
82
|
+
|
83
|
+
if new_metadata := tokens["AuthenticationResult"].get("NewDeviceMetadata"):
|
84
|
+
self.device_key = new_metadata["DeviceKey"]
|
85
|
+
|
86
|
+
def authenticate(self, password: str, client_metadata: dict[str, Any] | None = None, device_key: str | None = None):
|
87
|
+
"""
|
88
|
+
Authenticate the user using the SRP protocol. Overridden to add `confirm_device` call.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
password (str): The user's password
|
92
|
+
client_metadata (dict, optional): Any additional client metadata to send to Cognito
|
93
|
+
"""
|
94
|
+
aws = AWSSRP(
|
95
|
+
username=self.username,
|
96
|
+
password=password,
|
97
|
+
pool_id=self.user_pool_id,
|
98
|
+
client_id=self.client_id,
|
99
|
+
client=self.client,
|
100
|
+
client_secret=self.client_secret,
|
101
|
+
)
|
102
|
+
try:
|
103
|
+
tokens = aws.authenticate_user(client_metadata=client_metadata)
|
104
|
+
except MFAChallengeException as mfa_challenge:
|
105
|
+
self.mfa_tokens = mfa_challenge.get_tokens()
|
106
|
+
raise mfa_challenge
|
107
|
+
|
108
|
+
# Set the tokens and device metadata
|
109
|
+
self._set_tokens(tokens)
|
110
|
+
|
111
|
+
if not device_key:
|
112
|
+
# Confirm the device so we can use the refresh token
|
113
|
+
aws.confirm_device(tokens)
|
114
|
+
else:
|
115
|
+
self.device_key = device_key
|
116
|
+
try:
|
117
|
+
self.renew_access_token()
|
118
|
+
except TokenVerificationException:
|
119
|
+
logger.error("Failed to renew access token. Confirming device.")
|
120
|
+
self.device_key = None
|
121
|
+
aws.confirm_device(tokens)
|
122
|
+
|
123
|
+
def check_token(self, renew: bool = True) -> bool:
|
124
|
+
"""
|
125
|
+
Checks the exp attribute of the access_token and either refreshes
|
126
|
+
the tokens by calling the renew_access_tokens method or does nothing
|
127
|
+
:param renew: bool indicating whether to refresh on expiration
|
128
|
+
:return: bool indicating whether access_token has expired
|
129
|
+
"""
|
130
|
+
if not self.access_token:
|
131
|
+
raise AttributeError("Access Token Required to Check Token")
|
132
|
+
now = pendulum.now()
|
133
|
+
dec_access_token = jwt.decode(self.access_token, options={"verify_signature": False})
|
134
|
+
|
135
|
+
exp = pendulum.DateTime.fromtimestamp(dec_access_token["exp"])
|
136
|
+
if now > exp.subtract(minutes=15):
|
137
|
+
expired = True
|
138
|
+
if renew:
|
139
|
+
self.renew_access_token()
|
140
|
+
else:
|
141
|
+
expired = False
|
142
|
+
return expired
|
143
|
+
|
144
|
+
def renew_access_token(self):
|
145
|
+
"""Sets a new access token on the User using the cached refresh token and device metadata."""
|
146
|
+
auth_params = {"REFRESH_TOKEN": self.refresh_token}
|
147
|
+
self._add_secret_hash(auth_params, "SECRET_HASH")
|
148
|
+
|
149
|
+
if self.device_key:
|
150
|
+
logger.info("Using device key for refresh token")
|
151
|
+
auth_params["DEVICE_KEY"] = self.device_key
|
152
|
+
|
153
|
+
refresh_response = self.client.initiate_auth(
|
154
|
+
ClientId=self.client_id, AuthFlow="REFRESH_TOKEN_AUTH", AuthParameters=auth_params
|
155
|
+
)
|
156
|
+
self._set_tokens(refresh_response)
|
157
|
+
|
158
|
+
@classmethod
|
159
|
+
def from_token(
|
160
|
+
cls, access_token: str, id_token: str, refresh_token: str | None = None, device_key: str | None = None
|
161
|
+
) -> "OtfCognito":
|
162
|
+
"""Create an OtfCognito instance from an id token.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
access_token (str): The access token.
|
166
|
+
id_token (str): The id token.
|
167
|
+
refresh_token (str, optional): The refresh token. Defaults to None.
|
168
|
+
device_key (str, optional): The device key. Defaults
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
OtfCognito: The user instance
|
172
|
+
"""
|
173
|
+
cognito = OtfCognito(
|
174
|
+
USER_POOL_ID,
|
175
|
+
CLIENT_ID,
|
176
|
+
access_token=access_token,
|
177
|
+
id_token=id_token,
|
178
|
+
refresh_token=refresh_token,
|
179
|
+
device_key=device_key,
|
180
|
+
)
|
181
|
+
cognito.verify_tokens()
|
182
|
+
cognito.check_token()
|
183
|
+
return cognito
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def login(cls, username: str, password: str) -> "OtfCognito":
|
187
|
+
"""Create an OtfCognito instance from a username and password.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
username (str): The username to login with.
|
191
|
+
password (str): The password to login with.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
OtfCognito: The logged in user.
|
195
|
+
"""
|
196
|
+
cognito_user = OtfCognito(USER_POOL_ID, CLIENT_ID, username=username)
|
197
|
+
cognito_user.authenticate(password)
|
198
|
+
cognito_user.check_token()
|
199
|
+
return cognito_user
|
200
|
+
|
201
|
+
|
202
|
+
class IdClaimsData(OtfItemBase):
|
203
|
+
sub: str
|
204
|
+
email_verified: bool
|
205
|
+
iss: str
|
206
|
+
cognito_username: str = Field(alias="cognito:username")
|
207
|
+
given_name: str
|
208
|
+
locale: str
|
209
|
+
home_studio_id: str = Field(alias="custom:home_studio_id")
|
210
|
+
aud: str
|
211
|
+
event_id: str
|
212
|
+
token_use: str
|
213
|
+
auth_time: int
|
214
|
+
exp: int
|
215
|
+
is_migration: str = Field(alias="custom:isMigration")
|
216
|
+
iat: int
|
217
|
+
family_name: str
|
218
|
+
email: str
|
219
|
+
koji_person_id: str = Field(alias="custom:koji_person_id")
|
220
|
+
|
221
|
+
@property
|
222
|
+
def member_uuid(self) -> str:
|
223
|
+
return self.cognito_username
|
224
|
+
|
225
|
+
@property
|
226
|
+
def full_name(self) -> str:
|
227
|
+
return f"{self.given_name} {self.family_name}"
|
228
|
+
|
229
|
+
|
230
|
+
class AccessClaimsData(OtfItemBase):
|
231
|
+
sub: str
|
232
|
+
device_key: str
|
233
|
+
iss: str
|
234
|
+
client_id: str
|
235
|
+
event_id: str
|
236
|
+
token_use: str
|
237
|
+
scope: str
|
238
|
+
auth_time: int
|
239
|
+
exp: int
|
240
|
+
iat: int
|
241
|
+
jti: str
|
242
|
+
username: str
|
243
|
+
|
244
|
+
@property
|
245
|
+
def member_uuid(self) -> str:
|
246
|
+
return self.username
|
247
|
+
|
248
|
+
|
249
|
+
class OtfUser(OtfItemBase):
|
250
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
251
|
+
cognito: OtfCognito
|
252
|
+
|
253
|
+
def __init__(
|
254
|
+
self,
|
255
|
+
username: str | None = None,
|
256
|
+
password: str | None = None,
|
257
|
+
id_token: str | None = None,
|
258
|
+
access_token: str | None = None,
|
259
|
+
refresh_token: str | None = None,
|
260
|
+
device_key: str | None = None,
|
261
|
+
cognito: OtfCognito | None = None,
|
262
|
+
):
|
263
|
+
"""Create a User instance.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
username (str, optional): The username to login with. Defaults to None.
|
267
|
+
password (str, optional): The password to login with. Defaults to None.
|
268
|
+
id_token (str, optional): The id token. Defaults to None.
|
269
|
+
access_token (str, optional): The access token. Defaults to None.
|
270
|
+
refresh_token (str, optional): The refresh token. Defaults to None.
|
271
|
+
device_key (str, optional): The device key. Defaults to None.
|
272
|
+
cognito (OtfCognito, optional): A Cognito instance. Defaults to None.
|
273
|
+
|
274
|
+
Raises:
|
275
|
+
ValueError: Must provide either username and password or id token
|
276
|
+
|
277
|
+
|
278
|
+
"""
|
279
|
+
if cognito:
|
280
|
+
cognito = cognito
|
281
|
+
elif username and password:
|
282
|
+
cognito = OtfCognito.login(username, password)
|
283
|
+
elif access_token and id_token:
|
284
|
+
cognito = OtfCognito.from_token(access_token, id_token, refresh_token, device_key)
|
285
|
+
else:
|
286
|
+
raise ValueError("Must provide either username and password or id token.")
|
287
|
+
|
288
|
+
super().__init__(cognito=cognito)
|
289
|
+
|
290
|
+
@property
|
291
|
+
def member_id(self) -> str:
|
292
|
+
return self.id_claims_data.cognito_username
|
293
|
+
|
294
|
+
@property
|
295
|
+
def member_uuid(self) -> str:
|
296
|
+
return self.access_claims_data.sub
|
297
|
+
|
298
|
+
@property
|
299
|
+
def access_claims_data(self) -> AccessClaimsData:
|
300
|
+
return AccessClaimsData(**self.cognito.access_claims)
|
301
|
+
|
302
|
+
@property
|
303
|
+
def id_claims_data(self) -> IdClaimsData:
|
304
|
+
return IdClaimsData(**self.cognito.id_claims)
|
305
|
+
|
306
|
+
def get_tokens(self) -> dict[str, str]:
|
307
|
+
return {
|
308
|
+
"id_token": self.cognito.id_token,
|
309
|
+
"access_token": self.cognito.access_token,
|
310
|
+
"refresh_token": self.cognito.refresh_token,
|
311
|
+
}
|
312
|
+
|
313
|
+
@property
|
314
|
+
def device_key(self) -> str:
|
315
|
+
return self.cognito.device_key
|
otf_api/cli/app.py
CHANGED
@@ -12,10 +12,9 @@ from rich.theme import Theme
|
|
12
12
|
|
13
13
|
import otf_api
|
14
14
|
from otf_api.cli._utilities import is_async_fn, with_cli_exception_handling
|
15
|
-
from otf_api.models.auth import User
|
16
15
|
|
17
16
|
if typing.TYPE_CHECKING:
|
18
|
-
from otf_api
|
17
|
+
from otf_api import Otf
|
19
18
|
|
20
19
|
|
21
20
|
class OutputType(str, Enum):
|
@@ -79,7 +78,7 @@ class AsyncTyper(typer.Typer):
|
|
79
78
|
self.console = Console(highlight=False, theme=theme, color_system="auto")
|
80
79
|
|
81
80
|
# TODO: clean these up later, just don't want warnings everywhere that these could be None
|
82
|
-
self.api:
|
81
|
+
self.api: Otf = None # type: ignore
|
83
82
|
self.username: str = None # type: ignore
|
84
83
|
self.password: str = None # type: ignore
|
85
84
|
self.output: OutputType = None # type: ignore
|
@@ -91,10 +90,6 @@ class AsyncTyper(typer.Typer):
|
|
91
90
|
self.username = username
|
92
91
|
return
|
93
92
|
|
94
|
-
if User.cache_file_exists():
|
95
|
-
self.username = User.username_from_disk()
|
96
|
-
return
|
97
|
-
|
98
93
|
raise ValueError("Username not provided and not found in cache")
|
99
94
|
|
100
95
|
def set_log_level(self, level: str) -> None:
|
otf_api/cli/bookings.py
CHANGED
@@ -56,7 +56,7 @@ async def list_bookings(
|
|
56
56
|
bk_status = BookingStatus.get_from_key_insensitive(status.value) if status else None
|
57
57
|
|
58
58
|
if not base_app.api:
|
59
|
-
base_app.api = await otf_api.
|
59
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
60
60
|
bookings = await base_app.api.get_bookings(start_date, end_date, bk_status, limit, exclude_cancelled)
|
61
61
|
|
62
62
|
if base_app.output == "json":
|
@@ -82,7 +82,7 @@ async def book(class_uuid: str = typer.Option(help="Class UUID to cancel")) -> N
|
|
82
82
|
logger.info(f"Booking class {class_uuid}")
|
83
83
|
|
84
84
|
if not base_app.api:
|
85
|
-
base_app.api = await otf_api.
|
85
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
86
86
|
booking = await base_app.api.book_class(class_uuid)
|
87
87
|
|
88
88
|
base_app.console.print(booking)
|
@@ -115,7 +115,7 @@ async def book_interactive(
|
|
115
115
|
class_type_enums = None
|
116
116
|
|
117
117
|
if not base_app.api:
|
118
|
-
base_app.api = await otf_api.
|
118
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
119
119
|
|
120
120
|
classes = await base_app.api.get_classes(
|
121
121
|
studio_uuids,
|
@@ -152,7 +152,7 @@ async def cancel_interactive() -> None:
|
|
152
152
|
|
153
153
|
with base_app.console.status("Getting bookings...", spinner="arc"):
|
154
154
|
if not base_app.api:
|
155
|
-
base_app.api = await otf_api.
|
155
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
156
156
|
bookings = await base_app.api.get_bookings()
|
157
157
|
|
158
158
|
result = prompt_select_from_table(
|
@@ -177,7 +177,7 @@ async def cancel(booking_uuid: str = typer.Option(help="Booking UUID to cancel")
|
|
177
177
|
logger.info(f"Cancelling booking {booking_uuid}")
|
178
178
|
|
179
179
|
if not base_app.api:
|
180
|
-
base_app.api = await otf_api.
|
180
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
181
181
|
booking = await base_app.api.cancel_booking(booking_uuid)
|
182
182
|
|
183
183
|
base_app.console.print(booking)
|
@@ -211,7 +211,7 @@ async def list_classes(
|
|
211
211
|
class_type_enum = ClassType.get_from_key_insensitive(class_type.value) if class_type else None
|
212
212
|
|
213
213
|
if not base_app.api:
|
214
|
-
base_app.api = await otf_api.
|
214
|
+
base_app.api = await otf_api.Otf.create(base_app.username, base_app.password)
|
215
215
|
classes = await base_app.api.get_classes(
|
216
216
|
studio_uuids, include_home_studio, start_date, end_date, limit, class_type_enum, exclude_cancelled
|
217
217
|
)
|
otf_api/models/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
from .auth import User
|
2
1
|
from .responses import (
|
2
|
+
BodyCompositionList,
|
3
3
|
BookClass,
|
4
4
|
BookingList,
|
5
5
|
BookingStatus,
|
@@ -7,6 +7,8 @@ from .responses import (
|
|
7
7
|
ChallengeTrackerContent,
|
8
8
|
ChallengeTrackerDetailList,
|
9
9
|
ChallengeType,
|
10
|
+
ClassType,
|
11
|
+
DoW,
|
10
12
|
EquipmentType,
|
11
13
|
FavoriteStudioList,
|
12
14
|
HistoryClassStatus,
|
@@ -16,8 +18,11 @@ from .responses import (
|
|
16
18
|
MemberPurchaseList,
|
17
19
|
OtfClassList,
|
18
20
|
OutOfStudioWorkoutHistoryList,
|
21
|
+
Pagination,
|
19
22
|
PerformanceSummaryDetail,
|
20
23
|
PerformanceSummaryList,
|
24
|
+
StatsResponse,
|
25
|
+
StatsTime,
|
21
26
|
StudioDetail,
|
22
27
|
StudioDetailList,
|
23
28
|
StudioServiceList,
|
@@ -25,35 +30,39 @@ from .responses import (
|
|
25
30
|
TelemetryHrHistory,
|
26
31
|
TelemetryMaxHr,
|
27
32
|
TotalClasses,
|
28
|
-
WorkoutList,
|
29
33
|
)
|
30
34
|
|
31
35
|
__all__ = [
|
32
|
-
"
|
33
|
-
"
|
34
|
-
"BookingStatus",
|
35
|
-
"EquipmentType",
|
36
|
-
"HistoryClassStatus",
|
36
|
+
"BodyCompositionList",
|
37
|
+
"BookClass",
|
37
38
|
"BookingList",
|
39
|
+
"BookingStatus",
|
40
|
+
"CancelBooking",
|
38
41
|
"ChallengeTrackerContent",
|
39
42
|
"ChallengeTrackerDetailList",
|
43
|
+
"ChallengeType",
|
44
|
+
"ClassType",
|
45
|
+
"DoW",
|
46
|
+
"EquipmentType",
|
47
|
+
"FavoriteStudioList",
|
48
|
+
"HistoryClassStatus",
|
40
49
|
"LatestAgreement",
|
41
50
|
"MemberDetail",
|
42
51
|
"MemberMembership",
|
43
52
|
"MemberPurchaseList",
|
53
|
+
"OtfClassList",
|
44
54
|
"OutOfStudioWorkoutHistoryList",
|
55
|
+
"Pagination",
|
56
|
+
"PerformanceSummaryDetail",
|
57
|
+
"PerformanceSummaryList",
|
58
|
+
"StatsResponse",
|
59
|
+
"StatsTime",
|
60
|
+
"StudioDetail",
|
61
|
+
"StudioDetailList",
|
45
62
|
"StudioServiceList",
|
46
|
-
"
|
47
|
-
"WorkoutList",
|
48
|
-
"FavoriteStudioList",
|
49
|
-
"OtfClassList",
|
50
|
-
"TelemetryHrHistory",
|
63
|
+
"StudioStatus",
|
51
64
|
"Telemetry",
|
65
|
+
"TelemetryHrHistory",
|
52
66
|
"TelemetryMaxHr",
|
53
|
-
"
|
54
|
-
"StudioDetailList",
|
55
|
-
"PerformanceSummaryDetail",
|
56
|
-
"PerformanceSummaryList",
|
57
|
-
"BookClass",
|
58
|
-
"CancelBooking",
|
67
|
+
"TotalClasses",
|
59
68
|
]
|
@@ -1,52 +1,58 @@
|
|
1
|
+
from .body_composition_list import BodyCompositionList
|
1
2
|
from .book_class import BookClass
|
2
3
|
from .bookings import BookingList, BookingStatus
|
3
4
|
from .cancel_booking import CancelBooking
|
4
5
|
from .challenge_tracker_content import ChallengeTrackerContent
|
5
6
|
from .challenge_tracker_detail import ChallengeTrackerDetailList
|
6
|
-
from .classes import OtfClassList
|
7
|
+
from .classes import ClassType, DoW, OtfClassList
|
7
8
|
from .enums import ChallengeType, EquipmentType, HistoryClassStatus
|
8
9
|
from .favorite_studios import FavoriteStudioList
|
9
10
|
from .latest_agreement import LatestAgreement
|
11
|
+
from .lifetime_stats import StatsResponse, StatsTime
|
10
12
|
from .member_detail import MemberDetail
|
11
13
|
from .member_membership import MemberMembership
|
12
14
|
from .member_purchases import MemberPurchaseList
|
13
15
|
from .out_of_studio_workout_history import OutOfStudioWorkoutHistoryList
|
14
16
|
from .performance_summary_detail import PerformanceSummaryDetail
|
15
17
|
from .performance_summary_list import PerformanceSummaryList
|
16
|
-
from .studio_detail import StudioDetail, StudioDetailList
|
18
|
+
from .studio_detail import Pagination, StudioDetail, StudioDetailList
|
17
19
|
from .studio_services import StudioServiceList
|
18
20
|
from .telemetry import Telemetry
|
19
21
|
from .telemetry_hr_history import TelemetryHrHistory
|
20
22
|
from .telemetry_max_hr import TelemetryMaxHr
|
21
23
|
from .total_classes import TotalClasses
|
22
|
-
from .workouts import WorkoutList
|
23
24
|
|
24
25
|
__all__ = [
|
26
|
+
"BodyCompositionList",
|
27
|
+
"BookClass",
|
25
28
|
"BookingList",
|
29
|
+
"BookingStatus",
|
30
|
+
"CancelBooking",
|
26
31
|
"ChallengeTrackerContent",
|
27
32
|
"ChallengeTrackerDetailList",
|
33
|
+
"ChallengeType",
|
34
|
+
"ClassType",
|
35
|
+
"DoW",
|
36
|
+
"EquipmentType",
|
37
|
+
"FavoriteStudioList",
|
38
|
+
"HistoryClassStatus",
|
28
39
|
"LatestAgreement",
|
29
40
|
"MemberDetail",
|
30
41
|
"MemberMembership",
|
31
42
|
"MemberPurchaseList",
|
43
|
+
"OtfClassList",
|
32
44
|
"OutOfStudioWorkoutHistoryList",
|
45
|
+
"Pagination",
|
46
|
+
"PerformanceSummaryDetail",
|
47
|
+
"PerformanceSummaryList",
|
48
|
+
"StatsResponse",
|
49
|
+
"StatsTime",
|
50
|
+
"StudioDetail",
|
51
|
+
"StudioDetailList",
|
33
52
|
"StudioServiceList",
|
34
|
-
"TotalClasses",
|
35
|
-
"WorkoutList",
|
36
|
-
"ChallengeType",
|
37
|
-
"BookingStatus",
|
38
|
-
"EquipmentType",
|
39
|
-
"HistoryClassStatus",
|
40
53
|
"StudioStatus",
|
41
|
-
"FavoriteStudioList",
|
42
|
-
"OtfClassList",
|
43
|
-
"TelemetryHrHistory",
|
44
54
|
"Telemetry",
|
55
|
+
"TelemetryHrHistory",
|
45
56
|
"TelemetryMaxHr",
|
46
|
-
"
|
47
|
-
"StudioDetailList",
|
48
|
-
"PerformanceSummaryDetail",
|
49
|
-
"PerformanceSummaryList",
|
50
|
-
"BookClass",
|
51
|
-
"CancelBooking",
|
57
|
+
"TotalClasses",
|
52
58
|
]
|
@@ -33,7 +33,7 @@ class Studio(OtfItemBase):
|
|
33
33
|
class Class(OtfItemBase):
|
34
34
|
ot_base_class_uuid: str | None = None
|
35
35
|
starts_at_local: str
|
36
|
-
name: str
|
36
|
+
name: str | None = None
|
37
37
|
coach: Coach
|
38
38
|
studio: Studio
|
39
39
|
|
@@ -56,7 +56,7 @@ class Ratings(OtfItemBase):
|
|
56
56
|
|
57
57
|
|
58
58
|
class PerformanceSummaryEntry(OtfItemBase):
|
59
|
-
|
59
|
+
id: str = Field(..., alias="id")
|
60
60
|
details: Details
|
61
61
|
ratable: bool
|
62
62
|
otf_class: Class = Field(..., alias="class")
|
@@ -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
|
|
@@ -1,14 +1,14 @@
|
|
1
|
-
otf_api/__init__.py,sha256=
|
2
|
-
otf_api/api.py,sha256=
|
1
|
+
otf_api/__init__.py,sha256=Co48iPZnr9aWJ_ZXqQwnCvhd1kwgn_wGyPiFWMr5eGs,237
|
2
|
+
otf_api/api.py,sha256=QlRHXDwzbB97aCfacb_9FVox-5AuPLMweRQAZkz-EE0,34885
|
3
|
+
otf_api/auth.py,sha256=XzwLSi5M3DyG7bE7DmWAzXF2y6fkJyAZxHUA9lpW25M,10231
|
3
4
|
otf_api/cli/__init__.py,sha256=WI-882LPH7Tj_ygDHqE5ehsas_u7m3ulsplS9vXKByk,151
|
4
5
|
otf_api/cli/_utilities.py,sha256=epjEO9S6ag4HgJLXlTpCQXfdVQkqGWyNavp7DjwPL78,1753
|
5
|
-
otf_api/cli/app.py,sha256=
|
6
|
-
otf_api/cli/bookings.py,sha256=
|
6
|
+
otf_api/cli/app.py,sha256=88TuMwq3foRr1Cui0V3h0mxNkoANqd6QQifI9CIgLvI,6469
|
7
|
+
otf_api/cli/bookings.py,sha256=wSmZA-03etcL6Tvb1vDSvHZW8EA9CZUgKX6W1pps3Yw,8161
|
7
8
|
otf_api/cli/prompts.py,sha256=iyodQXVa5v9VsrMxw0ob1okGRBDbWCSxhrNEylsOTEQ,5358
|
8
|
-
otf_api/models/__init__.py,sha256=
|
9
|
-
otf_api/models/auth.py,sha256=DyFManp5RlfQinA0rWG_PjBZXrn2TpSM1-PJ630lamo,4073
|
9
|
+
otf_api/models/__init__.py,sha256=3GHBOirQA4yu06cgD9pYmCU8u8_F9nxNHeSXDuFpe5A,1428
|
10
10
|
otf_api/models/base.py,sha256=oTDxyliK64GyTNx1bGTd-b9dfVn0r3YPpSycs2qEuIw,7285
|
11
|
-
otf_api/models/responses/__init__.py,sha256=
|
11
|
+
otf_api/models/responses/__init__.py,sha256=xxwz-JwRd0upmI0VNdvInbAm2FOQvPo3pS0SEhWfkI4,1947
|
12
12
|
otf_api/models/responses/body_composition_list.py,sha256=RTC5bQpmMDUKqFl0nGFExdDxfnbOAGoBLWunjpOym80,12193
|
13
13
|
otf_api/models/responses/book_class.py,sha256=bWURKEjLZWPzwu3HNP2zUmHWo7q7h6_z43a9KTST0Ec,15413
|
14
14
|
otf_api/models/responses/bookings.py,sha256=0oQxdKTK-k30GVDKiVxTh0vvPTbrw78sqpQpYL7JnJU,11058
|
@@ -25,18 +25,17 @@ otf_api/models/responses/member_membership.py,sha256=_z301T9DrdQW9vIgnx_LeZmkRhv
|
|
25
25
|
otf_api/models/responses/member_purchases.py,sha256=JoTk3hYjsq4rXogVivZxeFaM-j3gIChmIAGVldOU7rE,6085
|
26
26
|
otf_api/models/responses/out_of_studio_workout_history.py,sha256=FwdnmTgFrMtQ8PngsmCv3UroWj3kDnQg6KfGLievoaU,1709
|
27
27
|
otf_api/models/responses/performance_summary_detail.py,sha256=H5yWxGShR4uiXvY2OaniENburTGM7DKQjN7gvF3MG6g,1585
|
28
|
-
otf_api/models/responses/performance_summary_list.py,sha256=
|
28
|
+
otf_api/models/responses/performance_summary_list.py,sha256=R__tsXGz5tVX5gfoRoVUNK4UP2pXRoK5jdSyHABsDXs,1234
|
29
29
|
otf_api/models/responses/studio_detail.py,sha256=CJBCsi4SMs_W5nrWE4hfCs1ugJ5t7GrH80hTv7Ie3eg,5007
|
30
30
|
otf_api/models/responses/studio_services.py,sha256=mFDClPtU0HCk5fb19gjGKpt2F8n8kto7sj1pE_l4RdQ,1836
|
31
31
|
otf_api/models/responses/telemetry.py,sha256=8dl8FKLeyb6jtqsZT7XD4JzXBMLlami448-Jt0tFbSY,1663
|
32
32
|
otf_api/models/responses/telemetry_hr_history.py,sha256=vDcLb4wTHVBw8O0mGblUujHfJegkflOCWW-bnTXNCI0,763
|
33
33
|
otf_api/models/responses/telemetry_max_hr.py,sha256=xKxH0fIlOqFyZv8UW98XsxF-GMoIs9gnCTAbu88ZQtg,266
|
34
34
|
otf_api/models/responses/total_classes.py,sha256=WrKkWbq0eK8J0RC4qhZ5kmXnv_ZTDbyzsoRm7XKGlss,288
|
35
|
-
otf_api/models/responses/workouts.py,sha256=4r6wQVY-yUsI83JYBpSCYhd7I5u-5OLvy1Vd1_gra88,3177
|
36
35
|
otf_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
|
-
otf_api-0.
|
38
|
-
otf_api-0.
|
39
|
-
otf_api-0.
|
40
|
-
otf_api-0.
|
41
|
-
otf_api-0.
|
42
|
-
otf_api-0.
|
36
|
+
otf_api-0.5.0.dist-info/AUTHORS.md,sha256=FcNWMxpe8KDuTq4Qau0SUXsabQwGs9TGnMp1WkXRnj8,123
|
37
|
+
otf_api-0.5.0.dist-info/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
|
38
|
+
otf_api-0.5.0.dist-info/METADATA,sha256=wE1IJvQLG8KVsIiQJjRoIz80amFtCv5zezo4j4tPqEg,2259
|
39
|
+
otf_api-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
40
|
+
otf_api-0.5.0.dist-info/entry_points.txt,sha256=V2jhhfsUo3DeF0CA9HmKrMnvSoOldn9ShIzbApbeHTY,44
|
41
|
+
otf_api-0.5.0.dist-info/RECORD,,
|