aioamazondevices 0.7.0__tar.gz → 0.7.2__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.7.0 → aioamazondevices-0.7.2}/PKG-INFO +3 -1
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/pyproject.toml +2 -1
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/src/aioamazondevices/api.py +73 -31
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/src/aioamazondevices/const.py +9 -1
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/LICENSE +0 -0
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/README.md +0 -0
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-0.7.0 → aioamazondevices-0.7.2}/src/aioamazondevices/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
Home-page: https://github.com/chemelli74/aioamazondevices
|
6
6
|
License: Apache Software License 2.0
|
@@ -15,8 +15,10 @@ Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
18
19
|
Classifier: Topic :: Software Development :: Libraries
|
19
20
|
Requires-Dist: beautifulsoup4
|
21
|
+
Requires-Dist: colorlog
|
20
22
|
Requires-Dist: httpx
|
21
23
|
Requires-Dist: orjson
|
22
24
|
Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "aioamazondevices"
|
3
|
-
version = "0.7.
|
3
|
+
version = "0.7.2"
|
4
4
|
description = "Python library to control Amazon devices"
|
5
5
|
authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
|
6
6
|
license = "Apache Software License 2.0"
|
@@ -24,6 +24,7 @@ packages = [
|
|
24
24
|
[tool.poetry.dependencies]
|
25
25
|
python = "^3.11"
|
26
26
|
beautifulsoup4 = "*"
|
27
|
+
colorlog = "*"
|
27
28
|
httpx = "*"
|
28
29
|
orjson = "*"
|
29
30
|
|
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
import base64
|
4
4
|
import hashlib
|
5
|
+
import json
|
6
|
+
import mimetypes
|
5
7
|
import secrets
|
6
8
|
import uuid
|
7
9
|
from dataclasses import dataclass
|
8
10
|
from datetime import UTC, datetime, timedelta
|
9
11
|
from http import HTTPStatus
|
10
12
|
from pathlib import Path
|
11
|
-
from typing import Any
|
13
|
+
from typing import Any, cast
|
12
14
|
from urllib.parse import parse_qs, urlencode
|
13
15
|
|
14
16
|
import orjson
|
@@ -24,8 +26,12 @@ from .const import (
|
|
24
26
|
AMAZON_CLIENT_OS,
|
25
27
|
AMAZON_DEVICE_SOFTWARE_VERSION,
|
26
28
|
AMAZON_DEVICE_TYPE,
|
29
|
+
DEFAULT_ASSOC_HANDLE,
|
27
30
|
DEFAULT_HEADERS,
|
28
31
|
DOMAIN_BY_COUNTRY,
|
32
|
+
HTML_EXTENSION,
|
33
|
+
JSON_EXTENSION,
|
34
|
+
SAVE_PATH,
|
29
35
|
URI_QUERIES,
|
30
36
|
)
|
31
37
|
from .exceptions import CannotAuthenticate, CannotRegisterDevice
|
@@ -61,9 +67,10 @@ class AmazonEchoApi:
|
|
61
67
|
locale = DOMAIN_BY_COUNTRY.get(country_code)
|
62
68
|
domain = locale["domain"] if locale else country_code
|
63
69
|
|
64
|
-
|
65
|
-
|
66
|
-
|
70
|
+
if locale and (assoc := locale.get("openid.assoc_handle")):
|
71
|
+
assoc_handle = assoc
|
72
|
+
else:
|
73
|
+
assoc_handle = f"{DEFAULT_ASSOC_HANDLE}_{country_code}"
|
67
74
|
self._assoc_handle = assoc_handle
|
68
75
|
|
69
76
|
self._login_email = login_email
|
@@ -73,10 +80,24 @@ class AmazonEchoApi:
|
|
73
80
|
self._cookies = self._build_init_cookies()
|
74
81
|
self._headers = DEFAULT_HEADERS
|
75
82
|
self._save_html = save_html
|
76
|
-
self._serial =
|
83
|
+
self._serial = self._serial_number()
|
77
84
|
|
78
85
|
self.session: AsyncClient
|
79
86
|
|
87
|
+
def _serial_number(self) -> str:
|
88
|
+
"""Get or calculate device serial number."""
|
89
|
+
fullpath = Path(SAVE_PATH, "login_data.json")
|
90
|
+
if not fullpath.exists():
|
91
|
+
# Create a new serial number
|
92
|
+
_LOGGER.debug("Cannot find previous login data, creating new serial number")
|
93
|
+
return uuid.uuid4().hex.upper()
|
94
|
+
|
95
|
+
with Path.open(fullpath, "rb") as file:
|
96
|
+
data = json.load(file)
|
97
|
+
|
98
|
+
_LOGGER.debug("Found previous login data, loading serial number")
|
99
|
+
return cast(str, data["device_info"]["device_serial_number"])
|
100
|
+
|
80
101
|
def _build_init_cookies(self) -> dict[str, str]:
|
81
102
|
"""Build initial cookies to prevent captcha in most cases."""
|
82
103
|
token_bytes = secrets.token_bytes(313)
|
@@ -195,32 +216,23 @@ class AmazonEchoApi:
|
|
195
216
|
url,
|
196
217
|
data=input_data,
|
197
218
|
)
|
198
|
-
content_type = resp.headers.get("Content-Type", "")
|
219
|
+
content_type: str = resp.headers.get("Content-Type", "")
|
199
220
|
_LOGGER.debug("Response content type: %s", content_type)
|
200
221
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
elif content_type == "application/json":
|
207
|
-
await self._save_to_file(
|
208
|
-
orjson.dumps(
|
209
|
-
orjson.loads(resp.text),
|
210
|
-
option=orjson.OPT_INDENT_2,
|
211
|
-
).decode("utf-8"),
|
212
|
-
url,
|
213
|
-
extension="json",
|
214
|
-
)
|
222
|
+
await self._save_to_file(
|
223
|
+
resp.text,
|
224
|
+
url,
|
225
|
+
mimetypes.guess_extension(content_type.split(";")[0]) or ".raw",
|
226
|
+
)
|
215
227
|
|
216
228
|
return BeautifulSoup(resp.content, "html.parser"), resp
|
217
229
|
|
218
230
|
async def _save_to_file(
|
219
231
|
self,
|
220
|
-
|
232
|
+
raw_data: str | dict,
|
221
233
|
url: str,
|
222
|
-
extension: str =
|
223
|
-
output_path: str =
|
234
|
+
extension: str = HTML_EXTENSION,
|
235
|
+
output_path: str = SAVE_PATH,
|
224
236
|
) -> None:
|
225
237
|
"""Save response data to disk."""
|
226
238
|
if not self._save_html:
|
@@ -229,10 +241,33 @@ class AmazonEchoApi:
|
|
229
241
|
output_dir = Path(output_path)
|
230
242
|
output_dir.mkdir(parents=True, exist_ok=True)
|
231
243
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
244
|
+
if url.startswith("http"):
|
245
|
+
url_split = url.split("/")
|
246
|
+
base_filename = f"{url_split[3]}-{url_split[4].split('?')[0]}"
|
247
|
+
else:
|
248
|
+
base_filename = url
|
249
|
+
fullpath = Path(output_dir, base_filename + extension)
|
250
|
+
|
251
|
+
if type(raw_data) is dict:
|
252
|
+
data = orjson.dumps(raw_data, option=orjson.OPT_INDENT_2).decode("utf-8")
|
253
|
+
elif extension == HTML_EXTENSION:
|
254
|
+
data = raw_data
|
255
|
+
else:
|
256
|
+
data = orjson.dumps(
|
257
|
+
orjson.loads(raw_data),
|
258
|
+
option=orjson.OPT_INDENT_2,
|
259
|
+
).decode("utf-8")
|
260
|
+
|
261
|
+
i = 2
|
262
|
+
while fullpath.exists():
|
263
|
+
filename = f"{base_filename}_{i!s}{extension}"
|
264
|
+
fullpath = Path(output_dir, filename)
|
265
|
+
i += 1
|
266
|
+
|
267
|
+
_LOGGER.warning("Saving data to %s", fullpath)
|
268
|
+
|
269
|
+
with Path.open(fullpath, "w+") as file:
|
270
|
+
file.write(data)
|
236
271
|
file.write("\n")
|
237
272
|
|
238
273
|
async def _register_device(
|
@@ -277,9 +312,9 @@ class AmazonEchoApi:
|
|
277
312
|
|
278
313
|
headers = {"Content-Type": "application/json"}
|
279
314
|
|
280
|
-
|
315
|
+
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
281
316
|
resp = await self.session.post(
|
282
|
-
|
317
|
+
register_url,
|
283
318
|
json=body,
|
284
319
|
headers=headers,
|
285
320
|
)
|
@@ -293,6 +328,11 @@ class AmazonEchoApi:
|
|
293
328
|
)
|
294
329
|
raise CannotRegisterDevice(resp_json)
|
295
330
|
|
331
|
+
await self._save_to_file(
|
332
|
+
resp.text,
|
333
|
+
url=register_url,
|
334
|
+
extension=JSON_EXTENSION,
|
335
|
+
)
|
296
336
|
success_response = resp_json["response"]["success"]
|
297
337
|
|
298
338
|
tokens = success_response["tokens"]
|
@@ -312,7 +352,7 @@ class AmazonEchoApi:
|
|
312
352
|
for cookie in tokens["website_cookies"]:
|
313
353
|
website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
|
314
354
|
|
315
|
-
|
355
|
+
login_data = {
|
316
356
|
"adp_token": adp_token,
|
317
357
|
"device_private_key": device_private_key,
|
318
358
|
"access_token": access_token,
|
@@ -323,6 +363,8 @@ class AmazonEchoApi:
|
|
323
363
|
"device_info": device_info,
|
324
364
|
"customer_info": customer_info,
|
325
365
|
}
|
366
|
+
await self._save_to_file(login_data, "login_data", JSON_EXTENSION)
|
367
|
+
return login_data
|
326
368
|
|
327
369
|
async def login(self, otp_code: str) -> dict[str, Any]:
|
328
370
|
"""Login to Amazon."""
|
@@ -386,7 +428,7 @@ class AmazonEchoApi:
|
|
386
428
|
|
387
429
|
register_device = await self._register_device(device_login_data)
|
388
430
|
|
389
|
-
_LOGGER.
|
431
|
+
_LOGGER.info("Register device: %s", register_device)
|
390
432
|
return register_device
|
391
433
|
|
392
434
|
async def close(self) -> None:
|
@@ -4,10 +4,12 @@ import logging
|
|
4
4
|
|
5
5
|
_LOGGER = logging.getLogger(__package__)
|
6
6
|
|
7
|
+
DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
|
8
|
+
|
7
9
|
DOMAIN_BY_COUNTRY = {
|
8
10
|
"us": {
|
9
11
|
"domain": "com",
|
10
|
-
"openid.assoc_handle":
|
12
|
+
"openid.assoc_handle": DEFAULT_ASSOC_HANDLE,
|
11
13
|
},
|
12
14
|
"uk": {
|
13
15
|
"domain": "co.uk",
|
@@ -17,6 +19,7 @@ DOMAIN_BY_COUNTRY = {
|
|
17
19
|
},
|
18
20
|
"jp": {
|
19
21
|
"domain": "co.jp",
|
22
|
+
"openid.assoc_handle": "jpflex",
|
20
23
|
},
|
21
24
|
"br": {
|
22
25
|
"domain": "com.br",
|
@@ -48,3 +51,8 @@ AMAZON_APP_VERSION = "2.2.556530.0"
|
|
48
51
|
AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
|
49
52
|
AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
|
50
53
|
AMAZON_CLIENT_OS = "16.6"
|
54
|
+
|
55
|
+
# File extensions
|
56
|
+
SAVE_PATH = "out"
|
57
|
+
HTML_EXTENSION = ".html"
|
58
|
+
JSON_EXTENSION = ".json"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|