aioamazondevices 1.8.0__tar.gz → 1.9.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.8.0 → aioamazondevices-1.9.0}/PKG-INFO +2 -1
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/pyproject.toml +2 -1
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/src/aioamazondevices/api.py +18 -7
- aioamazondevices-1.9.0/src/aioamazondevices/httpx.py +159 -0
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/LICENSE +0 -0
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/README.md +0 -0
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/src/aioamazondevices/const.py +0 -0
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-1.8.0 → aioamazondevices-1.9.0}/src/aioamazondevices/py.typed +0 -0
@@ -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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "aioamazondevices"
|
3
|
-
version = "1.
|
3
|
+
version = "1.9.0"
|
4
4
|
description = "Python library to control Amazon devices"
|
5
5
|
authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
|
6
6
|
license = "Apache-2.0"
|
@@ -23,6 +23,7 @@ packages = [
|
|
23
23
|
|
24
24
|
[tool.poetry.dependencies]
|
25
25
|
aiohttp = "*"
|
26
|
+
httpx = "*"
|
26
27
|
python = "^3.12"
|
27
28
|
babel = "*"
|
28
29
|
beautifulsoup4 = "*"
|
@@ -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
|
@@ -236,11 +240,18 @@ class AmazonEchoApi:
|
|
236
240
|
def _client_session(self) -> None:
|
237
241
|
"""Create HTTP client session."""
|
238
242
|
if not hasattr(self, "session") or self.session.closed:
|
239
|
-
_LOGGER.debug("Creating HTTP session (
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
+
)
|
244
255
|
|
245
256
|
async def _session_request(
|
246
257
|
self,
|
@@ -248,7 +259,7 @@ class AmazonEchoApi:
|
|
248
259
|
url: str,
|
249
260
|
input_data: dict[str, Any] | None = None,
|
250
261
|
json_data: bool = False,
|
251
|
-
) -> tuple[BeautifulSoup, ClientResponse]:
|
262
|
+
) -> tuple[BeautifulSoup, ClientResponse | HttpxClientResponseWrapper]:
|
252
263
|
"""Return request response context data."""
|
253
264
|
_LOGGER.debug(
|
254
265
|
"%s request: %s with payload %s [json=%s]",
|
@@ -519,7 +530,7 @@ class AmazonEchoApi:
|
|
519
530
|
async def close(self) -> None:
|
520
531
|
"""Close http client session."""
|
521
532
|
if hasattr(self, "session"):
|
522
|
-
_LOGGER.debug("Closing HTTP session (
|
533
|
+
_LOGGER.debug("Closing HTTP session (%s)", LIBRARY)
|
523
534
|
await self.session.close()
|
524
535
|
|
525
536
|
async def get_devices_data(
|
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|