aioamazondevices 0.13.0__tar.gz → 1.1.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.
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/PKG-INFO +2 -3
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/pyproject.toml +1 -1
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/src/aioamazondevices/api.py +36 -29
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/src/aioamazondevices/const.py +2 -2
- aioamazondevices-0.13.0/src/aioamazondevices/auth.py +0 -315
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/LICENSE +0 -0
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/README.md +0 -0
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-0.13.0 → aioamazondevices-1.1.0}/src/aioamazondevices/py.typed +0 -0
@@ -1,8 +1,7 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version:
|
3
|
+
Version: 1.1.0
|
4
4
|
Summary: Python library to control Amazon devices
|
5
|
-
Home-page: https://github.com/chemelli74/aioamazondevices
|
6
5
|
License: Apache-2.0
|
7
6
|
Author: Simone Chemelli
|
8
7
|
Author-email: simone.chemelli@gmail.com
|
@@ -14,9 +14,8 @@ from urllib.parse import parse_qs, urlencode
|
|
14
14
|
|
15
15
|
import orjson
|
16
16
|
from bs4 import BeautifulSoup, Tag
|
17
|
-
from httpx import URL, AsyncClient,
|
17
|
+
from httpx import URL, AsyncClient, Response
|
18
18
|
|
19
|
-
from .auth import Authenticator
|
20
19
|
from .const import (
|
21
20
|
_LOGGER,
|
22
21
|
AMAZON_APP_BUNDLE_ID,
|
@@ -28,7 +27,7 @@ from .const import (
|
|
28
27
|
AMAZON_DEVICE_TYPE,
|
29
28
|
DEFAULT_ASSOC_HANDLE,
|
30
29
|
DEFAULT_HEADERS,
|
31
|
-
|
30
|
+
DOMAIN_BY_ISO3166_COUNTRY,
|
32
31
|
HTML_EXTENSION,
|
33
32
|
JSON_EXTENSION,
|
34
33
|
NODE_BLUETOOTH,
|
@@ -72,7 +71,7 @@ class AmazonEchoApi:
|
|
72
71
|
# Force country digits as lower case
|
73
72
|
country_code = login_country_code.lower()
|
74
73
|
|
75
|
-
locale =
|
74
|
+
locale = DOMAIN_BY_ISO3166_COUNTRY.get(country_code)
|
76
75
|
domain = locale["domain"] if locale else country_code
|
77
76
|
|
78
77
|
if locale and (assoc := locale.get("openid.assoc_handle")):
|
@@ -84,7 +83,6 @@ class AmazonEchoApi:
|
|
84
83
|
self._login_email = login_email
|
85
84
|
self._login_password = login_password
|
86
85
|
self._domain = domain
|
87
|
-
self._url = f"https://www.amazon.{domain}"
|
88
86
|
self._cookies = self._build_init_cookies()
|
89
87
|
self._headers = DEFAULT_HEADERS
|
90
88
|
self._save_raw_data = save_raw_data
|
@@ -99,7 +97,7 @@ class AmazonEchoApi:
|
|
99
97
|
if not self._login_stored_data:
|
100
98
|
return {}
|
101
99
|
|
102
|
-
return cast(dict, self._login_stored_data["website_cookies"])
|
100
|
+
return cast("dict", self._login_stored_data["website_cookies"])
|
103
101
|
|
104
102
|
def _serial_number(self) -> str:
|
105
103
|
"""Get or calculate device serial number."""
|
@@ -110,7 +108,7 @@ class AmazonEchoApi:
|
|
110
108
|
|
111
109
|
_LOGGER.debug("Found previous login data, loading serial number")
|
112
110
|
return cast(
|
113
|
-
str,
|
111
|
+
"str",
|
114
112
|
self._login_stored_data["device_info"]["device_serial_number"],
|
115
113
|
)
|
116
114
|
|
@@ -208,16 +206,15 @@ class AmazonEchoApi:
|
|
208
206
|
parsed_url = parse_qs(url.query.decode())
|
209
207
|
return parsed_url["openid.oa2.authorization_code"][0]
|
210
208
|
|
211
|
-
def _client_session(self
|
212
|
-
"""Create
|
209
|
+
def _client_session(self) -> None:
|
210
|
+
"""Create HTTP client session."""
|
213
211
|
if not hasattr(self, "session") or self.session.is_closed:
|
214
|
-
_LOGGER.debug("Creating HTTP
|
212
|
+
_LOGGER.debug("Creating HTTP session (httpx)")
|
215
213
|
self.session = AsyncClient(
|
216
214
|
base_url=f"https://www.amazon.{self._domain}",
|
217
215
|
headers=DEFAULT_HEADERS,
|
218
216
|
cookies=self._cookies,
|
219
217
|
follow_redirects=True,
|
220
|
-
auth=auth,
|
221
218
|
)
|
222
219
|
|
223
220
|
async def _session_request(
|
@@ -225,17 +222,30 @@ class AmazonEchoApi:
|
|
225
222
|
method: str,
|
226
223
|
url: str,
|
227
224
|
input_data: dict[str, Any] | None = None,
|
225
|
+
json_data: bool = False,
|
228
226
|
) -> tuple[BeautifulSoup, Response]:
|
229
227
|
"""Return request response context data."""
|
230
|
-
_LOGGER.debug(
|
228
|
+
_LOGGER.debug(
|
229
|
+
"%s request: %s with payload %s [json=%s]",
|
230
|
+
method,
|
231
|
+
url,
|
232
|
+
input_data,
|
233
|
+
json_data,
|
234
|
+
)
|
231
235
|
resp = await self.session.request(
|
232
236
|
method,
|
233
237
|
url,
|
234
|
-
data=input_data,
|
238
|
+
data=input_data if not json_data else orjson.dumps(input_data),
|
235
239
|
cookies=self._website_cookies,
|
240
|
+
headers={"Content-Type": "application/json"} if json_data else None,
|
236
241
|
)
|
237
242
|
content_type: str = resp.headers.get("Content-Type", "")
|
238
|
-
_LOGGER.debug(
|
243
|
+
_LOGGER.debug(
|
244
|
+
"Response %s for url %s with content type: %s",
|
245
|
+
resp.status_code,
|
246
|
+
url,
|
247
|
+
content_type,
|
248
|
+
)
|
239
249
|
|
240
250
|
await self._save_to_file(
|
241
251
|
resp.text,
|
@@ -253,7 +263,7 @@ class AmazonEchoApi:
|
|
253
263
|
output_path: str = SAVE_PATH,
|
254
264
|
) -> None:
|
255
265
|
"""Save response data to disk."""
|
256
|
-
if not self._save_raw_data:
|
266
|
+
if not self._save_raw_data or not raw_data:
|
257
267
|
return
|
258
268
|
|
259
269
|
output_dir = Path(output_path)
|
@@ -328,13 +338,12 @@ class AmazonEchoApi:
|
|
328
338
|
],
|
329
339
|
}
|
330
340
|
|
331
|
-
headers = {"Content-Type": "application/json"}
|
332
|
-
|
333
341
|
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
334
|
-
resp = await self.
|
342
|
+
_, resp = await self._session_request(
|
343
|
+
"POST",
|
335
344
|
register_url,
|
336
|
-
|
337
|
-
|
345
|
+
input_data=body,
|
346
|
+
json_data=True,
|
338
347
|
)
|
339
348
|
resp_json = resp.json()
|
340
349
|
|
@@ -409,7 +418,9 @@ class AmazonEchoApi:
|
|
409
418
|
)
|
410
419
|
|
411
420
|
if not login_soup.find("input", id="auth-mfa-otpcode"):
|
412
|
-
_LOGGER.debug(
|
421
|
+
_LOGGER.debug(
|
422
|
+
'Cannot find "auth-mfa-otpcode" in html source [%s]', login_url
|
423
|
+
)
|
413
424
|
raise CannotAuthenticate
|
414
425
|
|
415
426
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
@@ -463,18 +474,14 @@ class AmazonEchoApi:
|
|
463
474
|
self._login_email,
|
464
475
|
)
|
465
476
|
|
466
|
-
|
467
|
-
self._login_stored_data,
|
468
|
-
self._domain,
|
469
|
-
)
|
470
|
-
self._client_session(auth)
|
477
|
+
self._client_session()
|
471
478
|
|
472
479
|
return self._login_stored_data
|
473
480
|
|
474
481
|
async def close(self) -> None:
|
475
|
-
"""Close
|
482
|
+
"""Close http client session."""
|
476
483
|
if hasattr(self, "session"):
|
477
|
-
_LOGGER.debug("Closing
|
484
|
+
_LOGGER.debug("Closing HTTP session (httpx)")
|
478
485
|
await self.session.aclose()
|
479
486
|
|
480
487
|
async def get_devices_data(
|
@@ -493,7 +500,7 @@ class AmazonEchoApi:
|
|
493
500
|
|
494
501
|
response_data = raw_resp.text
|
495
502
|
_LOGGER.debug("Response data: |%s|", response_data)
|
496
|
-
json_data = {} if len(
|
503
|
+
json_data = {} if len(response_data) == 0 else raw_resp.json()
|
497
504
|
|
498
505
|
_LOGGER.debug("JSON data: |%s|", json_data)
|
499
506
|
|
@@ -6,12 +6,12 @@ _LOGGER = logging.getLogger(__package__)
|
|
6
6
|
|
7
7
|
DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
|
8
8
|
|
9
|
-
|
9
|
+
DOMAIN_BY_ISO3166_COUNTRY = {
|
10
10
|
"us": {
|
11
11
|
"domain": "com",
|
12
12
|
"openid.assoc_handle": DEFAULT_ASSOC_HANDLE,
|
13
13
|
},
|
14
|
-
"
|
14
|
+
"gb": {
|
15
15
|
"domain": "co.uk",
|
16
16
|
},
|
17
17
|
"au": {
|
@@ -1,315 +0,0 @@
|
|
1
|
-
"""Custom authentication module for httpx."""
|
2
|
-
|
3
|
-
import base64
|
4
|
-
from collections.abc import Generator
|
5
|
-
from datetime import UTC, datetime, timedelta
|
6
|
-
from typing import (
|
7
|
-
Any,
|
8
|
-
cast,
|
9
|
-
)
|
10
|
-
|
11
|
-
import httpx
|
12
|
-
from httpx import Cookies
|
13
|
-
from pyasn1.codec.der import decoder
|
14
|
-
from pyasn1.type import namedtype, univ
|
15
|
-
from rsa import PrivateKey, pkcs1
|
16
|
-
|
17
|
-
from .const import _LOGGER, AMAZON_APP_NAME, AMAZON_APP_VERSION
|
18
|
-
from .exceptions import (
|
19
|
-
AuthFlowError,
|
20
|
-
AuthMissingAccessToken,
|
21
|
-
AuthMissingRefreshToken,
|
22
|
-
AuthMissingSigningData,
|
23
|
-
AuthMissingTimestamp,
|
24
|
-
AuthMissingWebsiteCookies,
|
25
|
-
)
|
26
|
-
|
27
|
-
|
28
|
-
def base64_der_to_pkcs1(base64_key: str) -> str:
|
29
|
-
"""Convert DER private key to PEM format."""
|
30
|
-
|
31
|
-
class PrivateKeyAlgorithm(univ.Sequence): # type: ignore[misc]
|
32
|
-
component_type = namedtype.NamedTypes(
|
33
|
-
namedtype.NamedType("algorithm", univ.ObjectIdentifier()),
|
34
|
-
namedtype.NamedType("parameters", univ.Any()),
|
35
|
-
)
|
36
|
-
|
37
|
-
class PrivateKeyInfo(univ.Sequence): # type: ignore[misc]
|
38
|
-
component_type = namedtype.NamedTypes(
|
39
|
-
namedtype.NamedType("version", univ.Integer()),
|
40
|
-
namedtype.NamedType("pkalgo", PrivateKeyAlgorithm()),
|
41
|
-
namedtype.NamedType("key", univ.OctetString()),
|
42
|
-
)
|
43
|
-
|
44
|
-
encoded_key = base64.b64decode(base64_key)
|
45
|
-
(key_info, _) = decoder.decode(encoded_key, asn1Spec=PrivateKeyInfo())
|
46
|
-
key_octet_string = key_info.components[2]
|
47
|
-
key = PrivateKey.load_pkcs1(key_octet_string, format="DER")
|
48
|
-
return cast(str, key.save_pkcs1().decode("utf-8"))
|
49
|
-
|
50
|
-
|
51
|
-
def refresh_access_token(
|
52
|
-
refresh_token: str,
|
53
|
-
domain: str,
|
54
|
-
) -> dict[str, Any]:
|
55
|
-
"""Refresh an access token."""
|
56
|
-
body = {
|
57
|
-
"app_name": AMAZON_APP_NAME,
|
58
|
-
"app_version": AMAZON_APP_VERSION,
|
59
|
-
"source_token": refresh_token,
|
60
|
-
"requested_token_type": "access_token",
|
61
|
-
"source_token_type": "refresh_token",
|
62
|
-
}
|
63
|
-
|
64
|
-
resp = httpx.post(f"https://api.amazon.{domain}/auth/token", data=body)
|
65
|
-
resp.raise_for_status()
|
66
|
-
resp_dict = resp.json()
|
67
|
-
|
68
|
-
expires_in_sec = int(resp_dict["expires_in"])
|
69
|
-
expires = (datetime.now(UTC) + timedelta(seconds=expires_in_sec)).timestamp()
|
70
|
-
|
71
|
-
return {"access_token": resp_dict["access_token"], "expires": expires}
|
72
|
-
|
73
|
-
|
74
|
-
def refresh_website_cookies(
|
75
|
-
refresh_token: str,
|
76
|
-
domain: str,
|
77
|
-
) -> dict[str, str]:
|
78
|
-
"""Fetch website cookies for a specific domain."""
|
79
|
-
url = f"https://www.amazon.{domain}/ap/exchangetoken/cookies"
|
80
|
-
|
81
|
-
body = {
|
82
|
-
"app_name": AMAZON_APP_NAME,
|
83
|
-
"app_version": AMAZON_APP_VERSION,
|
84
|
-
"source_token": refresh_token,
|
85
|
-
"requested_token_type": "auth_cookies",
|
86
|
-
"source_token_type": "refresh_token",
|
87
|
-
"domain": f".amazon.{domain}",
|
88
|
-
}
|
89
|
-
|
90
|
-
resp = httpx.post(url, data=body)
|
91
|
-
resp.raise_for_status()
|
92
|
-
resp_dict = resp.json()
|
93
|
-
|
94
|
-
raw_cookies = resp_dict["response"]["tokens"]["cookies"]
|
95
|
-
website_cookies = {}
|
96
|
-
for domain_cookies in raw_cookies:
|
97
|
-
for cookie in raw_cookies[domain_cookies]:
|
98
|
-
website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
|
99
|
-
|
100
|
-
return website_cookies
|
101
|
-
|
102
|
-
|
103
|
-
def user_profile(access_token: str, domain: str) -> dict[str, Any]:
|
104
|
-
"""Return Amazon user profile from Amazon."""
|
105
|
-
headers = {"Authorization": f"Bearer {access_token}"}
|
106
|
-
|
107
|
-
resp = httpx.get(f"https://api.amazon.{domain}/user/profile", headers=headers)
|
108
|
-
resp.raise_for_status()
|
109
|
-
profile: dict[str, Any] = resp.json()
|
110
|
-
|
111
|
-
if "user_id" not in profile:
|
112
|
-
raise ValueError("Malformed user profile response.")
|
113
|
-
|
114
|
-
return profile
|
115
|
-
|
116
|
-
|
117
|
-
def sign_request(
|
118
|
-
method: str,
|
119
|
-
path: str,
|
120
|
-
body: bytes,
|
121
|
-
adp_token: str,
|
122
|
-
private_key: str,
|
123
|
-
) -> dict[str, str]:
|
124
|
-
"""Create signed headers for http requests."""
|
125
|
-
date = datetime.now(UTC).isoformat("T") + "Z"
|
126
|
-
str_body = body.decode("utf-8")
|
127
|
-
|
128
|
-
data = f"{method}\n{path}\n{date}\n{str_body}\n{adp_token}"
|
129
|
-
|
130
|
-
key = PrivateKey.load_pkcs1(private_key.encode("utf-8"))
|
131
|
-
cipher = pkcs1.sign(data.encode(), key, "SHA-256")
|
132
|
-
signed_encoded = base64.b64encode(cipher)
|
133
|
-
|
134
|
-
signature = f"{signed_encoded.decode()}:{date}"
|
135
|
-
|
136
|
-
return {
|
137
|
-
"x-adp-token": adp_token,
|
138
|
-
"x-adp-alg": "SHA256withRSA:1.0",
|
139
|
-
"x-adp-signature": signature,
|
140
|
-
}
|
141
|
-
|
142
|
-
|
143
|
-
class Authenticator(httpx.Auth): # type: ignore[misc]
|
144
|
-
"""Amazon Authenticator class."""
|
145
|
-
|
146
|
-
access_token: str | None = None
|
147
|
-
activation_bytes: str | None = None
|
148
|
-
adp_token: str | None = None
|
149
|
-
customer_info: dict[str, Any] | None = None
|
150
|
-
device_info: dict[str, Any] | None = None
|
151
|
-
device_private_key: str | None = None
|
152
|
-
expires: float | None = None
|
153
|
-
domain: str
|
154
|
-
refresh_token: str | None = None
|
155
|
-
store_authentication_cookie: dict[str, Any] | None = None
|
156
|
-
website_cookies: dict[str, Any] | None = None
|
157
|
-
requires_request_body: bool = True
|
158
|
-
_forbid_new_attrs: bool = True
|
159
|
-
_apply_test_convert: bool = True
|
160
|
-
|
161
|
-
def update_attrs(self, **kwargs: Any) -> None: # noqa: ANN401
|
162
|
-
"""Update attributes from dict."""
|
163
|
-
for attr, value in kwargs.items():
|
164
|
-
setattr(self, attr, value)
|
165
|
-
|
166
|
-
@classmethod
|
167
|
-
def from_dict(
|
168
|
-
cls,
|
169
|
-
data: dict[str, Any],
|
170
|
-
domain: str,
|
171
|
-
) -> "Authenticator":
|
172
|
-
"""Instantiate an Authenticator from authentication dictionary."""
|
173
|
-
auth = cls()
|
174
|
-
|
175
|
-
auth.domain = domain
|
176
|
-
|
177
|
-
if "login_cookies" in data:
|
178
|
-
auth.website_cookies = data.pop("login_cookies")
|
179
|
-
|
180
|
-
if data["device_private_key"].startswith("MII"):
|
181
|
-
pk_der = data["device_private_key"]
|
182
|
-
data["device_private_key"] = base64_der_to_pkcs1(pk_der)
|
183
|
-
|
184
|
-
auth.update_attrs(**data)
|
185
|
-
|
186
|
-
_LOGGER.info("load data from dictionary for domain %s", auth.domain)
|
187
|
-
|
188
|
-
return auth
|
189
|
-
|
190
|
-
def auth_flow(
|
191
|
-
self,
|
192
|
-
request: httpx.Request,
|
193
|
-
) -> Generator[httpx.Request, httpx.Response, None]:
|
194
|
-
"""Auth flow to be executed on every request by :mod:`httpx`."""
|
195
|
-
available_modes = self.available_auth_modes
|
196
|
-
|
197
|
-
_LOGGER.debug("Auth flow modes: %s", available_modes)
|
198
|
-
if "signing" in available_modes:
|
199
|
-
self._apply_signing_auth_flow(request)
|
200
|
-
elif "bearer" in available_modes:
|
201
|
-
self._apply_bearer_auth_flow(request)
|
202
|
-
else:
|
203
|
-
message = "signing or bearer auth flow are not available."
|
204
|
-
_LOGGER.critical(message)
|
205
|
-
raise AuthFlowError(message)
|
206
|
-
|
207
|
-
yield request
|
208
|
-
|
209
|
-
def _apply_signing_auth_flow(self, request: httpx.Request) -> None:
|
210
|
-
if self.adp_token is None or self.device_private_key is None:
|
211
|
-
raise AuthMissingSigningData
|
212
|
-
|
213
|
-
headers = sign_request(
|
214
|
-
method=request.method,
|
215
|
-
path=request.url.raw_path.decode(),
|
216
|
-
body=request.content,
|
217
|
-
adp_token=self.adp_token,
|
218
|
-
private_key=self.device_private_key,
|
219
|
-
)
|
220
|
-
|
221
|
-
request.headers.update(headers)
|
222
|
-
_LOGGER.info("signing auth flow applied to request")
|
223
|
-
|
224
|
-
def _apply_bearer_auth_flow(self, request: httpx.Request) -> None:
|
225
|
-
if self.access_token_expired:
|
226
|
-
self.refresh_access_token()
|
227
|
-
|
228
|
-
if self.access_token is None:
|
229
|
-
raise AuthMissingAccessToken
|
230
|
-
|
231
|
-
headers = {"Authorization": "Bearer " + self.access_token, "client-id": "0"}
|
232
|
-
request.headers.update(headers)
|
233
|
-
_LOGGER.info("bearer auth flow applied to request")
|
234
|
-
|
235
|
-
def _apply_cookies_auth_flow(self, request: httpx.Request) -> None:
|
236
|
-
if self.website_cookies is None:
|
237
|
-
raise AuthMissingWebsiteCookies
|
238
|
-
cookies = self.website_cookies.copy()
|
239
|
-
|
240
|
-
Cookies(cookies).set_cookie_header(request)
|
241
|
-
_LOGGER.info("cookies auth flow applied to request")
|
242
|
-
|
243
|
-
@property
|
244
|
-
def available_auth_modes(self) -> list[str]:
|
245
|
-
"""List available authentication modes."""
|
246
|
-
available_modes = []
|
247
|
-
|
248
|
-
if self.adp_token and self.device_private_key:
|
249
|
-
available_modes.append("signing")
|
250
|
-
|
251
|
-
if self.access_token:
|
252
|
-
if self.access_token_expired and not self.refresh_token:
|
253
|
-
pass
|
254
|
-
else:
|
255
|
-
available_modes.append("bearer")
|
256
|
-
|
257
|
-
if self.website_cookies:
|
258
|
-
available_modes.append("cookies")
|
259
|
-
|
260
|
-
return available_modes
|
261
|
-
|
262
|
-
def refresh_access_token(self, force: bool = False) -> None:
|
263
|
-
"""Refresh access token."""
|
264
|
-
if force or self.access_token_expired:
|
265
|
-
if self.refresh_token is None:
|
266
|
-
message = "No refresh token found. Can't refresh access token."
|
267
|
-
_LOGGER.critical(message)
|
268
|
-
raise AuthMissingRefreshToken(message)
|
269
|
-
|
270
|
-
refresh_data = refresh_access_token(
|
271
|
-
refresh_token=self.refresh_token,
|
272
|
-
domain=self.domain,
|
273
|
-
)
|
274
|
-
|
275
|
-
self.update_attrs(**refresh_data)
|
276
|
-
else:
|
277
|
-
_LOGGER.info(
|
278
|
-
"Access Token not expired. No refresh necessary. "
|
279
|
-
"To force refresh please use force=True",
|
280
|
-
)
|
281
|
-
|
282
|
-
def set_website_cookies_for_country(self) -> None:
|
283
|
-
"""Set website cookies for country."""
|
284
|
-
if self.refresh_token is None:
|
285
|
-
raise AuthMissingRefreshToken
|
286
|
-
|
287
|
-
self.website_cookies = refresh_website_cookies(
|
288
|
-
self.refresh_token,
|
289
|
-
self.domain,
|
290
|
-
)
|
291
|
-
|
292
|
-
def user_profile(self) -> dict[str, Any]:
|
293
|
-
"""Get user profile."""
|
294
|
-
if self.access_token is None:
|
295
|
-
raise AuthMissingAccessToken
|
296
|
-
|
297
|
-
return user_profile(access_token=self.access_token, domain=self.domain)
|
298
|
-
|
299
|
-
@property
|
300
|
-
def access_token_expires(self) -> timedelta:
|
301
|
-
"""Time to access token expiration."""
|
302
|
-
if self.expires is None:
|
303
|
-
raise AuthMissingTimestamp
|
304
|
-
return datetime.fromtimestamp(self.expires, UTC) - datetime.now(
|
305
|
-
UTC,
|
306
|
-
)
|
307
|
-
|
308
|
-
@property
|
309
|
-
def access_token_expired(self) -> bool:
|
310
|
-
"""Return True if access token is expired."""
|
311
|
-
if self.expires is None:
|
312
|
-
raise AuthMissingTimestamp
|
313
|
-
return datetime.fromtimestamp(self.expires, UTC) <= datetime.now(
|
314
|
-
UTC,
|
315
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|