aioamazondevices 3.0.4__py3-none-any.whl → 3.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "3.0.4"
3
+ __version__ = "3.0.6"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -8,8 +8,8 @@ import uuid
8
8
  from dataclasses import dataclass
9
9
  from datetime import UTC, datetime, timedelta
10
10
  from enum import StrEnum
11
- from http import HTTPStatus
12
- from http.cookies import Morsel
11
+ from http import HTTPMethod, HTTPStatus
12
+ from http.cookies import Morsel, SimpleCookie
13
13
  from pathlib import Path
14
14
  from typing import Any, cast
15
15
  from urllib.parse import parse_qs, urlencode
@@ -18,9 +18,8 @@ import orjson
18
18
  from aiohttp import ClientResponse, ClientSession
19
19
  from babel import Locale
20
20
  from bs4 import BeautifulSoup, Tag
21
- from httpx import URL as HTTPX_URL
22
- from multidict import MultiDictProxy
23
- from yarl import URL as YARL_URL
21
+ from multidict import CIMultiDictProxy, MultiDictProxy
22
+ from yarl import URL
24
23
 
25
24
  from .const import (
26
25
  _LOGGER,
@@ -47,10 +46,6 @@ from .const import (
47
46
  URI_QUERIES,
48
47
  )
49
48
  from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
50
- from .httpx import HttpxClientResponseWrapper, HttpxClientSession
51
-
52
- # Values: "aiohttp", or "httpx"
53
- LIBRARY = "httpx"
54
49
 
55
50
 
56
51
  @dataclass
@@ -123,7 +118,7 @@ class AmazonEchoApi:
123
118
  self._serial = self._serial_number()
124
119
  self._list_for_clusters: dict[str, str] = {}
125
120
 
126
- self.session: ClientSession | HttpxClientSession
121
+ self.session: ClientSession
127
122
 
128
123
  def _load_website_cookies(self) -> dict[str, str]:
129
124
  """Get website cookies, if avaliables."""
@@ -234,7 +229,7 @@ class AmazonEchoApi:
234
229
  return method, url
235
230
  raise TypeError("Unable to extract form data from response")
236
231
 
237
- def _extract_code_from_url(self, url: YARL_URL | HTTPX_URL) -> str:
232
+ def _extract_code_from_url(self, url: URL) -> str:
238
233
  """Extract the access token from url query after login."""
239
234
  parsed_url: dict[str, list[str]] = {}
240
235
  if isinstance(url.query, bytes):
@@ -249,18 +244,28 @@ class AmazonEchoApi:
249
244
  def _client_session(self) -> None:
250
245
  """Create HTTP client session."""
251
246
  if not hasattr(self, "session") or self.session.closed:
252
- _LOGGER.debug("Creating HTTP session (%s)", LIBRARY)
253
- if LIBRARY == "httpx":
254
- self.session = HttpxClientSession(
255
- headers=DEFAULT_HEADERS,
256
- cookies=self._cookies,
257
- follow_redirects=True,
258
- )
259
- else:
260
- self.session = ClientSession(
261
- headers=DEFAULT_HEADERS,
262
- cookies=self._cookies,
263
- )
247
+ _LOGGER.debug("Creating HTTP session (aiohttp)")
248
+ self.session = ClientSession(
249
+ headers=DEFAULT_HEADERS,
250
+ cookies=self._cookies,
251
+ )
252
+
253
+ async def _parse_cookies_from_headers(
254
+ self, headers: CIMultiDictProxy[str]
255
+ ) -> dict[str, str]:
256
+ """Parse cookies with a value from headers."""
257
+ cookies_with_value: dict[str, str] = {}
258
+
259
+ for value in headers.getall("Set-Cookie", ()):
260
+ cookie = SimpleCookie()
261
+ cookie.load(value)
262
+
263
+ for name, morsel in cookie.items():
264
+ if morsel.value and morsel.value not in ("-", ""):
265
+ cookies_with_value[name] = morsel.value
266
+
267
+ _LOGGER.debug("Cookies from headers: %s", cookies_with_value)
268
+ return cookies_with_value
264
269
 
265
270
  async def _session_request(
266
271
  self,
@@ -268,7 +273,7 @@ class AmazonEchoApi:
268
273
  url: str,
269
274
  input_data: dict[str, Any] | None = None,
270
275
  json_data: bool = False,
271
- ) -> tuple[BeautifulSoup, ClientResponse | HttpxClientResponseWrapper]:
276
+ ) -> tuple[BeautifulSoup, ClientResponse]:
272
277
  """Return request response context data."""
273
278
  _LOGGER.debug(
274
279
  "%s request: %s with payload %s [json=%s]",
@@ -289,17 +294,18 @@ class AmazonEchoApi:
289
294
  _LOGGER.debug("Adding %s to headers", json_header)
290
295
  headers.update(json_header)
291
296
 
292
- _url: YARL_URL | str = url
293
- if LIBRARY == "aiohttp":
294
- _url = YARL_URL(url, encoded=True)
295
-
297
+ _cookies = (
298
+ self._load_website_cookies() if self._login_stored_data else self._cookies
299
+ )
296
300
  resp = await self.session.request(
297
301
  method,
298
- _url,
302
+ URL(url, encoded=True),
299
303
  data=input_data if not json_data else orjson.dumps(input_data),
300
- cookies=self._load_website_cookies(),
304
+ cookies=_cookies,
301
305
  headers=headers,
302
306
  )
307
+ self._cookies.update(**await self._parse_cookies_from_headers(resp.headers))
308
+
303
309
  content_type: str = resp.headers.get("Content-Type", "")
304
310
  _LOGGER.debug(
305
311
  "Response %s for url %s with content type: %s",
@@ -402,7 +408,7 @@ class AmazonEchoApi:
402
408
 
403
409
  register_url = f"https://api.amazon.{self._domain}/auth/register"
404
410
  _, resp = await self._session_request(
405
- method="POST",
411
+ method=HTTPMethod.POST,
406
412
  url=register_url,
407
413
  input_data=body,
408
414
  json_data=True,
@@ -466,7 +472,9 @@ class AmazonEchoApi:
466
472
  _LOGGER.debug("Build oauth URL")
467
473
  login_url = self._build_oauth_url(code_verifier, client_id)
468
474
 
469
- login_soup, _ = await self._session_request(method="GET", url=login_url)
475
+ login_soup, _ = await self._session_request(
476
+ method=HTTPMethod.GET, url=login_url
477
+ )
470
478
  login_method, login_url = self._get_request_from_soup(login_soup)
471
479
  login_inputs = self._get_inputs_from_soup(login_soup)
472
480
  login_inputs["email"] = self._login_email
@@ -535,7 +543,7 @@ class AmazonEchoApi:
535
543
  async def close(self) -> None:
536
544
  """Close http client session."""
537
545
  if hasattr(self, "session"):
538
- _LOGGER.debug("Closing HTTP session (%s)", LIBRARY)
546
+ _LOGGER.debug("Closing HTTP session (aiohttp)")
539
547
  await self.session.close()
540
548
 
541
549
  async def get_devices_data(
@@ -545,7 +553,7 @@ class AmazonEchoApi:
545
553
  devices: dict[str, Any] = {}
546
554
  for key in URI_QUERIES:
547
555
  _, raw_resp = await self._session_request(
548
- method="GET",
556
+ method=HTTPMethod.GET,
549
557
  url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
550
558
  )
551
559
  _LOGGER.debug("Response URL: %s", raw_resp.url)
@@ -615,7 +623,7 @@ class AmazonEchoApi:
615
623
  async def auth_check_status(self) -> bool:
616
624
  """Check AUTH status."""
617
625
  _, raw_resp = await self._session_request(
618
- method="GET",
626
+ method=HTTPMethod.GET,
619
627
  url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
620
628
  )
621
629
  if raw_resp.status != HTTPStatus.OK:
@@ -760,7 +768,7 @@ class AmazonEchoApi:
760
768
 
761
769
  _LOGGER.debug("Preview data payload: %s", node_data)
762
770
  await self._session_request(
763
- method="POST",
771
+ method=HTTPMethod.POST,
764
772
  url=f"https://alexa.amazon.{self._domain}/api/behaviors/preview",
765
773
  input_data=node_data,
766
774
  json_data=True,
aioamazondevices/const.py CHANGED
@@ -84,6 +84,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
84
84
  "model": "Echo Dot",
85
85
  "hw_version": "Gen3",
86
86
  },
87
+ "A1Z88NGR2BK6A2": {
88
+ "model": "Echo Show 8",
89
+ "hw_version": "Gen1",
90
+ },
87
91
  "A271DR1789MXDS": {
88
92
  "model": "Fire Tablet 7",
89
93
  "hw_version": "Gen12",
@@ -96,6 +100,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
96
100
  "model": "Echo Dot Clock",
97
101
  "hw_version": "Gen4",
98
102
  },
103
+ "A2JKHJ0PX4J3L3": {
104
+ "model": "Fire TV Cube",
105
+ "hw_version": "Gen2",
106
+ },
99
107
  "A2LWARUGJLBYEW": {
100
108
  "model": "Fire TV Stick",
101
109
  "hw_version": "Gen2",
@@ -108,6 +116,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
108
116
  "model": "Echo Dot Clock",
109
117
  "hw_version": "Gen4",
110
118
  },
119
+ "A2UONLFQW0PADH": {
120
+ "model": "Echo Show 8",
121
+ "hw_version": "Gen3",
122
+ },
111
123
  "A303PJF6ISQ7IC": {
112
124
  "model": "Echo Auto",
113
125
  "hw_version": "Gen1",
@@ -149,13 +161,17 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
149
161
  "hw_version": "Gen5",
150
162
  },
151
163
  "A7WXQPH584YP": {
152
- "model": "Echo Dot",
164
+ "model": "Echo",
153
165
  "hw_version": "Gen2",
154
166
  },
155
167
  "AB72C64C86AW2": {
156
168
  "model": "Echo Dot",
157
169
  "hw_version": "Gen2",
158
170
  },
171
+ "AIPK7MM90V7TB": {
172
+ "model": "Echo Show 10",
173
+ "hw_version": "Gen3",
174
+ },
159
175
  "AKNO1N0KSFN8L": {
160
176
  "model": "Echo Dot",
161
177
  "hw_version": "Gen1",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: Python library to control Amazon devices
5
5
  License: Apache-2.0
6
6
  Author: Simone Chemelli
@@ -86,7 +86,8 @@ The script accept command line arguments or a library_test.json config file:
86
86
  "single_device_name": "Echo Dot Livingroom",
87
87
  "cluster_device_name": "Everywhere",
88
88
  "login_data_file": "out/login_data.json",
89
- "save_raw_data": "True"
89
+ "save_raw_data": true,
90
+ "test": true
90
91
  }
91
92
  ```
92
93
 
@@ -97,8 +98,11 @@ Library logs a warning if an unknown device type is linked to your Amazon accoun
97
98
  Please open an issue [here](https://github.com/chemelli74/aioamazondevices/issues) and provide the following information:
98
99
 
99
100
  - device type
101
+ - brand
100
102
  - model
101
103
  - generation
104
+ - entities are available in Home Assistant
105
+ - entities work in Home Assistant
102
106
 
103
107
  Current device list: `DEVICE_TYPE_TO_MODEL` from [const.py](https://github.com/chemelli74/aioamazondevices/blob/main/src/aioamazondevices/const.py)
104
108
 
@@ -0,0 +1,10 @@
1
+ aioamazondevices/__init__.py,sha256=XSc83vzKGe3HPjCJbw9K72d_eyR0i-ofMB47-uUKoUA,276
2
+ aioamazondevices/api.py,sha256=pMpW51Wu4G56uVhT29O7C-hoHgHYiNbP-7Lz9UCXz0E,29943
3
+ aioamazondevices/const.py,sha256=gGj6Fakolq9gmr10IqG09ocgIW5LdKwAPaKHHuT6o28,4647
4
+ aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aioamazondevices/sounds.py,sha256=01pVCDFIuhrLypXInw4JNuHsC6zjMLsuKocet1R6we8,13409
7
+ aioamazondevices-3.0.6.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
8
+ aioamazondevices-3.0.6.dist-info/METADATA,sha256=aRbpJqVf5xRNNE73-k0WDWzRMluyhKz_48sGol6ieJ0,5546
9
+ aioamazondevices-3.0.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ aioamazondevices-3.0.6.dist-info/RECORD,,
aioamazondevices/httpx.py DELETED
@@ -1,159 +0,0 @@
1
- """Wrapper around httpx.Response to mimic aiohttp.ClientResponse."""
2
-
3
- import types
4
- from http.cookies import SimpleCookie
5
- from typing import Any, Self, cast
6
-
7
- from httpx import AsyncClient, Cookies, Response
8
- from httpx._types import RequestData
9
-
10
-
11
- def convert_httpx_cookies_to_simplecookie(httpx_cookies: Cookies) -> SimpleCookie:
12
- """Convert an httpx.Cookies object to a single SimpleCookie."""
13
- simple_cookie = SimpleCookie()
14
-
15
- for name, value in httpx_cookies.items():
16
- simple_cookie[name] = value
17
-
18
- return simple_cookie
19
-
20
-
21
- class HttpxClientResponseWrapper:
22
- """aiohttp-like Wrapper for httpx.Response."""
23
-
24
- def __init__(self, response: Response) -> None:
25
- """Init wrapper."""
26
- self._response = response
27
-
28
- # Basic aiohttp-like attributes
29
- self.status = response.status_code
30
- self.headers = response.headers
31
- self.url = response.url
32
- self.reason = response.reason_phrase
33
- self.cookies = convert_httpx_cookies_to_simplecookie(response.cookies)
34
- self.ok = response.is_success
35
-
36
- # Aiohttp compatibility
37
- self.content_type = response.headers.get("Content-Type", "")
38
- self.real_url = str(response.url)
39
- self.request_info = types.SimpleNamespace(
40
- real_url=self.url,
41
- method=response.request.method,
42
- headers=response.request.headers,
43
- )
44
-
45
- # History (aiohttp returns redirects as history)
46
- self.history = (
47
- [HttpxClientResponseWrapper(r) for r in response.history]
48
- if response.history
49
- else []
50
- )
51
-
52
- async def text(self, encoding: str | None = None) -> str:
53
- """Text."""
54
- # httpx auto-decodes
55
- if encoding:
56
- return cast("str", self._response.content.decode(encoding))
57
- return cast("str", self._response.text)
58
-
59
- async def json(self) -> Any: # noqa: ANN401
60
- """Json."""
61
- return self._response.json()
62
-
63
- async def read(self) -> bytes:
64
- """Read."""
65
- return cast("bytes", self._response.content)
66
-
67
- async def release(self) -> None:
68
- """Release."""
69
- # No-op: aiohttp requires this, httpx does not
70
-
71
- def raise_for_status(self) -> Response:
72
- """Raise for status."""
73
- return self._response.raise_for_status()
74
-
75
- def __repr__(self) -> str:
76
- """Repr."""
77
- return f"<HttpxClientResponseWrapper [{self.status} {self.reason}]>"
78
-
79
- async def __aenter__(self) -> Self:
80
- """Aenter."""
81
- return self
82
-
83
- async def __aexit__(
84
- self,
85
- exc_type: type[BaseException] | None,
86
- exc_val: BaseException | None,
87
- exc_tb: types.TracebackType | None,
88
- ) -> None:
89
- """Aexit."""
90
- await self.release()
91
-
92
-
93
- class HttpxClientSession:
94
- """aiohttp-like Wrapper for httpx.AsyncClient."""
95
-
96
- def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
97
- """Init session."""
98
- # Allow passing any httpx.AsyncClient init kwargs (e.g., headers, cookies)
99
- self._client = AsyncClient(**kwargs)
100
-
101
- @property
102
- def closed(self) -> bool:
103
- """Indicates whether the underlying client session is closed."""
104
- return cast("bool", self._client.is_closed)
105
-
106
- async def request(
107
- self,
108
- method: str,
109
- url: str,
110
- *,
111
- params: dict[str, str | int | float] | None = None,
112
- data: Any = None, # noqa: ANN401
113
- json: Any = None, # noqa: ANN401
114
- **kwargs: Any, # noqa: ANN401
115
- ) -> HttpxClientResponseWrapper:
116
- """Make a generic request method for any HTTP verb."""
117
- response = await self._client.request(
118
- method=method.upper(),
119
- url=url,
120
- params=params,
121
- data=data,
122
- json=json,
123
- **kwargs,
124
- )
125
- return HttpxClientResponseWrapper(response)
126
-
127
- async def get(self, url: str, **kwargs: Any) -> HttpxClientResponseWrapper: # noqa: ANN401
128
- """Get."""
129
- response = await self._client.get(url, **kwargs)
130
- return HttpxClientResponseWrapper(response)
131
-
132
- async def post(
133
- self,
134
- url: str,
135
- data: RequestData | None = None,
136
- json: Any = None, # noqa: ANN401
137
- **kwargs: Any, # noqa: ANN401
138
- ) -> HttpxClientResponseWrapper:
139
- """Post."""
140
- response = await self._client.post(url, data=data, json=json, **kwargs)
141
- return HttpxClientResponseWrapper(response)
142
-
143
- async def close(self) -> None:
144
- """Close."""
145
- await self._client.aclose()
146
-
147
- async def __aenter__(self) -> Self:
148
- """AEnter."""
149
- await self._client.__aenter__()
150
- return self
151
-
152
- async def __aexit__(
153
- self,
154
- exc_type: type[BaseException] | None,
155
- exc_val: BaseException | None,
156
- exc_tb: types.TracebackType | None,
157
- ) -> None:
158
- """AExit."""
159
- await self._client.__aexit__(exc_type, exc_val, exc_tb)
@@ -1,11 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=isQEyhdDy8nskzwkJ4S_C-xh93Hz_ijfqGQmHmb8Bfg,276
2
- aioamazondevices/api.py,sha256=ukafvMs2E5O43IRbukqL_uUzryFsy-PsWMqn9Yyw_Ho,29633
3
- aioamazondevices/const.py,sha256=hLQancDbkMKvjRkoLgU5g6Xk-uhqa_uhC11cJoTloFA,4278
4
- aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
- aioamazondevices/httpx.py,sha256=DyuD2HD3GGGbBq65qcjPCCxeuSkALKzDAdrUSeZcRMM,4935
6
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- aioamazondevices/sounds.py,sha256=01pVCDFIuhrLypXInw4JNuHsC6zjMLsuKocet1R6we8,13409
8
- aioamazondevices-3.0.4.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
9
- aioamazondevices-3.0.4.dist-info/METADATA,sha256=nZHfKVVea2LQLf0nBhYAz56gWkknw8X1FhVUyr1fb_c,5447
10
- aioamazondevices-3.0.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
- aioamazondevices-3.0.4.dist-info/RECORD,,