aioamazondevices 1.0.0__tar.gz → 1.2.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-1.0.0 → aioamazondevices-1.2.0}/PKG-INFO +16 -7
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/README.md +14 -4
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/pyproject.toml +1 -1
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/src/aioamazondevices/api.py +62 -36
- aioamazondevices-1.0.0/src/aioamazondevices/auth.py +0 -315
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/LICENSE +0 -0
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/src/aioamazondevices/const.py +0 -0
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-1.0.0 → aioamazondevices-1.2.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: 1.
|
3
|
+
Version: 1.2.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
|
@@ -70,12 +69,22 @@ Install this via pip (or your favourite package manager):
|
|
70
69
|
|
71
70
|
`pip install aioamazondevices`
|
72
71
|
|
73
|
-
##
|
72
|
+
## Test
|
74
73
|
|
75
|
-
|
74
|
+
Test the library with:
|
76
75
|
|
77
|
-
|
78
|
-
|
76
|
+
`python library_test.py`
|
77
|
+
|
78
|
+
The script accept command line arguments or a library_test.json config file:
|
79
|
+
|
80
|
+
```json
|
81
|
+
{
|
82
|
+
"country": "IT",
|
83
|
+
"email": "<my_address@gmail.com>",
|
84
|
+
"password": "<my_password>",
|
85
|
+
"login_data_file": "out/login_data.json",
|
86
|
+
"save_raw_data": "True"
|
87
|
+
}
|
79
88
|
```
|
80
89
|
|
81
90
|
## Contributors ✨
|
@@ -41,12 +41,22 @@ Install this via pip (or your favourite package manager):
|
|
41
41
|
|
42
42
|
`pip install aioamazondevices`
|
43
43
|
|
44
|
-
##
|
44
|
+
## Test
|
45
45
|
|
46
|
-
|
46
|
+
Test the library with:
|
47
47
|
|
48
|
-
|
49
|
-
|
48
|
+
`python library_test.py`
|
49
|
+
|
50
|
+
The script accept command line arguments or a library_test.json config file:
|
51
|
+
|
52
|
+
```json
|
53
|
+
{
|
54
|
+
"country": "IT",
|
55
|
+
"email": "<my_address@gmail.com>",
|
56
|
+
"password": "<my_password>",
|
57
|
+
"login_data_file": "out/login_data.json",
|
58
|
+
"save_raw_data": "True"
|
59
|
+
}
|
50
60
|
```
|
51
61
|
|
52
62
|
## Contributors ✨
|
@@ -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,
|
@@ -98,7 +97,7 @@ class AmazonEchoApi:
|
|
98
97
|
if not self._login_stored_data:
|
99
98
|
return {}
|
100
99
|
|
101
|
-
return cast(dict, self._login_stored_data["website_cookies"])
|
100
|
+
return cast("dict", self._login_stored_data["website_cookies"])
|
102
101
|
|
103
102
|
def _serial_number(self) -> str:
|
104
103
|
"""Get or calculate device serial number."""
|
@@ -109,7 +108,7 @@ class AmazonEchoApi:
|
|
109
108
|
|
110
109
|
_LOGGER.debug("Found previous login data, loading serial number")
|
111
110
|
return cast(
|
112
|
-
str,
|
111
|
+
"str",
|
113
112
|
self._login_stored_data["device_info"]["device_serial_number"],
|
114
113
|
)
|
115
114
|
|
@@ -207,16 +206,15 @@ class AmazonEchoApi:
|
|
207
206
|
parsed_url = parse_qs(url.query.decode())
|
208
207
|
return parsed_url["openid.oa2.authorization_code"][0]
|
209
208
|
|
210
|
-
def _client_session(self
|
211
|
-
"""Create
|
209
|
+
def _client_session(self) -> None:
|
210
|
+
"""Create HTTP client session."""
|
212
211
|
if not hasattr(self, "session") or self.session.is_closed:
|
213
|
-
_LOGGER.debug("Creating HTTP
|
212
|
+
_LOGGER.debug("Creating HTTP session (httpx)")
|
214
213
|
self.session = AsyncClient(
|
215
214
|
base_url=f"https://www.amazon.{self._domain}",
|
216
215
|
headers=DEFAULT_HEADERS,
|
217
216
|
cookies=self._cookies,
|
218
217
|
follow_redirects=True,
|
219
|
-
auth=auth,
|
220
218
|
)
|
221
219
|
|
222
220
|
async def _session_request(
|
@@ -224,19 +222,28 @@ class AmazonEchoApi:
|
|
224
222
|
method: str,
|
225
223
|
url: str,
|
226
224
|
input_data: dict[str, Any] | None = None,
|
225
|
+
json_data: bool = False,
|
227
226
|
) -> tuple[BeautifulSoup, Response]:
|
228
227
|
"""Return request response context data."""
|
229
|
-
_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
|
+
)
|
230
235
|
resp = await self.session.request(
|
231
236
|
method,
|
232
237
|
url,
|
233
|
-
data=input_data,
|
238
|
+
data=input_data if not json_data else orjson.dumps(input_data),
|
234
239
|
cookies=self._website_cookies,
|
240
|
+
headers={"Content-Type": "application/json"} if json_data else None,
|
235
241
|
)
|
236
242
|
content_type: str = resp.headers.get("Content-Type", "")
|
237
243
|
_LOGGER.debug(
|
238
|
-
"Response %s with content type: %s",
|
244
|
+
"Response %s for url %s with content type: %s",
|
239
245
|
resp.status_code,
|
246
|
+
url,
|
240
247
|
content_type,
|
241
248
|
)
|
242
249
|
|
@@ -331,13 +338,12 @@ class AmazonEchoApi:
|
|
331
338
|
],
|
332
339
|
}
|
333
340
|
|
334
|
-
headers = {"Content-Type": "application/json"}
|
335
|
-
|
336
341
|
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
337
|
-
resp = await self.
|
338
|
-
|
339
|
-
|
340
|
-
|
342
|
+
_, resp = await self._session_request(
|
343
|
+
method="POST",
|
344
|
+
url=register_url,
|
345
|
+
input_data=body,
|
346
|
+
json_data=True,
|
341
347
|
)
|
342
348
|
resp_json = resp.json()
|
343
349
|
|
@@ -398,7 +404,7 @@ class AmazonEchoApi:
|
|
398
404
|
_LOGGER.debug("Build oauth URL")
|
399
405
|
login_url = self._build_oauth_url(code_verifier, client_id)
|
400
406
|
|
401
|
-
login_soup, _ = await self._session_request("GET", login_url)
|
407
|
+
login_soup, _ = await self._session_request(method="GET", url=login_url)
|
402
408
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
403
409
|
login_inputs = self._get_inputs_from_soup(login_soup)
|
404
410
|
login_inputs["email"] = self._login_email
|
@@ -406,13 +412,15 @@ class AmazonEchoApi:
|
|
406
412
|
|
407
413
|
_LOGGER.debug("Register at %s", login_url)
|
408
414
|
login_soup, _ = await self._session_request(
|
409
|
-
login_method,
|
410
|
-
login_url,
|
411
|
-
login_inputs,
|
415
|
+
method=login_method,
|
416
|
+
url=login_url,
|
417
|
+
input_data=login_inputs,
|
412
418
|
)
|
413
419
|
|
414
420
|
if not login_soup.find("input", id="auth-mfa-otpcode"):
|
415
|
-
_LOGGER.debug(
|
421
|
+
_LOGGER.debug(
|
422
|
+
'Cannot find "auth-mfa-otpcode" in html source [%s]', login_url
|
423
|
+
)
|
416
424
|
raise CannotAuthenticate
|
417
425
|
|
418
426
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
@@ -423,9 +431,9 @@ class AmazonEchoApi:
|
|
423
431
|
login_inputs["rememberDevice"] = "false"
|
424
432
|
|
425
433
|
login_soup, login_resp = await self._session_request(
|
426
|
-
login_method,
|
427
|
-
login_url,
|
428
|
-
login_inputs,
|
434
|
+
method=login_method,
|
435
|
+
url=login_url,
|
436
|
+
input_data=login_inputs,
|
429
437
|
)
|
430
438
|
|
431
439
|
authcode_url = None
|
@@ -457,7 +465,7 @@ class AmazonEchoApi:
|
|
457
465
|
if not self._login_stored_data:
|
458
466
|
_LOGGER.debug(
|
459
467
|
"Cannot find previous login data,\
|
460
|
-
use
|
468
|
+
use login_mode_interactive() method instead",
|
461
469
|
)
|
462
470
|
raise WrongMethod
|
463
471
|
|
@@ -466,18 +474,14 @@ class AmazonEchoApi:
|
|
466
474
|
self._login_email,
|
467
475
|
)
|
468
476
|
|
469
|
-
|
470
|
-
self._login_stored_data,
|
471
|
-
self._domain,
|
472
|
-
)
|
473
|
-
self._client_session(auth)
|
477
|
+
self._client_session()
|
474
478
|
|
475
479
|
return self._login_stored_data
|
476
480
|
|
477
481
|
async def close(self) -> None:
|
478
|
-
"""Close
|
482
|
+
"""Close http client session."""
|
479
483
|
if hasattr(self, "session"):
|
480
|
-
_LOGGER.debug("Closing
|
484
|
+
_LOGGER.debug("Closing HTTP session (httpx)")
|
481
485
|
await self.session.aclose()
|
482
486
|
|
483
487
|
async def get_devices_data(
|
@@ -487,8 +491,8 @@ class AmazonEchoApi:
|
|
487
491
|
devices: dict[str, Any] = {}
|
488
492
|
for key in URI_QUERIES:
|
489
493
|
_, raw_resp = await self._session_request(
|
490
|
-
"GET",
|
491
|
-
f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
494
|
+
method="GET",
|
495
|
+
url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
492
496
|
)
|
493
497
|
_LOGGER.debug("Response URL: %s", raw_resp.url)
|
494
498
|
response_code = raw_resp.status_code
|
@@ -496,7 +500,7 @@ class AmazonEchoApi:
|
|
496
500
|
|
497
501
|
response_data = raw_resp.text
|
498
502
|
_LOGGER.debug("Response data: |%s|", response_data)
|
499
|
-
json_data = {} if len(
|
503
|
+
json_data = {} if len(response_data) == 0 else raw_resp.json()
|
500
504
|
|
501
505
|
_LOGGER.debug("JSON data: |%s|", json_data)
|
502
506
|
|
@@ -532,3 +536,25 @@ class AmazonEchoApi:
|
|
532
536
|
)
|
533
537
|
|
534
538
|
return final_devices_list
|
539
|
+
|
540
|
+
async def auth_check_status(self) -> bool:
|
541
|
+
"""Check AUTH status."""
|
542
|
+
_, raw_resp = await self._session_request(
|
543
|
+
method="GET",
|
544
|
+
url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
|
545
|
+
)
|
546
|
+
if raw_resp.status_code != HTTPStatus.OK:
|
547
|
+
_LOGGER.debug(
|
548
|
+
"Session not authenticated: reply error %s",
|
549
|
+
raw_resp.status_code,
|
550
|
+
)
|
551
|
+
return False
|
552
|
+
|
553
|
+
resp_json = raw_resp.json()
|
554
|
+
if not (authentication := resp_json.get("authentication")):
|
555
|
+
_LOGGER.debug('Session not authenticated: reply missing "authentication"')
|
556
|
+
return False
|
557
|
+
|
558
|
+
authenticated = authentication.get("authenticated")
|
559
|
+
_LOGGER.debug("Session authenticated: %s", authenticated)
|
560
|
+
return bool(authenticated)
|
@@ -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
|