|
8 | 8 | """ |
9 | 9 |
|
10 | 10 | import asyncio |
11 | | -from collections.abc import Mapping, Sequence |
12 | 11 | import functools |
13 | 12 | import hashlib |
14 | 13 | import logging |
15 | | -from typing import Any, Callable, Optional |
| 14 | +from typing import Any, Callable, Optional, TypeVar, overload |
16 | 15 |
|
| 16 | +import wrapt |
17 | 17 | from alexapy import AlexapyLoginCloseRequested, AlexapyLoginError, hide_email |
18 | 18 | from alexapy.alexalogin import AlexaLogin |
| 19 | +from dictor import dictor |
19 | 20 | from homeassistant.const import CONF_EMAIL, CONF_URL |
| 21 | +from homeassistant.core import HomeAssistant |
20 | 22 | from homeassistant.exceptions import ConditionErrorMessage |
21 | 23 | from homeassistant.helpers.entity_component import EntityComponent |
22 | 24 | from homeassistant.helpers.instance_id import async_get as async_get_instance_id |
23 | | -import wrapt |
24 | 25 |
|
25 | 26 | from .const import DATA_ALEXAMEDIA, EXCEPTION_TEMPLATE |
26 | 27 |
|
27 | 28 | _LOGGER = logging.getLogger(__name__) |
| 29 | +ArgType = TypeVar("ArgType") |
28 | 30 |
|
29 | 31 |
|
30 | 32 | async def add_devices( |
@@ -336,3 +338,84 @@ def alarm_just_dismissed( |
336 | 338 | # We also know the alarm's status rules out a snooze. |
337 | 339 | # The only remaining possibility is that this alarm was just dismissed. |
338 | 340 | return True |
| 341 | + |
| 342 | + |
| 343 | +def is_http2_enabled(hass: HomeAssistant | None, login_email: str) -> bool: |
| 344 | + """Whether HTTP2 push is enabled for the current account session""" |
| 345 | + if hass: |
| 346 | + return bool( |
| 347 | + safe_get( |
| 348 | + hass.data, |
| 349 | + [DATA_ALEXAMEDIA, "accounts", login_email, "http2"], |
| 350 | + ) |
| 351 | + ) |
| 352 | + return False |
| 353 | + |
| 354 | + |
| 355 | +@overload |
| 356 | +def safe_get( |
| 357 | + data: Any, |
| 358 | + path_list: list[str | int] | None = None, |
| 359 | + checknone: bool = False, |
| 360 | + ignorecase: bool = False, |
| 361 | + pathsep: str = ".", |
| 362 | + search: Any = None, |
| 363 | + pretty: bool = False, |
| 364 | + rtype: str | None = None, |
| 365 | +) -> Any | None: ... |
| 366 | + |
| 367 | + |
| 368 | +@overload |
| 369 | +def safe_get( |
| 370 | + data: Any, path_list: list[str | int] | None, default: ArgType, *args, **kwargs |
| 371 | +) -> ArgType: ... |
| 372 | + |
| 373 | + |
| 374 | +def safe_get( |
| 375 | + data: Any, path_list: list[str | int] | None = None, *args, **kwargs |
| 376 | +) -> None | Any: |
| 377 | + """Safely get nested value using path segments with optional type checking. |
| 378 | +
|
| 379 | + Args: |
| 380 | + data: Source data structure |
| 381 | + path_list: List of path segments (dots in segment names are auto-escaped) |
| 382 | + *args: Positional arguments passed to dictor (e.g., default value) |
| 383 | + **kwargs: Keyword arguments passed to dictor (checknone, ignorecase) |
| 384 | +
|
| 385 | + Returns: |
| 386 | + The value at the specified path, or None if: |
| 387 | + - The path doesn't exist and no default is provided |
| 388 | + or default if: |
| 389 | + - A default is provided and the path doesn't exist |
| 390 | + - A default is provided and the retrieved value's type doesn't match the default's type |
| 391 | +
|
| 392 | + Note: |
| 393 | + - Do not pass 'pathsep' in kwargs as the path is pre-built. |
| 394 | + - Type checking: When a default value is provided and a non-None value is retrieved, |
| 395 | + the result is validated against the default's type. If types don't match, default is returned. |
| 396 | + This prevents silent type errors from malformed data structures. |
| 397 | +
|
| 398 | + Examples: |
| 399 | + >>> safe_get({"a": {"b": "value"}}, ["a", "b"]) |
| 400 | + 'value' |
| 401 | +
|
| 402 | + >>> safe_get({"a": {"b": 123}}, ["a", "b"], "default") |
| 403 | + 'default' # Type mismatch: int vs str |
| 404 | +
|
| 405 | + >>> safe_get({"a": {"b": "value"}}, ["a", "b"], "default") |
| 406 | + 'value' # Type matches |
| 407 | + """ |
| 408 | + if not path_list: |
| 409 | + raise ValueError("path_list cannot be empty") |
| 410 | + |
| 411 | + if "pathsep" in kwargs: |
| 412 | + kwargs.pop("pathsep") # Ignore pathsep since we build the path |
| 413 | + |
| 414 | + escaped_segments = (str(seg).replace(".", "\\.") for seg in path_list) |
| 415 | + path = ".".join(escaped_segments) |
| 416 | + default = args[0] if args else (kwargs.get("default") if kwargs else None) |
| 417 | + result = dictor(data, path, *args, **kwargs) |
| 418 | + if default is not None and result is not None: |
| 419 | + if not isinstance(result, type(default)): |
| 420 | + result = default |
| 421 | + return result |
0 commit comments