aioamazondevices 1.7.0__py3-none-any.whl → 1.9.0__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.
- aioamazondevices/__init__.py +1 -1
- aioamazondevices/api.py +35 -7
- aioamazondevices/httpx.py +159 -0
- {aioamazondevices-1.7.0.dist-info → aioamazondevices-1.9.0.dist-info}/METADATA +2 -1
- aioamazondevices-1.9.0.dist-info/RECORD +10 -0
- aioamazondevices-1.7.0.dist-info/RECORD +0 -9
- {aioamazondevices-1.7.0.dist-info → aioamazondevices-1.9.0.dist-info}/LICENSE +0 -0
- {aioamazondevices-1.7.0.dist-info → aioamazondevices-1.9.0.dist-info}/WHEEL +0 -0
aioamazondevices/__init__.py
CHANGED
aioamazondevices/api.py
CHANGED
@@ -44,6 +44,10 @@ from .const import (
|
|
44
44
|
URI_QUERIES,
|
45
45
|
)
|
46
46
|
from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
|
47
|
+
from .httpx import HttpxClientResponseWrapper, HttpxClientSession
|
48
|
+
|
49
|
+
# Values: "aiohttp", or "httpx"
|
50
|
+
LIBRARY = "httpx"
|
47
51
|
|
48
52
|
|
49
53
|
@dataclass
|
@@ -71,6 +75,7 @@ class AmazonSequenceType(StrEnum):
|
|
71
75
|
Speak = "Alexa.Speak"
|
72
76
|
Sound = "Alexa.Sound"
|
73
77
|
Music = "Alexa.Music.PlaySearchPhrase"
|
78
|
+
TextCommand = "Alexa.TextCommand"
|
74
79
|
|
75
80
|
|
76
81
|
class AmazonMusicSource(StrEnum):
|
@@ -235,11 +240,18 @@ class AmazonEchoApi:
|
|
235
240
|
def _client_session(self) -> None:
|
236
241
|
"""Create HTTP client session."""
|
237
242
|
if not hasattr(self, "session") or self.session.closed:
|
238
|
-
_LOGGER.debug("Creating HTTP session (
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
+
_LOGGER.debug("Creating HTTP session (%s)", LIBRARY)
|
244
|
+
if LIBRARY == "httpx":
|
245
|
+
self.session = HttpxClientSession(
|
246
|
+
headers=DEFAULT_HEADERS,
|
247
|
+
cookies=self._cookies,
|
248
|
+
follow_redirects=True,
|
249
|
+
)
|
250
|
+
else:
|
251
|
+
self.session = ClientSession(
|
252
|
+
headers=DEFAULT_HEADERS,
|
253
|
+
cookies=self._cookies,
|
254
|
+
)
|
243
255
|
|
244
256
|
async def _session_request(
|
245
257
|
self,
|
@@ -247,7 +259,7 @@ class AmazonEchoApi:
|
|
247
259
|
url: str,
|
248
260
|
input_data: dict[str, Any] | None = None,
|
249
261
|
json_data: bool = False,
|
250
|
-
) -> tuple[BeautifulSoup, ClientResponse]:
|
262
|
+
) -> tuple[BeautifulSoup, ClientResponse | HttpxClientResponseWrapper]:
|
251
263
|
"""Return request response context data."""
|
252
264
|
_LOGGER.debug(
|
253
265
|
"%s request: %s with payload %s [json=%s]",
|
@@ -518,7 +530,7 @@ class AmazonEchoApi:
|
|
518
530
|
async def close(self) -> None:
|
519
531
|
"""Close http client session."""
|
520
532
|
if hasattr(self, "session"):
|
521
|
-
_LOGGER.debug("Closing HTTP session (
|
533
|
+
_LOGGER.debug("Closing HTTP session (%s)", LIBRARY)
|
522
534
|
await self.session.close()
|
523
535
|
|
524
536
|
async def get_devices_data(
|
@@ -700,6 +712,12 @@ class AmazonEchoApi:
|
|
700
712
|
"sanitizedSearchPhrase": message_body,
|
701
713
|
"musicProviderId": message_source,
|
702
714
|
}
|
715
|
+
elif message_type == AmazonSequenceType.TextCommand:
|
716
|
+
payload = {
|
717
|
+
**base_payload,
|
718
|
+
"skillId": "amzn1.ask.1p.tellalexa",
|
719
|
+
"text": message_body,
|
720
|
+
}
|
703
721
|
|
704
722
|
sequence = {
|
705
723
|
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
@@ -767,3 +785,13 @@ class AmazonEchoApi:
|
|
767
785
|
return await self._send_message(
|
768
786
|
device, AmazonSequenceType.Music, message_body, message_source
|
769
787
|
)
|
788
|
+
|
789
|
+
async def call_alexa_text_command(
|
790
|
+
self,
|
791
|
+
device: AmazonDevice,
|
792
|
+
message_body: str,
|
793
|
+
) -> None:
|
794
|
+
"""Call Alexa.Sound to play sound."""
|
795
|
+
return await self._send_message(
|
796
|
+
device, AmazonSequenceType.TextCommand, message_body
|
797
|
+
)
|
@@ -0,0 +1,159 @@
|
|
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,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.9.0
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
License: Apache-2.0
|
6
6
|
Author: Simone Chemelli
|
@@ -19,6 +19,7 @@ Requires-Dist: aiohttp
|
|
19
19
|
Requires-Dist: babel
|
20
20
|
Requires-Dist: beautifulsoup4
|
21
21
|
Requires-Dist: colorlog
|
22
|
+
Requires-Dist: httpx
|
22
23
|
Requires-Dist: orjson
|
23
24
|
Requires-Dist: yarl
|
24
25
|
Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
|
@@ -0,0 +1,10 @@
|
|
1
|
+
aioamazondevices/__init__.py,sha256=j0fW4NsfctzqB0gzPXx8Og0Owh-PWVIiN4wAl4J3z9M,276
|
2
|
+
aioamazondevices/api.py,sha256=Wqq80_IJwVT9eJlo1TdSv14p8sWDe19jg96zDITE4r8,28363
|
3
|
+
aioamazondevices/const.py,sha256=6BBEg_q2BkYVYJcz3hMrLNyEwOJBWziPSStMzftWQLg,2106
|
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-1.9.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
|
8
|
+
aioamazondevices-1.9.0.dist-info/METADATA,sha256=81EZrAn7bG1pdd7YLwXrd8knBu52DMhsVqVoV14uN8A,5031
|
9
|
+
aioamazondevices-1.9.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
+
aioamazondevices-1.9.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
aioamazondevices/__init__.py,sha256=L4W0ORfkNA0DX8VpSFxeXgqh0SCV-FEGz1MGVY5suBc,276
|
2
|
-
aioamazondevices/api.py,sha256=MqUfScgFXiD26AOSZfpKPN78xysmOkq7g-p2KutN9vs,27392
|
3
|
-
aioamazondevices/const.py,sha256=6BBEg_q2BkYVYJcz3hMrLNyEwOJBWziPSStMzftWQLg,2106
|
4
|
-
aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
|
5
|
-
aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
aioamazondevices-1.7.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
|
7
|
-
aioamazondevices-1.7.0.dist-info/METADATA,sha256=dxCWOsgRGz6qydT8btCfigvN9DZeYHkOvgmB5mRdbO8,5010
|
8
|
-
aioamazondevices-1.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
9
|
-
aioamazondevices-1.7.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|