aioamazondevices 3.1.17rc1__tar.gz → 3.1.19__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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 3.1.17rc1
3
+ Version: 3.1.19
4
4
  Summary: Python library to control Amazon devices
5
5
  License: Apache-2.0
6
6
  Author: Simone Chemelli
7
7
  Author-email: simone.chemelli@gmail.com
8
8
  Requires-Python: >=3.12
9
- Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Operating System :: OS Independent
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "3.1.17-rc.1"
3
+ version = "3.1.19"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -10,7 +10,7 @@ license = "Apache-2.0"
10
10
  readme = "README.md"
11
11
  repository = "https://github.com/chemelli74/aioamazondevices"
12
12
  classifiers = [
13
- "Development Status :: 2 - Pre-Alpha",
13
+ "Development Status :: 4 - Beta",
14
14
  "Intended Audience :: Developers",
15
15
  "Natural Language :: English",
16
16
  "Operating System :: OS Independent",
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "3.1.15"
3
+ __version__ = "3.1.19"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -58,6 +58,7 @@ from .exceptions import (
58
58
  CannotRetrieveData,
59
59
  WrongMethod,
60
60
  )
61
+ from .utils import obfuscate_email, scrub_fields
61
62
 
62
63
 
63
64
  @dataclass
@@ -317,7 +318,7 @@ class AmazonEchoApi:
317
318
  "%s request: %s with payload %s [json=%s]",
318
319
  method,
319
320
  url,
320
- input_data,
321
+ scrub_fields(input_data) if input_data else None,
321
322
  json_data,
322
323
  )
323
324
 
@@ -473,7 +474,7 @@ class AmazonEchoApi:
473
474
  msg = resp_json["response"]["error"]["message"]
474
475
  _LOGGER.error(
475
476
  "Cannot register device for %s: %s",
476
- self._login_email,
477
+ obfuscate_email(self._login_email),
477
478
  msg,
478
479
  )
479
480
  raise CannotRegisterDevice(f"{HTTPStatus(resp.status).phrase}: {msg}")
@@ -601,7 +602,11 @@ class AmazonEchoApi:
601
602
 
602
603
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
603
604
  """Login to Amazon interactively via OTP."""
604
- _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
605
+ _LOGGER.debug(
606
+ "Logging-in for %s [otp code: %s]",
607
+ obfuscate_email(self._login_email),
608
+ bool(otp_code),
609
+ )
605
610
  self._client_session()
606
611
 
607
612
  code_verifier = self._create_code_verifier()
@@ -671,7 +676,7 @@ class AmazonEchoApi:
671
676
 
672
677
  _LOGGER.debug(
673
678
  "Logging-in for %s with stored data",
674
- self._login_email,
679
+ obfuscate_email(self._login_email),
675
680
  )
676
681
 
677
682
  self._client_session()
@@ -723,7 +728,11 @@ class AmazonEchoApi:
723
728
 
724
729
  final_devices_list: dict[str, AmazonDevice] = {}
725
730
  for device in self._devices.values():
726
- devices_node = device[NODE_DEVICES]
731
+ # Remove stale, orphaned and virtual devices
732
+ devices_node = device.get(NODE_DEVICES)
733
+ if not devices_node or (devices_node.get("deviceType") in DEVICE_TO_IGNORE):
734
+ continue
735
+
727
736
  preferences_node = device.get(NODE_PREFERENCES)
728
737
  do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
729
738
  bluetooth_node = device[NODE_BLUETOOTH]
@@ -736,13 +745,6 @@ class AmazonEchoApi:
736
745
  if _device_id == identifier_node["entityId"]:
737
746
  sensors = _device_sensors
738
747
 
739
- # Remove stale, orphaned and virtual devices
740
- if (
741
- NODE_DEVICES not in device
742
- or devices_node.get("deviceType") in DEVICE_TO_IGNORE
743
- ):
744
- continue
745
-
746
748
  serial_number: str = devices_node["serialNumber"]
747
749
  final_devices_list[serial_number] = AmazonDevice(
748
750
  account_name=devices_node["accountName"],
@@ -6,6 +6,25 @@ _LOGGER = logging.getLogger(__package__)
6
6
 
7
7
  DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
8
8
 
9
+ TO_REDACT = {
10
+ "address1",
11
+ "address2",
12
+ "address3",
13
+ "city",
14
+ "county",
15
+ "deviceAccountId",
16
+ "deviceAddress",
17
+ "deviceOwnerCustomerId",
18
+ "given_name",
19
+ "name",
20
+ "password",
21
+ "postalCode",
22
+ "searchCustomerId",
23
+ "state",
24
+ "street",
25
+ "user_id",
26
+ }
27
+
9
28
  DOMAIN_BY_ISO3166_COUNTRY = {
10
29
  "ar": {
11
30
  "domain": "com",
@@ -0,0 +1,58 @@
1
+ """Utils module for Amazon devices."""
2
+
3
+ from collections.abc import Collection
4
+ from typing import Any
5
+
6
+ from .const import TO_REDACT
7
+
8
+
9
+ def obfuscate_email(email: str) -> str:
10
+ """Obfuscate an email address partially."""
11
+ try:
12
+ username, domain = email.split("@")
13
+ domain_name, domain_ext = domain.rsplit(".", 1)
14
+
15
+ def obfuscate_part(part: str, visible: int = 1) -> str:
16
+ if len(part) <= visible:
17
+ return "*" * len(part)
18
+ return part[:visible] + "*" * (len(part) - visible)
19
+
20
+ # Obfuscate username and domain parts
21
+ obf_user = ".".join(obfuscate_part(u, 1) for u in username.split("."))
22
+ obf_domain = obfuscate_part(domain_name, 1)
23
+
24
+ except (SyntaxError, ValueError):
25
+ return "[invalid email]"
26
+ else:
27
+ return f"{obf_user}@{obf_domain}.{domain_ext}"
28
+
29
+
30
+ def scrub_fields(
31
+ obj: Any, # noqa: ANN401
32
+ field_names: Collection[str] = TO_REDACT,
33
+ replacement: str = "[REDACTED]",
34
+ ) -> Any: # noqa: ANN401
35
+ """Return a deep-copied version of *obj* with redacted keys."""
36
+ if isinstance(obj, dict):
37
+ result = {}
38
+ for k, v in obj.items():
39
+ # If the key itself is sensitive → overwrite its value
40
+ if k in field_names:
41
+ result[k] = replacement
42
+ else:
43
+ # Otherwise keep walking
44
+ result[k] = scrub_fields(v, field_names, replacement)
45
+ return result
46
+
47
+ if isinstance(obj, list):
48
+ return [scrub_fields(item, field_names, replacement) for item in obj]
49
+
50
+ if isinstance(obj, tuple):
51
+ return tuple(scrub_fields(item, field_names, replacement) for item in obj)
52
+
53
+ if isinstance(obj, set):
54
+ # Note: a set cannot contain mutable/unhashable items like dicts,
55
+ # so we assume its members are hashable after scrubbing.
56
+ return {scrub_fields(item, field_names, replacement) for item in obj}
57
+
58
+ return obj