otf-api 0.3.0__tar.gz → 0.4.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.
Files changed (42) hide show
  1. {otf_api-0.3.0 → otf_api-0.4.0}/PKG-INFO +5 -6
  2. {otf_api-0.3.0 → otf_api-0.4.0}/README.md +1 -2
  3. {otf_api-0.3.0 → otf_api-0.4.0}/pyproject.toml +21 -22
  4. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/__init__.py +4 -4
  5. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/api.py +117 -68
  6. otf_api-0.4.0/src/otf_api/auth.py +314 -0
  7. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/cli/app.py +2 -7
  8. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/cli/bookings.py +6 -6
  9. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/__init__.py +28 -17
  10. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/__init__.py +25 -17
  11. otf_api-0.3.0/src/otf_api/models/auth.py +0 -147
  12. {otf_api-0.3.0 → otf_api-0.4.0}/AUTHORS.md +0 -0
  13. {otf_api-0.3.0 → otf_api-0.4.0}/LICENSE +0 -0
  14. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/cli/__init__.py +0 -0
  15. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/cli/_utilities.py +0 -0
  16. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/cli/prompts.py +0 -0
  17. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/base.py +0 -0
  18. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/body_composition_list.py +0 -0
  19. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/book_class.py +0 -0
  20. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/bookings.py +0 -0
  21. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/cancel_booking.py +0 -0
  22. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/challenge_tracker_content.py +0 -0
  23. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/challenge_tracker_detail.py +0 -0
  24. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/classes.py +0 -0
  25. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/enums.py +0 -0
  26. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/favorite_studios.py +0 -0
  27. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/latest_agreement.py +0 -0
  28. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/lifetime_stats.py +0 -0
  29. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/member_detail.py +0 -0
  30. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/member_membership.py +0 -0
  31. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/member_purchases.py +0 -0
  32. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/out_of_studio_workout_history.py +0 -0
  33. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/performance_summary_detail.py +0 -0
  34. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/performance_summary_list.py +0 -0
  35. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/studio_detail.py +0 -0
  36. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/studio_services.py +0 -0
  37. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/telemetry.py +0 -0
  38. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/telemetry_hr_history.py +0 -0
  39. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/telemetry_max_hr.py +0 -0
  40. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/total_classes.py +0 -0
  41. {otf_api-0.3.0 → otf_api-0.4.0}/src/otf_api/models/responses/workouts.py +0 -0
  42. {otf_api-0.3.0 → otf_api-0.4.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.0
3
+ Version: 0.4.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[cli] (>=3.0.0,<4.0.0)
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[cli] (>=4.1.0,<5.0.0)
31
- Requires-Dist: typer[cli] (>=0.12.3,<0.13.0)
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 `Api` class, providing your email address and password. This will authenticate you with the API and allow you to make requests. When the `Api` object is created it automatically grabs your member details and home studio, to simplify the process of making requests.
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 `Api` class, providing your email address and password. This will authenticate you with the API and allow you to make requests. When the `Api` object is created it automatically grabs your member details and home studio, to simplify the process of making requests.
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.0"
3
+ version = "0.4.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
- build = "1.2.1"
48
+ pytest-loguru = "0.4.0"
46
49
  ruff = "0.4.9"
47
- pre-commit = "3.7.1"
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 Api
7
- from .models.auth import User
6
+ from .api import Otf
7
+ from .auth import OtfUser
8
8
 
9
- __version__ = "0.3.0"
9
+ __version__ = "0.4.0"
10
10
 
11
11
 
12
- __all__ = ["Api", "User"]
12
+ __all__ = ["Otf", "OtfUser"]
13
13
 
14
14
  logger.remove()
15
15
  logger.add(sink=sys.stdout, level=os.getenv("OTF_LOG_LEVEL", "INFO"))
@@ -7,36 +7,41 @@ from math import ceil
7
7
  from typing import Any
8
8
 
9
9
  import aiohttp
10
+ import requests
10
11
  from loguru import logger
11
12
  from yarl import URL
12
13
 
13
- from otf_api.models.auth import User
14
- from otf_api.models.responses.body_composition_list import BodyCompositionList
15
- from otf_api.models.responses.book_class import BookClass
16
- from otf_api.models.responses.cancel_booking import CancelBooking
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 (
14
+ from otf_api.auth import OtfUser
15
+ from otf_api.models import (
16
+ BodyCompositionList,
17
+ BookClass,
28
18
  BookingList,
29
19
  BookingStatus,
20
+ CancelBooking,
30
21
  ChallengeTrackerContent,
31
22
  ChallengeTrackerDetailList,
32
23
  ChallengeType,
24
+ ClassType,
25
+ DoW,
33
26
  EquipmentType,
27
+ FavoriteStudioList,
34
28
  LatestAgreement,
35
29
  MemberDetail,
36
30
  MemberMembership,
37
31
  MemberPurchaseList,
32
+ OtfClassList,
38
33
  OutOfStudioWorkoutHistoryList,
34
+ Pagination,
35
+ PerformanceSummaryDetail,
36
+ PerformanceSummaryList,
37
+ StatsResponse,
38
+ StatsTime,
39
+ StudioDetail,
40
+ StudioDetailList,
39
41
  StudioServiceList,
42
+ Telemetry,
43
+ TelemetryHrHistory,
44
+ TelemetryMaxHr,
40
45
  TotalClasses,
41
46
  WorkoutList,
42
47
  )
@@ -56,40 +61,55 @@ API_TELEMETRY_BASE_URL = "api.yuzu.orangetheory.com"
56
61
  REQUEST_HEADERS = {"Authorization": None, "Content-Type": "application/json", "Accept": "application/json"}
57
62
 
58
63
 
59
- class Api:
60
- """The main class of the otf-api library. Create an instance using the async method `create`.
64
+ class Otf:
65
+ logger: "Logger" = logger
66
+ user: OtfUser
67
+ _session: aiohttp.ClientSession
61
68
 
62
- Example:
69
+ def __init__(
70
+ self,
71
+ username: str | None = None,
72
+ password: str | None = None,
73
+ access_token: str | None = None,
74
+ id_token: str | None = None,
75
+ refresh_token: str | None = None,
76
+ device_key: str | None = None,
77
+ user: OtfUser | None = None,
78
+ ):
79
+ """Create a new Otf instance.
80
+
81
+ Authentication methods:
63
82
  ---
64
- ```python
65
- import asyncio
66
- from otf_api import Api
67
-
68
- async def main():
69
- otf = await Api.create("username", "password")
70
- print(otf.member)
71
-
72
- if __name__ == "__main__":
73
- asyncio.run(main())
74
- ```
75
- """
83
+ - Provide a username and password.
84
+ - Provide an access token and id token.
85
+ - Provide a user object.
76
86
 
77
- logger: "Logger" = logger
78
- user: User
79
- session: aiohttp.ClientSession
87
+ Args:
88
+ username (str, optional): The username of the user. Default is None.
89
+ password (str, optional): The password of the user. Default is None.
90
+ access_token (str, optional): The access token. Default is None.
91
+ id_token (str, optional): The id token. Default is None.
92
+ refresh_token (str, optional): The refresh token. Default is None.
93
+ device_key (str, optional): The device key. Default is None.
94
+ user (OtfUser, optional): A user object. Default is None.
95
+ """
80
96
 
81
- def __init__(self, username: str, password: str):
82
97
  self.member: MemberDetail
83
- self.home_studio: StudioDetail
84
-
85
- self.user = User.login(username, password)
86
-
87
- headers = {
88
- "Authorization": f"Bearer {self.user.cognito.id_token}",
89
- "Content-Type": "application/json",
90
- "Accept": "application/json",
91
- }
92
- self.session = aiohttp.ClientSession(headers=headers)
98
+ self.home_studio_uuid: str
99
+
100
+ if user:
101
+ self.user = user
102
+ elif username and password or (access_token and id_token):
103
+ self.user = OtfUser(
104
+ username=username,
105
+ password=password,
106
+ access_token=access_token,
107
+ id_token=id_token,
108
+ refresh_token=refresh_token,
109
+ device_key=device_key,
110
+ )
111
+ else:
112
+ raise ValueError("No valid authentication method provided")
93
113
 
94
114
  # simplify access to member_id and member_uuid
95
115
  self._member_id = self.user.member_id
@@ -98,26 +118,48 @@ class Api:
98
118
  "koji-member-id": self._member_id,
99
119
  "koji-member-email": self.user.id_claims_data.email,
100
120
  }
121
+ self.member = self._get_member_details_sync()
122
+ self.home_studio_uuid = self.member.home_studio.studio_uuid
101
123
 
102
- @classmethod
103
- async def create(cls, username: str, password: str) -> "Api":
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.
124
+ def _get_member_details_sync(self) -> MemberDetail:
125
+ """Get the member details synchronously.
106
126
 
107
- Args:
108
- username (str): The username of the user.
109
- password (str): The password of the user.
127
+ This is used to get the member details when the API is first initialized, to let use initialize
128
+ without needing to await the member details.
129
+
130
+ Returns:
131
+ MemberDetail: The member details.
110
132
  """
111
- self = cls(username, password)
112
- self.member = await self.get_member_detail()
113
- self.home_studio = await self.get_studio_detail(self.member.home_studio.studio_uuid)
114
- return self
133
+ url = f"https://{API_BASE_URL}/member/members/{self._member_id}"
134
+ resp = requests.get(url, headers=self.headers)
135
+ return MemberDetail(**resp.json()["data"])
136
+
137
+ @property
138
+ def headers(self) -> dict[str, str]:
139
+ """Get the headers for the API request."""
140
+
141
+ # check the token before making a request in case it has expired
142
+
143
+ self.user.cognito.check_token()
144
+ return {
145
+ "Authorization": f"Bearer {self.user.cognito.id_token}",
146
+ "Content-Type": "application/json",
147
+ "Accept": "application/json",
148
+ }
149
+
150
+ @property
151
+ def session(self) -> aiohttp.ClientSession:
152
+ """Get the aiohttp session."""
153
+ if not getattr(self, "_session", None):
154
+ self._session = aiohttp.ClientSession(headers=self.headers)
155
+
156
+ return self._session
115
157
 
116
158
  def __del__(self) -> None:
117
159
  if not hasattr(self, "session"):
118
160
  return
161
+
119
162
  try:
120
- loop = asyncio.get_event_loop()
121
163
  asyncio.create_task(self._close_session()) # noqa
122
164
  except RuntimeError:
123
165
  loop = asyncio.new_event_loop()
@@ -145,6 +187,12 @@ class Api:
145
187
 
146
188
  logger.debug(f"Making {method!r} request to {full_url}, params: {params}")
147
189
 
190
+ # ensure we have headers that contain the most up-to-date token
191
+ if not headers:
192
+ headers = self.headers
193
+ else:
194
+ headers.update(self.headers)
195
+
148
196
  text = None
149
197
  async with self.session.request(method, full_url, headers=headers, params=params, **kwargs) as response:
150
198
  with contextlib.suppress(Exception):
@@ -155,10 +203,8 @@ class Api:
155
203
  except aiohttp.ClientResponseError as e:
156
204
  logger.exception(f"Error making request: {e}")
157
205
  logger.exception(f"Response: {text}")
158
- # raise
159
206
  except Exception as e:
160
207
  logger.exception(f"Error making request: {e}")
161
- # raise
162
208
 
163
209
  return await response.json()
164
210
 
@@ -231,7 +277,7 @@ class Api:
231
277
  start_date (str | None): The start date to get classes for, in the format "YYYY-MM-DD". Default is None.
232
278
  end_date (str | None): The end date to get classes for, in the format "YYYY-MM-DD". Default is None.
233
279
  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
280
+ class_type (ClassType | list[ClassType] | None): The class type to filter by. Default is None. Multiple\
235
281
  class types can be provided, if there are multiple there will be a call per class type.
236
282
  exclude_cancelled (bool): Whether to exclude cancelled classes. Default is False.
237
283
  day_of_week (list[DoW] | None): The days of the week to filter by. Default is None.
@@ -242,9 +288,9 @@ class Api:
242
288
  """
243
289
 
244
290
  if not studio_uuids:
245
- studio_uuids = [self.home_studio.studio_uuid]
246
- elif include_home_studio and self.home_studio.studio_uuid not in studio_uuids:
247
- studio_uuids.append(self.home_studio.studio_uuid)
291
+ studio_uuids = [self.home_studio_uuid]
292
+ elif include_home_studio and self.home_studio_uuid not in studio_uuids:
293
+ studio_uuids.append(self.home_studio_uuid)
248
294
 
249
295
  path = "/v1/classes"
250
296
 
@@ -280,7 +326,7 @@ class Api:
280
326
  classes_list.classes = [c for c in classes_list.classes if not c.canceled]
281
327
 
282
328
  for otf_class in classes_list.classes:
283
- otf_class.is_home_studio = otf_class.studio.id == self.home_studio.studio_uuid
329
+ otf_class.is_home_studio = otf_class.studio.id == self.home_studio_uuid
284
330
 
285
331
  if day_of_week:
286
332
  classes_list.classes = [c for c in classes_list.classes if c.day_of_week_enum in day_of_week]
@@ -415,7 +461,7 @@ class Api:
415
461
  for booking in data.bookings:
416
462
  if not booking.otf_class:
417
463
  continue
418
- if booking.otf_class.studio.studio_uuid == self.home_studio.studio_uuid:
464
+ if booking.otf_class.studio.studio_uuid == self.home_studio_uuid:
419
465
  booking.is_home_studio = True
420
466
  else:
421
467
  booking.is_home_studio = False
@@ -660,7 +706,7 @@ class Api:
660
706
  Returns:
661
707
  StudioServiceList: The services available at the studio.
662
708
  """
663
- studio_uuid = studio_uuid or self.home_studio.studio_uuid
709
+ studio_uuid = studio_uuid or self.home_studio_uuid
664
710
  data = await self._default_request("GET", f"/member/studios/{studio_uuid}/services")
665
711
  return StudioServiceList(data=data["data"])
666
712
 
@@ -711,7 +757,7 @@ class Api:
711
757
  Returns:
712
758
  StudioDetail: Detailed information about the studio.
713
759
  """
714
- studio_uuid = studio_uuid or self.home_studio.studio_uuid
760
+ studio_uuid = studio_uuid or self.home_studio_uuid
715
761
 
716
762
  path = f"/mobile/v1/studios/{studio_uuid}"
717
763
  params = {"include": "locations"}
@@ -747,8 +793,11 @@ class Api:
747
793
  """
748
794
  path = "/mobile/v1/studios"
749
795
 
750
- latitude = latitude or self.home_studio.studio_location.latitude
751
- longitude = longitude or self.home_studio.studio_location.longitude
796
+ if not latitude and not longitude:
797
+ home_studio = await self.get_studio_detail()
798
+
799
+ latitude = home_studio.studio_location.latitude
800
+ longitude = home_studio.studio_location.longitude
752
801
 
753
802
  if page_size > 50:
754
803
  self.logger.warning("The API does not support more than 50 results per page, limiting to 50.")
@@ -0,0 +1,314 @@
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
+ logger.info("Clearing device key")
67
+ self._device_key = value
68
+ return
69
+
70
+ redacted_value = value[:4] + "*" * (len(value) - 8) + value[-4:]
71
+ logger.info(f"Setting device key: {redacted_value}")
72
+ self._device_key = value
73
+
74
+ def _set_tokens(self, tokens: dict[str, Any]):
75
+ """Set the tokens and device metadata from the response.
76
+
77
+ Args:
78
+ tokens (dict): The response from the Cognito service.
79
+ """
80
+ super()._set_tokens(tokens)
81
+
82
+ if new_metadata := tokens["AuthenticationResult"].get("NewDeviceMetadata"):
83
+ self.device_key = new_metadata["DeviceKey"]
84
+
85
+ def authenticate(self, password: str, client_metadata: dict[str, Any] | None = None, device_key: str | None = None):
86
+ """
87
+ Authenticate the user using the SRP protocol. Overridden to add `confirm_device` call.
88
+
89
+ Args:
90
+ password (str): The user's password
91
+ client_metadata (dict, optional): Any additional client metadata to send to Cognito
92
+ """
93
+ aws = AWSSRP(
94
+ username=self.username,
95
+ password=password,
96
+ pool_id=self.user_pool_id,
97
+ client_id=self.client_id,
98
+ client=self.client,
99
+ client_secret=self.client_secret,
100
+ )
101
+ try:
102
+ tokens = aws.authenticate_user(client_metadata=client_metadata)
103
+ except MFAChallengeException as mfa_challenge:
104
+ self.mfa_tokens = mfa_challenge.get_tokens()
105
+ raise mfa_challenge
106
+
107
+ # Set the tokens and device metadata
108
+ self._set_tokens(tokens)
109
+
110
+ if not device_key:
111
+ # Confirm the device so we can use the refresh token
112
+ aws.confirm_device(tokens)
113
+ else:
114
+ self.device_key = device_key
115
+ try:
116
+ self.renew_access_token()
117
+ except TokenVerificationException:
118
+ logger.error("Failed to renew access token. Confirming device.")
119
+ self.device_key = None
120
+ aws.confirm_device(tokens)
121
+
122
+ def check_token(self, renew: bool = True) -> bool:
123
+ """
124
+ Checks the exp attribute of the access_token and either refreshes
125
+ the tokens by calling the renew_access_tokens method or does nothing
126
+ :param renew: bool indicating whether to refresh on expiration
127
+ :return: bool indicating whether access_token has expired
128
+ """
129
+ if not self.access_token:
130
+ raise AttributeError("Access Token Required to Check Token")
131
+ now = pendulum.now()
132
+ dec_access_token = jwt.decode(self.access_token, options={"verify_signature": False})
133
+
134
+ exp = pendulum.DateTime.fromtimestamp(dec_access_token["exp"])
135
+ if now > exp.subtract(minutes=15):
136
+ expired = True
137
+ if renew:
138
+ self.renew_access_token()
139
+ else:
140
+ expired = False
141
+ return expired
142
+
143
+ def renew_access_token(self):
144
+ """Sets a new access token on the User using the cached refresh token and device metadata."""
145
+ auth_params = {"REFRESH_TOKEN": self.refresh_token}
146
+ self._add_secret_hash(auth_params, "SECRET_HASH")
147
+
148
+ if self.device_key:
149
+ logger.info("Using device key for refresh token")
150
+ auth_params["DEVICE_KEY"] = self.device_key
151
+
152
+ refresh_response = self.client.initiate_auth(
153
+ ClientId=self.client_id, AuthFlow="REFRESH_TOKEN_AUTH", AuthParameters=auth_params
154
+ )
155
+ self._set_tokens(refresh_response)
156
+
157
+ @classmethod
158
+ def from_token(
159
+ cls, access_token: str, id_token: str, refresh_token: str | None = None, device_key: str | None = None
160
+ ) -> "OtfCognito":
161
+ """Create an OtfCognito instance from an id token.
162
+
163
+ Args:
164
+ access_token (str): The access token.
165
+ id_token (str): The id token.
166
+ refresh_token (str, optional): The refresh token. Defaults to None.
167
+ device_key (str, optional): The device key. Defaults
168
+
169
+ Returns:
170
+ OtfCognito: The user instance
171
+ """
172
+ cognito = OtfCognito(
173
+ USER_POOL_ID,
174
+ CLIENT_ID,
175
+ access_token=access_token,
176
+ id_token=id_token,
177
+ refresh_token=refresh_token,
178
+ device_key=device_key,
179
+ )
180
+ cognito.verify_tokens()
181
+ cognito.check_token()
182
+ return cognito
183
+
184
+ @classmethod
185
+ def login(cls, username: str, password: str) -> "OtfCognito":
186
+ """Create an OtfCognito instance from a username and password.
187
+
188
+ Args:
189
+ username (str): The username to login with.
190
+ password (str): The password to login with.
191
+
192
+ Returns:
193
+ OtfCognito: The logged in user.
194
+ """
195
+ cognito_user = OtfCognito(USER_POOL_ID, CLIENT_ID, username=username)
196
+ cognito_user.authenticate(password)
197
+ cognito_user.check_token()
198
+ return cognito_user
199
+
200
+
201
+ class IdClaimsData(OtfItemBase):
202
+ sub: str
203
+ email_verified: bool
204
+ iss: str
205
+ cognito_username: str = Field(alias="cognito:username")
206
+ given_name: str
207
+ locale: str
208
+ home_studio_id: str = Field(alias="custom:home_studio_id")
209
+ aud: str
210
+ event_id: str
211
+ token_use: str
212
+ auth_time: int
213
+ exp: int
214
+ is_migration: str = Field(alias="custom:isMigration")
215
+ iat: int
216
+ family_name: str
217
+ email: str
218
+ koji_person_id: str = Field(alias="custom:koji_person_id")
219
+
220
+ @property
221
+ def member_uuid(self) -> str:
222
+ return self.cognito_username
223
+
224
+ @property
225
+ def full_name(self) -> str:
226
+ return f"{self.given_name} {self.family_name}"
227
+
228
+
229
+ class AccessClaimsData(OtfItemBase):
230
+ sub: str
231
+ device_key: str
232
+ iss: str
233
+ client_id: str
234
+ event_id: str
235
+ token_use: str
236
+ scope: str
237
+ auth_time: int
238
+ exp: int
239
+ iat: int
240
+ jti: str
241
+ username: str
242
+
243
+ @property
244
+ def member_uuid(self) -> str:
245
+ return self.username
246
+
247
+
248
+ class OtfUser(OtfItemBase):
249
+ model_config = ConfigDict(arbitrary_types_allowed=True)
250
+ cognito: OtfCognito
251
+
252
+ def __init__(
253
+ self,
254
+ username: str | None = None,
255
+ password: str | None = None,
256
+ id_token: str | None = None,
257
+ access_token: str | None = None,
258
+ refresh_token: str | None = None,
259
+ device_key: str | None = None,
260
+ cognito: OtfCognito | None = None,
261
+ ):
262
+ """Create a User instance.
263
+
264
+ Args:
265
+ username (str, optional): The username to login with. Defaults to None.
266
+ password (str, optional): The password to login with. Defaults to None.
267
+ id_token (str, optional): The id token. Defaults to None.
268
+ access_token (str, optional): The access token. Defaults to None.
269
+ refresh_token (str, optional): The refresh token. Defaults to None.
270
+ device_key (str, optional): The device key. Defaults to None.
271
+ cognito (OtfCognito, optional): A Cognito instance. Defaults to None.
272
+
273
+ Raises:
274
+ ValueError: Must provide either username and password or id token
275
+
276
+
277
+ """
278
+ if cognito:
279
+ cognito = cognito
280
+ elif username and password:
281
+ cognito = OtfCognito.login(username, password)
282
+ elif access_token and id_token:
283
+ cognito = OtfCognito.from_token(access_token, id_token, refresh_token, device_key)
284
+ else:
285
+ raise ValueError("Must provide either username and password or id token.")
286
+
287
+ super().__init__(cognito=cognito)
288
+
289
+ @property
290
+ def member_id(self) -> str:
291
+ return self.id_claims_data.cognito_username
292
+
293
+ @property
294
+ def member_uuid(self) -> str:
295
+ return self.access_claims_data.sub
296
+
297
+ @property
298
+ def access_claims_data(self) -> AccessClaimsData:
299
+ return AccessClaimsData(**self.cognito.access_claims)
300
+
301
+ @property
302
+ def id_claims_data(self) -> IdClaimsData:
303
+ return IdClaimsData(**self.cognito.id_claims)
304
+
305
+ def get_tokens(self) -> dict[str, str]:
306
+ return {
307
+ "id_token": self.cognito.id_token,
308
+ "access_token": self.cognito.access_token,
309
+ "refresh_token": self.cognito.refresh_token,
310
+ }
311
+
312
+ @property
313
+ def device_key(self) -> str:
314
+ return self.cognito.device_key
@@ -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.api import 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: Api = None # type: ignore
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:
@@ -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.Api.create(base_app.username, base_app.password)
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.Api.create(base_app.username, base_app.password)
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.Api.create(base_app.username, base_app.password)
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.Api.create(base_app.username, base_app.password)
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.Api.create(base_app.username, base_app.password)
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.Api.create(base_app.username, base_app.password)
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
  )
@@ -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,
@@ -29,31 +34,37 @@ from .responses import (
29
34
  )
30
35
 
31
36
  __all__ = [
32
- "User",
33
- "ChallengeType",
34
- "BookingStatus",
35
- "EquipmentType",
36
- "HistoryClassStatus",
37
+ "BodyCompositionList",
38
+ "BookClass",
37
39
  "BookingList",
40
+ "BookingStatus",
41
+ "CancelBooking",
38
42
  "ChallengeTrackerContent",
39
43
  "ChallengeTrackerDetailList",
44
+ "ChallengeType",
45
+ "ClassType",
46
+ "DoW",
47
+ "EquipmentType",
48
+ "FavoriteStudioList",
49
+ "HistoryClassStatus",
40
50
  "LatestAgreement",
41
51
  "MemberDetail",
42
52
  "MemberMembership",
43
53
  "MemberPurchaseList",
54
+ "OtfClassList",
44
55
  "OutOfStudioWorkoutHistoryList",
56
+ "Pagination",
57
+ "PerformanceSummaryDetail",
58
+ "PerformanceSummaryList",
59
+ "StatsResponse",
60
+ "StatsTime",
61
+ "StudioDetail",
62
+ "StudioDetailList",
45
63
  "StudioServiceList",
46
- "TotalClasses",
47
- "WorkoutList",
48
- "FavoriteStudioList",
49
- "OtfClassList",
50
- "TelemetryHrHistory",
64
+ "StudioStatus",
51
65
  "Telemetry",
66
+ "TelemetryHrHistory",
52
67
  "TelemetryMaxHr",
53
- "StudioDetail",
54
- "StudioDetailList",
55
- "PerformanceSummaryDetail",
56
- "PerformanceSummaryList",
57
- "BookClass",
58
- "CancelBooking",
68
+ "TotalClasses",
69
+ "WorkoutList",
59
70
  ]
@@ -1,19 +1,21 @@
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
@@ -22,31 +24,37 @@ from .total_classes import TotalClasses
22
24
  from .workouts import WorkoutList
23
25
 
24
26
  __all__ = [
27
+ "BodyCompositionList",
28
+ "BookClass",
25
29
  "BookingList",
30
+ "BookingStatus",
31
+ "CancelBooking",
26
32
  "ChallengeTrackerContent",
27
33
  "ChallengeTrackerDetailList",
34
+ "ChallengeType",
35
+ "ClassType",
36
+ "DoW",
37
+ "EquipmentType",
38
+ "FavoriteStudioList",
39
+ "HistoryClassStatus",
28
40
  "LatestAgreement",
29
41
  "MemberDetail",
30
42
  "MemberMembership",
31
43
  "MemberPurchaseList",
44
+ "OtfClassList",
32
45
  "OutOfStudioWorkoutHistoryList",
46
+ "Pagination",
47
+ "PerformanceSummaryDetail",
48
+ "PerformanceSummaryList",
49
+ "StatsResponse",
50
+ "StatsTime",
51
+ "StudioDetail",
52
+ "StudioDetailList",
33
53
  "StudioServiceList",
34
- "TotalClasses",
35
- "WorkoutList",
36
- "ChallengeType",
37
- "BookingStatus",
38
- "EquipmentType",
39
- "HistoryClassStatus",
40
54
  "StudioStatus",
41
- "FavoriteStudioList",
42
- "OtfClassList",
43
- "TelemetryHrHistory",
44
55
  "Telemetry",
56
+ "TelemetryHrHistory",
45
57
  "TelemetryMaxHr",
46
- "StudioDetail",
47
- "StudioDetailList",
48
- "PerformanceSummaryDetail",
49
- "PerformanceSummaryList",
50
- "BookClass",
51
- "CancelBooking",
58
+ "TotalClasses",
59
+ "WorkoutList",
52
60
  ]
@@ -1,147 +0,0 @@
1
- import json
2
- from pathlib import Path
3
- from typing import ClassVar
4
-
5
- from pycognito import Cognito, TokenVerificationException
6
- from pydantic import Field
7
-
8
- from otf_api.models.base import OtfItemBase
9
-
10
- CLIENT_ID = "65knvqta6p37efc2l3eh26pl5o" # from otlive
11
- USER_POOL_ID = "us-east-1_dYDxUeyL1"
12
-
13
-
14
- class IdClaimsData(OtfItemBase):
15
- sub: str
16
- email_verified: bool
17
- iss: str
18
- cognito_username: str = Field(alias="cognito:username")
19
- given_name: str
20
- locale: str
21
- home_studio_id: str = Field(alias="custom:home_studio_id")
22
- aud: str
23
- event_id: str
24
- token_use: str
25
- auth_time: int
26
- exp: int
27
- is_migration: str = Field(alias="custom:isMigration")
28
- iat: int
29
- family_name: str
30
- email: str
31
- koji_person_id: str = Field(alias="custom:koji_person_id")
32
-
33
- @property
34
- def member_uuid(self) -> str:
35
- return self.cognito_username
36
-
37
- @property
38
- def full_name(self) -> str:
39
- return f"{self.given_name} {self.family_name}"
40
-
41
-
42
- class AccessClaimsData(OtfItemBase):
43
- sub: str
44
- device_key: str
45
- iss: str
46
- client_id: str
47
- event_id: str
48
- token_use: str
49
- scope: str
50
- auth_time: int
51
- exp: int
52
- iat: int
53
- jti: str
54
- username: str
55
-
56
- @property
57
- def member_uuid(self) -> str:
58
- return self.username
59
-
60
-
61
- class User:
62
- token_path: ClassVar[Path] = Path("~/.otf/.tokens").expanduser()
63
- cognito: Cognito
64
-
65
- def __init__(self, cognito: Cognito):
66
- self.cognito = cognito
67
-
68
- @property
69
- def member_id(self) -> str:
70
- return self.id_claims_data.cognito_username
71
-
72
- @property
73
- def member_uuid(self) -> str:
74
- return self.access_claims_data.sub
75
-
76
- @property
77
- def access_claims_data(self) -> AccessClaimsData:
78
- return AccessClaimsData(**self.cognito.access_claims)
79
-
80
- @property
81
- def id_claims_data(self) -> IdClaimsData:
82
- return IdClaimsData(**self.cognito.id_claims)
83
-
84
- def save_to_disk(self) -> None:
85
- self.token_path.parent.mkdir(parents=True, exist_ok=True)
86
- data = {
87
- "username": self.cognito.username,
88
- "id_token": self.cognito.id_token,
89
- "access_token": self.cognito.access_token,
90
- "refresh_token": self.cognito.refresh_token,
91
- }
92
- self.token_path.write_text(json.dumps(data))
93
-
94
- @classmethod
95
- def cache_file_exists(cls) -> bool:
96
- return cls.token_path.exists()
97
-
98
- @classmethod
99
- def username_from_disk(cls) -> str:
100
- val: str = json.loads(cls.token_path.read_text())["username"]
101
- return val
102
-
103
- @classmethod
104
- def load_from_disk(cls, username: str, password: str) -> "User":
105
- """Load a User instance from disk. If the token is invalid, reauthenticate with the provided credentials.
106
-
107
- Args:
108
- username (str): The username to reauthenticate with.
109
- password (str): The password to reauthenticate with.
110
-
111
- Returns:
112
- User: The loaded user.
113
-
114
- """
115
- attr_dict = json.loads(cls.token_path.read_text())
116
-
117
- cognito_user = Cognito(USER_POOL_ID, CLIENT_ID, **attr_dict)
118
- try:
119
- cognito_user.verify_tokens()
120
- return cls(cognito=cognito_user)
121
- except TokenVerificationException:
122
- user = cls.login(username, password)
123
- return user
124
-
125
- @classmethod
126
- def login(cls, username: str, password: str) -> "User":
127
- """Login and return a User instance. After a successful login, the user is saved to disk.
128
-
129
- Args:
130
- username (str): The username to login with.
131
- password (str): The password to login with.
132
-
133
- Returns:
134
- User: The logged in user.
135
- """
136
- cognito_user = Cognito(USER_POOL_ID, CLIENT_ID, username=username)
137
- cognito_user.authenticate(password)
138
- cognito_user.check_token()
139
- user = cls(cognito=cognito_user)
140
- user.save_to_disk()
141
- return user
142
-
143
- def refresh_token(self) -> "User":
144
- """Refresh the user's access token."""
145
- self.cognito.check_token()
146
- self.save_to_disk()
147
- return self
File without changes
File without changes
File without changes