aioamazondevices 0.9.0__py3-none-any.whl → 0.11.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.
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.11.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -12,6 +12,6 @@ from .exceptions import (
12
12
  __all__ = [
13
13
  "AmazonDevice",
14
14
  "AmazonEchoApi",
15
- "CannotConnect",
16
15
  "CannotAuthenticate",
16
+ "CannotConnect",
17
17
  ]
aioamazondevices/api.py CHANGED
@@ -15,8 +15,9 @@ from urllib.parse import parse_qs, urlencode
15
15
 
16
16
  import orjson
17
17
  from bs4 import BeautifulSoup, Tag
18
- from httpx import URL, AsyncClient, Response
18
+ from httpx import URL, AsyncClient, Auth, Response
19
19
 
20
+ from .auth import Authenticator
20
21
  from .const import (
21
22
  _LOGGER,
22
23
  AMAZON_APP_BUNDLE_ID,
@@ -35,7 +36,7 @@ from .const import (
35
36
  SAVE_PATH,
36
37
  URI_QUERIES,
37
38
  )
38
- from .exceptions import CannotAuthenticate, CannotRegisterDevice
39
+ from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
39
40
 
40
41
 
41
42
  @dataclass
@@ -59,6 +60,7 @@ class AmazonEchoApi:
59
60
  login_country_code: str,
60
61
  login_email: str,
61
62
  login_password: str,
63
+ login_data_file: str | None,
62
64
  save_raw_data: bool = False,
63
65
  ) -> None:
64
66
  """Initialize the scanner."""
@@ -81,23 +83,43 @@ class AmazonEchoApi:
81
83
  self._cookies = self._build_init_cookies()
82
84
  self._headers = DEFAULT_HEADERS
83
85
  self._save_raw_data = save_raw_data
86
+ self._login_stored_data: dict[str, Any] = self._load_data_file(login_data_file)
84
87
  self._serial = self._serial_number()
88
+ self._website_cookies: dict[str, Any] = self._load_website_cookies()
85
89
 
86
90
  self.session: AsyncClient
87
91
 
92
+ def _load_data_file(self, data_file: str | None) -> dict[str, Any]:
93
+ """Load stored login data from file."""
94
+ if not data_file or not (file := Path(data_file)).exists():
95
+ _LOGGER.debug(
96
+ "Cannot find previous login data file <%s>",
97
+ data_file,
98
+ )
99
+ return {}
100
+
101
+ with Path.open(file, "rb") as f:
102
+ return cast(dict[str, Any], json.load(f))
103
+
104
+ def _load_website_cookies(self) -> dict[str, Any]:
105
+ """Get website cookies, if avaliables."""
106
+ if not self._login_stored_data:
107
+ return {}
108
+
109
+ return cast(dict, self._login_stored_data["website_cookies"])
110
+
88
111
  def _serial_number(self) -> str:
89
112
  """Get or calculate device serial number."""
90
- fullpath = Path(SAVE_PATH, "login_data.json")
91
- if not fullpath.exists():
113
+ if not self._login_stored_data:
92
114
  # Create a new serial number
93
115
  _LOGGER.debug("Cannot find previous login data, creating new serial number")
94
116
  return uuid.uuid4().hex.upper()
95
117
 
96
- with Path.open(fullpath, "rb") as file:
97
- data = json.load(file)
98
-
99
118
  _LOGGER.debug("Found previous login data, loading serial number")
100
- return cast(str, data["device_info"]["device_serial_number"])
119
+ return cast(
120
+ str,
121
+ self._login_stored_data["device_info"]["device_serial_number"],
122
+ )
101
123
 
102
124
  def _build_init_cookies(self) -> dict[str, str]:
103
125
  """Build initial cookies to prevent captcha in most cases."""
@@ -193,7 +215,7 @@ class AmazonEchoApi:
193
215
  parsed_url = parse_qs(url.query.decode())
194
216
  return parsed_url["openid.oa2.authorization_code"][0]
195
217
 
196
- def _client_session(self) -> None:
218
+ def _client_session(self, auth: Auth | None = None) -> None:
197
219
  """Create httpx ClientSession."""
198
220
  if not hasattr(self, "session") or self.session.is_closed:
199
221
  _LOGGER.debug("Creating HTTP ClientSession")
@@ -202,6 +224,7 @@ class AmazonEchoApi:
202
224
  headers=DEFAULT_HEADERS,
203
225
  cookies=self._cookies,
204
226
  follow_redirects=True,
227
+ auth=auth,
205
228
  )
206
229
 
207
230
  async def _session_request(
@@ -216,6 +239,7 @@ class AmazonEchoApi:
216
239
  method,
217
240
  url,
218
241
  data=input_data,
242
+ cookies=self._website_cookies,
219
243
  )
220
244
  content_type: str = resp.headers.get("Content-Type", "")
221
245
  _LOGGER.debug("Response content type: %s", content_type)
@@ -367,8 +391,8 @@ class AmazonEchoApi:
367
391
  await self._save_to_file(login_data, "login_data", JSON_EXTENSION)
368
392
  return login_data
369
393
 
370
- async def login(self, otp_code: str) -> dict[str, Any]:
371
- """Login to Amazon."""
394
+ async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
395
+ """Login to Amazon interactively via OTP."""
372
396
  _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
373
397
  self._client_session()
374
398
 
@@ -432,6 +456,28 @@ class AmazonEchoApi:
432
456
  _LOGGER.info("Register device: %s", register_device)
433
457
  return register_device
434
458
 
459
+ async def login_mode_stored_data(self) -> dict[str, Any]:
460
+ """Login to Amazon using previously stored data."""
461
+ if not self._login_stored_data:
462
+ _LOGGER.debug(
463
+ "Cannot find previous login data,\
464
+ use login_interactive() method instead",
465
+ )
466
+ raise WrongMethod
467
+
468
+ _LOGGER.debug(
469
+ "Logging-in for %s with stored data",
470
+ self._login_email,
471
+ )
472
+
473
+ auth = Authenticator.from_dict(
474
+ self._login_stored_data,
475
+ self._domain,
476
+ )
477
+ self._client_session(auth)
478
+
479
+ return self._login_stored_data
480
+
435
481
  async def close(self) -> None:
436
482
  """Close httpx session."""
437
483
  if hasattr(self, "session"):
@@ -467,8 +513,7 @@ class AmazonEchoApi:
467
513
 
468
514
  # Remove stale, orphaned and virtual devices
469
515
  final_devices_list: dict[str, Any] = devices.copy()
470
- for serial in devices:
471
- device = devices[serial]
516
+ for serial, device in devices.items():
472
517
  if (
473
518
  DEVICES not in device
474
519
  or device[DEVICES].get("deviceType") == AMAZON_DEVICE_TYPE
@@ -0,0 +1,315 @@
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
+ )
aioamazondevices/const.py CHANGED
@@ -26,9 +26,18 @@ DOMAIN_BY_COUNTRY = {
26
26
  },
27
27
  }
28
28
 
29
+ # Amazon APP info
30
+ AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
31
+ AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
32
+ AMAZON_APP_NAME = "AioAmazonDevices"
33
+ AMAZON_APP_VERSION = "2.2.556530.0"
34
+ AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
35
+ AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
36
+ AMAZON_CLIENT_OS = "16.6"
37
+
29
38
  DEFAULT_HEADERS = {
30
39
  "User-Agent": (
31
- "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) "
40
+ f"Mozilla/5.0 (iPhone; CPU iPhone OS {AMAZON_CLIENT_OS.replace('.', '_')} like Mac OS X) " # noqa: E501
32
41
  "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
33
42
  ),
34
43
  "Accept-Language": "en-US",
@@ -43,16 +52,20 @@ URI_QUERIES = {
43
52
  "bluetoothStates": "/api/bluetooth",
44
53
  }
45
54
 
46
- # Amazon APP info
47
- AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
48
- AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
49
- AMAZON_APP_NAME = "AioAmazonDevices"
50
- AMAZON_APP_VERSION = "2.2.556530.0"
51
- AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
52
- AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
53
- AMAZON_CLIENT_OS = "16.6"
54
-
55
55
  # File extensions
56
56
  SAVE_PATH = "out"
57
57
  HTML_EXTENSION = ".html"
58
58
  JSON_EXTENSION = ".json"
59
+
60
+ DEVICE_TYPE_TO_MODEL = {
61
+ "A1RABVCI4QCIKC": "Echo Dot (Gen3)",
62
+ "A2DS1Q2TPDJ48U": "Echo Dot Clock (Gen5)",
63
+ "A2H4LV5GIZ1JFT": "Echo Dot Clock (Gen4)",
64
+ "A2U21SRK4QGSE1": "Echo Dot Clock (Gen4)",
65
+ "A32DDESGESSHZA": "Echo Dot (Gen3)",
66
+ "A32DOYMUN6DTXA": "Echo Dot (Gen3)",
67
+ "A3RMGO6LYLH7YN": "Echo Dot (Gen4)",
68
+ "A3S5BH2HU6VAYF": "Echo Dot (Gen2)",
69
+ "A4ZXE0RM7LQ7A": "Echo Dot (Gen5)",
70
+ "AKNO1N0KSFN8L": "Echo Dot (Gen1)",
71
+ }
@@ -21,3 +21,31 @@ class CannotRetrieveData(AmazonError):
21
21
 
22
22
  class CannotRegisterDevice(AmazonError):
23
23
  """Exception raised when device registration fails."""
24
+
25
+
26
+ class WrongMethod(AmazonError):
27
+ """Exception raised when the wrong login metho is used."""
28
+
29
+
30
+ class AuthFlowError(AmazonError):
31
+ """Exception raised when auth flow fails."""
32
+
33
+
34
+ class AuthMissingTimestamp(AmazonError):
35
+ """Exception raised when expires timestamp is missing."""
36
+
37
+
38
+ class AuthMissingAccessToken(AmazonError):
39
+ """Exception raised when access token is missing."""
40
+
41
+
42
+ class AuthMissingRefreshToken(AmazonError):
43
+ """Exception raised when refresh token is missing."""
44
+
45
+
46
+ class AuthMissingSigningData(AmazonError):
47
+ """Exception raised when some data for signing are missing."""
48
+
49
+
50
+ class AuthMissingWebsiteCookies(AmazonError):
51
+ """Exception raised when website cookies are missing."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioamazondevices
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: Python library to control Amazon devices
5
5
  Home-page: https://github.com/chemelli74/aioamazondevices
6
6
  License: Apache-2.0
@@ -20,6 +20,8 @@ Requires-Dist: beautifulsoup4
20
20
  Requires-Dist: colorlog
21
21
  Requires-Dist: httpx
22
22
  Requires-Dist: orjson
23
+ Requires-Dist: pyasn1
24
+ Requires-Dist: rsa
23
25
  Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
24
26
  Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
25
27
  Project-URL: Repository, https://github.com/chemelli74/aioamazondevices
@@ -0,0 +1,10 @@
1
+ aioamazondevices/__init__.py,sha256=eHj5kvzoE6UEcFSzlUNHQLMnf0dOnaLkg1fTo_HzBF8,277
2
+ aioamazondevices/api.py,sha256=e0hlC1sznOeu735ckO-ihHuDh6MofI_1PxL-r2sEaYc,18782
3
+ aioamazondevices/auth.py,sha256=vLJh7iOEUYu-44WOvmrmZZueOcwz5dmHAGQmqs9fJME,10099
4
+ aioamazondevices/const.py,sha256=T143A-tHoBTjIocWin9q4BQ_2kUozj-EwWGba3RoqSs,1852
5
+ aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
6
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ aioamazondevices-0.11.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
8
+ aioamazondevices-0.11.0.dist-info/METADATA,sha256=OfxPWgkBJEubCOu4Zo5r9qMIOAbU4BURJmx7CK_wv98,4743
9
+ aioamazondevices-0.11.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
10
+ aioamazondevices-0.11.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=uvo6LseWC7RF7rLYW5_5MxB9lsIE-azOJqwDtdoZmY4,276
2
- aioamazondevices/api.py,sha256=5WuQuXi0TX6pjuZBn5JcqPPNUe43E8FnLhSmYGfe4vE,17168
3
- aioamazondevices/const.py,sha256=MJYY90iUDvj1x3MKEh5RQoflQ-ftnIp0YvJLTSemGEI,1351
4
- aioamazondevices/exceptions.py,sha256=yQ9nL4UwBdHNXvdRj8TRemed6PXBmExP8lbHaAp04vY,546
5
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aioamazondevices-0.9.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
- aioamazondevices-0.9.0.dist-info/METADATA,sha256=apvr3suIW9DI-5_lw2K8YBnsuq38jdV9v61V9EsYc6s,4701
8
- aioamazondevices-0.9.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
9
- aioamazondevices-0.9.0.dist-info/RECORD,,