From f449b50e7b73794e793011b650425be8f6086e5d Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Thu, 27 Aug 2026 15:26:33 +0300 Subject: [PATCH 01/28] Initial commit [no ci] --- .gitignore | 54 +- README.md | 2 +- docker-compose.yml | 23 + integration_template/__init__.py | 9 - integration_template/controller.py | 77 -- integration_template/py.typed | 0 main.py | 7 + majordom_zwave/__init__.py | 9 + majordom_zwave/config.py | 3 + majordom_zwave/controller.py | 455 ++++++++ majordom_zwave/exceptions.py | 14 + majordom_zwave/mapper.py | 133 +++ majordom_zwave/model.py | 33 + majordom_zwave/zwave_spec.py | 148 +++ poetry.lock | 1638 +++++++++++++--------------- pyproject.toml | 126 +-- scripts/gen_key.sh | 26 + tests/conftest.py | 52 +- tests/test_controller.py | 95 +- tests/test_smoke.py | 18 +- 20 files changed, 1703 insertions(+), 1219 deletions(-) create mode 100644 docker-compose.yml delete mode 100644 integration_template/__init__.py delete mode 100644 integration_template/controller.py delete mode 100644 integration_template/py.typed create mode 100644 main.py create mode 100644 majordom_zwave/__init__.py create mode 100644 majordom_zwave/config.py create mode 100644 majordom_zwave/controller.py create mode 100644 majordom_zwave/exceptions.py create mode 100644 majordom_zwave/mapper.py create mode 100644 majordom_zwave/model.py create mode 100644 majordom_zwave/zwave_spec.py create mode 100755 scripts/gen_key.sh diff --git a/.gitignore b/.gitignore index bffc3c9..90cc466 100644 --- a/.gitignore +++ b/.gitignore @@ -1,53 +1,3 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -build/ -dist/ -*.egg-info/ -*.egg - -# Unit test / coverage reports -.coverage -.coverage.* -coverage.xml -htmlcov/ -.pytest_cache/ -.hypothesis/ - -# Environments -.env -.envrc -.venv -env/ -venv/ - -# Poetry -# poetry.lock # uncomment for applications, keep commented for libraries - -# Type checkers -.mypy_cache/ -.dmypy.json -.pyre/ -.pytype/ - -# Tools -.ruff_cache/ -.ropeproject - -# mkdocs (only relevant if you kept mkdocs.yml + docs/) -/site -.cache/ - -# IDEs -.idea/ .vscode/ - -# OS -.DS_Store -Thumbs.db +__pycache__/ +.env \ No newline at end of file diff --git a/README.md b/README.md index 095370a..df1525a 100644 --- a/README.md +++ b/README.md @@ -209,4 +209,4 @@ firmware versions tested against, limitations._ See [LICENSE](LICENSE). Your integration code is yours to license as you choose. For commercial licensing or partnership inquiries regarding MajorDom, contact us via -[parker-industries.org/partnership](https://parker-industries.org/partnership). +[parker-industries.org/partnership](https://parker-industries.org/partnership). \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..54dd95e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + zwave-js-ui: + container_name: zwave-js-ui + image: zwavejs/zwave-js-ui:latest + restart: unless-stopped + tty: true + stop_signal: SIGINT + environment: + - SESSION_SECRET=change_me_to_a_secure_string + - TZ=America/New-York + - KEY_S0_Legacy=${ZWAVE_KEY_S0_LEGACY} + - KEY_S2_Unauthenticated=${ZWAVE_KEY_S2_UNAUTHENTICATED} + - KEY_S2_Authenticated=${ZWAVE_KEY_S2_AUTHENTICATED} + - KEY_S2_AccessControl=${ZWAVE_KEY_S2_ACCESSCONTROL} + - KEY_LR_S2_Authenticated=${ZWAVE_KEY_LR_S2_AUTHENTICATED} + - KEY_LR_S2_AccessControl=${ZWAVE_KEY_LR_S2_ACCESSCONTROL} + devices: + - /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_f81b68219079ed11b995d30ffef7b791-if00-port0:/dev/zwave + volumes: + - ./zwave-store:/usr/src/app/store + ports: + - 8091:8091 + - 3000:3000 diff --git a/integration_template/__init__.py b/integration_template/__init__.py deleted file mode 100644 index 7018a1d..0000000 --- a/integration_template/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""integration_template — rename this package to majordom_ (see README). - -Exposes the integration's Controller, the entry point the Hub (or the SDK's standalone -dev runner) instantiates and drives. -""" - -from integration_template.controller import ExampleController - -__all__ = ["ExampleController"] diff --git a/integration_template/controller.py b/integration_template/controller.py deleted file mode 100644 index 8929494..0000000 --- a/integration_template/controller.py +++ /dev/null @@ -1,77 +0,0 @@ -"""A minimal MajorDom integration controller. - -This is a worked skeleton — replace the TODO bodies with real protocol calls. It shows the -full lifecycle the Hub drives: discovery -> pairing -> commands -> teardown. - -See the full guide at https://docs.majordom.io/device-integration. - -A Controller bridges one external protocol/platform into the MajorDom language. The Hub -instantiates it with injected `Dependencies` and calls the methods below; the controller -reports back to the Hub through `dependencies.output` (a `ControllerOutput`). -""" - -from __future__ import annotations - -from uuid import UUID - -from majordom_integration_sdk.controller import AbstractController -from majordom_integration_sdk.schemas.command import DeviceCommand -from majordom_integration_sdk.schemas.device import Device, Discovery, Parameter, ProvidedCredentials - -# Output methods to call (see the docs): -# async def controller_did_receive_discovery(self, controller: AbstractController, discovery: Discovery): ... -# async def controller_did_update_discovery(self, controller: AbstractController, discovery: Discovery): ... -# async def controller_did_lose_discovery(self, controller: AbstractController, discovery_id: UUID): ... -# async def controller_did_connect_device(self, controller: AbstractController, device_id: UUID): ... -# async def controller_did_lose_device(self, controller: AbstractController, device_id: UUID): ... -# async def controller_did_receive_events(self, controller: AbstractController, events: Iterable[Event]): ... - - -class ExampleController(AbstractController[Device, Parameter]): - """Bridges the Example protocol into MajorDom.""" - - def __init__(self, dependencies: AbstractController.Dependencies): - super().__init__(dependencies) - self._discoveries: dict[UUID, Discovery] = {} - - name = "Example" - - @property - def discoveries(self) -> dict[UUID, Discovery]: - # Return the cached snapshot only — never scan here. - return self._discoveries - - # Lifecycle ------------------------------------------------------------- - - async def start(self) -> None: - # Register discovery services, subscribe to protocol events, and reconcile the - # state of already-paired devices here. - # e.g. self.dependencies.zeroconf_discovery_service.add_listener("_example._tcp.local.", self) - ... - - async def stop(self) -> None: - # Cancel tasks and release every held resource. - ... - - # Hub -> device --------------------------------------------------------- - - async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> None: - # Establish a session with the device, then report success to the Hub: - # await self.dependencies.output.controller_did_connect_device(self, self.device_uuid(...)) - raise NotImplementedError - - async def unpair(self, device: Device) -> None: - raise NotImplementedError - - async def identify(self, device: Device) -> None: - # Ask the device to blink/beep so the user can locate it. - raise NotImplementedError - - async def fetch(self, device: Device) -> None: - # Refresh the device and its parameters, reporting changes via - # self.dependencies.output.controller_did_receive_events(self, [...]). - raise NotImplementedError - - async def send_command(self, command: DeviceCommand, device: Device, parameter: Parameter) -> None: - # Translate the MajorDom command into a protocol-level write. - raise NotImplementedError diff --git a/integration_template/py.typed b/integration_template/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/main.py b/main.py new file mode 100644 index 0000000..6c42d7f --- /dev/null +++ b/main.py @@ -0,0 +1,7 @@ +import asyncio +from majordom_integration_sdk.dev import run_controller +from majordom_zwave import ZwaveController +import logging + +logging.getLogger("majordom_zwave").setLevel(logging.DEBUG) +asyncio.run(run_controller(ZwaveController)) \ No newline at end of file diff --git a/majordom_zwave/__init__.py b/majordom_zwave/__init__.py new file mode 100644 index 0000000..bab4a91 --- /dev/null +++ b/majordom_zwave/__init__.py @@ -0,0 +1,9 @@ +"""MajorDom Z-Wave integration. + +Bridges a Z-Wave network (via a running `zwave-js-server` instance) to a MajorDom Hub. +See `controller.py` for the entry point the Hub actually instantiates. +""" + +from .controller import ZwaveController + +__all__ = ["ZWaveController"] \ No newline at end of file diff --git a/majordom_zwave/config.py b/majordom_zwave/config.py new file mode 100644 index 0000000..73cdb09 --- /dev/null +++ b/majordom_zwave/config.py @@ -0,0 +1,3 @@ +import os + +zwave_server_url = os.getenv("ZWAVE-SERVER-URL", "ws://localhost:3000") \ No newline at end of file diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py new file mode 100644 index 0000000..4e881b3 --- /dev/null +++ b/majordom_zwave/controller.py @@ -0,0 +1,455 @@ +import asyncio +import logging +from typing import ClassVar, override +from uuid import UUID + +from aiohttp import ClientSession +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass, InclusionStrategy, NodeStatus, SecurityClass +from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant +from zwave_js_server.model.node import Node +from zwave_js_server.model.value import Value +from majordom_integration_sdk.controller import AbstractController +from majordom_integration_sdk.schemas import DeviceCommand, Discovery, ProvidedCredentials +from majordom_integration_sdk.schemas.device import CredentialsType, NonEmptyStr +from majordom_integration_sdk.schemas.event import DeviceParameterChange +from majordom_integration_sdk.schemas.parameter import ParameterUnit + +from .config import zwave_server_url +from .model import ( + ZwaveDevice, + ZwaveDeviceState, + ZwaveDeviceIntegrationData, + ZwaveParameter, + ZwaveParameterIntegrationData, + ZwaveParameterState, +) +from .exceptions import ZwaveConnectionError, ZwaveUnexpectedError +from .mapper import ZwaveMapper +from .zwave_spec import IDENTIFY_INDICATOR_ID + +log = logging.getLogger(__name__) + + +class ZwaveController(AbstractController): + """Bridges the Hub to Z-Wave devices through zwave-js-server.""" + + name: ClassVar[str] = "ZWave" + + _zwave_session: ClientSession + _zwave_client: ZwaveClient + + _mapper: ZwaveMapper + + _majordom_discoveries: dict[UUID, Discovery] + _awaiting_zw_discoveries: dict[UUID, Node] + _connected_devices: dict[UUID, Node] + _availability: dict[UUID, bool] + _tasks: set[asyncio.Task] + + def __init__(self, dependencies: AbstractController.Dependencies): + super().__init__(dependencies) + self._mapper = ZwaveMapper() + self._majordom_discoveries = {} + self._awaiting_zw_discoveries = {} + self._connected_devices = {} + self._availability = {} + self._tasks = set() + + # ------------------------------------------------------------------------- + # AbstractController interface + # ------------------------------------------------------------------------- + + @property + def discoveries(self) -> dict[UUID, Discovery]: + return self._majordom_discoveries + + @property + @override + def device_type(self) -> type[ZwaveDevice]: + return ZwaveDevice + + @property + @override + def parameter_type(self) -> type[ZwaveParameter]: + return ZwaveParameter + + # ------------------------------------------------------------------------- + # Lifecycle + # ------------------------------------------------------------------------- + + async def start(self): + self._zwave_session = ClientSession() + self._zwave_client = ZwaveClient(zwave_server_url, self._zwave_session) + await self._zwave_client.connect() + + ready = asyncio.Event() + self._create_task(self._zwave_client.listen(ready)) + await ready.wait() + + controller = self._zwave_client.driver.controller + controller.on("node added", lambda data: self._create_task(self._node_added(data["node"]))) + controller.on("node removed", lambda data: self._create_task(self._node_removed(data["node"]))) + # S2 auto-grant only — no PIN prompt / no discovery raised for it. + controller.on("grant security classes", lambda data: self._create_task(self._grant_security_classes(data["requested"]))) + + log.debug("[READY] connected to %s", zwave_server_url) + + async with self.dependencies.make_device_repository() as device_repository: + try: + known_node_ids: set[int] = set() + for raw_device in await device_repository.get_all(): + try: + device = ZwaveDevice.model_validate(raw_device.model_dump()) + except Exception: + log.debug("[SKIP] device_id=%s not paired yet, skipping", raw_device.id) + continue + + node = controller.nodes.get(device.integration_data.node_id) + if node is None: + device.available = False + device.last_error = f"Device {device.name} is no longer connected to the Z-Wave network" + await device_repository.save(device, device.id) + log.debug("[MISSING] device_id=%s node_id=%s not on network", device.id, device.integration_data.node_id) + continue + known_node_ids.add(node.node_id) + self._connected_devices[device.id] = node + self._availability[device.id] = node.ready + self._subscribe(device.id, node) + log.debug("[KNOWN] node_id=%s", node.node_id) + + # Nodes already on the network but not ours yet (Hub restart, pre-existing node). + log.debug("[RECONCILE] %d node(s) on network, own_node_id=%s", len(controller.nodes), controller.own_node_id) + for node in controller.nodes.values(): + if node.node_id == controller.own_node_id or node.node_id in known_node_ids: + continue + await self._node_added(node) + except Exception: + log.exception("[START] node reconciliation failed") + raise + + async def stop(self): + self._require_client() + for task in self._tasks: + task.cancel() + if self._tasks: + await asyncio.gather(*self._tasks, return_exceptions=True) + self._tasks.clear() + await self._zwave_client.disconnect() + await self._zwave_session.close() + + self._majordom_discoveries.clear() + self._awaiting_zw_discoveries.clear() + self._connected_devices.clear() + + # ------------------------------------------------------------------------- + # Hub -> device operations + # ------------------------------------------------------------------------- + + async def start_pairing_window(self, duration_sec: int) -> None: + self._require_client() + controller = self._zwave_client.driver.controller + await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) + log.debug("[PAIRING] inclusion window opened for %ds", duration_sec) + self._create_task(self._close_pairing_window(duration_sec)) + + async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: + """Waits for the pending node's interview to finish, then builds and saves the device.""" + self._require_client() + + node = self._awaiting_zw_discoveries.pop(discovery.id, None) + if node is None: + raise ZwaveUnexpectedError(f"No pending Z-Wave discovery for {discovery.id}") + self._majordom_discoveries.pop(discovery.id, None) + + await self._wait_until_ready(node) + + device_id = self._mapper.device_uuid_from_node_id(node.node_id) + self._connected_devices[device_id] = node + + async with self.dependencies.make_device_repository() as device_repository: + device = await device_repository.state(discovery.id, ZwaveDeviceState) + assert device + device.id = device_id + + if device.integration_data: + device.integration_data.node_id = node.node_id + else: + device.integration_data = ZwaveDeviceIntegrationData(node_id=node.node_id) + + parameters = self._build_parameters(device_id, node) + device.parameters = parameters + + main_parameter_id, default_value = self._mapper.get_main_parameter(device_id, node) + device.main_parameter = main_parameter_id + if main_parameter_id and default_value is not None: + main_parameter = next((p for p in parameters if p.id == main_parameter_id), None) + if main_parameter is None: + device.main_parameter = None + else: + main_parameter.default_value = default_value + + log.debug( + f"[PAIR] node_id={node.node_id} mapped schema\n\t" + + "\n\t".join( + f" {p.role.value:8} {p.visibility.value:8} {p.data_type.value:10} {p.name} id={p.id}" + for p in parameters + ), + ) + + await device_repository.save(device, discovery.id) + + self._subscribe(device_id, node) + await self._set_availability(device_id, True) + await self.dependencies.output.controller_did_connect_device(self, device_id) + return device_id + + async def unpair(self, device: ZwaveDevice): + self._require_client() + controller = self._zwave_client.driver.controller + zwave_node = self._require_node(device) + + if await controller.async_is_failed_node(zwave_node): + await controller.async_remove_failed_node(zwave_node) + else: + # Live node: requires interactive exclusion, confirmed by the device itself. + removed = asyncio.Event() + + def _on_node_removed(data: dict) -> None: + if data["node"].node_id == zwave_node.node_id: + removed.set() + + unsubscribe = controller.on("node removed", _on_node_removed) + try: + await controller.async_begin_exclusion() + try: + await asyncio.wait_for(removed.wait(), timeout=60) + except asyncio.TimeoutError: + raise ZwaveUnexpectedError( + f"Node {zwave_node.node_id} was not excluded in time — make sure the " + f"device was put into exclusion mode" + ) from None + finally: + unsubscribe() + await controller.async_stop_exclusion() + + self._connected_devices.pop(device.id, None) + self._availability.pop(device.id, None) + + async def identify(self, device: ZwaveDevice): + self._require_client() + zwave_node = self._require_node(device) + + indicator_value = next( + ( + v + for v in zwave_node.values.values() + if v.command_class == CommandClass.INDICATOR + and v.property_ == "value" + and v.property_key == IDENTIFY_INDICATOR_ID + ), + None, + ) + if indicator_value is None: + raise ZwaveUnexpectedError(f"Node {zwave_node.node_id} does not support the Identify indicator") + + await zwave_node.async_set_value(indicator_value, 0xFF) + log.debug("[IDENTIFY] node_id=%s", zwave_node.node_id) + + async def fetch(self, device: ZwaveDevice): + self._require_client() + + zwave_node = self._require_node(device) + parameters: list[DeviceParameterChange] = [] + for value_id, value in zwave_node.values.items(): + parameters.append(DeviceParameterChange( + device_id=device.id, + parameter_id=self._mapper.parameter_uuid(device.id, value_id), + value=self._mapper.format_zwave_value(value), + )) + + await self.dependencies.output.controller_did_receive_events(self, parameters) + + async def send_command(self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter): + self._require_client() + + zwave_node = self._require_node(device) + zwave_value = self._require_value(parameter.integration_data.value_id, zwave_node) + if zwave_value.metadata.writeable is False: + raise ZwaveUnexpectedError(f"Value {parameter.integration_data.value_id} is not writeable") + try: + awake = zwave_node.status != NodeStatus.ASLEEP + result = await zwave_node.async_set_value(zwave_value, command.value, wait_for_result=awake) + log.info(result) + except Exception as e: + raise ZwaveUnexpectedError( + f"Failed to set value {parameter.integration_data.value_id} on node {zwave_node.node_id}: {e}" + ) from None + if awake and result is not None and not result.status: + log.warning("[CMD] node_id=%s value_id=%s result=%s", zwave_node.node_id, parameter.integration_data.value_id, result) + + await self.dependencies.output.controller_did_receive_events( + self, [DeviceParameterChange(device_id=device.id, parameter_id=parameter.id, value=command.value)] + ) + + # ========================================================================= + # Private helpers + # ========================================================================= + + # ------------------------------------------------------------------------- + # Generic guards / lookups + # ------------------------------------------------------------------------- + + def _create_task(self, coro) -> asyncio.Task: + task = asyncio.create_task(coro) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + return task + + def _require_client(self) -> ZwaveClient: + if self._zwave_client and self._zwave_client.connected: + return self._zwave_client + raise ZwaveConnectionError("Zwave client is not started") + + def _require_node(self, device: ZwaveDevice) -> Node: + node = self._zwave_client.driver.controller.nodes.get(device.integration_data.node_id) + if not node: + raise ZwaveUnexpectedError(f"Node for device {device.id} not found") + return node + + def _require_value(self, value_id: str, node: Node) -> Value: + value = node.values.get(value_id) + if not value: + raise ZwaveUnexpectedError(f"Value with {value_id} id for node {node.node_id} not found") + return value + + # ------------------------------------------------------------------------- + # Pairing + # ------------------------------------------------------------------------- + + async def _close_pairing_window(self, duration_sec: int) -> None: + await asyncio.sleep(duration_sec) + controller = self._zwave_client.driver.controller + await controller.async_stop_inclusion() + log.debug("[PAIRING] inclusion window closed") + + # Anything discovered but never claimed via pair_device gets dropped. + for device_id in list(self._majordom_discoveries): + self._majordom_discoveries.pop(device_id, None) + node = self._awaiting_zw_discoveries.pop(device_id, None) + if node is None: + continue + if await controller.async_is_failed_node(node): + await controller.async_remove_failed_node(node) + else: + log.warning( + "[PAIRING] node_id=%s joined but was never paired, can't force-remove while alive", + node.node_id, + ) + + async def _wait_until_ready(self, node: Node) -> None: + """Waits for the background interview to populate node.values.""" + if node.ready: + return + became_ready = asyncio.Event() + unsubscribe = node.on("ready", lambda data: became_ready.set()) + try: + await asyncio.wait_for(became_ready.wait(), timeout=120) + except asyncio.TimeoutError: + log.warning("[PAIR] node_id=%s not ready after 120s, pairing with partial values", node.node_id) + finally: + unsubscribe() + + def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameterState]: + """Maps every Z-Wave value on a node into a majordom parameter state.""" + parameters: list[ZwaveParameterState] = [] + for value_id, value in node.values.items(): + metadata = value.metadata + parameter = ZwaveParameterState( + id=self._mapper.parameter_uuid(device_id, value_id), + name=metadata.label or value.property_name or value.property_key_name or str(value.property_), + data_type=self._mapper.parse_zwave_data_type(value), + role=self._mapper.get_role(value.command_class, metadata), + visibility=self._mapper.get_visibility(value.command_class, metadata), + min_value=metadata.min, + max_value=metadata.max, + min_step=self._mapper.get_min_step(value), + unit=self._mapper.get_unit(metadata.unit) or ParameterUnit.plain, + valid_values=self._mapper.parse_zwave_valid_values(metadata), + integration_data=ZwaveParameterIntegrationData(value_id=value_id), + value=self._mapper.format_zwave_value(value), + ) + parameters.append(parameter) + return parameters + + async def _grant_security_classes(self, requested: dict) -> None: + """Grants exactly what the joining device requested — no user choice involved.""" + grant = InclusionGrant( + security_classes=[SecurityClass(c) for c in requested["securityClasses"]], + client_side_auth=requested["clientSideAuth"], + ) + await self._zwave_client.driver.controller.async_grant_security_classes(grant) + log.debug("[S2] granted security classes=%s", requested["securityClasses"]) + + # ------------------------------------------------------------------------- + # Device <-> Hub: Z-Wave network events & availability + # ------------------------------------------------------------------------- + + async def _node_added(self, node: Node): + """New node joined — surfaces a discovery unless it's already known.""" + log.debug("[JOIN] node_id=%s", node.node_id) + + device_id = self._mapper.device_uuid_from_node_id(node.node_id) + + if device_id in self._connected_devices: + self._connected_devices[device_id] = node + self._subscribe(device_id, node) + await self._set_availability(device_id, True) + return + + if device_id in self._majordom_discoveries: + self._awaiting_zw_discoveries[device_id] = node + return + + discovery = Discovery( + id=device_id, + integration=NonEmptyStr(self.name), + expected_credentials_options=[CredentialsType.none], + expiration=None, + transport=NonEmptyStr("ZWAVE"), + device_manufacturer=None, + device_name=NonEmptyStr(node.name or node.device_config.description or "Unknown"), + device_category=None, + device_icon=None, + ) + self._majordom_discoveries[device_id] = discovery + self._awaiting_zw_discoveries[device_id] = node + log.debug("[DISCOVERY] node_id=%s discovery_id=%s", node.node_id, device_id) + await self.dependencies.output.controller_did_receive_discovery(self, discovery) + + async def _node_removed(self, node: Node): + device_id = self._mapper.device_uuid_from_node_id(node.node_id) + log.debug("[REMOVED] node_id=%s", node.node_id) + if device_id in self._connected_devices: + self._connected_devices.pop(device_id, None) + await self._set_availability(device_id, False) + + async def _set_availability(self, device_id: UUID, available: bool) -> None: + """Only reports on actual change.""" + if self._availability.get(device_id) == available: + return + self._availability[device_id] = available + if available: + await self.dependencies.output.controller_did_connect_device(self, device_id) + else: + await self.dependencies.output.controller_did_lose_device(self, device_id) + + def _subscribe(self, device_id: UUID, node: Node) -> None: + node.on("value updated", lambda data: self._create_task(self._value_updated(device_id, data["value"]))) + node.on("dead", lambda data: self._create_task(self._set_availability(device_id, False))) + node.on("alive", lambda data: self._create_task(self._set_availability(device_id, True))) + + async def _value_updated(self, device_id: UUID, value: Value) -> None: + parameter_id = self._mapper.parameter_uuid(device_id, value.value_id) + event = DeviceParameterChange(device_id=device_id, parameter_id=parameter_id, value=self._mapper.format_zwave_value(value)) + await self.dependencies.output.controller_did_receive_events(self, [event]) \ No newline at end of file diff --git a/majordom_zwave/exceptions.py b/majordom_zwave/exceptions.py new file mode 100644 index 0000000..674cb8d --- /dev/null +++ b/majordom_zwave/exceptions.py @@ -0,0 +1,14 @@ +class ZwaveConnectionError(Exception): + """Raised when Matter client is not started or not available.""" + + +class ZwaveUnexpectedError(Exception): + """Raised when an unexpected internal error occurs.""" + + +# class MatterUnsupportedParameter(Exception): +# """Raised when a parameter is not supported by the device.""" + + +# class MatterNotFoundParameter(Exception): + # """Raised when a parameter was not found on the device.""" \ No newline at end of file diff --git a/majordom_zwave/mapper.py b/majordom_zwave/mapper.py new file mode 100644 index 0000000..a929476 --- /dev/null +++ b/majordom_zwave/mapper.py @@ -0,0 +1,133 @@ +from typing import Any +from uuid import UUID, NAMESPACE_DNS, uuid5 + +from majordom_integration_sdk.schemas.parameter import ParameterDataType, ParameterRole, ParameterUnit, ParameterVisibility +from zwave_js_server.model.duration import Duration +from zwave_js_server.model.node import Node +from zwave_js_server.model.value import AllowedRangeValue, Value, ValueMetadata, ValueType + +from .zwave_spec import ( + DIAGNOSTIC_COMMAND_CLASSES, + EVENT_COMMAND_CLASSES, + MAIN_VALUE_BY_COMMAND_CLASS, + SYSTEM_COMMAND_CLASSES, + UNIT_MAP, +) + + +class ZwaveMapper: + def device_uuid_from_node_id(self, node_id: int) -> UUID: + return uuid5(NAMESPACE_DNS, f"device-{node_id}") + + def parameter_uuid(self, device_id: UUID, value_id: str) -> UUID: + return uuid5(NAMESPACE_DNS, f"parameter-{device_id}-{value_id}") + + # ------------------------------------------------------------------------- + # Value metadata -> SDK parameter schema + # ------------------------------------------------------------------------- + + def get_unit(self, raw_unit: str | None) -> ParameterUnit | None: + """Maps a raw unit string to ParameterUnit; None if not in UNIT_MAP.""" + if not raw_unit: + return None + return UNIT_MAP.get(raw_unit) + + def _is_duration_property(self, value: Value) -> bool: + """True for CC-specific "duration" properties, matched by name since + zwave-js-server-python doesn't wrap them in its Duration class.""" + return value.property_name == "duration" or value.property_ == "duration" + + def format_zwave_value(self, value: Value) -> Any: + """Formats a raw value to match parse_zwave_data_type() — keep in sync.""" + if self._is_duration_property(value): + # None means "unknown" here; Duration itself expects the string "unknown". + return str(Duration(value.value)) if value.value is not None else "unknown" + return value.value + + def get_min_step(self, value: Value) -> int | float | None: + """Step size if zwave-js reported one; most devices don't, so no default.""" + for entry in value.metadata.allowed or []: + if isinstance(entry, AllowedRangeValue) and entry.step: + return entry.step + return None + + def get_role(self, command_class: int, metadata: ValueMetadata) -> ParameterRole: + """Sensor/control/event from command class + read/write access.""" + if command_class in EVENT_COMMAND_CLASSES: + return ParameterRole.event + if metadata.writeable: + return ParameterRole.control + return ParameterRole.sensor + + def get_visibility(self, command_class: int, metadata: ValueMetadata) -> ParameterVisibility: + """Readable -> user, writeable-only -> setting, else system; SYSTEM/DIAGNOSTIC + command classes override this for plumbing/diagnostic values.""" + if command_class in SYSTEM_COMMAND_CLASSES: + return ParameterVisibility.system + if command_class in DIAGNOSTIC_COMMAND_CLASSES: + return ParameterVisibility.setting + if metadata.readable or metadata.writeable: + return ParameterVisibility.user + return ParameterVisibility.system + + def parse_zwave_data_type(self, value: Value) -> ParameterDataType: + """Maps a Value to ParameterDataType. ValueType alone can't tell integer from + decimal/enum, or resolve ValueType.ANY, so those fall back to the raw value.""" + metadata = value.metadata + + if metadata.type == ValueType.BOOLEAN: + return ParameterDataType.bool + if metadata.type == ValueType.STRING: + return ParameterDataType.string + if metadata.type == ValueType.NUMBER: + if metadata.states: + return ParameterDataType.enum + raw = value.value + if isinstance(raw, float) and not raw.is_integer(): + return ParameterDataType.decimal + return ParameterDataType.integer + + # ValueType.ANY: keep in sync with format_zwave_value's duration handling. + if self._is_duration_property(value): + return ParameterDataType.string + raw = value.value + if isinstance(raw, bool): + return ParameterDataType.bool + if isinstance(raw, int): + return ParameterDataType.integer + if isinstance(raw, float): + return ParameterDataType.decimal + if isinstance(raw, str): + return ParameterDataType.string + if isinstance(raw, dict): + return ParameterDataType.struct + # Opaque CC-specific object — expose as data rather than guessing a shape. + return ParameterDataType.data + + def parse_zwave_valid_values(self, metadata: ValueMetadata) -> dict[int | float | str, str] | None: + """Converts states (`{"0": "Off"}`) to valid_values, coercing keys to int.""" + if not metadata.states: + return None + result: dict[int | float | str, str] = {} + for raw_key, label in metadata.states.items(): + try: + result[int(raw_key)] = label + except ValueError: + result[raw_key] = label + return result + + def get_main_parameter(self, device_id: UUID, node: Node) -> tuple[UUID | None, bool | int | float | None]: + """Picks the value for the device's one-tap action, by MAIN_VALUE_BY_COMMAND_CLASS + priority. Returns (None, None) if none applies.""" + for command_class, spec in MAIN_VALUE_BY_COMMAND_CLASS.items(): + value = next( + ( + v + for v in node.values.values() + if v.command_class == command_class and v.property_name == spec.property_name + ), + None, + ) + if value is not None: + return self.parameter_uuid(device_id, value.value_id), spec.default_value + return None, None \ No newline at end of file diff --git a/majordom_zwave/model.py b/majordom_zwave/model.py new file mode 100644 index 0000000..05459da --- /dev/null +++ b/majordom_zwave/model.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel +from majordom_integration_sdk.schemas import Device, DeviceState, Parameter, ParameterState + + +class ZwaveDeviceIntegrationData(BaseModel): + node_id: int = 0 + + +class ZwaveParameterIntegrationData(BaseModel): + value_id: str + + +class ZwaveDevice(Device): + integration_data: ZwaveDeviceIntegrationData | None = None + + @property + def node_id(self) -> int: + assert self.integration_data + if isinstance(self.integration_data, dict): + return self.integration_data.get("node_id", -1) + return self.integration_data.node_id + + +class ZwaveParameter(Parameter): + integration_data: ZwaveParameterIntegrationData + + +class ZwaveParameterState(ParameterState): + integration_data: ZwaveParameterIntegrationData + + +class ZwaveDeviceState(ZwaveDevice, DeviceState): + parameters: list[ZwaveParameterState] \ No newline at end of file diff --git a/majordom_zwave/zwave_spec.py b/majordom_zwave/zwave_spec.py new file mode 100644 index 0000000..f635d02 --- /dev/null +++ b/majordom_zwave/zwave_spec.py @@ -0,0 +1,148 @@ +"""Static Z-Wave classification data. + +Unlike Zigbee, zwave-js already reports `min`/`max`/`unit`/`states` directly on `ValueMetadata`, +so this module only needs to cover what the wire format *doesn't* give us: which command classes +are protocol plumbing or diagnostics-only, how to normalize zwave-js's freeform unit strings, +and which property is a device's one-tap "main" action. + +Pure data only — the functions/methods that use these tables live on ZwaveMapper in mapper.py. +""" + +from dataclasses import dataclass + +from majordom_integration_sdk.schemas.parameter import ParameterUnit +from zwave_js_server.const import CommandClass + +# The standard, cross-vendor Indicator CC value for "make the device blink so I can find it" +# (Indicator CC spec, indicator id 0x50 "Node Identify"). Setting it triggers the device's own +# built-in identify blink pattern and it self-resets — there's nothing to turn back off. +IDENTIFY_INDICATOR_ID = 0x50 + + +# Protocol/network plumbing: never shown to the user, regardless of read/write access. +# (association & multi channel wiring, security/transport encapsulation, inclusion, S2, etc.) +SYSTEM_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ + CommandClass.NO_OPERATION, + CommandClass.CONTROLLER_REPLICATION, + CommandClass.APPLICATION_STATUS, + CommandClass.ZIP, + CommandClass.NETWORK_MANAGEMENT_INCLUSION, + CommandClass.NETWORK_MANAGEMENT_BASIC, + CommandClass.NETWORK_MANAGEMENT_PROXY, + CommandClass.NETWORK_MANAGEMENT_PRIMARY, + CommandClass.NETWORK_MANAGEMENT_INSTALLATION_MAINTENANCE, + CommandClass.TRANSPORT_SERVICE, + CommandClass.CRC_16_ENCAP, + CommandClass.APPLICATION_CAPABILITY, + CommandClass.ASSOCIATION, + CommandClass.ASSOCIATION_GRP_INFO, + CommandClass.MULTI_CHANNEL, + CommandClass.MULTI_CHANNEL_ASSOCIATION, + CommandClass.MULTI_CMD, + CommandClass.SUPERVISION, + CommandClass.ZWAVEPLUS_INFO, + CommandClass.INCLUSION_CONTROLLER, + CommandClass.NODE_NAMING, + CommandClass.NODE_PROVISIONING, + CommandClass.MANUFACTURER_PROPRIETARY, + CommandClass.PROPRIETARY, + CommandClass.SECURITY, + CommandClass.SECURITY_2, + CommandClass.SECURITY_SCHEME0_MARK, + CommandClass.MARK, + CommandClass.TIME, + CommandClass.TIME_PARAMETERS, + CommandClass.CLOCK, + CommandClass.GEOGRAPHIC_LOCATION, + CommandClass.FIRMWARE_UPDATE_MD, + CommandClass.GROUPING_NAME, + CommandClass.REMOTE_ASSOCIATION_ACTIVATE, + CommandClass.REMOTE_ASSOCIATION, + CommandClass.SCREEN_MD, + CommandClass.SCREEN_ATTRIBUTES, + CommandClass.DEVICE_RESET_LOCALLY, +}) + +# Technically readable/reportable, but the "settings/advanced" kind of reading (battery %, +# firmware version, RSSI-ish diagnostics, wake-up interval) rather than a main everyday control. +# ParameterVisibility.setting explicitly covers this case per its docstring. +DIAGNOSTIC_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ + CommandClass.BATTERY, + CommandClass.VERSION, + CommandClass.MANUFACTURER_SPECIFIC, + CommandClass.POWERLEVEL, + CommandClass.WAKE_UP, + CommandClass.INDICATOR, + CommandClass.PROTECTION, + CommandClass.CONFIGURATION, # device settings behind an "advanced" tap, not main interaction +}) + +# CCs that report one-shot occurrences rather than a persistent state (button presses, +# scene activations, alarms) — modeled as ParameterRole.event regardless of read/write flags. +EVENT_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ + CommandClass.CENTRAL_SCENE, + CommandClass.SCENE_ACTIVATION, + CommandClass.NOTIFICATION, +}) + +# zwave-js's ValueMetadata.unit is a freeform string straight from the device's CC report, not a +# controlled vocabulary — map the ones we recognize, leave the rest unmapped rather than guessed. +# Extend this as new unit strings show up in the field. +UNIT_MAP: dict[str, ParameterUnit] = { + "%": ParameterUnit.percentage, + "s": ParameterUnit.second, + "Hz": ParameterUnit.hertz, + "kg": ParameterUnit.kilogram, + "°": ParameterUnit.arcdegree, + "deg": ParameterUnit.arcdegree, + "m": ParameterUnit.meters, + "m/s": ParameterUnit.mps, + "m/s2": ParameterUnit.mps2, + "rpm": ParameterUnit.rpm, + "N": ParameterUnit.newton, + "J": ParameterUnit.joule, + "W": ParameterUnit.watt, + "°C": ParameterUnit.celsius, + "K": ParameterUnit.kelvin, + "V": ParameterUnit.volt, + "A": ParameterUnit.ampere, + "lx": ParameterUnit.lux, + "Pa": ParameterUnit.pascal, + "ppm": ParameterUnit.ppm, + "b": ParameterUnit.bytes, + "B": ParameterUnit.bytes, + # Deliberately unmapped (no ParameterUnit equivalent yet — falls back to `plain`): + # "kWh" (energy-over-time, not instantaneous joule), "dB"/"dBm" (log scale), "UV index". +} + + +@dataclass(frozen=True) +class MainValueSpec: + """Identifies a device's one-tap main parameter and the value to send for it. + + Z-Wave has no invokable "commands" the way Zigbee/Matter clusters do — every action is a + property get/set — so every Z-Wave main parameter is attribute-like (the same shape as + Matter's FanControl case, the one cluster where Matter's own main parameter is attribute-type + too, rather than a command). `default_value` is what a tap sends when the app doesn't already + know the device's current state (e.g. right after pairing); once telemetry is flowing, the + app is expected to flip between the parameter's known states itself rather than always + sending this same value on every tap. + """ + + property_name: str + default_value: bool | int | float + + +# Priority order matters: the first command class present on the node wins, same as +# ZigBeeController's MAIN_PARAMETER_BY_CLUSTER. Only CCs with an unambiguous "main" action are +# listed — e.g. Thermostat Mode is deliberately left out, since there's no single mode that's +# the obviously-correct one-tap action across vendors. +MAIN_VALUE_BY_COMMAND_CLASS: dict[CommandClass, MainValueSpec] = { + CommandClass.SWITCH_BINARY: MainValueSpec("targetValue", True), + # 255 = "restore last non-zero level" per the Multilevel Switch CC spec, i.e. "turn on". + CommandClass.SWITCH_MULTILEVEL: MainValueSpec("targetValue", 255), + # BarrierState.OPEN = 255. + CommandClass.BARRIER_OPERATOR: MainValueSpec("targetState", 255), + # DoorLockMode.SECURED = 255 — lock, not unlock, is the safer default one-tap action. + CommandClass.DOOR_LOCK: MainValueSpec("targetMode", 255), +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 963b25a..5ff190d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,197 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] @@ -42,141 +224,6 @@ winrt-windows-storage-streams = {version = ">=3.1", markers = "sys_platform == \ [package.extras] pythonista = ["bleak-pythonista (>=0.1.1)"] -[[package]] -name = "cachecontrol" -version = "0.12.10" -description = "httplib2 caching for requests" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "CacheControl-0.12.10-py2.py3-none-any.whl", hash = "sha256:b0d43d8f71948ef5ebdee5fe236b86c6ffc7799370453dccb0e894c20dfa487c"}, - {file = "CacheControl-0.12.10.tar.gz", hash = "sha256:d8aca75b82eec92d84b5d6eb8c8f66ea16f09d2adb09dbca27fe2d5fc8d3732d"}, -] - -[package.dependencies] -msgpack = ">=0.5.2" -requests = "*" - -[package.extras] -filecache = ["lockfile (>=0.9)"] -redis = ["redis (>=2.10.5)"] - -[[package]] -name = "certifi" -version = "2026.6.17" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.9" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, - {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, - {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, -] - [[package]] name = "colorama" version = "0.4.6" @@ -190,110 +237,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "coverage" -version = "7.15.2" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d"}, - {file = "coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846"}, - {file = "coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf"}, - {file = "coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376"}, - {file = "coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd"}, - {file = "coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb"}, - {file = "coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1"}, - {file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9"}, - {file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a"}, - {file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287"}, - {file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89"}, - {file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88"}, - {file = "coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443"}, - {file = "coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629"}, - {file = "coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036"}, - {file = "coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660"}, - {file = "coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589"}, - {file = "coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee"}, - {file = "coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0"}, - {file = "coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487"}, - {file = "coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f"}, - {file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1"}, - {file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5"}, - {file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0"}, - {file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad"}, - {file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db"}, - {file = "coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9"}, - {file = "coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688"}, - {file = "coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934"}, - {file = "coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9"}, - {file = "coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73"}, - {file = "coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d"}, - {file = "coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b"}, - {file = "coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296"}, - {file = "coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6"}, - {file = "coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098"}, - {file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a"}, - {file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b"}, - {file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2"}, - {file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440"}, - {file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e"}, - {file = "coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd"}, - {file = "coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40"}, - {file = "coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3"}, - {file = "coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8"}, - {file = "coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1"}, - {file = "coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578"}, - {file = "coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1"}, - {file = "coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6"}, - {file = "coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7"}, - {file = "coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d"}, - {file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026"}, - {file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa"}, - {file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d"}, - {file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b"}, - {file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188"}, - {file = "coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050"}, - {file = "coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c"}, - {file = "coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b"}, - {file = "coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a"}, - {file = "coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2"}, - {file = "coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138"}, - {file = "coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f"}, - {file = "coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984"}, - {file = "coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7"}, - {file = "coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3"}, - {file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee"}, - {file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1"}, - {file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a"}, - {file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145"}, - {file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446"}, - {file = "coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243"}, - {file = "coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc"}, - {file = "coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635"}, - {file = "coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be"}, - {file = "coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072"}, - {file = "coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328"}, - {file = "coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a"}, - {file = "coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0"}, - {file = "coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5"}, - {file = "coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743"}, - {file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c"}, - {file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071"}, - {file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a"}, - {file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d"}, - {file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c"}, - {file = "coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688"}, - {file = "coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199"}, - {file = "coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658"}, - {file = "coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c"}, - {file = "coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d"}, -] - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - [[package]] name = "dbus-fast" version = "5.0.22" @@ -355,64 +298,152 @@ files = [ ] [[package]] -name = "faker" -version = "40.32.0" -description = "Faker is a Python package that generates fake data for you." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "faker-40.32.0-py3-none-any.whl", hash = "sha256:2fd913ad02dabbea84d729937db68f9ca9773dce93cec45f1fd6fe0fd1b41840"}, - {file = "faker-40.32.0.tar.gz", hash = "sha256:78271bfa3a0e982f1b90e1345b2b8f4a04a132864e0372262074e60c2cddd4ef"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -tzdata = ["tzdata"] - -[[package]] -name = "gprof2dot" -version = "2025.4.14" -description = "Generate a dot graph from the output of several profilers." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, - {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, -] - -[[package]] -name = "html5lib" -version = "1.1" -description = "HTML parser based on the WHATWG HTML specification" +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, - {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] -[package.dependencies] -six = ">=1.9" -webencodings = "*" - -[package.extras] -all = ["chardet (>=2.2)", "genshi", "lxml ; platform_python_implementation == \"CPython\""] -chardet = ["chardet (>=2.2)"] -genshi = ["genshi"] -lxml = ["lxml ; platform_python_implementation == \"CPython\""] - [[package]] name = "idna" version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -445,28 +476,16 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] -[[package]] -name = "lockfile" -version = "0.12.2" -description = "Platform-independent file locking module" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] - [[package]] name = "majordom-integration-sdk" -version = "0.1.5" +version = "0.1.6" description = "Models, protocols, and tooling for building MajorDom integrations." optional = false python-versions = ">=3.12" groups = ["main"] files = [ - {file = "majordom_integration_sdk-0.1.5-py3-none-any.whl", hash = "sha256:758bcf3430e75bba5e2dd54fccde81461dce892674ba2dc5189f694011d2145e"}, - {file = "majordom_integration_sdk-0.1.5.tar.gz", hash = "sha256:7f17d8fc2dba446f855d367c762aeca338f11c4db84d0ec32bf4624e61340669"}, + {file = "majordom_integration_sdk-0.1.6-py3-none-any.whl", hash = "sha256:a35e39c1ee2c6b254803d8967b20e0c90722514fea54fd51d72463ab9d7d5d3a"}, + {file = "majordom_integration_sdk-0.1.6.tar.gz", hash = "sha256:cdd3bd5d30c1e004f2b2784995182838f12e5c6b0c0764a5f8e9d558024512f9"}, ] [package.dependencies] @@ -479,79 +498,159 @@ zeroconf = ">=0.130" cli = ["rich (>=13)", "typer-shell (>=0.2)"] [[package]] -name = "msgpack" -version = "1.2.1" -description = "MessagePack serializer" +name = "multidict" +version = "6.7.1" +description = "multidict implementation" optional = false -python-versions = ">=3.10" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, - {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, - {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, - {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, - {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, - {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, - {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, - {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, - {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, - {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, - {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, - {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, - {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, - {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, - {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, - {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, - {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, - {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, ] [[package]] @@ -566,69 +665,6 @@ files = [ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] -[[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, -] - -[[package]] -name = "pip" -version = "26.1.2" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab"}, - {file = "pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605"}, -] - -[[package]] -name = "pip-api" -version = "0.0.34" -description = "An unofficial, importable pip API" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb"}, - {file = "pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625"}, -] - -[package.dependencies] -pip = "*" - -[[package]] -name = "pip-audit" -version = "0.0.5" -description = "A tool for scanning Python environments for known vulnerabilities" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pip-audit-0.0.5.tar.gz", hash = "sha256:5048871bcf9a8cf38422f2476caeb7489324ef794e9d7952b3cd72cf1f490985"}, - {file = "pip_audit-0.0.5-py3-none-any.whl", hash = "sha256:e037c67c32c80cb3613faa423ab8e0b53aa4b93ca3e4e942870c848107f23ccf"}, -] - -[package.dependencies] -CacheControl = "0.12.10" -html5lib = ">=1.1" -lockfile = ">=0.12.2" -packaging = ">=21.0.0" -pip-api = ">=0.0.23" -progress = ">=1.6" -resolvelib = ">=0.8.0" - -[package.extras] -dev = ["black", "bump", "coverage[toml]", "flake8", "interrogate", "isort", "mypy", "pretend", "pytest", "pytest-cov", "types-dataclasses", "types-html5lib", "types-requests"] - [[package]] name = "pluggy" version = "1.6.0" @@ -646,81 +682,134 @@ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] -name = "poethepoet" -version = "0.48.0" -description = "A task runner that works well with poetry and uv." +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" optional = false python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "poethepoet-0.48.0-py3-none-any.whl", hash = "sha256:98da6096d060f49b8d84034770265863fb7dc92a40233b7694b9d216ac68737d"}, - {file = "poethepoet-0.48.0.tar.gz", hash = "sha256:a06f49d244fadfc2e2e7faa78b54e64a9694727e4ce1d50e08f23cea3ded74f1"}, -] - -[package.dependencies] -pastel = ">=0.2.1,<0.3.0" -pyyaml = ">=6.0.3,<7.0" - -[package.extras] -poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] - -[[package]] -name = "progress" -version = "1.6.1" -description = "Easy to use progress bars" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "progress-1.6.1-py3-none-any.whl", hash = "sha256:5239f22f305c12fdc8ce6e0e47f70f21622a935e16eafc4535617112e7c7ea0b"}, - {file = "progress-1.6.1.tar.gz", hash = "sha256:c1ba719f862ce885232a759eab47971fe74dfc7bb76ab8a51ef5940bad35086c"}, -] - -[[package]] -name = "psutil" -version = "7.2.2" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, - {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, - {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, - {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, - {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, - {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, - {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, - {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, - {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, - {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, - {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, - {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, - {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, - {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, - {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -description = "Get CPU info with pure Python" -optional = false -python-versions = "*" -groups = ["dev"] +groups = ["main"] files = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, ] [[package]] @@ -893,21 +982,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pympler" -version = "1.1" -description = "A development tool to measure, monitor and analyze the memory behavior of Python objects." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "Pympler-1.1-py3-none-any.whl", hash = "sha256:5b223d6027d0619584116a0cbc28e8d2e378f7a79c1e5e024f9ff3b673c58506"}, - {file = "pympler-1.1.tar.gz", hash = "sha256:1eaa867cb8992c218430f1708fdaccda53df064144d1c5656b1e6f1ee6000424"}, -] - -[package.dependencies] -pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} - [[package]] name = "pyobjc-core" version = "12.2.1" @@ -1045,112 +1119,6 @@ typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] -[[package]] -name = "pytest-benchmark" -version = "5.2.3" -description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803"}, - {file = "pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779"}, -] - -[package.dependencies] -py-cpuinfo = "*" -pytest = ">=8.1" - -[package.extras] -aspect = ["aspectlib"] -elasticsearch = ["elasticsearch"] -histogram = ["pygal", "pygaljs", "setuptools"] - -[[package]] -name = "pytest-cov" -version = "7.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, - {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-profiling" -version = "1.8.1" -description = "Profiling plugin for py.test" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d"}, - {file = "pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea"}, -] - -[package.dependencies] -gprof2dot = "*" -pytest = "*" -six = "*" - -[[package]] -name = "pytest-repeat" -version = "0.9.4" -description = "pytest plugin for repeating tests" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3"}, - {file = "pytest_repeat-0.9.4.tar.gz", hash = "sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485"}, -] - -[package.dependencies] -pytest = "*" - -[[package]] -name = "pytest-resource-usage" -version = "1.0.0" -description = "Pytest plugin for reporting running time and peak memory usage" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-resource-usage-1.0.0.tar.gz", hash = "sha256:3a200cee007fc3e7496ff34620b2841494356c92063aa6fa79e9201fd1e583cc"}, - {file = "pytest_resource_usage-1.0.0-py3-none-any.whl", hash = "sha256:94534f378c13ccdf1413d461f0d780ff2cebf45f07de1622d6e1c07f0a86154b"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -report-uss = ["psutil (>=4.0.0)"] - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -description = "pytest plugin to abort hanging tests" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, - {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - [[package]] name = "python-slugify" version = "8.0.4" @@ -1169,200 +1137,6 @@ text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] -[[package]] -name = "pywin32" -version = "312" -description = "Python for Windows Extensions" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, - {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, - {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, - {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, - {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, - {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, - {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, - {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, - {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, - {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, - {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, - {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, - {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, - {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, - {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, - {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, - {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, - {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, - {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, - {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, - {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "requests" -version = "2.34.2" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, - {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, -] - -[package.dependencies] -certifi = ">=2023.5.7" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.26,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] - -[[package]] -name = "resolvelib" -version = "1.2.1" -description = "Resolve abstract dependencies into concrete ones" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2"}, - {file = "resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c"}, -] - -[package.extras] -lint = ["mypy", "ruff", "types-requests"] -release = ["build", "towncrier", "twine"] -test = ["packaging", "pytest"] - -[[package]] -name = "ruff" -version = "0.15.22" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8"}, - {file = "ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697"}, - {file = "ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64"}, - {file = "ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf"}, - {file = "ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde"}, - {file = "ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661"}, - {file = "ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809"}, -] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - [[package]] name = "text-unidecode" version = "1.3" @@ -1375,34 +1149,6 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] -[[package]] -name = "ty" -version = "0.0.61" -description = "An extremely fast Python type checker, written in Rust." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "ty-0.0.61-py3-none-linux_armv6l.whl", hash = "sha256:148779b8675eac93f40ec58bd70037fe67537117f20a23272264f8f136d41336"}, - {file = "ty-0.0.61-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:08217382b3385808ee7288501ea3214b32631b08d1fd091ece6799b0c95264c5"}, - {file = "ty-0.0.61-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d99c729011b47dec20e78a32ac9c8f6defd4cf62f7bb851bbccf70dde6cee50"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cda607978ae271b77e51c947663218bce635c3507e256865444b10c37cdb60d"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d78f160a0f9434d570cdcdbc4dafba1f6aac3c47a32f9f63995b3cb55ffe4b6"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09aeab4800b36e93e4ce918699004da642d74988cac920b7592a6a2b9be6611c"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dccc8136df44142a109953a168be17b4915c99876b047d0b6672c31dae939bdf"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:220760c2d13a887d027ee1093172c24ac35b6e634805329c93a30908ae4d3f5c"}, - {file = "ty-0.0.61-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effefbb89da7128d18059529d1c2ea390fe7f1f3882690d257ca2143d49a0c34"}, - {file = "ty-0.0.61-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba8b28a5ef811d5bb6461e37d76110c06fd20487474865c323d3d18b08b972b2"}, - {file = "ty-0.0.61-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88ecd6d9b05e8174b1860dac9bd3e188d6cef5702b0d3239fd9f94f6ac73a29d"}, - {file = "ty-0.0.61-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb0cdfe4c48542ffb9a1139825dfa3d4aae49e96e966682ef7da762ab97831ff"}, - {file = "ty-0.0.61-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dff03873c0c3d0b44738f8b6d403b0756a31cf54c65136397df7624c6159b1f0"}, - {file = "ty-0.0.61-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a9210e80e3d41c1dfc751e9e8e0980272f475031fafd0fb0f48aee233c78da03"}, - {file = "ty-0.0.61-py3-none-win32.whl", hash = "sha256:e3e1fe06f49a5492a922a5df2739834aa5ee978c7dd10414119dc8755cc40c9c"}, - {file = "ty-0.0.61-py3-none-win_amd64.whl", hash = "sha256:25f2291169e0298fcdbba1b1fea64f8207a6c1908dddef32346fd5e3e6ac9221"}, - {file = "ty-0.0.61-py3-none-win_arm64.whl", hash = "sha256:3e496f7698bc4b5bbb1eb66d8b5799ba87596d88d36604ca359083893fa2fc49"}, - {file = "ty-0.0.61.tar.gz", hash = "sha256:acbf0d914cc7e2e57ccc440036af36114819e2a604a5ffb554e72e4ca7dd65a2"}, -] - [[package]] name = "typing-extensions" version = "4.16.0" @@ -1431,49 +1177,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" -[[package]] -name = "tzdata" -version = "2026.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931"}, - {file = "tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415"}, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - [[package]] name = "winrt-runtime" version = "3.2.1" @@ -1795,6 +1498,125 @@ winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.System[all] (>=3.2.1.0,<3.3.0.0)"] +[[package]] +name = "yarl" +version = "1.24.5" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [[package]] name = "zeroconf" version = "0.150.0" @@ -1876,7 +1698,23 @@ files = [ [package.dependencies] ifaddr = ">=0.1.7" +[[package]] +name = "zwave-js-server-python" +version = "0.73.0" +description = "Python wrapper for zwave-js-server" +optional = false +python-versions = ">=3.12" +groups = ["main"] +files = [ + {file = "zwave_js_server_python-0.73.0-py3-none-any.whl", hash = "sha256:62ce12a619a420a59eb760a60fbb150c5b39d87159af42c4b7201b5895752e2e"}, + {file = "zwave_js_server_python-0.73.0.tar.gz", hash = "sha256:cc863597929900638e3db0288133370cd8a111a6f18f262a080a128cb6581f75"}, +] + +[package.dependencies] +aiohttp = ">3" +pydantic = ">=2.0.0" + [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "dd98b7dcfe6dff16ed13d485d3dc95cb7da96a266994eb9a52f29b529ff225a3" +content-hash = "5c3d4da7828f7457e1b92474d20c4a12a72bc7541df76e5ac72265d8b5579039" diff --git a/pyproject.toml b/pyproject.toml index 9c1cf6b..030115f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,53 +1,15 @@ -# Rename `integration-template` (PyPI/distribution name, hyphens) and the -# `integration_template/` package directory (import name, underscores) to your own — by -# convention `majordom-` / `majordom_` (e.g. majordom-homekit / -# majordom_homekit). Keep them matching so poetry-core auto-detects the package, and update -# `pypi-package-name` in the release stub + `--cov=` below to match. [project] -name = "integration-template" +name = "integration-zwave" version = "0.1.0" -description = "A MajorDom integration." -authors = [{ name = "Mark Parker", email = "mark@parker-programs.com" }] -readme = "README.md" -# The template scaffold is Apache-2.0. Relicense your integration as you see fit — note -# that linking a copyleft (GPL/AGPL) protocol library may constrain your choice. -license = "Apache-2.0" -license-files = ["LICENSE"] -keywords = ["majordom", "smart-home", "home-automation", "iot", "integration", "template", "scaffold"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Topic :: Home Automation", - "Programming Language :: Python :: 3", +description = "" +authors = [ + {name = "Ehor Lukianov",email = "ehorlukianov@gmail.com"} ] +readme = "README.md" requires-python = ">=3.12" dependencies = [ - "majordom-integration-sdk (>=0.1.5,<1.0)", -] - -[project.urls] -Homepage = "https://majordom.io" -Documentation = "https://docs.majordom.io/device-integration" -Repository = "https://github.com/MajorDom-Systems/integration-template" - -[dependency-groups] -dev = [ - "poethepoet (>=0.36.0,<1.0.0)", - "ty", # type checker; swap for mypy/pyright by editing the `check` task below - "ruff", - "pip-audit", # dependency vuln scan — gates release (not everyday CI); run locally with `poetry run pip-audit` - "pytest", - "pytest-asyncio", - "pytest-cov", - "pytest-timeout", - "pytest-repeat", - "pytest-benchmark", - "pytest-profiling", - "pytest-resource-usage", - "coverage", - "pympler", # object memory introspection, leak hunting - "psutil", # process/system resource usage - "faker", # realistic fake data for tests + "majordom-integration-sdk (>=0.1.2,<0.2.0)", + "zwave-js-server-python (>=0.73.0,<0.74.0)" ] [build-system] @@ -55,74 +17,12 @@ requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -requires-poetry = ">=2.0" -packages = [{ include = "integration_template" }] +package-mode = false + +[tool.poetry.group.dev.dependencies] +pytest = "^9.1.1" +pytest-asyncio = "^1.4.0" [tool.pytest.ini_options] -asyncio_mode = "auto" pythonpath = ["."] -filterwarnings = ["once"] - -[tool.coverage.run] -omit = ["tests/*"] - -[tool.ruff] -line-length = 120 -indent-width = 4 - -[tool.ruff.lint] -select = [ - "E", # pycodestyle - "F", # pyflakes - "UP", # pyupgrade - "B", # flake8-bugbear - "SIM", # flake8-simplify - "I", # isort -] -fixable = ["ALL"] - -[tool.ruff.format] -quote-style = "double" -indent-style = "space" -line-ending = "auto" -docstring-code-format = true - -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401"] -"tests/*" = ["E402"] - -[tool.poe.tasks.install] -shell = """ -poetry install -echo '#!/bin/sh\npoetry run poe check' > .git/hooks/pre-commit -chmod +x .git/hooks/pre-commit -echo "Pre-commit hook installed." -""" - -[tool.poe.tasks.check] -shell = """ -code=0 - -echo "\nRunning ruff lint:" -poetry run ruff check --fix || code=1 - -echo "\nRunning ruff format:" -poetry run ruff format || code=1 - -echo "\nRunning ty:" -poetry run ty check || code=1 - -echo "\nRunning pytest:" -poetry run pytest --cov=integration_template --cov-report=xml || code=1 - -echo "\nRunning poetry build:" -poetry build || code=1 - -echo "\nRunning poetry check:" -poetry check || code=1 - -${ci:+git diff --exit-code} - -exit $code -""" -args = [{ name = "ci", type = "boolean" }] +asyncio_mode = "auto" \ No newline at end of file diff --git a/scripts/gen_key.sh b/scripts/gen_key.sh new file mode 100755 index 0000000..564c21c --- /dev/null +++ b/scripts/gen_key.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +ENV_FILE=".env" + +if [ -f "$ENV_FILE" ]; then + echo "$ENV_FILE already exists — leaving Z-Wave keys untouched." + exit 0 +fi + +gen_key() { + # 16 bytes -> 32 hex chars, cryptographically random + openssl rand -hex 16 | tr '[:lower:]' '[:upper:]' +} + +cat > "$ENV_FILE" < AbstractController.Dependencies: """SDK-provided test dependencies (recording output + in-memory repo + fake discovery).""" return build_test_dependencies(integration=INTEGRATION) -@pytest.fixture -def controller(deps: AbstractController.Dependencies) -> ExampleController: - return ExampleController(deps) +@pytest.fixture(scope="session") +def controller(deps: AbstractController.Dependencies) -> ZwaveController: + return ZwaveController(deps) # A virtual / simulated device — a fake endpoint that speaks your protocol in-process (no @@ -52,7 +52,7 @@ def controller(deps: AbstractController.Dependencies) -> ExampleController: # # @pytest.fixture # def virtual_device(): -# dev = VirtualExampleDevice() # your in-process fake +# dev = VirtualZwaveDevice() # your in-process fake # dev.start() # yield dev # dev.stop() @@ -73,32 +73,28 @@ def discovery() -> Discovery: expected_credentials_options=[CredentialsType.none], transport="wifi", device_manufacturer="ACME", - device_name="Example Lamp", + device_name="Zwave Lamp", device_category=None, device_icon=None, ) @pytest.fixture -async def provisional_device(deps: AbstractController.Dependencies, discovery: Discovery) -> DeviceState: - """The device as it exists when the Hub calls `pair_device`. - - Before pairing, the Hub has already created the row from the user's input: name and room - mapped, and its id set to the discovery's (still provisional) id — but no parameters yet; - the controller discovers/maps those while pairing. After pairing the controller reports - the real device id and the Hub reconciles the row. We seed the repository to match. - """ - device = DeviceState( - id=discovery.id, # provisional id == discovery id, until pairing assigns the real one - name="Living Room Lamp", # mapped by the Hub from the user's input - room_id=uuid4(), - transport=discovery.transport, - integration=INTEGRATION, - manufacturer=discovery.device_manufacturer, - parameters=[], - ) - await _seed(deps, device) - return device +def make_provisional_device(deps: AbstractController.Dependencies): + async def _factory(discovery: Discovery) -> DeviceState: + device = DeviceState( + id=discovery.id, + name="Living Room Lamp", + room_id=uuid4(), + transport=discovery.transport, + integration=INTEGRATION, + manufacturer=discovery.device_manufacturer, + parameters=[], + ) + await _seed(deps, device) + return device + + return _factory @pytest.fixture @@ -137,4 +133,4 @@ def device(device_state: DeviceState) -> Device: @pytest.fixture def parameter(device_state: DeviceState) -> ParameterState: """The target parameter for `send_command`, paired with `device`.""" - return device_state.parameters[0] + return device_state.parameters[0] \ No newline at end of file diff --git a/tests/test_controller.py b/tests/test_controller.py index 16fb3ba..67a0347 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -19,23 +19,44 @@ `.github/workflows/test.yml`. Your integration repo runs it in full, and it's red until you implement the controller.) """ +import asyncio from majordom_integration_sdk.controller import AbstractController from majordom_integration_sdk.schemas.command import DeviceCommand from majordom_integration_sdk.schemas.device import Device, Discovery from majordom_integration_sdk.schemas.parameter import ParameterState -from integration_template import ExampleController +from majordom_zwave import ZwaveController +from majordom_zwave.model import ZwaveDevice, ZwaveDeviceState async def test_pairs_a_discovered_device( - controller: ExampleController, + controller: ZwaveController, deps: AbstractController.Dependencies, - discovery: Discovery, - provisional_device: Device, # creates a device with discovery.id and saves it to the db + make_provisional_device, # already stored under discovery.id by the Hub-side fixture ) -> None: - # The Hub has already stored `provisional_device` (id == discovery.id) before this call. + await controller.start() + await controller.start_pairing_window(30) + + # Physically trigger inclusion on the device now (put it into pairing mode). + print("\n>>> Put the Z-Wave device into inclusion mode now (you have 30s)...") + await asyncio.sleep(30) + + assert controller.discoveries, "No device was discovered during the pairing window" + discovery = next(iter(controller.discoveries.values())) + await make_provisional_device(discovery) await controller.pair_device(discovery, credentials=None) + await controller.stop() + + # # Hub-side: controller reported the device as connected. + assert discovery.id in deps.output.connected_devices + + # # Repo-side: parameters were mapped and persisted during pairing. + async with deps.make_device_repository() as repo: + paired = await repo.get(discovery.id, as_=ZwaveDevice) + assert paired is not None + # assert paired.parameters, "Expected at least one parameter to be mapped during pairing" + assert paired.integration_data.node_id is not None # TODO: verify pairing actually completed — the controller established the session and # reported back (it does not persist the device itself): # - Hub side: `discovery.id` is in `deps.output.connected_devices`. @@ -45,33 +66,53 @@ async def test_pairs_a_discovered_device( async def test_fetches_state( - controller: ExampleController, + controller: ZwaveController, deps: AbstractController.Dependencies, - device: Device, ) -> None: - await controller.fetch(device) + await controller.start() + async with deps.make_device_repository() as repo: + devices = await repo.get_all(as_=ZwaveDevice) + device = devices[0] + await controller.fetch(device) + await controller.stop() # TODO: verify the refresh reached the device and propagated back: # - Set a known state on the virtual device first, then assert `deps.output.events` # (or the repository's parameter states) now reflect exactly that state — not stale # or default values. -async def test_sends_a_command(controller: ExampleController, device: Device, parameter: ParameterState) -> None: - command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=False) - await controller.send_command(command, device, parameter) - # TODO: verify the command actually applied on the device, not just that the call - # returned: assert the virtual device received this exact write and its state changed - # to `value` (read it back). A no-op that silently succeeds must fail this test. - - -async def test_identifies(controller: ExampleController, device: Device) -> None: - await controller.identify(device) - # TODO: assert the virtual device recorded an identify request (e.g. its identify counter - # incremented / the identify command was received). - - -async def test_unpairs(controller: ExampleController, device: Device) -> None: - await controller.unpair(device) - # TODO: verify removal took effect: - # - Device side: the virtual device saw the session torn down / was de-registered. - # - Controller: the device is gone — no more connections or commands should be accepted. +async def test_sends_a_command(deps: AbstractController.Dependencies, controller: ZwaveController) -> None: + await controller.start() + async with deps.make_device_repository() as repo: + devices = await repo.get_all(as_=ZwaveDevice) + device = devices[0] + state = await repo.state(device.id, as_=ZwaveDeviceState) + + parameter = next(p for p in state.parameters if p.id == state.main_parameter) + + command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=0) + await controller.send_command(command, device, parameter) + await controller.stop() + + +async def test_identifies(controller: ZwaveController, deps: AbstractController.Dependencies) -> None: + await controller.start() + async with deps.make_device_repository() as repo: + devices = await repo.get_all(as_=ZwaveDevice) + device = devices[0] + await controller.identify(device) + await controller.stop() +# # TODO: assert the virtual device recorded an identify request (e.g. its identify counter +# # incremented / the identify command was received). + + +async def test_unpairs(controller: ZwaveController, deps: AbstractController.Dependencies) -> None: + await controller.start() + async with deps.make_device_repository() as repo: + devices = await repo.get_all(as_=ZwaveDevice) + device = devices[0] + await controller.unpair(device) + await controller.stop() +# # TODO: verify removal took effect: +# # - Device side: the virtual device saw the session torn down / was de-registered. +# # - Controller: the device is gone — no more connections or commands should be accepted. \ No newline at end of file diff --git a/tests/test_smoke.py b/tests/test_smoke.py index e7ae398..1985cd2 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,17 +1,11 @@ -"""Basic tests that pass out of the box — they don't depend on protocol logic. +from majordom_zwave import ZwaveController -Kept separate from ``test_controller.py`` so the template repo's own CI (which excludes the -prefilled, expected-to-fail controller suite) still has tests to run and stays green. -""" -from integration_template import ExampleController - - -async def test_starts_and_stops(controller: ExampleController) -> None: +async def test_starts_and_stops(controller: ZwaveController) -> None: await controller.start() - await controller.stop() + await controller.stop() -async def test_name_and_slug(controller: ExampleController) -> None: - assert controller.name == "Example" - assert controller.name_slug == "example" +async def test_name_and_slug(controller: ZwaveController) -> None: + assert controller.name == "ZWave" + assert controller.name_slug == "zwave" From 39156c19839151af8e9bbea192085b1ca4fc32ab Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Fri, 28 Aug 2026 10:57:52 +0300 Subject: [PATCH 02/28] Removed MatterExceptions --- majordom_zwave/exceptions.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/majordom_zwave/exceptions.py b/majordom_zwave/exceptions.py index 674cb8d..e8f2bd8 100644 --- a/majordom_zwave/exceptions.py +++ b/majordom_zwave/exceptions.py @@ -4,11 +4,3 @@ class ZwaveConnectionError(Exception): class ZwaveUnexpectedError(Exception): """Raised when an unexpected internal error occurs.""" - - -# class MatterUnsupportedParameter(Exception): -# """Raised when a parameter is not supported by the device.""" - - -# class MatterNotFoundParameter(Exception): - # """Raised when a parameter was not found on the device.""" \ No newline at end of file From 6db270bee315b6d31a2045efb833c1ce54e1a1ab Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 29 Aug 2026 12:41:29 +0300 Subject: [PATCH 03/28] Added credentials into start_pairing_window method added last_error message into unpair and added identify for v1 devices --- majordom_zwave/controller.py | 72 ++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 4e881b3..2133b47 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -9,6 +9,7 @@ from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant from zwave_js_server.model.node import Node from zwave_js_server.model.value import Value +from zwave_js_server.model.utils import async_parse_qr_code_string from majordom_integration_sdk.controller import AbstractController from majordom_integration_sdk.schemas import DeviceCommand, Discovery, ProvidedCredentials from majordom_integration_sdk.schemas.device import CredentialsType, NonEmptyStr @@ -30,6 +31,7 @@ log = logging.getLogger(__name__) +#TODO: заполнить ридми под интеграцию по примерам меттера и зигби class ZwaveController(AbstractController): """Bridges the Hub to Z-Wave devices through zwave-js-server.""" @@ -146,11 +148,30 @@ async def stop(self): # Hub -> device operations # ------------------------------------------------------------------------- - async def start_pairing_window(self, duration_sec: int) -> None: + async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCredentials | None = None) -> None: self._require_client() controller = self._zwave_client.driver.controller - await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) - log.debug("[PAIRING] inclusion window opened for %ds", duration_sec) + + if credentials and credentials.type is CredentialsType.qr: + if not credentials.value: + raise ZwaveUnexpectedError("QR credentials provided without QR data") + provisioning_info = await async_parse_qr_code_string(self._zwave_client, credentials.value) + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, provisioning=provisioning_info) + log.debug("[PAIRING] inclusion window opened for %ds via QR", duration_sec) + + elif credentials and credentials.type is CredentialsType.code: + if not credentials.value: + raise ZwaveUnexpectedError("PIN credentials provided without a value") + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, dsk=credentials.value) + log.debug("[PAIRING] inclusion window opened for %ds with pre-supplied PIN", duration_sec) + + elif credentials and credentials.type is CredentialsType.secret: + raise ZwaveUnexpectedError("Z-Wave does not support secret-based pairing credentials") + + else: + await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) + log.debug("[PAIRING] inclusion window opened for %ds", duration_sec) + self._create_task(self._close_pairing_window(duration_sec)) async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: @@ -213,6 +234,15 @@ async def unpair(self, device: ZwaveDevice): await controller.async_remove_failed_node(zwave_node) else: # Live node: requires interactive exclusion, confirmed by the device itself. + async with self.dependencies.make_device_repository() as device_repository: + stored = await device_repository.get(device.id, as_=ZwaveDevice) + if stored: + stored.last_error = ( + "Trigger exclusion mode on the device manually " + "(see manufacturer instructions) to complete removal" + ) + await device_repository.save(stored, device.id) + removed = asyncio.Event() def _on_node_removed(data: dict) -> None: @@ -250,11 +280,39 @@ async def identify(self, device: ZwaveDevice): ), None, ) - if indicator_value is None: - raise ZwaveUnexpectedError(f"Node {zwave_node.node_id} does not support the Identify indicator") + if indicator_value is not None: + await zwave_node.async_set_value(indicator_value, 0xFF) + log.debug("[IDENTIFY] node_id=%s via Indicator CC", zwave_node.node_id) + return + + switch_value = next( + ( + v + for v in zwave_node.values.values() + if v.command_class in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) + and v.property_ == "targetValue" + ), + None, + ) + if switch_value is None: + raise ZwaveUnexpectedError( + f"Node {zwave_node.node_id} supports neither Indicator CC nor a switch to blink" + ) + + current_value = zwave_node.values.get( + switch_value.value_id.replace("targetValue", "currentValue") + ) + restore_value = current_value.value if current_value and current_value.value is not None else 0 + + on_value = 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 + for _ in range(10): + await zwave_node.async_set_value(switch_value, 0) + await asyncio.sleep(0.6) + await zwave_node.async_set_value(switch_value, on_value) + await asyncio.sleep(0.6) - await zwave_node.async_set_value(indicator_value, 0xFF) - log.debug("[IDENTIFY] node_id=%s", zwave_node.node_id) + await zwave_node.async_set_value(switch_value, restore_value) + log.debug("[IDENTIFY] node_id=%s via switch blink fallback", zwave_node.node_id) async def fetch(self, device: ZwaveDevice): self._require_client() From 000d4d068f2e78c3b659fcdffa8438b54f6ffb01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 05:55:03 +0000 Subject: [PATCH 04/28] chore(deps-pip): bump zwave-js-server-python in the python-deps group (#2) Bumps the python-deps group with 1 update: [zwave-js-server-python](https://github.com/home-assistant-libs/zwave-js-server-python). Updates `zwave-js-server-python` from 0.73.0 to 0.73.1 - [Release notes](https://github.com/home-assistant-libs/zwave-js-server-python/releases) - [Commits](https://github.com/home-assistant-libs/zwave-js-server-python/compare/0.73.0...0.73.1) --- updated-dependencies: - dependency-name: zwave-js-server-python dependency-version: 0.73.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5ff190d..69a5f54 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1700,14 +1700,14 @@ ifaddr = ">=0.1.7" [[package]] name = "zwave-js-server-python" -version = "0.73.0" +version = "0.73.1" description = "Python wrapper for zwave-js-server" optional = false python-versions = ">=3.12" groups = ["main"] files = [ - {file = "zwave_js_server_python-0.73.0-py3-none-any.whl", hash = "sha256:62ce12a619a420a59eb760a60fbb150c5b39d87159af42c4b7201b5895752e2e"}, - {file = "zwave_js_server_python-0.73.0.tar.gz", hash = "sha256:cc863597929900638e3db0288133370cd8a111a6f18f262a080a128cb6581f75"}, + {file = "zwave_js_server_python-0.73.1-py3-none-any.whl", hash = "sha256:07ca62957392dff6fdf211e40dc36041c459f9943e78b613687d3aea6c316c46"}, + {file = "zwave_js_server_python-0.73.1.tar.gz", hash = "sha256:5f4bfe7268a7de093a8f107159e2d8d6828f00b442e3ec52b9d116ef514e4e20"}, ] [package.dependencies] @@ -1717,4 +1717,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "5c3d4da7828f7457e1b92474d20c4a12a72bc7541df76e5ac72265d8b5579039" +content-hash = "fe6a80760cb931dc9fa8e6906aa819eda6df17f3f23280f4f66c9d1a142be34e" diff --git a/pyproject.toml b/pyproject.toml index 030115f..a221f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "majordom-integration-sdk (>=0.1.2,<0.2.0)", - "zwave-js-server-python (>=0.73.0,<0.74.0)" + "zwave-js-server-python (>=0.73.1,<0.74.0)" ] [build-system] From f7cc2c9e8be44cec43d3d3e6d48ff62b88304c3d Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 5 Sep 2026 13:39:48 +0300 Subject: [PATCH 05/28] Updated .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 90cc466..a26d7d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ __pycache__/ -.env \ No newline at end of file +.env +.zed/ From 255b2c2a801885b273dd7a84e6b01cf2d2d4a237 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 5 Sep 2026 13:40:01 +0300 Subject: [PATCH 06/28] Added mock in tests --- tests/conftest.py | 50 +++------ tests/test_controller.py | 133 ++++++++++++++---------- tests/virtual_zwave_network.py | 184 +++++++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 91 deletions(-) create mode 100644 tests/virtual_zwave_network.py diff --git a/tests/conftest.py b/tests/conftest.py index 35727ec..4e88e68 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,3 @@ -"""Shared pytest fixtures for the whole suite live here. - -conftest.py is auto-discovered by pytest — no import needed. - -The fixtures wire the integration's Controller with the SDK's test doubles (a recording -`ControllerOutput`, an in-memory device repository, and fake discovery services) and build -sample `Discovery`/`DeviceState` objects, so tests can drive the controller exactly as the -Hub would — without a running Hub. - -**Device persistence is the Hub's core business logic, not the integration's.** The Hub -creates and stores device rows (name/room mapping, id assignment, the provisional→final id -reconciliation on pairing); an integration only reads through the injected repository and -writes parameter *state*. These fixtures therefore seed the repository the way the Hub -would, so the controller sees realistic state — the tests never make the integration -persist a device itself. -""" - from uuid import uuid4 import pytest @@ -30,12 +13,13 @@ from majordom_zwave import ZwaveController +from virtual_zwave_network import VirtualZwaveNetwork + INTEGRATION = "Zwave" @pytest.fixture(scope="session") def deps() -> AbstractController.Dependencies: - """SDK-provided test dependencies (recording output + in-memory repo + fake discovery).""" return build_test_dependencies(integration=INTEGRATION) @@ -44,29 +28,21 @@ def controller(deps: AbstractController.Dependencies) -> ZwaveController: return ZwaveController(deps) -# A virtual / simulated device — a fake endpoint that speaks your protocol in-process (no -# hardware, no network) so tests are deterministic and can assert what actually reached the -# device. Stand one up here and have the controller talk to it (e.g. via a base URL / mock -# transport injected through the controller). Tests then assert on both the controller's -# reports (deps.output) and this object's observed state. -# -# @pytest.fixture -# def virtual_device(): -# dev = VirtualZwaveDevice() # your in-process fake -# dev.start() -# yield dev -# dev.stop() +@pytest.fixture(autouse=True) +def virtual_zwave(): + # autouse: any test calling controller.start() needs this active, even if + # it never touches nodes directly (otherwise it hits a real socket). + with VirtualZwaveNetwork() as net: + yield net async def _seed(deps: AbstractController.Dependencies, device: DeviceState) -> None: - """Persist a device the way the Hub would, before handing control to the integration.""" async with deps.make_device_repository() as repo: await repo.save(device) @pytest.fixture def discovery() -> Discovery: - """A sample discovered-but-unpaired device (the controller surfaced this).""" return Discovery( id=uuid4(), integration=INTEGRATION, @@ -82,6 +58,9 @@ def discovery() -> Discovery: @pytest.fixture def make_provisional_device(deps: AbstractController.Dependencies): async def _factory(discovery: Discovery) -> DeviceState: + # parameters=[] on purpose: a placeholder param here needs Zwave-specific + # integration_data (not None), or pair_device's re-read as ZwaveDeviceState + # fails validation. Pairing is what maps and persists real parameters. device = DeviceState( id=discovery.id, name="Living Room Lamp", @@ -99,8 +78,6 @@ async def _factory(discovery: Discovery) -> DeviceState: @pytest.fixture async def device_state(deps: AbstractController.Dependencies) -> DeviceState: - """An already-paired device with one on/off parameter, as the Hub *stores* it (the full - state, with parameters). Seeded into the repository.""" power = ParameterState( id=uuid4(), name="Power", @@ -125,12 +102,9 @@ async def device_state(deps: AbstractController.Dependencies) -> DeviceState: @pytest.fixture def device(device_state: DeviceState) -> Device: - """The device as the Hub *passes* it to `fetch`/`identify`/`unpair`/`send_command` — a - `Device` (info + integration_data), without the parameter list.""" return Device.model_validate(device_state.model_dump()) @pytest.fixture def parameter(device_state: DeviceState) -> ParameterState: - """The target parameter for `send_command`, paired with `device`.""" - return device_state.parameters[0] \ No newline at end of file + return device_state.parameters[0] diff --git a/tests/test_controller.py b/tests/test_controller.py index 67a0347..4f4d872 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,46 +1,34 @@ -"""Tests that drive the controller through its lifecycle. - -These are **prefilled to fail** against the unimplemented skeleton: every test below calls a -method that still raises `NotImplementedError`, so CI stays red until you implement the -controller. - -Run these against a **virtual / simulated device** — a fake endpoint that speaks your -protocol in-process (no real hardware, no network), so CI is deterministic. Add a fixture -for it in `conftest.py` and have the controller talk to it. Each test should then assert on -**both sides**: what the controller reported back to the Hub (via `deps.output`) *and* what -actually landed on the device (the virtual device observed the command / changed state) — -don't just call the method and assume it worked. - -Persistence is the Hub's job (see `conftest.py`): the fixtures seed the repository the way -the Hub would, so assert on the controller's protocol behaviour and reports — not that the -integration created a device row. - -(The template's own CI excludes this file so the template repo stays green — see -`.github/workflows/test.yml`. Your integration repo runs it in full, and it's red until you -implement the controller.) +"""Run order matters: `controller`/`deps` are session-scoped, so pairing happens +once in test_pairs_a_discovered_device and later tests read that same device +back from the shared repo. `virtual_zwave` is function-scoped though (fresh, +empty node registry each test), so any test that needs to reach the "device" +re-creates the node with the same NODE_ID. """ import asyncio from majordom_integration_sdk.controller import AbstractController from majordom_integration_sdk.schemas.command import DeviceCommand -from majordom_integration_sdk.schemas.device import Device, Discovery -from majordom_integration_sdk.schemas.parameter import ParameterState from majordom_zwave import ZwaveController from majordom_zwave.model import ZwaveDevice, ZwaveDeviceState +from virtual_zwave_network import VirtualZwaveNetwork + +NODE_ID = 5 + async def test_pairs_a_discovered_device( controller: ZwaveController, deps: AbstractController.Dependencies, - make_provisional_device, # already stored under discovery.id by the Hub-side fixture + virtual_zwave: VirtualZwaveNetwork, + make_provisional_device, ) -> None: await controller.start() - await controller.start_pairing_window(30) + await controller.start_pairing_window(10) - # Physically trigger inclusion on the device now (put it into pairing mode). - print("\n>>> Put the Z-Wave device into inclusion mode now (you have 30s)...") - await asyncio.sleep(30) + virtual_zwave.node_found(NODE_ID) + virtual_zwave.node_joined({"nodeId": NODE_ID}) + await asyncio.sleep(0.05) assert controller.discoveries, "No device was discovered during the pairing window" discovery = next(iter(controller.discoveries.values())) @@ -48,71 +36,110 @@ async def test_pairs_a_discovered_device( await controller.pair_device(discovery, credentials=None) await controller.stop() - # # Hub-side: controller reported the device as connected. assert discovery.id in deps.output.connected_devices - # # Repo-side: parameters were mapped and persisted during pairing. async with deps.make_device_repository() as repo: paired = await repo.get(discovery.id, as_=ZwaveDevice) + state = await repo.state(discovery.id, as_=ZwaveDeviceState) assert paired is not None - # assert paired.parameters, "Expected at least one parameter to be mapped during pairing" - assert paired.integration_data.node_id is not None - # TODO: verify pairing actually completed — the controller established the session and - # reported back (it does not persist the device itself): - # - Hub side: `discovery.id` is in `deps.output.connected_devices`. - # - Device side: the virtual device accepted the credentials / opened a session. - # - Parameters: if you map parameters during pairing, assert their initial states were - # written via the repository (`repo.save_parameter_state`). + assert paired.integration_data.node_id == NODE_ID + assert state.parameters, "Expected at least one parameter to be mapped during pairing" async def test_fetches_state( controller: ZwaveController, deps: AbstractController.Dependencies, + virtual_zwave: VirtualZwaveNetwork, ) -> None: await controller.start() + virtual_zwave.node_joined({"nodeId": NODE_ID}) + await asyncio.sleep(0.05) + async with deps.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] - await controller.fetch(device) + + virtual_zwave.emit_value_updated( + NODE_ID, + command_class=37, + command_class_name="Binary Switch", + property="currentValue", + new_value=True, + prev_value=False, + ) + events_before = len(deps.output.events) + await controller.fetch(device) + new_events = deps.output.events[events_before:] + + assert any(e.device_id == device.id and e.value is True for e in new_events), ( + "fetch() never reported the current value back to the Hub" + ) await controller.stop() - # TODO: verify the refresh reached the device and propagated back: - # - Set a known state on the virtual device first, then assert `deps.output.events` - # (or the repository's parameter states) now reflect exactly that state — not stale - # or default values. -async def test_sends_a_command(deps: AbstractController.Dependencies, controller: ZwaveController) -> None: +async def test_sends_a_command( + deps: AbstractController.Dependencies, + controller: ZwaveController, + virtual_zwave: VirtualZwaveNetwork, +) -> None: await controller.start() + virtual_zwave.node_joined({"nodeId": NODE_ID}) + async with deps.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] state = await repo.state(device.id, as_=ZwaveDeviceState) - parameter = next(p for p in state.parameters if p.id == state.main_parameter) command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=0) await controller.send_command(command, device, parameter) await controller.stop() + sent = [ + c for c in virtual_zwave.async_send_command.call_args_list + if c.args[0].get("command") == "node.set_value" + ] + assert sent, "Controller never sent a node.set_value command to the device" + assert sent[-1].args[0]["value"] == 0 -async def test_identifies(controller: ZwaveController, deps: AbstractController.Dependencies) -> None: + +async def test_identifies( + controller: ZwaveController, + deps: AbstractController.Dependencies, + virtual_zwave: VirtualZwaveNetwork, +) -> None: await controller.start() + virtual_zwave.node_joined({"nodeId": NODE_ID}) + async with deps.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] await controller.identify(device) - await controller.stop() -# # TODO: assert the virtual device recorded an identify request (e.g. its identify counter -# # incremented / the identify command was received). + await controller.stop() + + sent = [ + c for c in virtual_zwave.async_send_command.call_args_list + if c.args[0].get("command") == "node.set_value" + ] + assert sent, "controller.identify() never sent a node.set_value command to the device" -async def test_unpairs(controller: ZwaveController, deps: AbstractController.Dependencies) -> None: +async def test_unpairs( + controller: ZwaveController, + deps: AbstractController.Dependencies, + virtual_zwave: VirtualZwaveNetwork, +) -> None: await controller.start() + virtual_zwave.node_joined({"nodeId": NODE_ID}) + async with deps.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] - await controller.unpair(device) + + unpair_task = asyncio.create_task(controller.unpair(device)) + await asyncio.sleep(0.05) + virtual_zwave.node_removed(NODE_ID) + await unpair_task + + assert NODE_ID not in virtual_zwave.controller.nodes await controller.stop() -# # TODO: verify removal took effect: -# # - Device side: the virtual device saw the session torn down / was de-registered. -# # - Controller: the device is gone — no more connections or commands should be accepted. \ No newline at end of file diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py new file mode 100644 index 0000000..44e0092 --- /dev/null +++ b/tests/virtual_zwave_network.py @@ -0,0 +1,184 @@ +"""In-memory zwave-js-server stub. Patches zwave_js_server.client.Client so any +Client the controller creates becomes an in-memory stub instead of hitting a +real server. Everything else (Driver/Controller/Node/Value) is the real library. +""" +from __future__ import annotations + +import asyncio +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +from zwave_js_server.client import Client +from zwave_js_server.event import Event +from zwave_js_server.model.driver import Driver +from zwave_js_server.model.node import Node + +_LOG_CONFIG = { + "enabled": True, "level": "info", "logToFile": False, + "filename": "", "forceConsole": False, +} + +_DEFAULT_CONTROLLER_STATE: dict[str, Any] = { + "controller": { + "sdkVersion": "Z-Wave 3.95", "type": 1, "homeId": 1, "ownNodeId": 1, + "isSecondary": False, "isUsingHomeIdFromOtherNetwork": False, + "isSISPresent": True, "wasRealPrimary": True, "isStaticUpdateController": True, + "isSlave": False, "firmwareVersion": "1.0", "manufacturerId": 1, + "productType": 1, "productId": 1, "supportedFunctionTypes": [], + "sucNodeId": 1, "supportsTimers": False, "isRebuildingRoutes": False, + "inclusionState": 0, + }, + "nodes": [], +} + +_DEFAULT_VALUE_METADATA = {"type": "boolean", "readable": True, "writeable": True} + +_DEFAULT_NODE_VALUES: list[dict[str, Any]] = [ + { + "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, + "property": "currentValue", "propertyName": "currentValue", + "value": False, "ccVersion": 1, + "metadata": {"type": "boolean", "readable": True, "writeable": False, "label": "Current value"}, + }, + { + "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, + "property": "targetValue", "propertyName": "targetValue", + "value": False, "ccVersion": 1, + "metadata": {"type": "boolean", "readable": True, "writeable": True, "label": "Target value"}, + }, +] + +_DEFAULT_NODE_ENDPOINTS: list[dict[str, Any]] = [ + {"index": 0, "deviceClass": None, "commandClasses": [ + {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, + ]}, +] + +# Client.async_send_command's return value IS the server message's "result" +# field already unwrapped once -- each command needs the exact key(s) its +# caller reads off that dict (e.g. is_failed_node reads data["failed"]). +_DEFAULT_COMMAND_RESPONSES: dict[str, dict[str, Any]] = { + "controller.begin_inclusion": {"success": True}, + "controller.stop_inclusion": {"success": True}, + "controller.begin_exclusion": {"success": True}, + "controller.stop_exclusion": {"success": True}, + "controller.is_failed_node": {"failed": False}, + "controller.remove_failed_node": {}, + "controller.replace_failed_node": {"success": True}, + "node.set_value": {"result": {"status": 255}}, + "node.refresh_info": {}, + "node.refresh_values": {}, +} + + +class VirtualZwaveNetwork: + def __init__(self, controller_state: dict | None = None) -> None: + self.controller_state = controller_state or _DEFAULT_CONTROLLER_STATE + self._command_responses = dict(_DEFAULT_COMMAND_RESPONSES) + self.async_send_command = AsyncMock(side_effect=self._handle_send_command) + self.async_send_command_no_wait = AsyncMock(return_value=None) + self._client: Client | None = None + self._patches: list[Any] = [] + + async def _handle_send_command(self, message: dict, **_kwargs: Any) -> dict: + return self._command_responses.get(message.get("command", ""), {"success": True}) + + def set_response(self, command: str, response: dict) -> None: + self._command_responses[command] = response + + def __enter__(self) -> "VirtualZwaveNetwork": + network = self + + async def fake_connect(client_self: Client) -> None: + # Client.connected checks `_client is not None and not _client.closed`. + client_self._client = Mock(closed=False) + + async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: + client_self.driver = Driver(client_self, network.controller_state, _LOG_CONFIG) + network._client = client_self + driver_ready.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + pass + + async def fake_disconnect(client_self: Client) -> None: + client_self.driver = None + if client_self._client is not None: + client_self._client.closed = True + + self._patches = [ + patch.object(Client, "connect", new=fake_connect), + patch.object(Client, "listen", new=fake_listen), + patch.object(Client, "disconnect", new=fake_disconnect), + patch.object(Client, "async_send_command", new=self.async_send_command), + patch.object(Client, "async_send_command_no_wait", new=self.async_send_command_no_wait), + ] + for p in self._patches: + p.start() + return self + + def __exit__(self, *exc_info: Any) -> None: + for p in reversed(self._patches): + p.stop() + + @property + def controller(self): + assert self._client and self._client.driver, ( + "virtual_zwave.controller accessed before controller.start() ran" + ) + return self._client.driver.controller + + def node_found(self, node_id: int) -> None: + self.controller.emit("node found", {"node": {"nodeId": node_id}}) + + def node_joined(self, node_data: dict) -> Node: + payload = { + "status": 4, # NodeStatus.ALIVE, not 1 (ASLEEP) -- ASLEEP makes + # async_send_command silently switch to no_wait + "ready": True, + "interviewStage": "Complete", + "endpoints": _DEFAULT_NODE_ENDPOINTS, + "values": _DEFAULT_NODE_VALUES, + **node_data, + } + node = Node(self._client, payload) + self.controller.nodes[node.node_id] = node + self.controller.emit("node added", {"node": node}) + return node + + def node_removed(self, node_id: int, reason: int = 0) -> None: + node = self.controller.nodes.pop(node_id, None) + if node is not None: + self.controller.emit("node removed", {"node": node, "reason": reason}) + + def emit_value_updated( + self, + node_id: int, + *, + command_class: int, + command_class_name: str, + property: int | str, # noqa: A002 + new_value: Any, + prev_value: Any = None, + property_name: str | None = None, + endpoint: int = 0, + metadata: dict | None = None, + ) -> None: + node = self.controller.nodes[node_id] + event = Event("value updated", { + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClass": command_class, + "commandClassName": command_class_name, + "endpoint": endpoint, + "property": property, + "propertyName": property_name or str(property), + "newValue": new_value, + "prevValue": prev_value, + "metadata": metadata or _DEFAULT_VALUE_METADATA, + }, + }) + node.receive_event(event) From 8c904a13ac9f0a59f77d22119ee8f88cfdd8e502 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 5 Sep 2026 13:48:04 +0300 Subject: [PATCH 07/28] Updated readme file --- README.md | 240 +++++++++++++++++++++--------------------------------- 1 file changed, 95 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index df1525a..7b2b1cf 100644 --- a/README.md +++ b/README.md @@ -12,71 +12,24 @@ - - -# Creating a new MajorDom integration from this template - -Click **Use this template → Create a new repository** (name it `integration-`, -e.g. `integration-hue`), then: - -**1. Rename the placeholders** (find & replace across the repo) -- `integration-template` → your distribution name, by convention `majordom-` - (hyphens): in `pyproject.toml` (`[project].name`) and `.github/workflows/release.yml` - (`pypi-package-name`). -- `integration_template` → your import name `majordom_` (underscores): the - `integration_template/` directory, plus the `--cov=` and `packages` references in - `pyproject.toml`. -- Rename `ExampleController` and fill in `integration_template/controller.py` with real - protocol logic. `tests/test_controller.py` is prefilled to fail until you do — work the - **Progress** checklist below and tick items off as your CI goes green. -- In `.github/workflows/test.yml`, delete the `template-selfcheck` job and the `if:` guard - on the `test` job — they exist only to keep the *template* repo green and aren't wanted in - a real integration. -- Fill in the **About this integration** section below (it doubles as your PR summary — a - reviewer reads the checklist to see what's actually implemented). - -**2. Install + pre-commit hook** -```sh -pip install poetry poethepoet && poe install -``` - -**3. Create the `develop` branch** -```sh -git checkout -b develop && git push origin develop -``` - -**4. GitHub repo settings** — same as any package repo built on the shared workflows: - -- **General** → enable **Allow auto-merge**; -- **Branches** → protect `master` (require the `test / check` status, restrict push to `github-actions[bot]`) and `develop` (require `test / check`); -- **Environments** → create `release` (deployment branch `develop` only, required reviewer). Then configure PyPI trusted publishing for your package (Owner = the GitHub user or org that owns **this** repo, Repository = this repo, Workflow `release.yml`, Environment `release`). - -The reusable CI/CD lives in [ParkerIndustries/workflows](https://github.com/ParkerIndustries/workflows). - - - -# integration-template +# integration-zwave -A [MajorDom](https://majordom.io) integration — bridges the Example protocol into the -MajorDom language. +A [MajorDom](https://majordom.io) integration — bridges **Z-Wave** devices into the MajorDom +language. Built for the **MajorDom Hub**, but it doesn't need it: this is a standalone, standardized -library for the Example protocol that you can use on its own (see **Run it standalone** below). -Built on the [MajorDom Integration SDK](https://github.com/MajorDom-Systems/integration-sdk). -The integration's entry point is a `Controller` (`integration_template/controller.py`) that the -Hub — or the SDK's dev runner — instantiates and drives through its lifecycle: discovery → -pairing → commands → teardown. +library for Z-Wave that you can use on its own (see **Run it standalone** below). Built on the +[MajorDom Integration SDK](https://github.com/MajorDom-Systems/integration-sdk). The entry point +is `ZwaveController` (`majordom_zwave/controller.py`), which the Hub — or the SDK's dev runner — +instantiates and drives through its lifecycle: pairing → commands → teardown. - **Other protocols:** browse the [MajorDom integrations](https://github.com/orgs/MajorDom-Systems/repositories?q=integration-). - **Create your own:** start from the [integration template](https://github.com/MajorDom-Systems/integration-template). ## Documentation -Full integration-author docs — the controller lifecycle, data models, storing data, -discovery, and a worked example — live at **[docs.majordom.io](https://docs.majordom.io/device-integration)**. +Full integration-author docs — the controller lifecycle, data models, storing data, discovery, +and a worked example — live at **[docs.majordom.io](https://docs.majordom.io/device-integration)**. ## Development @@ -89,124 +42,121 @@ poetry install && poetry run poe install | `poe check` | Full quality pipeline (ruff, ty, pytest, poetry build/check) | | `poe check --ci` | Same, plus `git diff --exit-code` | -Work lands on `develop`; `master` is protected and released via **Actions → Release**. -Tests drive the controller with the SDK's test doubles (`majordom_integration_sdk.testing`) -against a virtual/simulated device — see `tests/`. +Work lands on `develop`; `master` is protected and released via **Actions → Release**. Tests drive +the controller with the SDK's test doubles against an in-memory `zwave-js-server-python` +stub — no real server, radio, or Z-Wave stick required (see `tests/`). ## Run it standalone (without the Hub) -Your integration is a standalone library — import it into another app, or run **just this +`majordom-zwave` is a standalone library — import it into your own app, or run **just this integration** interactively (discover, pair, control, and inspect devices from a prompt) with no Hub. -Note here whatever prerequisites your protocol needs to run on its own (a broker, a radio, a local -server, …). +It needs a reachable `zwave-js-server` instance (see below). See **[Standalone mode](https://docs.majordom.io/device-integration/standalone)** for the interactive CLI, watch mode, and the programmatic API. -## About this integration - - +## Connecting to zwave-js-server -- **Protocol / platform:** _e.g. Philips Hue (Zigbee via a bridge)_ -- **Transport(s):** _wifi / ble / zigbee / …_ -- **Supported devices:** _…_ -- **Credentials needed to pair:** _none / code / secret / qr_ +Unlike a radio-based integration, this one never talks to the Z-Wave stick directly. It connects +over WebSocket, via [`zwave-js-server-python`](https://github.com/home-assistant-libs/zwave-js-server-python), +to a running **[zwave-js-server](https://github.com/zwave-js/zwave-js-server)** process (Node.js) — +the same server Home Assistant's Z-Wave JS integration uses. `zwave-js-server` is what actually owns +the serial connection to the Z-Wave USB controller and speaks the Z-Wave protocol; this integration +just talks JSON over a WebSocket to it. -### Required harness +- Run `zwave-js-server` yourself (commonly via Docker, `ghcr.io/zwave-js/zwave-js-server`) on + whatever machine has the Z-Wave USB controller attached. +- Point this integration at it with the `ZWAVE_SERVER_URL` environment variable + (e.g. `ws://localhost:3000`) — see `majordom_zwave/config.py`. - +## About this integration -- **Hardware adapters:** _e.g. an 802.15.4 radio (SkyConnect / a Thread or Zigbee dongle), a - USB serial gateway …_ — the Hub assigns OS device paths through - `dependencies.hardware_interfaces` (e.g. `/dev/ttyACM0`). -- **Third-party software services:** _e.g. an OpenThread Border Router (OTBR), a vendor - bridge/hub, an MQTT broker, a matter-server instance …_ — what must be running and reachable. -- **OS / permissions:** _e.g. Bluetooth access, host networking, mDNS/SSDP on the LAN …_ +- **Protocol / platform:** Z-Wave, via [`zwave-js-server`](https://github.com/zwave-js/zwave-js-server) + / [`zwave-js-server-python`](https://github.com/home-assistant-libs/zwave-js-server-python). +- **Transport(s):** Z-Wave (sub-GHz RF mesh). +- **Supported devices:** any Z-Wave / Z-Wave Plus device exposing standard Command Classes + (Binary/Multilevel Switch, Indicator, etc.) that Z-Wave JS supports. +- **Credentials needed to pair:** `none` (S0 / unauthenticated inclusion), `code` (S2 PIN), or + `qr` (S2 SmartStart / DSK QR code). `secret` is not supported. -### Protocol stack +### Required harness -Every integration is two things stacked: the **MajorDom integration layer** — mapping the -protocol to MajorDom's domain model — sitting on top of the **protocol stack** it bridges. The -top layer is *always this repo*. How much of the stack *below* it is also this repo's code -varies: some integrations only map an existing application-level protocol (a vendor library / the -OS / the harness provides everything under them), while others implement the protocol themselves, -down to raw UDP or even a custom radio. +- **Hardware adapters:** a Z-Wave USB controller (e.g. Aeotec Z-Stick, Zooz ZST10) — but it's + attached to whatever host runs `zwave-js-server`, not necessarily the Hub. This integration + doesn't use `dependencies.hardware_interfaces`; the radio is abstracted behind the server. +- **Third-party software services:** [`zwave-js-server`](https://github.com/zwave-js/zwave-js-server) + (Node.js) must be running and reachable at `ZWAVE_SERVER_URL`. +- **OS / permissions:** network reachability (TCP) to the `zwave-js-server` host:port. No special + permissions needed on the Hub's own host. - +### Protocol stack | Layer | Protocol | Implemented by | |-------|----------|----------------| -| **MajorDom integration** | maps the protocol ↔ MajorDom domain model | **this repo, always** | -| Application (7) | _Matter clusters / data model_ | this integration (via `chip` lib) | -| Session (5) | _CASE / PASE secure session_ | library | -| Transport (4) | _UDP_ | OS | -| Network (3) | _IPv6 · 6LoWPAN_ | OS · OTBR (harness) | -| Data link / Physical (1–2) | _Thread · IEEE 802.15.4_ | radio adapter (harness) | - -If your integration implements the protocol itself (no vendor library), more of the lower rows -become **this integration** — a custom-radio integration can own everything from the application -layer down to the physical. +| **MajorDom integration** | maps Z-Wave devices/values ↔ MajorDom domain model | **this repo, always** | +| Z-Wave JS Server API | `zwave-js-server`'s WebSocket/JSON-RPC protocol | library (`zwave-js-server-python`) | +| Application | Z-Wave Command Classes (CC) | `zwave-js-server` (external Node.js service) | +| Network / MAC / PHY | Z-Wave mesh, sub-GHz RF | Z-Wave USB controller + its driver (harness) | ### Progress -Two checklists — this README is where you track them (tick items as you implement them and the -matching test in `tests/` goes green). The docs explain the *why* behind each item: -[Implementation Checklist](https://docs.majordom.io/device-integration) (gets it working) and -[Quality Checklist](https://docs.majordom.io/device-integration/quality) (gets it releasable). - **Implementation** — makes the integration functional: -- [ ] Discovery services registered via `self.dependencies.zeroconf_discovery_service`, `ssdp_discovery_service`, and/or `ble_discovery_service` as appropriate; cancel closures saved and called in `stop` -- [ ] Discovery service listeners fire when devices are found, and the controller calls `self.dependencies.output.controller_did_receive_discovery` -- [ ] Discovery of devices already paired to the Hub on reconnect, e.g. after a reboot (`self.dependencies.output.controller_did_connect_device` is called) -- [ ] `start_pairing_window` is implemented but only if the protocol requires an explicit scan (like zigbee) -- [ ] Device pairing -- [ ] Device schema is properly mapped: device info, parameter list, and each parameter's metadata are translated to MajorDom's domain model -- [ ] Hub → Device control (`send_command` is implemented) -- [ ] Device → Hub event subscription (`self.dependencies.output.controller_did_receive_events` is called on incoming events) -- [ ] `identify` is implemented -- [ ] `unpair` is implemented -- [ ] `fetch` is implemented -- [ ] Paired devices going offline/coming back online *while the Hub is running* (not just on reboot) — set `device.available` accordingly (report `controller_did_lose_device`), and clear/set `last_error` to match -- [ ] Graceful shutdown in `stop`, cancelling any running tasks, discovery stopped, all connections closed -- [ ] Tests pass against a virtual/simulated device (`tests/test_controller.py`) -- [ ] README fully filled in: delete the template-setup section above, complete **About this integration**, **Required harness**, **Protocol stack**, and **Notes** with real content +- [x] Discovery of joining devices via `zwave-js-server`'s `"node added"` event; also reconciles + nodes already on the network but not yet claimed (e.g. after a Hub restart) — + `controller_did_receive_discovery` called +- [ ] Discovery of already-paired devices on reconnect — currently `start()` marks known devices + available internally on startup without going through `controller_did_connect_device` (see Notes) +- [x] `start_pairing_window` (default S0, S2 PIN, S2 QR/SmartStart, auto-grant of requested + security classes with no user prompt) +- [x] Device pairing +- [x] Device schema mapped (via `ZwaveMapper`: values → parameters, role/visibility/units) +- [x] Hub → Device control (`send_command`) +- [x] Device → Hub event subscription (`controller_did_receive_events` on `"value updated"`) +- [x] `identify` (Indicator CC, falls back to blinking a Binary/Multilevel Switch) +- [x] `unpair` (failed-node removal, or live interactive exclusion confirmed by the device) +- [x] `fetch` +- [x] Availability tracking while running (`"dead"`/`"alive"` → `controller_did_lose_device` / + `controller_did_connect_device`) — `last_error` isn't set/cleared on these specific + transitions though (see Notes) +- [x] Graceful shutdown in `stop` (tasks cancelled, client disconnected, session closed) +- [x] Tests pass against a virtual/simulated device (`tests/test_controller.py`) **Quality** — makes it reliable and maintainable (the bar for release): -- [ ] **Recovers automatically** from connection loss / offline device / restarted backend — retried with backoff, no manual restart -- [ ] **No exception escapes the controller** — every background task, subscription loop, and callback catches its own errors; nothing raised into `self.dependencies.output.*` -- [ ] **Failures are surfaced, not raised** — logged once (no spam) and reflected on the device (`available` / `last_error`), cleared on recovery -- [ ] **Re-authenticates automatically** when credentials expire/are rejected (if the protocol uses credentials) -- [ ] **Fully asynchronous** — no blocking I/O on the event loop; heavy/blocking work runs off-loop -- [ ] **Stable identity** — device and parameter UUIDs derived through the SDK helpers, identical across restarts/re-pairs -- [ ] **End-to-end tests** drive pair → command → fetch → events → `unpair` against a virtual device (`majordom_integration_sdk.testing`) -- [ ] **Failure paths tested** — offline device, transport error, rejected credentials degrade gracefully (no raise) -- [ ] **Broad device coverage** where the protocol has many device/parameter types (a virtual-device catalogue in CI is ideal) -- [ ] **Fully typed** (`ty`, no package-wide ignores) and **clean** (`poe check`) with no warnings -- [ ] **Readable & structured** — conversion logic in a mapper, models separated, comments where intent isn't obvious -- [ ] **Efficient** — subscriptions over polling; batch/chunk reads; no redundant work -- [ ] **Diagnosable** — logging at the right levels to debug a device problem from logs alone -- [ ] **Rich parameter metadata** — correct `visibility` per parameter and a sensible `main_parameter`, so the app presents a clean control-center action and a tidy parameter list ([Parameter UX](https://docs.majordom.io/device-integration/parameter-ux)) -- [ ] **Owned** — a listed maintainer who keeps it working as the protocol/library evolve -- [ ] _nice-to-have:_ localizable naming · firmware/software updates (where supported) · user-facing supported-device docs +- [ ] **Recovers automatically** from connection loss / offline device / restarted backend — no + reconnect/backoff around `Client.connect()`/`listen()` yet +- [ ] **No exception escapes the controller** — event handlers (`_node_added`, `_value_updated`, + `_set_availability`, `_grant_security_classes`) run via fire-and-forget tasks with no + individual `try`/`except`; an exception there surfaces only as an unretrieved task exception +- [ ] **Failures are surfaced, not raised** — done in a couple of places (reconnect-missing, + pending exclusion) but not consistently across all paths +- [ ] **Re-authenticates automatically** — not applicable to Z-Wave (no expiring credential concept) +- [x] **Fully asynchronous** — no blocking I/O found on the event loop +- [x] **Stable identity** — device/parameter UUIDs derived via `ZwaveMapper`'s SDK-backed helpers +- [x] **End-to-end tests** drive pair → command → fetch → events → `unpair` against a virtual device +- [ ] **Failure paths tested** — offline device / transport error / rejected credentials not covered yet +- [ ] **Broad device coverage** — only a single virtual Binary Switch device is exercised in CI +- [ ] **Fully typed** (`ty`) and **clean** (`poe check`) — TODO: confirm with a clean `poe check` run +- [x] **Readable & structured** — conversion logic lives in `ZwaveMapper`, models separated +- [x] **Efficient** — subscription-based (`"value updated"`/`"dead"`/`"alive"`), not polling +- [ ] **Diagnosable** — decent per-area log tags (`[PAIR]`, `[CMD]`, `[JOIN]`, …), but + `send_command`'s exception handler re-raises with `from None`, discarding the original traceback +- [x] **Rich parameter metadata** — role/visibility/`main_parameter` resolved per value via `ZwaveMapper` +- [ ] **Owned** — TODO: add a listed maintainer +- [ ] _nice-to-have:_ localizable naming · firmware/software updates · user-facing supported-device docs ### Notes - - -_e.g. "Hobby project, maintained best-effort." · "IP transport works; BLE pairing is not -implemented yet." · "Help wanted: reliable re-pair after a bridge reboot." · known quirks, -firmware versions tested against, limitations._ +- Z-Wave discovery doesn't use `zeroconf`/`ssdp`/`ble` — joined/reconciled nodes are surfaced + directly from `zwave-js-server`'s own `"node added"` event. +- On startup, devices already known to the Hub are matched against the live node list and marked + available/subscribed directly, without a corresponding `controller_did_connect_device` call — + worth revisiting if the Hub relies on that call to refresh its own state after a restart. +- `last_error` is set explicitly when a paired device isn't found on the network at startup and + while an exclusion is pending, but isn't cleared/set on ordinary `"dead"`/`"alive"` transitions. ## License -See [LICENSE](LICENSE). Your integration code is yours to license as you choose. For -commercial licensing or partnership inquiries regarding MajorDom, contact us via -[parker-industries.org/partnership](https://parker-industries.org/partnership). \ No newline at end of file +See [LICENSE](LICENSE). For commercial licensing or partnership inquiries regarding MajorDom, +contact us via [parker-industries.org/partnership](https://parker-industries.org/partnership). From 00e4e41525f87381b1459995424b4ce119ec04ce Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:11:33 +0300 Subject: [PATCH 08/28] style: apply ruff auto-fixes --- main.py | 6 +- majordom_zwave/__init__.py | 2 +- majordom_zwave/config.py | 2 +- majordom_zwave/controller.py | 229 ++++++++--- majordom_zwave/mapper.py | 37 +- majordom_zwave/model.py | 9 +- majordom_zwave/zwave_spec.py | 120 +++--- poetry.lock | 694 ++++++++++++++++++++------------- pyproject.toml | 36 +- tests/conftest.py | 24 +- tests/test_controller.py | 59 +-- tests/test_smoke.py | 2 +- tests/virtual_zwave_network.py | 158 +++++--- 13 files changed, 910 insertions(+), 468 deletions(-) diff --git a/main.py b/main.py index 6c42d7f..c288b9e 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,9 @@ import asyncio +import logging + from majordom_integration_sdk.dev import run_controller + from majordom_zwave import ZwaveController -import logging logging.getLogger("majordom_zwave").setLevel(logging.DEBUG) -asyncio.run(run_controller(ZwaveController)) \ No newline at end of file +asyncio.run(run_controller(ZwaveController)) diff --git a/majordom_zwave/__init__.py b/majordom_zwave/__init__.py index bab4a91..b439343 100644 --- a/majordom_zwave/__init__.py +++ b/majordom_zwave/__init__.py @@ -6,4 +6,4 @@ from .controller import ZwaveController -__all__ = ["ZWaveController"] \ No newline at end of file +__all__ = ["ZWaveController"] diff --git a/majordom_zwave/config.py b/majordom_zwave/config.py index 73cdb09..b0993d3 100644 --- a/majordom_zwave/config.py +++ b/majordom_zwave/config.py @@ -1,3 +1,3 @@ import os -zwave_server_url = os.getenv("ZWAVE-SERVER-URL", "ws://localhost:3000") \ No newline at end of file +zwave_server_url = os.getenv("ZWAVE-SERVER-URL", "ws://localhost:3000") diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 2133b47..900998d 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -4,34 +4,44 @@ from uuid import UUID from aiohttp import ClientSession -from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass, InclusionStrategy, NodeStatus, SecurityClass -from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant -from zwave_js_server.model.node import Node -from zwave_js_server.model.value import Value -from zwave_js_server.model.utils import async_parse_qr_code_string from majordom_integration_sdk.controller import AbstractController -from majordom_integration_sdk.schemas import DeviceCommand, Discovery, ProvidedCredentials +from majordom_integration_sdk.schemas import ( + DeviceCommand, + Discovery, + ProvidedCredentials, +) from majordom_integration_sdk.schemas.device import CredentialsType, NonEmptyStr from majordom_integration_sdk.schemas.event import DeviceParameterChange from majordom_integration_sdk.schemas.parameter import ParameterUnit +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import ( + CommandClass, + InclusionStrategy, + NodeStatus, + SecurityClass, +) +from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant +from zwave_js_server.model.node import Node +from zwave_js_server.model.utils import async_parse_qr_code_string +from zwave_js_server.model.value import Value from .config import zwave_server_url +from .exceptions import ZwaveConnectionError, ZwaveUnexpectedError +from .mapper import ZwaveMapper from .model import ( ZwaveDevice, - ZwaveDeviceState, ZwaveDeviceIntegrationData, + ZwaveDeviceState, ZwaveParameter, ZwaveParameterIntegrationData, ZwaveParameterState, ) -from .exceptions import ZwaveConnectionError, ZwaveUnexpectedError -from .mapper import ZwaveMapper from .zwave_spec import IDENTIFY_INDICATOR_ID log = logging.getLogger(__name__) -#TODO: заполнить ридми под интеграцию по примерам меттера и зигби +# TODO: заполнить ридми под интеграцию по примерам меттера и зигби + class ZwaveController(AbstractController): """Bridges the Hub to Z-Wave devices through zwave-js-server.""" @@ -90,10 +100,20 @@ async def start(self): await ready.wait() controller = self._zwave_client.driver.controller - controller.on("node added", lambda data: self._create_task(self._node_added(data["node"]))) - controller.on("node removed", lambda data: self._create_task(self._node_removed(data["node"]))) + controller.on( + "node added", lambda data: self._create_task(self._node_added(data["node"])) + ) + controller.on( + "node removed", + lambda data: self._create_task(self._node_removed(data["node"])), + ) # S2 auto-grant only — no PIN prompt / no discovery raised for it. - controller.on("grant security classes", lambda data: self._create_task(self._grant_security_classes(data["requested"]))) + controller.on( + "grant security classes", + lambda data: self._create_task( + self._grant_security_classes(data["requested"]) + ), + ) log.debug("[READY] connected to %s", zwave_server_url) @@ -104,7 +124,10 @@ async def start(self): try: device = ZwaveDevice.model_validate(raw_device.model_dump()) except Exception: - log.debug("[SKIP] device_id=%s not paired yet, skipping", raw_device.id) + log.debug( + "[SKIP] device_id=%s not paired yet, skipping", + raw_device.id, + ) continue node = controller.nodes.get(device.integration_data.node_id) @@ -112,7 +135,11 @@ async def start(self): device.available = False device.last_error = f"Device {device.name} is no longer connected to the Z-Wave network" await device_repository.save(device, device.id) - log.debug("[MISSING] device_id=%s node_id=%s not on network", device.id, device.integration_data.node_id) + log.debug( + "[MISSING] device_id=%s node_id=%s not on network", + device.id, + device.integration_data.node_id, + ) continue known_node_ids.add(node.node_id) self._connected_devices[device.id] = node @@ -121,9 +148,16 @@ async def start(self): log.debug("[KNOWN] node_id=%s", node.node_id) # Nodes already on the network but not ours yet (Hub restart, pre-existing node). - log.debug("[RECONCILE] %d node(s) on network, own_node_id=%s", len(controller.nodes), controller.own_node_id) + log.debug( + "[RECONCILE] %d node(s) on network, own_node_id=%s", + len(controller.nodes), + controller.own_node_id, + ) for node in controller.nodes.values(): - if node.node_id == controller.own_node_id or node.node_id in known_node_ids: + if ( + node.node_id == controller.own_node_id + or node.node_id in known_node_ids + ): continue await self._node_added(node) except Exception: @@ -148,25 +182,38 @@ async def stop(self): # Hub -> device operations # ------------------------------------------------------------------------- - async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCredentials | None = None) -> None: + async def start_pairing_window( + self, duration_sec: int, credentials: ProvidedCredentials | None = None + ) -> None: self._require_client() controller = self._zwave_client.driver.controller if credentials and credentials.type is CredentialsType.qr: if not credentials.value: raise ZwaveUnexpectedError("QR credentials provided without QR data") - provisioning_info = await async_parse_qr_code_string(self._zwave_client, credentials.value) - await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, provisioning=provisioning_info) + provisioning_info = await async_parse_qr_code_string( + self._zwave_client, credentials.value + ) + await controller.async_begin_inclusion( + InclusionStrategy.SECURITY_S2, provisioning=provisioning_info + ) log.debug("[PAIRING] inclusion window opened for %ds via QR", duration_sec) elif credentials and credentials.type is CredentialsType.code: if not credentials.value: raise ZwaveUnexpectedError("PIN credentials provided without a value") - await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, dsk=credentials.value) - log.debug("[PAIRING] inclusion window opened for %ds with pre-supplied PIN", duration_sec) + await controller.async_begin_inclusion( + InclusionStrategy.SECURITY_S2, dsk=credentials.value + ) + log.debug( + "[PAIRING] inclusion window opened for %ds with pre-supplied PIN", + duration_sec, + ) elif credentials and credentials.type is CredentialsType.secret: - raise ZwaveUnexpectedError("Z-Wave does not support secret-based pairing credentials") + raise ZwaveUnexpectedError( + "Z-Wave does not support secret-based pairing credentials" + ) else: await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) @@ -174,13 +221,17 @@ async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCre self._create_task(self._close_pairing_window(duration_sec)) - async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: + async def pair_device( + self, discovery: Discovery, credentials: ProvidedCredentials | None + ) -> UUID: """Waits for the pending node's interview to finish, then builds and saves the device.""" self._require_client() node = self._awaiting_zw_discoveries.pop(discovery.id, None) if node is None: - raise ZwaveUnexpectedError(f"No pending Z-Wave discovery for {discovery.id}") + raise ZwaveUnexpectedError( + f"No pending Z-Wave discovery for {discovery.id}" + ) self._majordom_discoveries.pop(discovery.id, None) await self._wait_until_ready(node) @@ -196,15 +247,21 @@ async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentia if device.integration_data: device.integration_data.node_id = node.node_id else: - device.integration_data = ZwaveDeviceIntegrationData(node_id=node.node_id) + device.integration_data = ZwaveDeviceIntegrationData( + node_id=node.node_id + ) parameters = self._build_parameters(device_id, node) device.parameters = parameters - main_parameter_id, default_value = self._mapper.get_main_parameter(device_id, node) + main_parameter_id, default_value = self._mapper.get_main_parameter( + device_id, node + ) device.main_parameter = main_parameter_id if main_parameter_id and default_value is not None: - main_parameter = next((p for p in parameters if p.id == main_parameter_id), None) + main_parameter = next( + (p for p in parameters if p.id == main_parameter_id), None + ) if main_parameter is None: device.main_parameter = None else: @@ -254,7 +311,7 @@ def _on_node_removed(data: dict) -> None: await controller.async_begin_exclusion() try: await asyncio.wait_for(removed.wait(), timeout=60) - except asyncio.TimeoutError: + except TimeoutError: raise ZwaveUnexpectedError( f"Node {zwave_node.node_id} was not excluded in time — make sure the " f"device was put into exclusion mode" @@ -289,7 +346,8 @@ async def identify(self, device: ZwaveDevice): ( v for v in zwave_node.values.values() - if v.command_class in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) + if v.command_class + in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) and v.property_ == "targetValue" ), None, @@ -302,9 +360,15 @@ async def identify(self, device: ZwaveDevice): current_value = zwave_node.values.get( switch_value.value_id.replace("targetValue", "currentValue") ) - restore_value = current_value.value if current_value and current_value.value is not None else 0 + restore_value = ( + current_value.value + if current_value and current_value.value is not None + else 0 + ) - on_value = 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 + on_value = ( + 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 + ) for _ in range(10): await zwave_node.async_set_value(switch_value, 0) await asyncio.sleep(0.6) @@ -320,34 +384,54 @@ async def fetch(self, device: ZwaveDevice): zwave_node = self._require_node(device) parameters: list[DeviceParameterChange] = [] for value_id, value in zwave_node.values.items(): - parameters.append(DeviceParameterChange( - device_id=device.id, - parameter_id=self._mapper.parameter_uuid(device.id, value_id), - value=self._mapper.format_zwave_value(value), - )) + parameters.append( + DeviceParameterChange( + device_id=device.id, + parameter_id=self._mapper.parameter_uuid(device.id, value_id), + value=self._mapper.format_zwave_value(value), + ) + ) await self.dependencies.output.controller_did_receive_events(self, parameters) - async def send_command(self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter): + async def send_command( + self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter + ): self._require_client() zwave_node = self._require_node(device) - zwave_value = self._require_value(parameter.integration_data.value_id, zwave_node) + zwave_value = self._require_value( + parameter.integration_data.value_id, zwave_node + ) if zwave_value.metadata.writeable is False: - raise ZwaveUnexpectedError(f"Value {parameter.integration_data.value_id} is not writeable") + raise ZwaveUnexpectedError( + f"Value {parameter.integration_data.value_id} is not writeable" + ) try: awake = zwave_node.status != NodeStatus.ASLEEP - result = await zwave_node.async_set_value(zwave_value, command.value, wait_for_result=awake) + result = await zwave_node.async_set_value( + zwave_value, command.value, wait_for_result=awake + ) log.info(result) except Exception as e: raise ZwaveUnexpectedError( f"Failed to set value {parameter.integration_data.value_id} on node {zwave_node.node_id}: {e}" ) from None if awake and result is not None and not result.status: - log.warning("[CMD] node_id=%s value_id=%s result=%s", zwave_node.node_id, parameter.integration_data.value_id, result) + log.warning( + "[CMD] node_id=%s value_id=%s result=%s", + zwave_node.node_id, + parameter.integration_data.value_id, + result, + ) await self.dependencies.output.controller_did_receive_events( - self, [DeviceParameterChange(device_id=device.id, parameter_id=parameter.id, value=command.value)] + self, + [ + DeviceParameterChange( + device_id=device.id, parameter_id=parameter.id, value=command.value + ) + ], ) # ========================================================================= @@ -370,7 +454,9 @@ def _require_client(self) -> ZwaveClient: raise ZwaveConnectionError("Zwave client is not started") def _require_node(self, device: ZwaveDevice) -> Node: - node = self._zwave_client.driver.controller.nodes.get(device.integration_data.node_id) + node = self._zwave_client.driver.controller.nodes.get( + device.integration_data.node_id + ) if not node: raise ZwaveUnexpectedError(f"Node for device {device.id} not found") return node @@ -378,7 +464,9 @@ def _require_node(self, device: ZwaveDevice) -> Node: def _require_value(self, value_id: str, node: Node) -> Value: value = node.values.get(value_id) if not value: - raise ZwaveUnexpectedError(f"Value with {value_id} id for node {node.node_id} not found") + raise ZwaveUnexpectedError( + f"Value with {value_id} id for node {node.node_id} not found" + ) return value # ------------------------------------------------------------------------- @@ -413,19 +501,27 @@ async def _wait_until_ready(self, node: Node) -> None: unsubscribe = node.on("ready", lambda data: became_ready.set()) try: await asyncio.wait_for(became_ready.wait(), timeout=120) - except asyncio.TimeoutError: - log.warning("[PAIR] node_id=%s not ready after 120s, pairing with partial values", node.node_id) + except TimeoutError: + log.warning( + "[PAIR] node_id=%s not ready after 120s, pairing with partial values", + node.node_id, + ) finally: unsubscribe() - def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameterState]: + def _build_parameters( + self, device_id: UUID, node: Node + ) -> list[ZwaveParameterState]: """Maps every Z-Wave value on a node into a majordom parameter state.""" parameters: list[ZwaveParameterState] = [] for value_id, value in node.values.items(): metadata = value.metadata parameter = ZwaveParameterState( id=self._mapper.parameter_uuid(device_id, value_id), - name=metadata.label or value.property_name or value.property_key_name or str(value.property_), + name=metadata.label + or value.property_name + or value.property_key_name + or str(value.property_), data_type=self._mapper.parse_zwave_data_type(value), role=self._mapper.get_role(value.command_class, metadata), visibility=self._mapper.get_visibility(value.command_class, metadata), @@ -476,7 +572,9 @@ async def _node_added(self, node: Node): expiration=None, transport=NonEmptyStr("ZWAVE"), device_manufacturer=None, - device_name=NonEmptyStr(node.name or node.device_config.description or "Unknown"), + device_name=NonEmptyStr( + node.name or node.device_config.description or "Unknown" + ), device_category=None, device_icon=None, ) @@ -498,16 +596,33 @@ async def _set_availability(self, device_id: UUID, available: bool) -> None: return self._availability[device_id] = available if available: - await self.dependencies.output.controller_did_connect_device(self, device_id) + await self.dependencies.output.controller_did_connect_device( + self, device_id + ) else: await self.dependencies.output.controller_did_lose_device(self, device_id) def _subscribe(self, device_id: UUID, node: Node) -> None: - node.on("value updated", lambda data: self._create_task(self._value_updated(device_id, data["value"]))) - node.on("dead", lambda data: self._create_task(self._set_availability(device_id, False))) - node.on("alive", lambda data: self._create_task(self._set_availability(device_id, True))) + node.on( + "value updated", + lambda data: self._create_task( + self._value_updated(device_id, data["value"]) + ), + ) + node.on( + "dead", + lambda data: self._create_task(self._set_availability(device_id, False)), + ) + node.on( + "alive", + lambda data: self._create_task(self._set_availability(device_id, True)), + ) async def _value_updated(self, device_id: UUID, value: Value) -> None: parameter_id = self._mapper.parameter_uuid(device_id, value.value_id) - event = DeviceParameterChange(device_id=device_id, parameter_id=parameter_id, value=self._mapper.format_zwave_value(value)) - await self.dependencies.output.controller_did_receive_events(self, [event]) \ No newline at end of file + event = DeviceParameterChange( + device_id=device_id, + parameter_id=parameter_id, + value=self._mapper.format_zwave_value(value), + ) + await self.dependencies.output.controller_did_receive_events(self, [event]) diff --git a/majordom_zwave/mapper.py b/majordom_zwave/mapper.py index a929476..5608de7 100644 --- a/majordom_zwave/mapper.py +++ b/majordom_zwave/mapper.py @@ -1,10 +1,20 @@ from typing import Any -from uuid import UUID, NAMESPACE_DNS, uuid5 +from uuid import NAMESPACE_DNS, UUID, uuid5 -from majordom_integration_sdk.schemas.parameter import ParameterDataType, ParameterRole, ParameterUnit, ParameterVisibility +from majordom_integration_sdk.schemas.parameter import ( + ParameterDataType, + ParameterRole, + ParameterUnit, + ParameterVisibility, +) from zwave_js_server.model.duration import Duration from zwave_js_server.model.node import Node -from zwave_js_server.model.value import AllowedRangeValue, Value, ValueMetadata, ValueType +from zwave_js_server.model.value import ( + AllowedRangeValue, + Value, + ValueMetadata, + ValueType, +) from .zwave_spec import ( DIAGNOSTIC_COMMAND_CLASSES, @@ -59,7 +69,9 @@ def get_role(self, command_class: int, metadata: ValueMetadata) -> ParameterRole return ParameterRole.control return ParameterRole.sensor - def get_visibility(self, command_class: int, metadata: ValueMetadata) -> ParameterVisibility: + def get_visibility( + self, command_class: int, metadata: ValueMetadata + ) -> ParameterVisibility: """Readable -> user, writeable-only -> setting, else system; SYSTEM/DIAGNOSTIC command classes override this for plumbing/diagnostic values.""" if command_class in SYSTEM_COMMAND_CLASSES: @@ -104,7 +116,9 @@ def parse_zwave_data_type(self, value: Value) -> ParameterDataType: # Opaque CC-specific object — expose as data rather than guessing a shape. return ParameterDataType.data - def parse_zwave_valid_values(self, metadata: ValueMetadata) -> dict[int | float | str, str] | None: + def parse_zwave_valid_values( + self, metadata: ValueMetadata + ) -> dict[int | float | str, str] | None: """Converts states (`{"0": "Off"}`) to valid_values, coercing keys to int.""" if not metadata.states: return None @@ -116,7 +130,9 @@ def parse_zwave_valid_values(self, metadata: ValueMetadata) -> dict[int | float result[raw_key] = label return result - def get_main_parameter(self, device_id: UUID, node: Node) -> tuple[UUID | None, bool | int | float | None]: + def get_main_parameter( + self, device_id: UUID, node: Node + ) -> tuple[UUID | None, bool | int | float | None]: """Picks the value for the device's one-tap action, by MAIN_VALUE_BY_COMMAND_CLASS priority. Returns (None, None) if none applies.""" for command_class, spec in MAIN_VALUE_BY_COMMAND_CLASS.items(): @@ -124,10 +140,13 @@ def get_main_parameter(self, device_id: UUID, node: Node) -> tuple[UUID | None, ( v for v in node.values.values() - if v.command_class == command_class and v.property_name == spec.property_name + if v.command_class == command_class + and v.property_name == spec.property_name ), None, ) if value is not None: - return self.parameter_uuid(device_id, value.value_id), spec.default_value - return None, None \ No newline at end of file + return self.parameter_uuid( + device_id, value.value_id + ), spec.default_value + return None, None diff --git a/majordom_zwave/model.py b/majordom_zwave/model.py index 05459da..f7d7bb3 100644 --- a/majordom_zwave/model.py +++ b/majordom_zwave/model.py @@ -1,5 +1,10 @@ +from majordom_integration_sdk.schemas import ( + Device, + DeviceState, + Parameter, + ParameterState, +) from pydantic import BaseModel -from majordom_integration_sdk.schemas import Device, DeviceState, Parameter, ParameterState class ZwaveDeviceIntegrationData(BaseModel): @@ -30,4 +35,4 @@ class ZwaveParameterState(ParameterState): class ZwaveDeviceState(ZwaveDevice, DeviceState): - parameters: list[ZwaveParameterState] \ No newline at end of file + parameters: list[ZwaveParameterState] diff --git a/majordom_zwave/zwave_spec.py b/majordom_zwave/zwave_spec.py index f635d02..d405b3f 100644 --- a/majordom_zwave/zwave_spec.py +++ b/majordom_zwave/zwave_spec.py @@ -21,69 +21,75 @@ # Protocol/network plumbing: never shown to the user, regardless of read/write access. # (association & multi channel wiring, security/transport encapsulation, inclusion, S2, etc.) -SYSTEM_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ - CommandClass.NO_OPERATION, - CommandClass.CONTROLLER_REPLICATION, - CommandClass.APPLICATION_STATUS, - CommandClass.ZIP, - CommandClass.NETWORK_MANAGEMENT_INCLUSION, - CommandClass.NETWORK_MANAGEMENT_BASIC, - CommandClass.NETWORK_MANAGEMENT_PROXY, - CommandClass.NETWORK_MANAGEMENT_PRIMARY, - CommandClass.NETWORK_MANAGEMENT_INSTALLATION_MAINTENANCE, - CommandClass.TRANSPORT_SERVICE, - CommandClass.CRC_16_ENCAP, - CommandClass.APPLICATION_CAPABILITY, - CommandClass.ASSOCIATION, - CommandClass.ASSOCIATION_GRP_INFO, - CommandClass.MULTI_CHANNEL, - CommandClass.MULTI_CHANNEL_ASSOCIATION, - CommandClass.MULTI_CMD, - CommandClass.SUPERVISION, - CommandClass.ZWAVEPLUS_INFO, - CommandClass.INCLUSION_CONTROLLER, - CommandClass.NODE_NAMING, - CommandClass.NODE_PROVISIONING, - CommandClass.MANUFACTURER_PROPRIETARY, - CommandClass.PROPRIETARY, - CommandClass.SECURITY, - CommandClass.SECURITY_2, - CommandClass.SECURITY_SCHEME0_MARK, - CommandClass.MARK, - CommandClass.TIME, - CommandClass.TIME_PARAMETERS, - CommandClass.CLOCK, - CommandClass.GEOGRAPHIC_LOCATION, - CommandClass.FIRMWARE_UPDATE_MD, - CommandClass.GROUPING_NAME, - CommandClass.REMOTE_ASSOCIATION_ACTIVATE, - CommandClass.REMOTE_ASSOCIATION, - CommandClass.SCREEN_MD, - CommandClass.SCREEN_ATTRIBUTES, - CommandClass.DEVICE_RESET_LOCALLY, -}) +SYSTEM_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( + { + CommandClass.NO_OPERATION, + CommandClass.CONTROLLER_REPLICATION, + CommandClass.APPLICATION_STATUS, + CommandClass.ZIP, + CommandClass.NETWORK_MANAGEMENT_INCLUSION, + CommandClass.NETWORK_MANAGEMENT_BASIC, + CommandClass.NETWORK_MANAGEMENT_PROXY, + CommandClass.NETWORK_MANAGEMENT_PRIMARY, + CommandClass.NETWORK_MANAGEMENT_INSTALLATION_MAINTENANCE, + CommandClass.TRANSPORT_SERVICE, + CommandClass.CRC_16_ENCAP, + CommandClass.APPLICATION_CAPABILITY, + CommandClass.ASSOCIATION, + CommandClass.ASSOCIATION_GRP_INFO, + CommandClass.MULTI_CHANNEL, + CommandClass.MULTI_CHANNEL_ASSOCIATION, + CommandClass.MULTI_CMD, + CommandClass.SUPERVISION, + CommandClass.ZWAVEPLUS_INFO, + CommandClass.INCLUSION_CONTROLLER, + CommandClass.NODE_NAMING, + CommandClass.NODE_PROVISIONING, + CommandClass.MANUFACTURER_PROPRIETARY, + CommandClass.PROPRIETARY, + CommandClass.SECURITY, + CommandClass.SECURITY_2, + CommandClass.SECURITY_SCHEME0_MARK, + CommandClass.MARK, + CommandClass.TIME, + CommandClass.TIME_PARAMETERS, + CommandClass.CLOCK, + CommandClass.GEOGRAPHIC_LOCATION, + CommandClass.FIRMWARE_UPDATE_MD, + CommandClass.GROUPING_NAME, + CommandClass.REMOTE_ASSOCIATION_ACTIVATE, + CommandClass.REMOTE_ASSOCIATION, + CommandClass.SCREEN_MD, + CommandClass.SCREEN_ATTRIBUTES, + CommandClass.DEVICE_RESET_LOCALLY, + } +) # Technically readable/reportable, but the "settings/advanced" kind of reading (battery %, # firmware version, RSSI-ish diagnostics, wake-up interval) rather than a main everyday control. # ParameterVisibility.setting explicitly covers this case per its docstring. -DIAGNOSTIC_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ - CommandClass.BATTERY, - CommandClass.VERSION, - CommandClass.MANUFACTURER_SPECIFIC, - CommandClass.POWERLEVEL, - CommandClass.WAKE_UP, - CommandClass.INDICATOR, - CommandClass.PROTECTION, - CommandClass.CONFIGURATION, # device settings behind an "advanced" tap, not main interaction -}) +DIAGNOSTIC_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( + { + CommandClass.BATTERY, + CommandClass.VERSION, + CommandClass.MANUFACTURER_SPECIFIC, + CommandClass.POWERLEVEL, + CommandClass.WAKE_UP, + CommandClass.INDICATOR, + CommandClass.PROTECTION, + CommandClass.CONFIGURATION, # device settings behind an "advanced" tap, not main interaction + } +) # CCs that report one-shot occurrences rather than a persistent state (button presses, # scene activations, alarms) — modeled as ParameterRole.event regardless of read/write flags. -EVENT_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({ - CommandClass.CENTRAL_SCENE, - CommandClass.SCENE_ACTIVATION, - CommandClass.NOTIFICATION, -}) +EVENT_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( + { + CommandClass.CENTRAL_SCENE, + CommandClass.SCENE_ACTIVATION, + CommandClass.NOTIFICATION, + } +) # zwave-js's ValueMetadata.unit is a freeform string straight from the device's CC report, not a # controlled vocabulary — map the ones we recognize, leave the rest unmapped rather than guessed. @@ -145,4 +151,4 @@ class MainValueSpec: CommandClass.BARRIER_OPERATOR: MainValueSpec("targetState", 255), # DoorLockMode.SECURED = 255 — lock, not unlock, is the safer default one-tap action. CommandClass.DOOR_LOCK: MainValueSpec("targetMode", 255), -} \ No newline at end of file +} diff --git a/poetry.lock b/poetry.lock index 69a5f54..efbfed8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -439,18 +439,18 @@ files = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, + {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, + {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, ] [package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] [[package]] name = "ifaddr" @@ -655,14 +655,26 @@ files = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] [[package]] @@ -681,6 +693,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "poethepoet" +version = "0.48.0" +description = "A task runner that works well with poetry and uv." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "poethepoet-0.48.0-py3-none-any.whl", hash = "sha256:98da6096d060f49b8d84034770265863fb7dc92a40233b7694b9d216ac68737d"}, + {file = "poethepoet-0.48.0.tar.gz", hash = "sha256:a06f49d244fadfc2e2e7faa78b54e64a9694727e4ce1d50e08f23cea3ded74f1"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.3,<7.0" + +[package.extras] +poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] + [[package]] name = "propcache" version = "0.5.2" @@ -814,19 +845,19 @@ files = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, - {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.46.4" +pydantic-core = "2.46.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -836,132 +867,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, - {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, ] [package.dependencies] @@ -969,14 +1000,14 @@ typing-extensions = ">=4.14.1" [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] @@ -984,98 +1015,98 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyobjc-core" -version = "12.2.1" +version = "12.2.2" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.10" groups = ["main"] markers = "sys_platform == \"darwin\"" files = [ - {file = "pyobjc_core-12.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa5c889961d79d7704f17eeb27f80ee791335819c1bb14babeff7b9ea665b5f0"}, - {file = "pyobjc_core-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24b76a63caf0b5369d4a377c7c0438cd70df81539057af3db839bfaa3579e04a"}, - {file = "pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b"}, - {file = "pyobjc_core-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af101222762665a4125157906cb4b23f5d5a63d3851d5e0504f72a1eaaa2cfd2"}, - {file = "pyobjc_core-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:efe465e3ecc6fc73f7c7622620345d134a8d34564ab1c29d8247e45f4ed55071"}, - {file = "pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a"}, - {file = "pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7"}, - {file = "pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e"}, - {file = "pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb"}, - {file = "pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07"}, + {file = "pyobjc_core-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:56c6c39f1de059fcbb174ebca5525505fc8feaa89be2a28c329bf09b6b25ee75"}, + {file = "pyobjc_core-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9cdd686e32db8e451feb19f8a85bc4cd52c2893103881d04aca51e1f35371d1"}, + {file = "pyobjc_core-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:122e6ad302a2abf5d4d4adb0156db751600ddf2768441696cba17b31323085e7"}, + {file = "pyobjc_core-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950bd2d9c74634398c4e3d24ef2f213d4e23d705083697464fa67afedc53c1ad"}, + {file = "pyobjc_core-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3772b406edb3ff78171530a17cda1c4a7817f87b87ded0d8715b3fa664df16db"}, + {file = "pyobjc_core-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2062e8ad30a310441cd022544a897553408bebeaa7820d5edba3c96fd7fd693b"}, + {file = "pyobjc_core-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2c7ef3d2f865b4b3ebb14ec3556f7a3e8abb6d130c67275cd9daa08dbd6e4e4e"}, + {file = "pyobjc_core-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89acc6bc13aaa6e3f52b0ce652ede7e201edb6bf062741b246b0c5a44582f25f"}, + {file = "pyobjc_core-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59a77038ebe0ab1240f61c341e7fb67b8674f2b4cd41bc71a6472511a12b50f7"}, + {file = "pyobjc_core-12.2.2.tar.gz", hash = "sha256:3906452339cd06a3bb07df103c2511d4cb0f7a22d8771c0b802eba15d9a642b6"}, ] [[package]] name = "pyobjc-framework-cocoa" -version = "12.2.1" +version = "12.2.2" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "sys_platform == \"darwin\"" files = [ - {file = "pyobjc_framework_cocoa-12.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05443c1494532779cccff83e9792fdd6f0a4800cac56620132bf28e0bf966e9d"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dc6da84f4fc62cc25463bbb85e77a57b8d5ac6caf9a60702daf2edb601332f15"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3b74a78fa7803e547b32e5e8ec1b49987b52fe318383e793bc6cd49b80efbd9f"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dc2eaca2f13c7bcd8e41e51a372e47825dea9dd3126108760eed7ba883d2945c"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103"}, - {file = "pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1"}, - {file = "pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5a751c8033a3b51f7996f0327e0675eb44dcfdfe7920fae01e3d78b662723fff"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:851dca4c16e70b405e5cd5a8c166cf7c445ae54a4cdd95ce9a523803172f32d1"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e106f395531e67694376b0f1184612cbeea3ec8b9bf56b55ef41d026171d2a2d"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:600b1723184ca094931330e79355274949965460e23de38628d601b5a967baf9"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:875f2aad73963faa81a6b36ae674fd494a4658d6d999e1075e0e2aca3d2391df"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:889d7bbd4ba2d4941078bfbbfb882138e51dbead27df006abfe0f2e0d49b5b2e"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de69c5933750f3a4599ed962eccd92b6a71914c7e4318dacc7895738a8ae60d7"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0e8ace0d44a00d281281a723d17fcd05eea7544a38a6a512e1fd018ddb7aece2"}, + {file = "pyobjc_framework_cocoa-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8fe5b2e79c9530f667b4e58a87a3a15ea62f86a5d19eec405517ecbd4f454868"}, + {file = "pyobjc_framework_cocoa-12.2.2.tar.gz", hash = "sha256:c96c0ef69a71afbbb0e6a7d594b455c5fe47d62e0db376ee7a2b4b828c16ace9"}, ] [package.dependencies] -pyobjc-core = ">=12.2.1" +pyobjc-core = ">=12.2.2" [[package]] name = "pyobjc-framework-corebluetooth" -version = "12.2.1" +version = "12.2.2" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "sys_platform == \"darwin\"" files = [ - {file = "pyobjc_framework_corebluetooth-12.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dd9c9b4390e7316d189ba32d624ce933f7b25d351268b6e8cbea44f65e55f5"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f11df7052fa2d0524a9dadbc9c578fd3b8f3a350431edfa4ffa9e0a74b6faa32"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2f8d2ed65c0e98ba044b6a7fc2f9a290a5c579a28d33a57c642f8617ccbcca0f"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:30a26eef36c250fc14e73335641e24f764b32b7e42bae945a5d8a1c2347040b5"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:756933b9ce6160a986c8877ab667659fa2d8e15aff28d0981b5284fbdd1ea735"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2a2e6d56f51e4ca3e3b9766ef34150a9a7ce5f0cf4f9ee879ec10923af58e97e"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50d7e4245dbdc8789dcc1f11fca2e633aa126a298b09db62f8216531fe107ee2"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c8d126c56b71c25218be186930a1b41739f83e931726a52e3298beeb170c5e5b"}, - {file = "pyobjc_framework_corebluetooth-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:81023518feb75e9b2b676b28198955c51ae00548cf23c73c524c7101263b68db"}, - {file = "pyobjc_framework_corebluetooth-12.2.1.tar.gz", hash = "sha256:7dbb285295097205bebbcb11f55161e5faa02111108fb7b17536176e31971eb0"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8a40fbce765b14ba49126156b33b6a07bc60b0fe93b7c321268e8c5c52112e1"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6820800a38777b1f20bfabb96f724ec71a26683c378640a115eccfa2b52eea13"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78bb5ac453f4bebec59b2c918c8fb71fe054ab8c69d88cd21d6c231cc9a39593"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0c483a90fb7ebf269af5f235fed82e437d8ff892c133f064af25623bf43efe16"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ff95d28591e1b83b4c3e835279194ca03cb6fa6e0b7fa224c143311cc71ed66b"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c8d748b3ece787c750cdfc0ab820bd0316a084015453ca48b7885fcf56d3f782"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:289ea13ba2b7746235ef47e2d7e7606752d08384022197ccc0e6943b91d4c62d"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e8a964e4d32335fd77dd6e9ec0e510208884988c6d63360f80c699405f0beef2"}, + {file = "pyobjc_framework_corebluetooth-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:61f4ac689b3a505218b470866931e8ce67e59b70f0877085f9332b169b96d991"}, + {file = "pyobjc_framework_corebluetooth-12.2.2.tar.gz", hash = "sha256:75aa13f5355be549252d3192864bb3b82eb74c1f667d0527c1388117efffd688"}, ] [package.dependencies] -pyobjc-core = ">=12.2.1" -pyobjc-framework-Cocoa = ">=12.2.1" +pyobjc-core = ">=12.2.2" +pyobjc-framework-Cocoa = ">=12.2.2" [[package]] name = "pyobjc-framework-libdispatch" -version = "12.2.1" +version = "12.2.2" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "sys_platform == \"darwin\"" files = [ - {file = "pyobjc_framework_libdispatch-12.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a47e8c53511eaf739f3972c48e0813bf8fd10dc88dc5543afceaec6554f5c4c2"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e295775c76eace23f60e53ef74b0f79429c665f91a870e4665c4b9887466efa"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8f41b5021ff70bc51220a79b41ebd1eacb55fe3ceb67448594f30e491a2c42a5"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9c0ebf99520083bf17c007a544c100056a0d4ae5c346fb89e1bdfe6d041f16f2"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3f56fd71b963a0b6e440ed2f0ea2fb635221758b7eb908ba38f96f5144b83ca3"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:999bad9a2c9198c837ba8f57a3ca9f05b4fc4bf7b69318baaa266dd2ab2fc8f7"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3fc93971f40d9757995c1e4b995a1614a468a5178be27e3d81e9bdc0b5e3cf75"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:26a096c81c8cf272f4f1bb8f6c4b7565e005d273d218b53b83d925da5292633f"}, - {file = "pyobjc_framework_libdispatch-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:82c6512fb4985f3bcd6b60b0cff79a4b483b44d1d2e5405010e34dd4b60aa01b"}, - {file = "pyobjc_framework_libdispatch-12.2.1.tar.gz", hash = "sha256:0d24eda41c6c258135077f60d410e704bc7b5a67adcb2ca463918896c7363795"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b392d482781893c9f71b22611648d3291d6a2fc85e1ff9f97377210074e4f93c"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e8e5411ed58ed1643f39a486880193cb1b1103df45d64ebd1072a193dce09a22"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57e004479c4a7b697c1cee2c6aad80051bf447beafd1d780cb7427d99cc92b64"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9c62b8c03b9e8c8925fdbb9d5a9541c1e9a4023ec06f1312441edfd166f33398"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b67f7433d2fbc0d5ff2ce4cb81ef08e0d0717fe88c2a95e335512c2cdb52d722"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2488964343f9282e3521ab2f136948a8eaf7438351dc45c39e34ba23b9f5bc6b"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:74cea0ec7803147496c8b7b62e7ad1b9eda525aa692d8856464cec489951aa5d"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:436828e39577ec9a889c59efa1e8a85fa0ba43af5069060a67dfead1df029ea4"}, + {file = "pyobjc_framework_libdispatch-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b64b150207552b67caf1061e013ae6a1989aef34a18f36d9a3a455c51cd210c2"}, + {file = "pyobjc_framework_libdispatch-12.2.2.tar.gz", hash = "sha256:7cb799a7c5766cc1b68b68655a02c950646adfd2a743b7cbe8e4a04a51c44ecb"}, ] [package.dependencies] -pyobjc-core = ">=12.2.1" -pyobjc-framework-Cocoa = ">=12.2.1" +pyobjc-core = ">=12.2.2" +pyobjc-framework-Cocoa = ">=12.2.2" [[package]] name = "pytest" @@ -1137,6 +1168,117 @@ text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "ruff" +version = "0.16.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8"}, + {file = "ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32"}, + {file = "ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718"}, + {file = "ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25"}, + {file = "ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39"}, + {file = "ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5"}, + {file = "ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050"}, +] + [[package]] name = "text-unidecode" version = "1.3" @@ -1149,6 +1291,34 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] +[[package]] +name = "ty" +version = "0.0.78" +description = "An extremely fast Python type checker, written in Rust." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ty-0.0.78-py3-none-linux_armv6l.whl", hash = "sha256:122700b98f9d45785c1ce91a9418154562e7f64f4bf34679f6f7b38c5b97453f"}, + {file = "ty-0.0.78-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbe7e3709ccf29ef3d9f58f5914cc30ec36fb647008a5a4ff8483abe401bfe8d"}, + {file = "ty-0.0.78-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c3528897b3ab9d3589561bc2b5e61a4d5d686de527a4400bb09a439b922a6c70"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bfba4c44a06484093f527b8ef43a317abcf91395b49396170266629eedf74aa"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f903c06fdee17baf8373173039ab28f1bce28e66b1c734929a5f50b37eb54d9"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd83e5fe3f07291d1bd4e591f8d2607c204f3eab53bcb1b8060c40e876e1aa61"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325285377319cf7168a2b8b771ae5027f411530e3c7eab1faf4583cdd72fd7c9"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b7846a404da6492697b524a769755ee9bee157d67674063ecd9c5f69dc52ff"}, + {file = "ty-0.0.78-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:169d3b9d134c0b48fe1af8a844142d374655552054a9d6c15a4b6e51bb0382ea"}, + {file = "ty-0.0.78-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6108cb3b2d28dac5981d4e25008a0a5547d8c877a67f6da9e8c38e7e946be44f"}, + {file = "ty-0.0.78-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:589ad03608d9d2975ef4b23c41c4f847e325bcc7686f51d4c66066b8dbc18a2a"}, + {file = "ty-0.0.78-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b3fa0786edc1af06030f872d83499c0cea7c261dbbb651e0aee8306bf2c8a869"}, + {file = "ty-0.0.78-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92c5639befc577578c8abd4e5a7fccf98d828db98b09e8d4dd81605dc0e54aef"}, + {file = "ty-0.0.78-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0dce70bc51652b2775debd1d1e422fb0179f5207c73deb4d5d0aca37e5f61695"}, + {file = "ty-0.0.78-py3-none-win32.whl", hash = "sha256:1c80976ca9185d7a9d1baab1fb57240331b8dac58d3b11e46200284086a6de8d"}, + {file = "ty-0.0.78-py3-none-win_amd64.whl", hash = "sha256:32e82b704471eab34f67b51c151660ca8a00815977b28278905d76fba54f7415"}, + {file = "ty-0.0.78-py3-none-win_arm64.whl", hash = "sha256:3a14d641a3c04fa9a80f2a46be1531d915f60d4fb79d4b894627bbe46bb35d64"}, + {file = "ty-0.0.78.tar.gz", hash = "sha256:770b45854f85fa11595208f08c0f28df80943164d10a2832d86be6ac29f135b2"}, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -1164,18 +1334,18 @@ markers = {dev = "python_version == \"3.12\""} [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "winrt-runtime" @@ -1619,80 +1789,82 @@ propcache = ">=0.2.1" [[package]] name = "zeroconf" -version = "0.150.0" +version = "0.151.3" description = "A pure python implementation of multicast DNS service discovery" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "zeroconf-0.150.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d5cfe83cfa85bf8f9bc31b1b33c20ae708d9ca76cef3ae92333e13f4147b169"}, - {file = "zeroconf-0.150.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34062fd3cb0f49d7f29fddf1d266c3c72a62bef2d6dd3829db036acb6b63473a"}, - {file = "zeroconf-0.150.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:525fa1476e5cff7cfd0194a2a1e189150d180ae4b071db632693409165d9a282"}, - {file = "zeroconf-0.150.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e66e5eaae928f6f7e21259652c7b8c20601361b1298c49bab5e8eb6e2741ae32"}, - {file = "zeroconf-0.150.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b3d7579f23421087abbcba52a307b7645fdf23da5f382e9d08c984bece0da80"}, - {file = "zeroconf-0.150.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6b730d14ca9d19bbc67df8d29a05a3775a2c06f1bf987e5db20a9766e8cf558"}, - {file = "zeroconf-0.150.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:34cfd84ab4480008874c224905246acf450637ac826f2c211a91932f17c7f23e"}, - {file = "zeroconf-0.150.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e31aaf0e8376acd279712d85f8303331c16dfc004d4117a58e8a5e233e96a1"}, - {file = "zeroconf-0.150.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20a240044db62d51f6cd487226638ad50f3ebac4d06945dae4ac7c1c0008f3b3"}, - {file = "zeroconf-0.150.0-cp310-cp310-win32.whl", hash = "sha256:903c7745e2dd0e9d19d742a034651d397d4e3706e9c21b8140e22a8cd56fcc22"}, - {file = "zeroconf-0.150.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc3c29ed9ec32db0458f1ecfb8dfc42fc608bf3c78f1b8a4d50593f595f5289"}, - {file = "zeroconf-0.150.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad2e3e3062491f56fd16388f1abce6f6185bdc84f221698a38fa9df7b8664c95"}, - {file = "zeroconf-0.150.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e066cc28c7a11703dce80054c5bfc04fae79d18885da9817f64346e842a6a668"}, - {file = "zeroconf-0.150.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f2d2042e73aa04b85781f52d6bf0dba3e2dee83af7b9c64d4af3e0cbc99c2702"}, - {file = "zeroconf-0.150.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cfe30e667e93a1d60761ff04ec9a9d30808a60d33c3b30a4d280418172dbfa3"}, - {file = "zeroconf-0.150.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b19d651cdca9fb5882401a53da8145faa10761593f50d7f3532a7779951a8ba2"}, - {file = "zeroconf-0.150.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e63fe609d574c3da6b55a4b947286d72546f9f79319415a163a8f70bca69350a"}, - {file = "zeroconf-0.150.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:69fe797b459411e269af2ee3f80f078a4e12293b26726de9032dc66595ad1fa5"}, - {file = "zeroconf-0.150.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d86ea29921db3d9d1126f5a47c66b5361699a2ae0c22fa899d7ba23a3fd79d3f"}, - {file = "zeroconf-0.150.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ea447817614b40c745f9772a2713464654fb1cf9a2567cc30d2b137108898e3"}, - {file = "zeroconf-0.150.0-cp311-cp311-win32.whl", hash = "sha256:b302879d78a04ebd6f933ea8eaea1d0ca7e72186cc4a483463ec92bd775fc850"}, - {file = "zeroconf-0.150.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c7d5f7416e48503211f87c438c8617ee0cb6f1b3fd47013aa59db030e85ee9"}, - {file = "zeroconf-0.150.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1aba87b6c1cdb6859d0c2291c67dc7d3ac06612250fddd95f68b7632e1e89a79"}, - {file = "zeroconf-0.150.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17a3adc2e4e425c59a1f2a72e289706ec6655fead4d5287f23e1748663df7dc"}, - {file = "zeroconf-0.150.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00fa39069437e68fe7c32856f7d041f46cd57f4f5a13ca75ae8f1c49e39932be"}, - {file = "zeroconf-0.150.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e50a7b3d5922ac9bb53129c4dbc381b0af2f49067670f35eb9c9b4ad341bde28"}, - {file = "zeroconf-0.150.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b93e7cc90a150e6e0a706251c745f2425782cdbc3a7a8b8a12123b96d167960e"}, - {file = "zeroconf-0.150.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cd46be1653a194ffb583012ffdf284a5042a1dacd7f44b7befc5de1e8491ba8"}, - {file = "zeroconf-0.150.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e4f973c9f284031bcc698f1357a44900254ba7f6488c3b9a18541f0221b3f091"}, - {file = "zeroconf-0.150.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:626d057f5a65b2126a6799e538ad78b329cb366c3a5fb9f69757ee23ee06ce5f"}, - {file = "zeroconf-0.150.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ddb105722ac7dfdd7b6e0880ef52f955e679fd37a5f3247c6050c74259bda96"}, - {file = "zeroconf-0.150.0-cp312-cp312-win32.whl", hash = "sha256:191972a56d69cb1c6dd472d6af9813730c1937e2e6f89c01e5373df9ab878e11"}, - {file = "zeroconf-0.150.0-cp312-cp312-win_amd64.whl", hash = "sha256:e68cb166f6b535c42550a554ed00939256045e18df8b998f8af3a3bd50506e99"}, - {file = "zeroconf-0.150.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f79d9ce9cc40e14d2f7b8e4af9dcc893b7f7793be91d14a61a77ccb1ab5c90b"}, - {file = "zeroconf-0.150.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f9ba6980ad92820f56a02a5035e21338c116145db44749462fc46f8f50028bb"}, - {file = "zeroconf-0.150.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1a67dfae0d99b4f29f34721c7bbb2f3529507c64089addec9a565dc791524688"}, - {file = "zeroconf-0.150.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:489a51320d20e7f07b2f447cd515f3645d17ceced0212f7249a83ce6d3b748e0"}, - {file = "zeroconf-0.150.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc160b8ba6a2420d665fb677c37fd2ab006141344bbf92abea72328bfd38bd3b"}, - {file = "zeroconf-0.150.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33843e0317bff2e03e56e246c3f25aab72ddb0a704f05bd792a4e78bdb5db801"}, - {file = "zeroconf-0.150.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:024b00e64aae022ac6bdd7fe5afdabae570937c10d1242438ca04a4c62dfcd17"}, - {file = "zeroconf-0.150.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2ee2297e383ac21193d84982e714029eafec470926315ab78b27a024858e4e5d"}, - {file = "zeroconf-0.150.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a03d493c3e9c4b4a5aaaf0408da7c025ed014f3c3df639cdd2c6cb518c42b4b"}, - {file = "zeroconf-0.150.0-cp313-cp313-win32.whl", hash = "sha256:dc054a6b7772ec97fe215e8f8f35f43509cc9f1bf909791fe003acbaa78fd9b2"}, - {file = "zeroconf-0.150.0-cp313-cp313-win_amd64.whl", hash = "sha256:122c6bec1f462c0feaa7f8fe4a4bc605f5c0d8f8e82e2ebd5aeae48a24da4564"}, - {file = "zeroconf-0.150.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcfba6f9d344ade1f60aa8b5621a2fa8f65fbe740edc40b05627516bc55eec8f"}, - {file = "zeroconf-0.150.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31832790ed1bcf829b91d4697c6497fade20dbe1094c35d10d5af456cd0e8360"}, - {file = "zeroconf-0.150.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ed611401d1e39f7d27e9cec8ef0b06fffad06955cc3c54714075b037d7b6a8b"}, - {file = "zeroconf-0.150.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0aac92f26cfe8075d6e210e5edb8194cc0b1d16eecdcdb04f31056a14cbc6466"}, - {file = "zeroconf-0.150.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2fdaffee6f71432c64b609ade44e48763484b6084c7954eafeeff0cf4dbc4aef"}, - {file = "zeroconf-0.150.0-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:2bce10cb490119ad051f3caf74088317fec9f5e9acbe36e3690abe8d3de2ac31"}, - {file = "zeroconf-0.150.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:048533286135c467a3bb90a268a9ca3ee3b98c3809d0106359e4d4c0e375e8ad"}, - {file = "zeroconf-0.150.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12f14afb42c08b8365d638ba2a01e5a73641dc8d5433fcdf8751d636210db053"}, - {file = "zeroconf-0.150.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cf19d67827af63c54175ef76a62a004734c0d18ac67ff383a5df36c26049707e"}, - {file = "zeroconf-0.150.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49ae4f9109b2b7aa04fd75677ce33d552bb3db6f9e757acc6e2f6a547cbe26b2"}, - {file = "zeroconf-0.150.0-cp314-cp314-win32.whl", hash = "sha256:afad9aae3a10c4e54a0b55925c29b9d82d1e807f996e5e106a2b2fade6a9e3cc"}, - {file = "zeroconf-0.150.0-cp314-cp314-win_amd64.whl", hash = "sha256:cd09bf1242af33769c88e1f0236ce52f6c74144a741c848fe38a79e68df80414"}, - {file = "zeroconf-0.150.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fed39ccd107ee219ad805ee4c38dca253effc1c96541b4954dee3c70dca13a71"}, - {file = "zeroconf-0.150.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55d0c37be291364268688fe5dcd33bd5156f669289bb9b71a23b041ca53c9017"}, - {file = "zeroconf-0.150.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b97234eda197d4243d859b64004d175de76353dbca17527d11705f841fb02977"}, - {file = "zeroconf-0.150.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a31f0ec4bc3f125ae541fb96259a081f3f83d201f1fca59929155b90185df4"}, - {file = "zeroconf-0.150.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ee162b43e455043e22753cdc1fae0f2990d3fe3767f2e88695fb92273d7229b"}, - {file = "zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:178b6141e34a777593a16b8bf986b2d7ae806e47849a3ad517597f63bcaade47"}, - {file = "zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:410e30a80db9b73bfdee3c54dfe306ebb140e053dfe3b28cefd54756a19a73fe"}, - {file = "zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c539950bba739bc3980dd0f01187637e6237ada8bd70d64f93d8eb5eb2837a7b"}, - {file = "zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ccc9e1772ce0030f77e6a49542e9b7ca4e8ae9ce1bef84a3a983be68ddf81c"}, - {file = "zeroconf-0.150.0-cp314-cp314t-win32.whl", hash = "sha256:576e72a9e9140d96ae110ceb0935f06fa41eeea8b2be53e442a6a00c84035194"}, - {file = "zeroconf-0.150.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bdae152e80b848117691ab59aa8b16bf15323c0e9f3ddc38c6345af95a700a84"}, - {file = "zeroconf-0.150.0.tar.gz", hash = "sha256:a5fe7feab1de6ef5e541e0a3d07e534fd91629b813fc27281593584100f63164"}, + {file = "zeroconf-0.151.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b5ac9625b008e13a0571d12d9650afbf66c7d01b35cc986894fa591e7d606a2"}, + {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb9b7933f4e002131710855d79556f7860e617316de969742c3d38ad06ff4722"}, + {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:871e72adae8ae206445debfff0b2f049c7c1f984408346574ed38a2e7f0de172"}, + {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c33836e55d7f8edacac0dc2283bb60b33aa81e176207cc5f52813c41f52191c0"}, + {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e979f2f565719590d19c4d8313f69ea040cc74b4c12658b8ecd5e51fc7266cd"}, + {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5061fbb4052411c1e23d1164e885a7b83846f4823b069e985a74895cd34a8e6c"}, + {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3ca29e53e3e64e6bac06c4132db45d01060cfdf16a3070925a6a49a5d1278cbf"}, + {file = "zeroconf-0.151.3-cp310-cp310-win32.whl", hash = "sha256:13dfca4530a1fddca4ff18cc2794c2fea0a7c5a4c66cbcbe7ad51e968edaf5e7"}, + {file = "zeroconf-0.151.3-cp310-cp310-win_amd64.whl", hash = "sha256:b0c9aed238135f9adcf8c686ca49c479a6715ef3fc1ab826b24232d2dda7b761"}, + {file = "zeroconf-0.151.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:675c559d287fe7385d7aa87b4f649de49ec8431396d70f7ad25ae05b0ef0c0ea"}, + {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35edcd3d61c945b4d5e98a04a3d2e2ee2bd7ab92a588b0475356e144ccc2680"}, + {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b80006e2f9ca9f7d094209438688b1b7ab1f8231ecd41b1917669d09fdd0f62f"}, + {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8872c85242c46286849c7cb1c0d44d0c75db9fc654ccce351b3dcf26d580b04"}, + {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c74baa9873150f5a7ebf0d7800a22171ec75ae76942f2464b951cb2b3fbcf00d"}, + {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1eee2348d9863a8821ba6cbdc9af97bfb8a79109c8b400cce59679d4d100eb52"}, + {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:657797cc284ab1da4feb20de476806db9a39f1535c5f8e40e5f2ebe149335eea"}, + {file = "zeroconf-0.151.3-cp311-cp311-win32.whl", hash = "sha256:fb544b545c725a13d208cb67f5f8a168b18a568ad0a78e07894f3b913f8225f9"}, + {file = "zeroconf-0.151.3-cp311-cp311-win_amd64.whl", hash = "sha256:4c01d8ca4fc85c107448c2802edfbc39845b06c3a1cb72a3f4337c030f4a9ff3"}, + {file = "zeroconf-0.151.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbec6e928e75629063bd29775611c390bbd3737e9a5b6546545bcf6cebbc85d0"}, + {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8193a17d718ae5a9059c4fbbaefbf937c9bb5eeccb9dd66f9aafcacfec4cefc"}, + {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b76cdefeb7ebe5a3cd8cdd0b1761113d5a64e3031c38c1de873a289ced574eff"}, + {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbd2b73902a44426080190d653fa35638a93b6c6725c2be76888b4b614bc0b48"}, + {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5307ca6ad418200e37dcb12ff5fe5a689f3e559da6342442a82ca98ac703e60"}, + {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b69c26aca186d2b3e4f4b7789a74d9ea81202ecae04105a663939dd85e561d05"}, + {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e573682524d71e8e44b6cdbadbeb9c472a8c3af21de7eead794661b250e04fa1"}, + {file = "zeroconf-0.151.3-cp312-cp312-win32.whl", hash = "sha256:c883d1d64c92df375a6bbee909e65fb76fc47ae92c88261e338bc79b5b505fd5"}, + {file = "zeroconf-0.151.3-cp312-cp312-win_amd64.whl", hash = "sha256:fb1880444668e2a384e925490f1d969ae702bda4472c112a18255446f98ad86b"}, + {file = "zeroconf-0.151.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7bba8a4e7639c1ae5aee9bcf18d994a8b5fcc00b862ca5e82a52a1134b4bf517"}, + {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45bd0e92fcbb6f8d5851f5575792b5f18a85856ae69fee601da4ed3d7cdcfa09"}, + {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:07a21a694d9496ab68aba2aa7d9d628e24d6f3f89766a8cac526395cf7bbb4ae"}, + {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b514f47729ae5b9b42f066163ad509d020c34cacdda3c5efb972f3336213318"}, + {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1aa55ec953ffd0c393d59260a7e26dfd57d6d0347c13c95f9987ff5cfe0d98d"}, + {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9bac55a6aa70797b436c2fc0f25435389bc6443bc7ba1b39a459df3173152b63"}, + {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d542814f914ee78876276db7417f515f53d33a61b84b68f7256a7a603dc73e59"}, + {file = "zeroconf-0.151.3-cp313-cp313-win32.whl", hash = "sha256:2e54bcec212e030b06d9af35b19d5f8144427b52d3fb2c0e06f015a25fe50f08"}, + {file = "zeroconf-0.151.3-cp313-cp313-win_amd64.whl", hash = "sha256:aab19746b9878ed64f7631c0fbe336cec65679c6c31c1489aaea933adf66b2d6"}, + {file = "zeroconf-0.151.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1aa300ba3cc084264b9be6fc2659f003577a1c83bf67920c7cdb4de9b571543a"}, + {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d6cf45c64be5fe4e42ea08f27edc63293357ccc04f850911a96e0fa5647dd4f"}, + {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a91538270ab1e10e0df10c66373e68f6e3e884701593c943e67f82a73ad728f1"}, + {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b903d00e9689a103d453554d794b8ed0f13148366cee08fa8c802e5d85f1ae51"}, + {file = "zeroconf-0.151.3-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:a154a96a84b04b868596ab17eb45c16bf2af25fda8356ea36da35463b25b31bb"}, + {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59bde1d42ca1e9aa665787d97b0a9d38cfb1466d01bcd28733c21507146e7d85"}, + {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9e260a6f2f4441b83c86981fe29191076b4350d5868b630a6545b386dd4223c4"}, + {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e42be39f72e06feaadc40509b081ab7c468fed1f19d1dc3714bc0f3a29be2dd"}, + {file = "zeroconf-0.151.3-cp314-cp314-win32.whl", hash = "sha256:df3b7c3166646a941c151da5981640e930658e96829dc6177834e763d8327d8d"}, + {file = "zeroconf-0.151.3-cp314-cp314-win_amd64.whl", hash = "sha256:688d0282e94376aec608505266a04b8d36b1f026eabc9237ce2b31e7b8c0f2ee"}, + {file = "zeroconf-0.151.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed1a629cbed1d040229b0a3fc3447f4c996e184cc4a98afa43e8ab675de07841"}, + {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3a404649c8dd1ba3cedbb56807dd2b8339ec27b2864df333f90a98f931a5286"}, + {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b97d3eb3343627a5b3ce18600f819001fbb8f29271e19beb586c10df8c32654"}, + {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:022470d54a941805ea30ab57ca2abf443dd6e6a3d79e178149e427864756cd88"}, + {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab58e7a159b0cc91db41783e2790371bb0ba4a701ab215b3ccce12cdc48a3f81"}, + {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fef170e0c995ddeb8828caf07921077f940d634ab6fafd6955f1df88922ee29d"}, + {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ea24e790f9f4601c7b591395e2b6a0d418345854e395ec65a6f9b4b58da7af3"}, + {file = "zeroconf-0.151.3-cp314-cp314t-win32.whl", hash = "sha256:38d1373c0e0dee5941cccef5550e0de34a49c2abbd83199b8ef42a42fbc549c6"}, + {file = "zeroconf-0.151.3-cp314-cp314t-win_amd64.whl", hash = "sha256:6bb60b8e80101cc749ff517f0d3fa77c54e6b06f29cba10faea75f32f253cbbd"}, + {file = "zeroconf-0.151.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:98c752236f6e2415b33d24e99ae4dee9f36181bc322f2103053f23dc8d699cca"}, + {file = "zeroconf-0.151.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa644376895d00b7a556370c7cc7979af94053c012e61df44d431c25db1ade99"}, + {file = "zeroconf-0.151.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:702f94d6d1a61c01ab68dd3a6e7a0ee9128302d1f79683707e92e9dab46a8e21"}, + {file = "zeroconf-0.151.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ebdacd288faddd519cef12c87e02f6af1d37ee99e15ad6cd2fd91a8043cb9832"}, + {file = "zeroconf-0.151.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:89914b9ba91604e9f6c67d115631e74aa03161e415780d8f5dabe0b4e59b31ca"}, + {file = "zeroconf-0.151.3-cp315-cp315-win32.whl", hash = "sha256:ec78ae3d72cf6050fcc4179a0c9ecda935b495396336560a26c49f2c0f8bb0ad"}, + {file = "zeroconf-0.151.3-cp315-cp315-win_amd64.whl", hash = "sha256:2904563547eb34368b9b988e657672bad2e7732a4a9990732a9666ae82970c0c"}, + {file = "zeroconf-0.151.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:04f0801e0f9955b44971cd9aac4204c2f3bcca779f4e4bb27d6ef7db5d85f22d"}, + {file = "zeroconf-0.151.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8d855f783936b7b50a72c30d74330229bbbeda799dfcd1981e0d38973b1ce69"}, + {file = "zeroconf-0.151.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13e3f70fcd64a75eccadfa91baafc9a5e498736299af7511b02421a0f3bcc7a1"}, + {file = "zeroconf-0.151.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:e226fd343924b685c09f9e20cadf4e50693f08824846feef6c6cb05173e407ad"}, + {file = "zeroconf-0.151.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c3e87bb627e469c6e901c47ab9fab3a38e98250dc4032d505cf7f83c2e2eee2f"}, + {file = "zeroconf-0.151.3-cp315-cp315t-win32.whl", hash = "sha256:facffd6e4392869b3a53b674df502f9f90846a6ccb70045eebe8e40306f5b0d9"}, + {file = "zeroconf-0.151.3-cp315-cp315t-win_amd64.whl", hash = "sha256:397a824856fad1aea642640bad40e9a52a6ea9fe5ee2954b5ec7f46d9985bf08"}, + {file = "zeroconf-0.151.3.tar.gz", hash = "sha256:ce6c548e665759b6150cef4db9ab9d7bdd89857e90c513abd6b7340bdd7dbd6a"}, ] [package.dependencies] @@ -1717,4 +1889,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "fe6a80760cb931dc9fa8e6906aa819eda6df17f3f23280f4f66c9d1a142be34e" +content-hash = "a4b402812f14a3ed40e2b682670e0b49129dc00916f481fa52e054a09a2fb5ff" diff --git a/pyproject.toml b/pyproject.toml index a221f85..92974d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "majordom-integration-sdk (>=0.1.2,<0.2.0)", - "zwave-js-server-python (>=0.73.1,<0.74.0)" + "zwave-js-server-python (>=0.73.1,<0.74.0)", ] [build-system] @@ -18,11 +18,43 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] package-mode = false +packages = [{ include = "majordom_zwave" }] [tool.poetry.group.dev.dependencies] pytest = "^9.1.1" pytest-asyncio = "^1.4.0" +ruff = "^0.16.6" +ty = "^0.0.78" +poethepoet = "^0.48.0" [tool.pytest.ini_options] pythonpath = ["."] -asyncio_mode = "auto" \ No newline at end of file +asyncio_mode = "auto" + +[tool.poe.tasks.install] +shell = """ +poetry install +echo '#!/bin/sh\npoetry run poe check' > .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit +echo "Pre-commit hook installed." +""" + +[tool.poe.tasks.check] +shell = """ +code=0 +echo "\nRunning ruff lint:" +poetry run ruff check --fix || code=1 +echo "\nRunning ruff format:" +poetry run ruff format || code=1 +echo "\nRunning ty:" +poetry run ty check || code=1 +echo "\nRunning pytest:" +poetry run pytest --cov=majordom_matter --cov-report=xml || code=1 +echo "\nRunning poetry build:" +poetry build || code=1 +echo "\nRunning poetry check:" +poetry check || code=1 +${ci:+git diff --exit-code} +exit $code +""" +args = [{ name = "ci", type = "boolean" }] diff --git a/tests/conftest.py b/tests/conftest.py index 4e88e68..6857c73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,28 @@ +from typing import cast from uuid import uuid4 import pytest from majordom_integration_sdk.controller import AbstractController -from majordom_integration_sdk.schemas.device import CredentialsType, Device, DeviceState, Discovery +from majordom_integration_sdk.schemas.device import ( + CredentialsType, + Device, + DeviceState, + Discovery, +) from majordom_integration_sdk.schemas.parameter import ( ParameterDataType, ParameterRole, ParameterState, ParameterVisibility, ) -from majordom_integration_sdk.testing import build_test_dependencies +from majordom_integration_sdk.testing import ( + RecordingControllerOutput, + build_test_dependencies, +) +from virtual_zwave_network import VirtualZwaveNetwork from majordom_zwave import ZwaveController -from virtual_zwave_network import VirtualZwaveNetwork - INTEGRATION = "Zwave" @@ -23,6 +31,14 @@ def deps() -> AbstractController.Dependencies: return build_test_dependencies(integration=INTEGRATION) +@pytest.fixture(scope="session") +def output(deps: AbstractController.Dependencies) -> RecordingControllerOutput: + # deps.output is typed as the abstract ControllerOutput; build_test_dependencies() + # always wires the concrete RecordingControllerOutput, so this cast is safe and + # gives tests access to its recorded fields (.events, .connected_devices, ...). + return cast(RecordingControllerOutput, deps.output) + + @pytest.fixture(scope="session") def controller(deps: AbstractController.Dependencies) -> ZwaveController: return ZwaveController(deps) diff --git a/tests/test_controller.py b/tests/test_controller.py index 4f4d872..d12ad47 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -4,22 +4,22 @@ empty node registry each test), so any test that needs to reach the "device" re-creates the node with the same NODE_ID. """ + import asyncio -from majordom_integration_sdk.controller import AbstractController from majordom_integration_sdk.schemas.command import DeviceCommand +from majordom_integration_sdk.testing import RecordingControllerOutput +from virtual_zwave_network import VirtualZwaveNetwork from majordom_zwave import ZwaveController -from majordom_zwave.model import ZwaveDevice, ZwaveDeviceState - -from virtual_zwave_network import VirtualZwaveNetwork +from majordom_zwave.model import ZwaveDevice, ZwaveDeviceState, ZwaveParameter NODE_ID = 5 async def test_pairs_a_discovered_device( controller: ZwaveController, - deps: AbstractController.Dependencies, + output: RecordingControllerOutput, virtual_zwave: VirtualZwaveNetwork, make_provisional_device, ) -> None: @@ -36,26 +36,30 @@ async def test_pairs_a_discovered_device( await controller.pair_device(discovery, credentials=None) await controller.stop() - assert discovery.id in deps.output.connected_devices + assert discovery.id in output.connected_devices - async with deps.make_device_repository() as repo: + async with controller.dependencies.make_device_repository() as repo: paired = await repo.get(discovery.id, as_=ZwaveDevice) state = await repo.state(discovery.id, as_=ZwaveDeviceState) assert paired is not None + assert paired.integration_data is not None assert paired.integration_data.node_id == NODE_ID - assert state.parameters, "Expected at least one parameter to be mapped during pairing" + assert state is not None + assert state.parameters, ( + "Expected at least one parameter to be mapped during pairing" + ) async def test_fetches_state( controller: ZwaveController, - deps: AbstractController.Dependencies, + output: RecordingControllerOutput, virtual_zwave: VirtualZwaveNetwork, ) -> None: await controller.start() virtual_zwave.node_joined({"nodeId": NODE_ID}) await asyncio.sleep(0.05) - async with deps.make_device_repository() as repo: + async with controller.dependencies.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] @@ -67,9 +71,11 @@ async def test_fetches_state( new_value=True, prev_value=False, ) - events_before = len(deps.output.events) + # fetch() doesn't touch the repo -- it reports straight to output.events + # (which accumulates for the whole session), so only check what this call added. + events_before = len(output.events) await controller.fetch(device) - new_events = deps.output.events[events_before:] + new_events = output.events[events_before:] assert any(e.device_id == device.id and e.value is True for e in new_events), ( "fetch() never reported the current value back to the Hub" @@ -78,25 +84,29 @@ async def test_fetches_state( async def test_sends_a_command( - deps: AbstractController.Dependencies, controller: ZwaveController, virtual_zwave: VirtualZwaveNetwork, ) -> None: await controller.start() virtual_zwave.node_joined({"nodeId": NODE_ID}) - async with deps.make_device_repository() as repo: + async with controller.dependencies.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] state = await repo.state(device.id, as_=ZwaveDeviceState) - parameter = next(p for p in state.parameters if p.id == state.main_parameter) + assert state is not None + parameter_state = next( + p for p in state.parameters if p.id == state.main_parameter + ) + parameter = ZwaveParameter.model_validate(parameter_state.model_dump()) command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=0) await controller.send_command(command, device, parameter) await controller.stop() sent = [ - c for c in virtual_zwave.async_send_command.call_args_list + c + for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value" ] assert sent, "Controller never sent a node.set_value command to the device" @@ -105,41 +115,46 @@ async def test_sends_a_command( async def test_identifies( controller: ZwaveController, - deps: AbstractController.Dependencies, virtual_zwave: VirtualZwaveNetwork, ) -> None: await controller.start() virtual_zwave.node_joined({"nodeId": NODE_ID}) - async with deps.make_device_repository() as repo: + async with controller.dependencies.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] await controller.identify(device) await controller.stop() sent = [ - c for c in virtual_zwave.async_send_command.call_args_list + c + for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value" ] - assert sent, "controller.identify() never sent a node.set_value command to the device" + assert sent, ( + "controller.identify() never sent a node.set_value command to the device" + ) async def test_unpairs( controller: ZwaveController, - deps: AbstractController.Dependencies, virtual_zwave: VirtualZwaveNetwork, ) -> None: await controller.start() virtual_zwave.node_joined({"nodeId": NODE_ID}) - async with deps.make_device_repository() as repo: + async with controller.dependencies.make_device_repository() as repo: devices = await repo.get_all(as_=ZwaveDevice) device = devices[0] + # unpair() waits (up to 60s) for a real "node removed" event, so it has + # to run concurrently with the simulated confirmation below. unpair_task = asyncio.create_task(controller.unpair(device)) await asyncio.sleep(0.05) virtual_zwave.node_removed(NODE_ID) await unpair_task + # Must check before stop(): stop() disconnects the virtual client, after + # which virtual_zwave.controller is torn down. assert NODE_ID not in virtual_zwave.controller.nodes await controller.stop() diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 1985cd2..72a18de 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -3,7 +3,7 @@ async def test_starts_and_stops(controller: ZwaveController) -> None: await controller.start() - await controller.stop() + await controller.stop() async def test_name_and_slug(controller: ZwaveController) -> None: diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py index 44e0092..2861154 100644 --- a/tests/virtual_zwave_network.py +++ b/tests/virtual_zwave_network.py @@ -2,30 +2,48 @@ Client the controller creates becomes an in-memory stub instead of hitting a real server. Everything else (Driver/Controller/Node/Value) is the real library. """ + from __future__ import annotations import asyncio -from typing import Any +from typing import Any, Self, cast from unittest.mock import AsyncMock, Mock, patch from zwave_js_server.client import Client from zwave_js_server.event import Event from zwave_js_server.model.driver import Driver +from zwave_js_server.model.log_config import LogConfigDataType from zwave_js_server.model.node import Node - -_LOG_CONFIG = { - "enabled": True, "level": "info", "logToFile": False, - "filename": "", "forceConsole": False, +from zwave_js_server.model.node.data_model import NodeDataType + +_LOG_CONFIG: LogConfigDataType = { + "enabled": True, + "level": "info", + "logToFile": False, + "filename": "", + "forceConsole": False, } _DEFAULT_CONTROLLER_STATE: dict[str, Any] = { "controller": { - "sdkVersion": "Z-Wave 3.95", "type": 1, "homeId": 1, "ownNodeId": 1, - "isSecondary": False, "isUsingHomeIdFromOtherNetwork": False, - "isSISPresent": True, "wasRealPrimary": True, "isStaticUpdateController": True, - "isSlave": False, "firmwareVersion": "1.0", "manufacturerId": 1, - "productType": 1, "productId": 1, "supportedFunctionTypes": [], - "sucNodeId": 1, "supportsTimers": False, "isRebuildingRoutes": False, + "sdkVersion": "Z-Wave 3.95", + "type": 1, + "homeId": 1, + "ownNodeId": 1, + "isSecondary": False, + "isUsingHomeIdFromOtherNetwork": False, + "isSISPresent": True, + "wasRealPrimary": True, + "isStaticUpdateController": True, + "isSlave": False, + "firmwareVersion": "1.0", + "manufacturerId": 1, + "productType": 1, + "productId": 1, + "supportedFunctionTypes": [], + "sucNodeId": 1, + "supportsTimers": False, + "isRebuildingRoutes": False, "inclusionState": 0, }, "nodes": [], @@ -35,23 +53,45 @@ _DEFAULT_NODE_VALUES: list[dict[str, Any]] = [ { - "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, - "property": "currentValue", "propertyName": "currentValue", - "value": False, "ccVersion": 1, - "metadata": {"type": "boolean", "readable": True, "writeable": False, "label": "Current value"}, + "commandClass": 37, + "commandClassName": "Binary Switch", + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "value": False, + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": False, + "label": "Current value", + }, }, { - "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, - "property": "targetValue", "propertyName": "targetValue", - "value": False, "ccVersion": 1, - "metadata": {"type": "boolean", "readable": True, "writeable": True, "label": "Target value"}, + "commandClass": 37, + "commandClassName": "Binary Switch", + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "value": False, + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, }, ] _DEFAULT_NODE_ENDPOINTS: list[dict[str, Any]] = [ - {"index": 0, "deviceClass": None, "commandClasses": [ - {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, - ]}, + { + "index": 0, + "deviceClass": None, + "commandClasses": [ + {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, + ], + }, ] # Client.async_send_command's return value IS the server message's "result" @@ -81,12 +121,14 @@ def __init__(self, controller_state: dict | None = None) -> None: self._patches: list[Any] = [] async def _handle_send_command(self, message: dict, **_kwargs: Any) -> dict: - return self._command_responses.get(message.get("command", ""), {"success": True}) + return self._command_responses.get( + message.get("command", ""), {"success": True} + ) def set_response(self, command: str, response: dict) -> None: self._command_responses[command] = response - def __enter__(self) -> "VirtualZwaveNetwork": + def __enter__(self) -> Self: network = self async def fake_connect(client_self: Client) -> None: @@ -94,7 +136,9 @@ async def fake_connect(client_self: Client) -> None: client_self._client = Mock(closed=False) async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: - client_self.driver = Driver(client_self, network.controller_state, _LOG_CONFIG) + client_self.driver = Driver( + client_self, network.controller_state, _LOG_CONFIG + ) network._client = client_self driver_ready.set() try: @@ -104,45 +148,58 @@ async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: async def fake_disconnect(client_self: Client) -> None: client_self.driver = None - if client_self._client is not None: - client_self._client.closed = True + # Don't touch `_client.closed` (read-only on the real + # ClientWebSocketResponse) -- dropping the reference entirely + # makes `.connected` False too, which is what matters here. + client_self._client = None self._patches = [ patch.object(Client, "connect", new=fake_connect), patch.object(Client, "listen", new=fake_listen), patch.object(Client, "disconnect", new=fake_disconnect), patch.object(Client, "async_send_command", new=self.async_send_command), - patch.object(Client, "async_send_command_no_wait", new=self.async_send_command_no_wait), + patch.object( + Client, + "async_send_command_no_wait", + new=self.async_send_command_no_wait, + ), ] for p in self._patches: p.start() return self - def __exit__(self, *exc_info: Any) -> None: + def __exit__(self, *exc_info: object) -> None: for p in reversed(self._patches): p.stop() + def _connected_client(self) -> Client: + assert self._client is not None, ( + "virtual_zwave used before controller.start() ran" + ) + return self._client + @property def controller(self): - assert self._client and self._client.driver, ( + client = self._connected_client() + assert client.driver is not None, ( "virtual_zwave.controller accessed before controller.start() ran" ) - return self._client.driver.controller + return client.driver.controller def node_found(self, node_id: int) -> None: self.controller.emit("node found", {"node": {"nodeId": node_id}}) def node_joined(self, node_data: dict) -> Node: - payload = { + payload: dict[str, Any] = { "status": 4, # NodeStatus.ALIVE, not 1 (ASLEEP) -- ASLEEP makes - # async_send_command silently switch to no_wait + # async_send_command silently switch to no_wait "ready": True, "interviewStage": "Complete", "endpoints": _DEFAULT_NODE_ENDPOINTS, "values": _DEFAULT_NODE_VALUES, **node_data, } - node = Node(self._client, payload) + node = Node(self._connected_client(), cast(NodeDataType, payload)) self.controller.nodes[node.node_id] = node self.controller.emit("node added", {"node": node}) return node @@ -158,7 +215,7 @@ def emit_value_updated( *, command_class: int, command_class_name: str, - property: int | str, # noqa: A002 + property: int | str, new_value: Any, prev_value: Any = None, property_name: str | None = None, @@ -166,19 +223,22 @@ def emit_value_updated( metadata: dict | None = None, ) -> None: node = self.controller.nodes[node_id] - event = Event("value updated", { - "source": "node", - "event": "value updated", - "nodeId": node_id, - "args": { - "commandClass": command_class, - "commandClassName": command_class_name, - "endpoint": endpoint, - "property": property, - "propertyName": property_name or str(property), - "newValue": new_value, - "prevValue": prev_value, - "metadata": metadata or _DEFAULT_VALUE_METADATA, + event = Event( + "value updated", + { + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClass": command_class, + "commandClassName": command_class_name, + "endpoint": endpoint, + "property": property, + "propertyName": property_name or str(property), + "newValue": new_value, + "prevValue": prev_value, + "metadata": metadata or _DEFAULT_VALUE_METADATA, + }, }, - }) + ) node.receive_event(event) From 90e4f37e16553952fd7bbbe086ef0c1d42b84eec Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:13:32 +0300 Subject: [PATCH 09/28] Fixed pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 92974d1..5d0a289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ poetry run ruff format || code=1 echo "\nRunning ty:" poetry run ty check || code=1 echo "\nRunning pytest:" -poetry run pytest --cov=majordom_matter --cov-report=xml || code=1 +poetry run pytest --cov=majordom_zwave --cov-report=xml || code=1 echo "\nRunning poetry build:" poetry build || code=1 echo "\nRunning poetry check:" From f4df8bf6c9afead699a52bc9327702ffb5151b17 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:20:15 +0300 Subject: [PATCH 10/28] Added poetry-cov --- poetry.lock | 156 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index efbfed8..afdf111 100644 --- a/poetry.lock +++ b/poetry.lock @@ -237,6 +237,140 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.16.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36aed4951aedf04cbe9465e76f8e71219980a52b73d07afe69746cba6ba7b97a"}, + {file = "coverage-7.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb953835dbfa6d641ac3943e0986bc680f8abbdc2985af15b46c54985347146a"}, + {file = "coverage-7.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97051c4903689b1afedc2a354d6118223051e03588078b53048603bda9014577"}, + {file = "coverage-7.16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:770d4244c423dcafb5c31db393f429fe952b1bba23bbff7cc3886f8133769ba5"}, + {file = "coverage-7.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e7de0cb87960c6c9b5cad760068dab767b2b49a3b9376e1992c1e2691a015e"}, + {file = "coverage-7.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c2c45ee1853668f0ea1a0ddff396421c9dc5ad25a56bfb94a895970c2d8e7c2"}, + {file = "coverage-7.16.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6b2b9599e7513b0a9c5bf0357f9f8deaa4c2c821025b0693d420e6602748981"}, + {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fde65e0ea945920265dfe4a2108fc45eee2e2ea3d9c3073af6373ff9836aa71"}, + {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78103e79f9378cb0e43ddaa728629a373c070df903c5dfa98b63ba2cfb4e8c42"}, + {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e40e323711b485592354069b1c027ef879cc2d11657eac09a6e5ad0b49ab7406"}, + {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c94ef980f7b94d9dab9dac076d44ca706654cd51bad19734e029084adf528c8e"}, + {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b37ad5cbb77776f446e1b55b461eec2eef5c3e7130c72dc0e1447c3a9da2d199"}, + {file = "coverage-7.16.0-cp310-cp310-win32.whl", hash = "sha256:9421dde689e68d9fd2b6cd7d8c4498e79b5431467b6298517e3f3e60fdbe80a7"}, + {file = "coverage-7.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:81d63b68b26304e3668edb103311c17fe13c2ed1c7fe973309819f27bf61c5b8"}, + {file = "coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d"}, + {file = "coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db"}, + {file = "coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3"}, + {file = "coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4"}, + {file = "coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719"}, + {file = "coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3"}, + {file = "coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a"}, + {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c"}, + {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9"}, + {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d"}, + {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6"}, + {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce"}, + {file = "coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce"}, + {file = "coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a"}, + {file = "coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7"}, + {file = "coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5"}, + {file = "coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831"}, + {file = "coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19"}, + {file = "coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44"}, + {file = "coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24"}, + {file = "coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352"}, + {file = "coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06"}, + {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4"}, + {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02"}, + {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c"}, + {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb"}, + {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e"}, + {file = "coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91"}, + {file = "coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950"}, + {file = "coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b"}, + {file = "coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42"}, + {file = "coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13"}, + {file = "coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5"}, + {file = "coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354"}, + {file = "coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261"}, + {file = "coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a"}, + {file = "coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87"}, + {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292"}, + {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa"}, + {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21"}, + {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f"}, + {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17"}, + {file = "coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67"}, + {file = "coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449"}, + {file = "coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c"}, + {file = "coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697"}, + {file = "coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883"}, + {file = "coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575"}, + {file = "coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62"}, + {file = "coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0"}, + {file = "coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f"}, + {file = "coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6"}, + {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9"}, + {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3"}, + {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06"}, + {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04"}, + {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad"}, + {file = "coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af"}, + {file = "coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d"}, + {file = "coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f"}, + {file = "coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3"}, + {file = "coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7"}, + {file = "coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c"}, + {file = "coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22"}, + {file = "coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e"}, + {file = "coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5"}, + {file = "coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313"}, + {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867"}, + {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267"}, + {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a"}, + {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b"}, + {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd"}, + {file = "coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61"}, + {file = "coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3"}, + {file = "coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11"}, + {file = "coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024"}, + {file = "coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c"}, + {file = "coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633"}, + {file = "coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b"}, + {file = "coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f"}, + {file = "coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68"}, + {file = "coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be"}, + {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a"}, + {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae"}, + {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee"}, + {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f"}, + {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73"}, + {file = "coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889"}, + {file = "coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627"}, + {file = "coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9"}, + {file = "coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025"}, + {file = "coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3"}, + {file = "coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a"}, + {file = "coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178"}, + {file = "coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2"}, + {file = "coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0"}, + {file = "coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b"}, + {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0"}, + {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e"}, + {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54"}, + {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb"}, + {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897"}, + {file = "coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743"}, + {file = "coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3"}, + {file = "coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc"}, + {file = "coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73"}, + {file = "coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "dbus-fast" version = "5.0.22" @@ -1150,6 +1284,26 @@ typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "python-slugify" version = "8.0.4" @@ -1889,4 +2043,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "a4b402812f14a3ed40e2b682670e0b49129dc00916f481fa52e054a09a2fb5ff" +content-hash = "215e859439a04fc2bed9ed8f4c394ed89148c8d8c49f85b1bc4b21985f408eb7" diff --git a/pyproject.toml b/pyproject.toml index 5d0a289..f6ab423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -package-mode = false +package-mode = true packages = [{ include = "majordom_zwave" }] [tool.poetry.group.dev.dependencies] @@ -26,6 +26,7 @@ pytest-asyncio = "^1.4.0" ruff = "^0.16.6" ty = "^0.0.78" poethepoet = "^0.48.0" +pytest-cov = "^7.1.0" [tool.pytest.ini_options] pythonpath = ["."] From ebeb327da6df08deb1e1245bc7eff7641456ffff Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:20:25 +0300 Subject: [PATCH 11/28] Fix poe errors --- majordom_zwave/__init__.py | 2 +- majordom_zwave/controller.py | 41 +++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/majordom_zwave/__init__.py b/majordom_zwave/__init__.py index b439343..965ac48 100644 --- a/majordom_zwave/__init__.py +++ b/majordom_zwave/__init__.py @@ -6,4 +6,4 @@ from .controller import ZwaveController -__all__ = ["ZWaveController"] +__all__ = ["ZwaveController"] diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 900998d..7f60943 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -20,6 +20,7 @@ NodeStatus, SecurityClass, ) +from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant from zwave_js_server.model.node import Node from zwave_js_server.model.utils import async_parse_qr_code_string @@ -40,8 +41,6 @@ log = logging.getLogger(__name__) -# TODO: заполнить ридми под интеграцию по примерам меттера и зигби - class ZwaveController(AbstractController): """Bridges the Hub to Z-Wave devices through zwave-js-server.""" @@ -99,7 +98,7 @@ async def start(self): self._create_task(self._zwave_client.listen(ready)) await ready.wait() - controller = self._zwave_client.driver.controller + controller = self._get_controller() controller.on( "node added", lambda data: self._create_task(self._node_added(data["node"])) ) @@ -123,17 +122,22 @@ async def start(self): for raw_device in await device_repository.get_all(): try: device = ZwaveDevice.model_validate(raw_device.model_dump()) - except Exception: + except Exception: # noqa: BLE001 log.debug( "[SKIP] device_id=%s not paired yet, skipping", raw_device.id, ) continue + if device.integration_data is None: + continue + node = controller.nodes.get(device.integration_data.node_id) if node is None: device.available = False - device.last_error = f"Device {device.name} is no longer connected to the Z-Wave network" + device.last_error = ( + f"Device {device.name} is no longer connected to the Z-Wave network" + ) await device_repository.save(device, device.id) log.debug( "[MISSING] device_id=%s node_id=%s not on network", @@ -143,7 +147,7 @@ async def start(self): continue known_node_ids.add(node.node_id) self._connected_devices[device.id] = node - self._availability[device.id] = node.ready + self._availability[device.id] = bool(node.ready) self._subscribe(device.id, node) log.debug("[KNOWN] node_id=%s", node.node_id) @@ -186,7 +190,7 @@ async def start_pairing_window( self, duration_sec: int, credentials: ProvidedCredentials | None = None ) -> None: self._require_client() - controller = self._zwave_client.driver.controller + controller = self._get_controller() if credentials and credentials.type is CredentialsType.qr: if not credentials.value: @@ -284,7 +288,7 @@ async def pair_device( async def unpair(self, device: ZwaveDevice): self._require_client() - controller = self._zwave_client.driver.controller + controller = self._get_controller() zwave_node = self._require_node(device) if await controller.async_is_failed_node(zwave_node): @@ -413,7 +417,7 @@ async def send_command( zwave_value, command.value, wait_for_result=awake ) log.info(result) - except Exception as e: + except Exception as e: # noqa: BLE001 raise ZwaveUnexpectedError( f"Failed to set value {parameter.integration_data.value_id} on node {zwave_node.node_id}: {e}" ) from None @@ -438,9 +442,10 @@ async def send_command( # Private helpers # ========================================================================= - # ------------------------------------------------------------------------- - # Generic guards / lookups - # ------------------------------------------------------------------------- + def _get_controller(self) -> Controller: + if self._zwave_client.driver is None: + raise ZwaveUnexpectedError("Z-Wave driver is not connected") + return self._zwave_client.driver.controller def _create_task(self, coro) -> asyncio.Task: task = asyncio.create_task(coro) @@ -454,9 +459,10 @@ def _require_client(self) -> ZwaveClient: raise ZwaveConnectionError("Zwave client is not started") def _require_node(self, device: ZwaveDevice) -> Node: - node = self._zwave_client.driver.controller.nodes.get( - device.integration_data.node_id - ) + if device.integration_data is None: + raise ZwaveUnexpectedError(f"Device {device.id} missing integration_data") + controller = self._get_controller() + node = controller.nodes.get(device.integration_data.node_id) if not node: raise ZwaveUnexpectedError(f"Node for device {device.id} not found") return node @@ -475,7 +481,7 @@ def _require_value(self, value_id: str, node: Node) -> Value: async def _close_pairing_window(self, duration_sec: int) -> None: await asyncio.sleep(duration_sec) - controller = self._zwave_client.driver.controller + controller = self._get_controller() await controller.async_stop_inclusion() log.debug("[PAIRING] inclusion window closed") @@ -542,7 +548,8 @@ async def _grant_security_classes(self, requested: dict) -> None: security_classes=[SecurityClass(c) for c in requested["securityClasses"]], client_side_auth=requested["clientSideAuth"], ) - await self._zwave_client.driver.controller.async_grant_security_classes(grant) + controller = self._get_controller() + await controller.async_grant_security_classes(grant) log.debug("[S2] granted security classes=%s", requested["securityClasses"]) # ------------------------------------------------------------------------- From 6d2cd0c6d17104d214e89c2e199fdd080e0d7031 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:21:51 +0300 Subject: [PATCH 12/28] Fixed poe code style --- majordom_zwave/controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 7f60943..118ae5d 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -135,9 +135,7 @@ async def start(self): node = controller.nodes.get(device.integration_data.node_id) if node is None: device.available = False - device.last_error = ( - f"Device {device.name} is no longer connected to the Z-Wave network" - ) + device.last_error = f"Device {device.name} is no longer connected to the Z-Wave network" await device_repository.save(device, device.id) log.debug( "[MISSING] device_id=%s node_id=%s not on network", From 3f86f373938bfd98b5ebad1840a2cc133a6ab8b2 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Tue, 8 Sep 2026 22:24:35 +0300 Subject: [PATCH 13/28] Fixed ty errors and code style --- tests/test_controller.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index d12ad47..16e8b36 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -8,6 +8,7 @@ import asyncio from majordom_integration_sdk.schemas.command import DeviceCommand +from majordom_integration_sdk.schemas.event import DeviceParameterChange from majordom_integration_sdk.testing import RecordingControllerOutput from virtual_zwave_network import VirtualZwaveNetwork @@ -77,9 +78,12 @@ async def test_fetches_state( await controller.fetch(device) new_events = output.events[events_before:] - assert any(e.device_id == device.id and e.value is True for e in new_events), ( - "fetch() never reported the current value back to the Hub" - ) + assert any( + isinstance(e, DeviceParameterChange) + and e.device_id == device.id + and e.value is True + for e in new_events + ), "fetch() never reported the current value back to the Hub" await controller.stop() From c78bfc53f6683adb1460e0a6d4cf93dc7976b576 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:56:11 +0000 Subject: [PATCH 14/28] chore(deps-pip): bump the python-deps group with 2 updates (#3) Bumps the python-deps group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [ty](https://github.com/astral-sh/ty). Updates `ruff` from 0.16.6 to 0.16.7 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.16.6...0.16.7) Updates `ty` from 0.0.78 to 0.0.80 - [Release notes](https://github.com/astral-sh/ty/releases) - [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ty/compare/0.0.78...0.0.80) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.16.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: ty dependency-version: 0.0.80 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 80 +++++++++++++++++++++++++------------------------- pyproject.toml | 4 +-- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/poetry.lock b/poetry.lock index afdf111..40745f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1407,30 +1407,30 @@ files = [ [[package]] name = "ruff" -version = "0.16.6" +version = "0.16.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8"}, - {file = "ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32"}, - {file = "ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718"}, - {file = "ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25"}, - {file = "ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39"}, - {file = "ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5"}, - {file = "ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050"}, + {file = "ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9"}, + {file = "ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929"}, + {file = "ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55"}, + {file = "ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f"}, + {file = "ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656"}, + {file = "ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750"}, + {file = "ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5"}, + {file = "ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f"}, + {file = "ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7"}, + {file = "ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6"}, + {file = "ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b"}, + {file = "ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6"}, ] [[package]] @@ -1447,30 +1447,30 @@ files = [ [[package]] name = "ty" -version = "0.0.78" +version = "0.0.80" description = "An extremely fast Python type checker, written in Rust." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "ty-0.0.78-py3-none-linux_armv6l.whl", hash = "sha256:122700b98f9d45785c1ce91a9418154562e7f64f4bf34679f6f7b38c5b97453f"}, - {file = "ty-0.0.78-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbe7e3709ccf29ef3d9f58f5914cc30ec36fb647008a5a4ff8483abe401bfe8d"}, - {file = "ty-0.0.78-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c3528897b3ab9d3589561bc2b5e61a4d5d686de527a4400bb09a439b922a6c70"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bfba4c44a06484093f527b8ef43a317abcf91395b49396170266629eedf74aa"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f903c06fdee17baf8373173039ab28f1bce28e66b1c734929a5f50b37eb54d9"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd83e5fe3f07291d1bd4e591f8d2607c204f3eab53bcb1b8060c40e876e1aa61"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325285377319cf7168a2b8b771ae5027f411530e3c7eab1faf4583cdd72fd7c9"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b7846a404da6492697b524a769755ee9bee157d67674063ecd9c5f69dc52ff"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:169d3b9d134c0b48fe1af8a844142d374655552054a9d6c15a4b6e51bb0382ea"}, - {file = "ty-0.0.78-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6108cb3b2d28dac5981d4e25008a0a5547d8c877a67f6da9e8c38e7e946be44f"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:589ad03608d9d2975ef4b23c41c4f847e325bcc7686f51d4c66066b8dbc18a2a"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b3fa0786edc1af06030f872d83499c0cea7c261dbbb651e0aee8306bf2c8a869"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92c5639befc577578c8abd4e5a7fccf98d828db98b09e8d4dd81605dc0e54aef"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0dce70bc51652b2775debd1d1e422fb0179f5207c73deb4d5d0aca37e5f61695"}, - {file = "ty-0.0.78-py3-none-win32.whl", hash = "sha256:1c80976ca9185d7a9d1baab1fb57240331b8dac58d3b11e46200284086a6de8d"}, - {file = "ty-0.0.78-py3-none-win_amd64.whl", hash = "sha256:32e82b704471eab34f67b51c151660ca8a00815977b28278905d76fba54f7415"}, - {file = "ty-0.0.78-py3-none-win_arm64.whl", hash = "sha256:3a14d641a3c04fa9a80f2a46be1531d915f60d4fb79d4b894627bbe46bb35d64"}, - {file = "ty-0.0.78.tar.gz", hash = "sha256:770b45854f85fa11595208f08c0f28df80943164d10a2832d86be6ac29f135b2"}, + {file = "ty-0.0.80-py3-none-linux_armv6l.whl", hash = "sha256:738d1cfca466c577aea24547c348629c00c63548cf7da2c46b497b3840f85dee"}, + {file = "ty-0.0.80-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:56060164bb8ee43770fa367524fbdba2611fc90f8923a02cc91f80eb37d5a1b0"}, + {file = "ty-0.0.80-py3-none-macosx_11_0_arm64.whl", hash = "sha256:da4062e0fbf3923d9b71688157c5753394f243348f00732e9edbb27498397169"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9eb94b2659f506a3a07a9ea1415d5c18aaa1e554adc1612614d617a0ed320e1"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f5e39a87da48c1af1a3b3135f02be7a83a93da30b0d6a5f5d27806e7c747ce"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e00149e7779c6b98f3ba12e10a631a861bd7ca1a42bbf064e2677ada7d2c0c2"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a377268f359fb6e7a2cc9026a09223c764f58d020dec8e6baeaba9caba6ff829"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4511dbf1b266b9ce5b73e9b28d94468096c2b6da00e2fe205168c32d3b352ca7"}, + {file = "ty-0.0.80-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe95feffa7156800c6f804195acb9fb5846a39671c7192c30fff23351aaf7c31"}, + {file = "ty-0.0.80-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba26b39f06bc8c3c2c5acd3a147239482b36620cdbca1b9d02e915294854f2cc"}, + {file = "ty-0.0.80-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e6167320888c115a6fbe69893b63fd1c848f9121a454e75e5f612674528e3f3"}, + {file = "ty-0.0.80-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7aecb62b4de70b479eab07d1779ea66da26dd8b068f50a9330867157312f103c"}, + {file = "ty-0.0.80-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4fed06adacd16b7e2d722f37449021b1419598d0440769701c0f4827faddde63"}, + {file = "ty-0.0.80-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09329af6303ce611ec2cfffc995dc1ddf76c9f47194fc1a4d64a984759c25f5b"}, + {file = "ty-0.0.80-py3-none-win32.whl", hash = "sha256:a81b3b512f7b4c68e42fbd1835633de658809666e9aafd5defa52bbc5197698d"}, + {file = "ty-0.0.80-py3-none-win_amd64.whl", hash = "sha256:8043f99878a2ae434781cb881c8a3840dff70c4a5b60bdc5ae84251f35ee063f"}, + {file = "ty-0.0.80-py3-none-win_arm64.whl", hash = "sha256:e277ef034331da5319efc839c064968d24f35715b71c70369cf97d36fe4dcbdb"}, + {file = "ty-0.0.80.tar.gz", hash = "sha256:fe86bc91327e45ff5e3593b7e306e7f57a44bc0608f38d647b8d97bd99c96013"}, ] [[package]] @@ -2043,4 +2043,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "215e859439a04fc2bed9ed8f4c394ed89148c8d8c49f85b1bc4b21985f408eb7" +content-hash = "78252abd468bc67d9321c5801a258dbc4f0fbf937aad8ef71dc2137b73d72a04" diff --git a/pyproject.toml b/pyproject.toml index f6ab423..fcfe820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,8 @@ packages = [{ include = "majordom_zwave" }] [tool.poetry.group.dev.dependencies] pytest = "^9.1.1" pytest-asyncio = "^1.4.0" -ruff = "^0.16.6" -ty = "^0.0.78" +ruff = "^0.16.7" +ty = "^0.0.80" poethepoet = "^0.48.0" pytest-cov = "^7.1.0" From 5ae3f2b7ea5775347d6f217da1a9dd861cec196c Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:22:54 +0300 Subject: [PATCH 15/28] Keep pending discovery alive until node is actually removed --- majordom_zwave/controller.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 118ae5d..7e1927f 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -106,7 +106,6 @@ async def start(self): "node removed", lambda data: self._create_task(self._node_removed(data["node"])), ) - # S2 auto-grant only — no PIN prompt / no discovery raised for it. controller.on( "grant security classes", lambda data: self._create_task( @@ -483,19 +482,33 @@ async def _close_pairing_window(self, duration_sec: int) -> None: await controller.async_stop_inclusion() log.debug("[PAIRING] inclusion window closed") - # Anything discovered but never claimed via pair_device gets dropped. + # Anything discovered but never claimed via pair_device: try to undo the + # inclusion. Only forget the discovery if the node actually left the + # network -- otherwise it's still a real Z-Wave node and the user must + # still be able to pair it (or explicitly remove it) later. for device_id in list(self._majordom_discoveries): - self._majordom_discoveries.pop(device_id, None) - node = self._awaiting_zw_discoveries.pop(device_id, None) + node = self._awaiting_zw_discoveries.get(device_id) if node is None: + # Nothing to reconcile against -- drop the stale discovery entry. + self._majordom_discoveries.pop(device_id, None) continue - if await controller.async_is_failed_node(node): - await controller.async_remove_failed_node(node) - else: + + if not await controller.async_is_failed_node(node): log.warning( - "[PAIRING] node_id=%s joined but was never paired, can't force-remove while alive", + "[PAIRING] node_id=%s joined but was never paired and is still " + "alive on the network -- keeping the discovery so the user can " + "pair or remove it", node.node_id, ) + continue + + await controller.async_remove_failed_node(node) + self._majordom_discoveries.pop(device_id, None) + self._awaiting_zw_discoveries.pop(device_id, None) + log.debug( + "[PAIRING] node_id=%s force-removed after failing to pair in time", + node.node_id, + ) async def _wait_until_ready(self, node: Node) -> None: """Waits for the background interview to populate node.values.""" From 48d56cc2bb3fac1fe36d0c11177ccb6c05b62fa6 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:37:56 +0300 Subject: [PATCH 16/28] Files fixed for the template --- .gitignore | 53 +- poetry.lock | 1280 +++++++++++++++++------------------------------- pyproject.toml | 93 +++- 3 files changed, 589 insertions(+), 837 deletions(-) diff --git a/.gitignore b/.gitignore index a26d7d9..bffc3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,53 @@ -.vscode/ +# Byte-compiled / optimized / DLL files __pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +*.egg + +# Unit test / coverage reports +.coverage +.coverage.* +coverage.xml +htmlcov/ +.pytest_cache/ +.hypothesis/ + +# Environments .env -.zed/ +.envrc +.venv +env/ +venv/ + +# Poetry +# poetry.lock # uncomment for applications, keep commented for libraries + +# Type checkers +.mypy_cache/ +.dmypy.json +.pyre/ +.pytype/ + +# Tools +.ruff_cache/ +.ropeproject + +# mkdocs (only relevant if you kept mkdocs.yml + docs/) +/site +.cache/ + +# IDEs +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/poetry.lock b/poetry.lock index afdf111..04e9fdc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -224,153 +224,6 @@ winrt-windows-storage-streams = {version = ">=3.1", markers = "sys_platform == \ [package.extras] pythonista = ["bleak-pythonista (>=0.1.1)"] -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.16.0" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "coverage-7.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36aed4951aedf04cbe9465e76f8e71219980a52b73d07afe69746cba6ba7b97a"}, - {file = "coverage-7.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb953835dbfa6d641ac3943e0986bc680f8abbdc2985af15b46c54985347146a"}, - {file = "coverage-7.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97051c4903689b1afedc2a354d6118223051e03588078b53048603bda9014577"}, - {file = "coverage-7.16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:770d4244c423dcafb5c31db393f429fe952b1bba23bbff7cc3886f8133769ba5"}, - {file = "coverage-7.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e7de0cb87960c6c9b5cad760068dab767b2b49a3b9376e1992c1e2691a015e"}, - {file = "coverage-7.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c2c45ee1853668f0ea1a0ddff396421c9dc5ad25a56bfb94a895970c2d8e7c2"}, - {file = "coverage-7.16.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6b2b9599e7513b0a9c5bf0357f9f8deaa4c2c821025b0693d420e6602748981"}, - {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fde65e0ea945920265dfe4a2108fc45eee2e2ea3d9c3073af6373ff9836aa71"}, - {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78103e79f9378cb0e43ddaa728629a373c070df903c5dfa98b63ba2cfb4e8c42"}, - {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e40e323711b485592354069b1c027ef879cc2d11657eac09a6e5ad0b49ab7406"}, - {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c94ef980f7b94d9dab9dac076d44ca706654cd51bad19734e029084adf528c8e"}, - {file = "coverage-7.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b37ad5cbb77776f446e1b55b461eec2eef5c3e7130c72dc0e1447c3a9da2d199"}, - {file = "coverage-7.16.0-cp310-cp310-win32.whl", hash = "sha256:9421dde689e68d9fd2b6cd7d8c4498e79b5431467b6298517e3f3e60fdbe80a7"}, - {file = "coverage-7.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:81d63b68b26304e3668edb103311c17fe13c2ed1c7fe973309819f27bf61c5b8"}, - {file = "coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d"}, - {file = "coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db"}, - {file = "coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3"}, - {file = "coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4"}, - {file = "coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719"}, - {file = "coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3"}, - {file = "coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a"}, - {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c"}, - {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9"}, - {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d"}, - {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6"}, - {file = "coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce"}, - {file = "coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce"}, - {file = "coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a"}, - {file = "coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7"}, - {file = "coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5"}, - {file = "coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831"}, - {file = "coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19"}, - {file = "coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44"}, - {file = "coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24"}, - {file = "coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352"}, - {file = "coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06"}, - {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4"}, - {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02"}, - {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c"}, - {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb"}, - {file = "coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e"}, - {file = "coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91"}, - {file = "coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950"}, - {file = "coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b"}, - {file = "coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42"}, - {file = "coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13"}, - {file = "coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5"}, - {file = "coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354"}, - {file = "coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261"}, - {file = "coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a"}, - {file = "coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87"}, - {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292"}, - {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa"}, - {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21"}, - {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f"}, - {file = "coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17"}, - {file = "coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67"}, - {file = "coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449"}, - {file = "coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c"}, - {file = "coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697"}, - {file = "coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883"}, - {file = "coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575"}, - {file = "coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62"}, - {file = "coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0"}, - {file = "coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f"}, - {file = "coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6"}, - {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9"}, - {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3"}, - {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06"}, - {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04"}, - {file = "coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad"}, - {file = "coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af"}, - {file = "coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d"}, - {file = "coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f"}, - {file = "coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3"}, - {file = "coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7"}, - {file = "coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c"}, - {file = "coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22"}, - {file = "coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e"}, - {file = "coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5"}, - {file = "coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313"}, - {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867"}, - {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267"}, - {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a"}, - {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b"}, - {file = "coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd"}, - {file = "coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61"}, - {file = "coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3"}, - {file = "coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11"}, - {file = "coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024"}, - {file = "coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c"}, - {file = "coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633"}, - {file = "coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b"}, - {file = "coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f"}, - {file = "coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68"}, - {file = "coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be"}, - {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a"}, - {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae"}, - {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee"}, - {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f"}, - {file = "coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73"}, - {file = "coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889"}, - {file = "coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627"}, - {file = "coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9"}, - {file = "coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025"}, - {file = "coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3"}, - {file = "coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a"}, - {file = "coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178"}, - {file = "coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2"}, - {file = "coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0"}, - {file = "coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b"}, - {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0"}, - {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e"}, - {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54"}, - {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb"}, - {file = "coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897"}, - {file = "coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743"}, - {file = "coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3"}, - {file = "coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc"}, - {file = "coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73"}, - {file = "coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd"}, -] - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - [[package]] name = "dbus-fast" version = "5.0.22" @@ -573,14 +426,14 @@ files = [ [[package]] name = "idna" -version = "3.19" +version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, - {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [package.extras] @@ -598,18 +451,6 @@ files = [ {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, ] -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - [[package]] name = "majordom-integration-sdk" version = "0.1.6" @@ -633,348 +474,331 @@ cli = ["rich (>=13)", "typer-shell (>=0.2)"] [[package]] name = "multidict" -version = "6.7.1" +version = "6.9.0" description = "multidict implementation" optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, - {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, - {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, - {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, - {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, - {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, - {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, - {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, - {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, - {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, - {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, - {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, - {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, - {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, - {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, - {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, - {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, - {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, - {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, - {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, - {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, - {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, - {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, - {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, - {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, - {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, - {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, -] - -[[package]] -name = "packaging" -version = "26.3" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, - {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, -] - -[[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "poethepoet" -version = "0.48.0" -description = "A task runner that works well with poetry and uv." -optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main"] files = [ - {file = "poethepoet-0.48.0-py3-none-any.whl", hash = "sha256:98da6096d060f49b8d84034770265863fb7dc92a40233b7694b9d216ac68737d"}, - {file = "poethepoet-0.48.0.tar.gz", hash = "sha256:a06f49d244fadfc2e2e7faa78b54e64a9694727e4ce1d50e08f23cea3ded74f1"}, + {file = "multidict-6.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:63c9da87fa6d4284e57d2a6e549dfe8f2938ab578fddab3f8ed9742812c9d0f3"}, + {file = "multidict-6.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3663ce6cb19c5e3123c9aad5907804a42d384f8cd4b67e59291efc9480f8da9a"}, + {file = "multidict-6.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7f340a4a6ad4dacb0f866143452d794ea32a0d9ddf79d93ecb4c3ac5515d0d4"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf44632149302f7fdcf5baecc277d816ab77a47a268f2392dc5d2d01c56770cf"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:095b091158c89a03ad1237b4880f857d27628f93c23da1616ee5640011439c53"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3958fa87a161a6b9c6661606784abdd8f1afa21a0d6e99d1b99512e70e6b4f1"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f99e0eec05aca200b66735a38f8de5c9be03a6afd62ba7f28860203d936a03f"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d98efa7b004b67cb8cca26b218012efa50c7103cf4ae54898f2bbf89a3c62f"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a23926c06835ad71e6bc755ebc07ad34655847de63c01a5d06b6501997712c4c"}, + {file = "multidict-6.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:06723cd13ace09a0ce1ed010fc9e57cd762e57ee23691d6cdb13b7fcf1a35468"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:27dee31d6f3a0d2be018b71721adbe24aed9b08bdc69d133c88eead07beee600"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90d341b2c64eacef24fc0a35aaabc5227834ce93bb1da416d61601663df29456"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:986de92227e44b257fef664a69613a4346a090553c186d0ae3bf15ceab447f5e"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c8da6d6fc78f971ebb9e29563c891a429374861afda55e4692a7962e3822637d"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8903ce696f0aa9dc301c5621fa0f0e24e985d4e6cc247cd63bc9abcd5a081628"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bf345dacf96fd9c698ad30c620fbdbe6e1edadf83944203ce5b5688f52e3d141"}, + {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e63e05228c1f1c1bef09c08abd2f97b6fe597c9ca14a1c94bec4e76b4664046c"}, + {file = "multidict-6.9.0-cp310-cp310-win32.whl", hash = "sha256:f25229fddbd6bb3d47f1141c319bead19fa3c9e62790c6d51201564fde05fc2a"}, + {file = "multidict-6.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:05f197459db34dee24ae7e3560affa5c6f93989805899c9bba13358b5116a12e"}, + {file = "multidict-6.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:d4464437dbbd7a5a71a5e86f2a9ec92ccbb8d96706f5a173ca985175f23adaf9"}, + {file = "multidict-6.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e46237ef18c8ee183dde740f0174d6ecf9c26d362b506b5d23fb57bc3634bc0"}, + {file = "multidict-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e33fabca25633cf0e4f9dd4495733d09b7f0628fc9424c5b5e58da644fd4ab94"}, + {file = "multidict-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e82873b56ed37b328501fb063962299ea6f7fc1caf1a5c47966028824bde0807"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e2169fdb886288fc6bf9d8e48abca9487f8b3ace1f5ab269b074864d212351e"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eefec2a6b0381846ce54d88f30de4b3a5c15ff0bddc5264cccba85b710a8785d"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c63013a210a09d4a8f30814b785450f1d0a2aaef8230c1e068e83f3afceb6030"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fd82f81dd0676059f9bc3a418a79f27feab19486100a255bf8488cccb3e60d3"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:deea26b6979ce46f2fb3b2db9b651e12eb7f94c26a1ee457b79f66ad36a64fd1"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58a4aee7188676c8317e3131bd844faf70e427979e1f20b8598c61f6cbd8b073"}, + {file = "multidict-6.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eee11705013394ebeeb383c92beb0fc9a3c449ac3382a6ee3aa23b38c6b06c3a"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51e77d089f82ed2e288d3a1cbf6cb7ffa927c8055d8ef61d57b27f4d8da557b6"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:79640bb8746b703971fb16569e7bfa82d90548137cf4094769aa45cbafda9d9a"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:092214366f729803010a5c260f4a4f44d223c688641dfc27da851588ac762ffe"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:da469a672bf3d343eb6757d53ffbac36f1a29b93a9fe76558b5d0fd7a7efa599"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4216a8b4214a1ee4511c551c59d51534637394e9433765fbd8face86db43c532"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f05f5b3171e23801e72fd818de236be9f239b10ba699f23e43bb9596651f8b34"}, + {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0c69b098c85685ba39efef1d8a9711243df8a4c4b051a35d3cce7f323381f8bb"}, + {file = "multidict-6.9.0-cp311-cp311-win32.whl", hash = "sha256:855be03f39b97e352d54eaa7b14ba2a46846cf14719ddb689cdaaee811462440"}, + {file = "multidict-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a8040dde3fe35ead6b04adf3cd3fdd3bfc7c941505775d230b4501d18227c77b"}, + {file = "multidict-6.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:70b3097b838d9fc336f426c7aa7d45b7881ee9b06a10f4bcffd0dcda0cbd44e5"}, + {file = "multidict-6.9.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fdd484b84d3394e805689c56be3ab1f877ae7ff0eb9ff90a3ee7a755cbebab4f"}, + {file = "multidict-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f93c9058a0eceac0df2ce9d4c8823b84786ee598194753c2ca0c100224405e47"}, + {file = "multidict-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c7ab60b91e11b25e7682c5cd8763fdd17929ea83f234ba441091f1492e631ea3"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:640113258c5925a9eed2c12523410b25565ac5df2fa6735fbae88fb09bcdd212"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ea72901860ccbe94517421681c60b13533ce03ba2f7bd96293c3a4d16ac4ccb"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:408fac672931f2458be3bc8c89d9facd16dac2aad17c7cee2ca1693eee99f07e"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:67bed23e9803945b0760650ec3e903af772c49abe869f3bc03c66b2e7649d5ef"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb769ccc72e15d7d441e1a08f169d418376be77cdc387e813129c26b357fe50"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a5111a2bd824c821a3dd09da29680391b0caaa18fea7761358f4001e6898d1c"}, + {file = "multidict-6.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e991677c4bdc5d9f2e71c74717a4e32cfe98930ca05becdf722b5eae1329d6a"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb69b724c345420ba49187a17a894f146099f5b2e501df42ddb4452ac8be37fa"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cff3cff5a725bdb8359962de8d7429aea592d7693dbd197eeabbdfab9b6300e9"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fca5b74b5909c29041f857c40d51d9273636fb4221cf020e4c452deb1c448a40"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11e32ccf23cdbfcf8299a6a825930a858ec9a6aa05d6752d0f90f2bdf19489e1"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ec1e387b1f8a85ae5b94aa8c4e0576912ffa4d31bd0578f24c950d4f05ee476"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4826f6b56456fb1e98111d7bc20cbdc7fa0a41b1f9ad80ff2dd3f2f5b226fe1"}, + {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e71a072c52c78b7f97cd4611df6cef10977e4f2367cd0654a7626192f931adde"}, + {file = "multidict-6.9.0-cp312-cp312-win32.whl", hash = "sha256:95d339c3b75b4a50c665bdcf8417428cd71c3e5cd48e194cb1d336fcb856beac"}, + {file = "multidict-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f06c4da5315a6f709b13408c3e13f3b475f8c559ec7608c3c67062512871235"}, + {file = "multidict-6.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:0db5bf96ec2ce45a8bc7fbbe8a486089969bb2791a66b6789ee3aed0d5dd562e"}, + {file = "multidict-6.9.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:662315f8621b3559268813b11134c1f9633edd4ab5f7b713688a243c13f4026d"}, + {file = "multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4bef8cb5edea9c9daeb8396a75eeb97f912c3fb3fa408fe659edfecf91e47424"}, + {file = "multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:75f7fc21ffce9a792cb919f0e0b0b52117bd672f2a55d1929d573b6fc437f374"}, + {file = "multidict-6.9.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bb6be697065cbf31051465f9939d65624573d2219c3da8316db9e7cf5e0f5a7"}, + {file = "multidict-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67bcff396d2ad62197c95488a716a88462792b384b5ea6e44bf7c1070eddb8fc"}, + {file = "multidict-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:beda95a6bd0a2e2265b941b90ff72543afbeaebb6a450e3a6a010d10d4a2ffca"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e78870909e9f9e3d672ba99f1eb75130d7e01c42e642e6a70f434df32ca0ee1"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91092d597fcf0940cd6a59e64ba7156ee22d6899993fd7c5b1897fda71482f8f"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be612f23990a261060ccd8f15e89dad1f9b7bb0c1021c5c3987f5fc57959391"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7aaa14f0b9ffa2780c5d3b21da0e9b58778ed47af3369df72e5fd6dc10ff8041"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97555ad30ad20a8eb90fa86522088eebbbd68aba03e56d53f4850b3535c65e61"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c51fd8d59e72e45c64907bf7f81fbd94f4c8dcd1c8b2f23f4c2d7ede788fcfc3"}, + {file = "multidict-6.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22067e88ff266e6a5dc59114529332692a01cc04b10dbdc2c14ab91217dee819"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e231de8ce43d4fd10bec4f67f71238ed88a974de30a9f20be7a4ab16c970a988"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b33e499a7b1f722547d57b4865ed68c03161643b9afe49fe097a0832078d183d"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1eb7939025bd9289d9dfe3a399102b1642f4fbb105a6af82f284052ee89e97a9"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:51a08dceed4b42ef25755ee2cbf50df325e90e6b415d00955ffdea2c394ad6c0"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb847cb4002e725f88ffe29883da8290fd0754fa0461ed8988735e20964a9f65"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:516fa4817cd070088f616a901380db56189f86daee9da79aada3c9b653f49ed7"}, + {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:254e53be2ec70518bb82dfa9b0c7166baaf2bd0e918bd17b65caaf489d5a5522"}, + {file = "multidict-6.9.0-cp313-cp313-win32.whl", hash = "sha256:9869105ab61db13004f9ed610cd29ce4237b2c609f5f7b8bb8f2339fb9dad17d"}, + {file = "multidict-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:85ed0f3c3b01174aea5a8a8fcd456f64c2e9719c0f42c612c2694ecd35974a65"}, + {file = "multidict-6.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:1126782e3c3b1a7ccd990be3d3221709348e4b07d6ecf8b50965a93ae2624145"}, + {file = "multidict-6.9.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:1b34b10b615db5a3ed424b174a362a3cdf06220bd892d03d151a619bb157c4e9"}, + {file = "multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f7f0b996a20776018a253061a781ba983e7b944e17a0748822026048b8b0bb07"}, + {file = "multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b612842f7f286c42abc5dcbda1db50e9dbed39720dd15feb07dbc9974bfeaa7c"}, + {file = "multidict-6.9.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:71ee2b3198323cbff7a846538f01599f309dddddba79b5bf5a3dca2e49460ef6"}, + {file = "multidict-6.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0dc1f9c7dc07d86e5a4ed24f3a55b945106f3e7c70a984ff8dd9267270414ed"}, + {file = "multidict-6.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a26c8672d70d2c12c2f1c4df91fe92422e905efaf1d639bd4d7192868a21e711"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:186cc28558e3d6921ba797e433cd58ce64eb8b2f553e93f75dccbb9a604b7457"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebc02af6ac170631774219c26fd63b4404d1e836a1aa92b738a3127eaa2aebad"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62c4ffba9f81a2b17cf081bfe4e6b7eaf17193dc17d5870c01b2763e48efed24"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a7e2aaa0e9a6413b54cd90fa0fe5944487872a7130a1c07347627974e677a34"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26e34dbbd6c3438ee19e0db1f5b846a7ca957085d193ec312b488924c3941a0d"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d811799f821c06883404a171faa880660ef04c2687fc32fdaef22fb645fe80"}, + {file = "multidict-6.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fde003cec43783193360c27de2fbb0c8ea2db1cfebc193c3fc657c436f9d38"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9bfa4ba6d97ae6d9c1f5336c2abad5690bce1802a040f9aa5fa39cb0b3099c02"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a776fbaf700549943b647f21479d69191c238c84fb55d5a48eec1b4cf6cf6e00"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ed04614c47379bcdfa01d3e165149518017ea034cd5f48e203c287cc3d2ebd4b"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9c65d9912983991f368bd01f993191b1593c90ecde6be990d73add46084d235f"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4b89b102be78ed286123e75a30efda519e1ef6b9d4a4d01a08213f5e01a18e45"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e3ed6563ea143214c63bb4452ae5e8787a39226b22101c179fa4d2eda5972267"}, + {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:448c05bd13f8675b3629fbc3fabb9e7bb19a5636a7a3eb677ea25a08d0544a56"}, + {file = "multidict-6.9.0-cp314-cp314-win32.whl", hash = "sha256:2b5db4f2ca34fce55d34b9c66a0eb5d0d01962e2420a1218ba836d8e46aa6ea3"}, + {file = "multidict-6.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccf9f7f07e9ab6e24f68ccb2b4da8ded65c1fbc5390ebd897a5fe55cdd4c064f"}, + {file = "multidict-6.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:64ef289ca3c6eaafaa705978686631cb5acc90a92a09bef39c95a810c5f4b804"}, + {file = "multidict-6.9.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f17f485e1837a7d31216be8be991a51265bcfc3cfd2457d5c94ade6086793ad7"}, + {file = "multidict-6.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3fccacf47e756a0f487b2b7e4afc9f167db0770ecc4e887b38ae2aec99dcb510"}, + {file = "multidict-6.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:527655c10fca73fd336cbcd5ccaaba5d8ded1512a3183ed7ebcc1569db5f1de3"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d3520f774e78dad488c97398e5c7253333baa4eea09785aca56d477c4dc05c7"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518a76bff2567fb0941603b9ccecdd830dd2d717605e7a28c1edc9e64688759e"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff308def558cbbff74ea889ec659d117ac550d4d554467ed6cb3bf6adadac7e6"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:875a77927cd7bc0bc1d0a0520a7ed2267bb169c5db03cc133eaa746526d4182b"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2182da6ffc9793dd66d7d3632eea2b131f1a1e26eaedd51956845501830d84fe"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e680a0b07e44b87a975eca63f84860a772667b0037066a6e93d998eb41ad2814"}, + {file = "multidict-6.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5066b7877e1e2ae2c3712c8037eff40a80d4cf35b393b4c42c81f1c4b73b346d"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fd0ec96e9877cb4dddf7942dffa777837809663f8e55d4f79a2bad06453446b0"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:98b0738ac1a1593f609a7b67d25e09c7de57c0f91c2879eb01c46b2dae55c0ba"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0548fdffa634c495b866d89e02ae58a9588cedaeeb972f6ced433e20dee1db5f"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7d0649ae61a341b9a47a2afab57424bd2a0ce98d36ec170ce9a51ce92c7284f6"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6d470d693ff45678b9ab237113546f6dc18918be55b3c852a69ed05ae7404a8"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ac05ce26d2c902d2eb5afe70f2012d19f3b9510ed5ef054bab27a3d43bc7f39c"}, + {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e82ea3ca4409c20172074f27dc1ba5858174f143d0ee637dd058b8061ff024c6"}, + {file = "multidict-6.9.0-cp314-cp314t-win32.whl", hash = "sha256:d61680c1969b5b1b900fece510077e7732336eae02f68dda02162e04ff6ff321"}, + {file = "multidict-6.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a9af5d28024c1de9e0efe06a64b5a8561e6517daa66788169e60d2c588e15c5"}, + {file = "multidict-6.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:810ae53369d957fcc3e3855612e3a35a39960adaf4b3b1cb4cb4a2e94185bcfe"}, + {file = "multidict-6.9.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:b9bcbd708bb5e1ce8cf9c58908ae9777a90f777406315b046bb10cb3e87f8621"}, + {file = "multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27ed5aaee62eeff74786a3259de81ada4f2bf84b75122bc06d7d8eadc1be5643"}, + {file = "multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30ba80aa7ec7340e80140d29f563e33acb21f3756e42df13612f2a26c80c90ba"}, + {file = "multidict-6.9.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c26e56c68bde47f8028265b3829915450c944febc75a1e4d0365b8a75557861a"}, + {file = "multidict-6.9.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:f6705ae71e4b14c693877be3cc09fc62af10197ca08ee08bb57ffce6e0f9c5f4"}, + {file = "multidict-6.9.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:1fc7b1ce735737b7abf38935928729433ef2c81306dcab7f360ef3c3076bc8e9"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7731a591920d77bd9372a9a8ab3f447129dbf17d29b10928c94005e1606641c"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a9295177c33756d7a6f8da0f1db345ac951f2436be6ad4b90f53056526e767"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b4ae468e341cc84c1ae035ee12f032cc0716f618b1eb417fb14e8454a17c620"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb9f52b89e339007a06c88cb95068e9d612ce252baaaa5ece85de96e2d51fca9"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69190050f74fe139a7b3759c730e4e87484db9cf74b1786910a24496ab7a40f9"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5f04bbfb064b87455a8012f5829b20201d608cf4b3a1a0e50c7a14173a75109"}, + {file = "multidict-6.9.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d81d0ec43a73056458e844cf90239d2c3a7aeb4ef05fea8c0f64775d5111847"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b8aea3d54f3cc20a557fb951c68294d9aa88d614cf7151aa5629b1d5e4033b74"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:31fd2961bd754ac3177aae9438c01becc22eae029911f13c558e5767a10e5dc2"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:04e6f4eddeff93c778a9e7137568284f28856d960275085fcbd87cf77a53a057"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:319160c73795ce31e86494773e0f12c446d32dae7d1a76f4829007ef69eb80bf"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:0d7fbd38d52119423ed8c69672ca47eac121062c30c976e00ba732d114774874"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:981280a946256fc151584fb3b82b99db091fccac81cfecce530585db559ec62c"}, + {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:fb3d1f3c63d49656a83e1604ef3ac573016cf2e62da84d1937f47107d5689c8b"}, + {file = "multidict-6.9.0-cp315-cp315-win32.whl", hash = "sha256:0c5388c41bc7c551548c4ce840349f0708f7ced6df877d82900bac82268160fc"}, + {file = "multidict-6.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:49e47de3823bee4896f1eedc17ce4fc4f42c433bab168ece7911f42e69a4f666"}, + {file = "multidict-6.9.0-cp315-cp315-win_arm64.whl", hash = "sha256:1ac339b944616d4061a9ee9571c89767543db38c19c8c7917a9c08b1462c4800"}, + {file = "multidict-6.9.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:cb6be563dd43b2d4d528afded1878b559ef1c2f45f89e94d273ef515485727f9"}, + {file = "multidict-6.9.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:76f0df301516563085342d9cdad03fc3c0301b36c3cfa8454e49ca73878aa59c"}, + {file = "multidict-6.9.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:34c00a534e948eb5f8b3c50cacea9daa4fc4dfd503f6edd114bb532f53742c9e"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cce5e33383904e32b58bef7765c38917899a9dc87058f75c421c82e7c11440ee"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c375408881861294207c1d038a711120776989c5e8c489b251a818cd74d39886"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f95940278766f273b668bbd9dd412ab5dd170ff40fc836b714c38de7852f1fc"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6009b58d6994881c53f56d2f2010f0b3cd30139c7b14156c2329d00c0d592e8e"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7d5ab831301decfb7c29d1c243823f6ccbb3766117c5957bdc5b9f283355d0f1"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17b74ffc94e63843cf34b7eb874ab1653afac0eda3edfa3de1207db7fc8c0ed9"}, + {file = "multidict-6.9.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62dc471198ac2f1085f6c6ba9aa5ad3343c0cbac0756d2fbe551659d55e14212"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:da97c0a501c5da00d9c3af6d481ed97dace148f9030f4f5578534981d58682bb"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:986f2a23330e8823b389872ae63b83b862a10e63e8cfbd11b42ab8664d1eaa7e"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ed96992cac94b32a9c3da5870b05967271397a813029d68197939397860b1470"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:94e767d30be457da0ef134e688cd2b6a438d19b85bf000b1c4a2b9b6f55b4211"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d1f98df09bc22d298d88dee07eb5365dba1fbf3b41e876a6edb2652a8b3a6ca1"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c5290038269f7d96037afc47b05b286e4791adfa7046911e2a0483b6220c84fb"}, + {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8858d2ddd0bc21b4aaf189016c95840b29ddf0236a049af70a9cb4379bef9abc"}, + {file = "multidict-6.9.0-cp315-cp315t-win32.whl", hash = "sha256:f90724dade5865b1c9429d9b058f11096e44a7f1f0e0a21eff18d98ef32c22aa"}, + {file = "multidict-6.9.0-cp315-cp315t-win_amd64.whl", hash = "sha256:150f1e5bcc14234e99aacc931deafa52713157df947efeecc1a3b7601f2714ab"}, + {file = "multidict-6.9.0-cp315-cp315t-win_arm64.whl", hash = "sha256:734c985bbd60e84d0ea523bafe139394e46b8a3f35754a70111dbcbba243f7f4"}, + {file = "multidict-6.9.0-py3-none-any.whl", hash = "sha256:57c2445049f7d8e66306f712868219da7ff7168ef42263dc032401211bf1205c"}, + {file = "multidict-6.9.0.tar.gz", hash = "sha256:d7d32c0543494efbc9394e2b571725071d08e295993486bc9a43f6f89375ee01"}, ] -[package.dependencies] -pastel = ">=0.2.1,<0.3.0" -pyyaml = ">=6.0.3,<7.0" - -[package.extras] -poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] - [[package]] name = "propcache" -version = "0.5.2" +version = "0.5.4" description = "Accelerated property cache" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, - {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, - {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, - {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, - {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, - {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, - {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, - {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, - {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, - {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, - {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, - {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, - {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, - {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, - {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, - {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, - {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, - {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, - {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, - {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, - {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, - {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, - {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, - {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f"}, + {file = "propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8"}, + {file = "propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745"}, + {file = "propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd"}, + {file = "propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9"}, + {file = "propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858"}, + {file = "propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161"}, + {file = "propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8"}, + {file = "propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb"}, + {file = "propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c"}, + {file = "propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25"}, + {file = "propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77"}, + {file = "propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b"}, + {file = "propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea"}, + {file = "propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983"}, + {file = "propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57"}, + {file = "propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97"}, + {file = "propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12"}, + {file = "propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38"}, + {file = "propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123"}, + {file = "propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3"}, + {file = "propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad"}, + {file = "propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec"}, + {file = "propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432"}, + {file = "propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e"}, + {file = "propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468"}, + {file = "propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558"}, ] [[package]] @@ -1132,21 +956,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" -[[package]] -name = "pygments" -version = "2.21.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, - {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - [[package]] name = "pyobjc-core" version = "12.2.2" @@ -1242,197 +1051,25 @@ files = [ pyobjc-core = ">=12.2.2" pyobjc-framework-Cocoa = ">=12.2.2" -[[package]] -name = "pytest" -version = "9.1.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, - {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.4.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, - {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, -] - -[package.dependencies] -pytest = ">=8.4,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "7.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, - {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - [[package]] name = "python-slugify" -version = "8.0.4" +version = "9.1.0" description = "A Python slugify application that also handles Unicode" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, - {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, + {file = "python_slugify-9.1.0-py3-none-any.whl", hash = "sha256:ab2d1d0e7ad5fa46b3756f0f3fcfb37d6d5baa006ccd1f22c2481a6ab53b8c12"}, + {file = "python_slugify-9.1.0.tar.gz", hash = "sha256:3f02e8a0639c61e37eca5a783aae09112796d15d331ee3d5372241b61ee05f1c"}, ] [package.dependencies] text-unidecode = ">=1.3" [package.extras] +anyascii = ["anyascii (>=0.3.2)"] unidecode = ["Unidecode (>=1.1.1)"] -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "ruff" -version = "0.16.6" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8"}, - {file = "ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32"}, - {file = "ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757"}, - {file = "ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953"}, - {file = "ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718"}, - {file = "ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25"}, - {file = "ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39"}, - {file = "ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5"}, - {file = "ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050"}, -] - [[package]] name = "text-unidecode" version = "1.3" @@ -1445,46 +1082,17 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] -[[package]] -name = "ty" -version = "0.0.78" -description = "An extremely fast Python type checker, written in Rust." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "ty-0.0.78-py3-none-linux_armv6l.whl", hash = "sha256:122700b98f9d45785c1ce91a9418154562e7f64f4bf34679f6f7b38c5b97453f"}, - {file = "ty-0.0.78-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbe7e3709ccf29ef3d9f58f5914cc30ec36fb647008a5a4ff8483abe401bfe8d"}, - {file = "ty-0.0.78-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c3528897b3ab9d3589561bc2b5e61a4d5d686de527a4400bb09a439b922a6c70"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bfba4c44a06484093f527b8ef43a317abcf91395b49396170266629eedf74aa"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f903c06fdee17baf8373173039ab28f1bce28e66b1c734929a5f50b37eb54d9"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd83e5fe3f07291d1bd4e591f8d2607c204f3eab53bcb1b8060c40e876e1aa61"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325285377319cf7168a2b8b771ae5027f411530e3c7eab1faf4583cdd72fd7c9"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b7846a404da6492697b524a769755ee9bee157d67674063ecd9c5f69dc52ff"}, - {file = "ty-0.0.78-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:169d3b9d134c0b48fe1af8a844142d374655552054a9d6c15a4b6e51bb0382ea"}, - {file = "ty-0.0.78-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6108cb3b2d28dac5981d4e25008a0a5547d8c877a67f6da9e8c38e7e946be44f"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:589ad03608d9d2975ef4b23c41c4f847e325bcc7686f51d4c66066b8dbc18a2a"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b3fa0786edc1af06030f872d83499c0cea7c261dbbb651e0aee8306bf2c8a869"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92c5639befc577578c8abd4e5a7fccf98d828db98b09e8d4dd81605dc0e54aef"}, - {file = "ty-0.0.78-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0dce70bc51652b2775debd1d1e422fb0179f5207c73deb4d5d0aca37e5f61695"}, - {file = "ty-0.0.78-py3-none-win32.whl", hash = "sha256:1c80976ca9185d7a9d1baab1fb57240331b8dac58d3b11e46200284086a6de8d"}, - {file = "ty-0.0.78-py3-none-win_amd64.whl", hash = "sha256:32e82b704471eab34f67b51c151660ca8a00815977b28278905d76fba54f7415"}, - {file = "ty-0.0.78-py3-none-win_arm64.whl", hash = "sha256:3a14d641a3c04fa9a80f2a46be1531d915f60d4fb79d4b894627bbe46bb35d64"}, - {file = "ty-0.0.78.tar.gz", hash = "sha256:770b45854f85fa11595208f08c0f28df80943164d10a2832d86be6ac29f135b2"}, -] - [[package]] name = "typing-extensions" version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] -markers = {dev = "python_version == \"3.12\""} [[package]] name = "typing-inspection" @@ -1824,116 +1432,150 @@ all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt- [[package]] name = "yarl" -version = "1.24.5" +version = "1.25.1" description = "Yet another URL library" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, - {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, - {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, - {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, - {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, - {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, - {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, - {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, - {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, - {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, - {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, - {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, - {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, - {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, - {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426"}, + {file = "yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b"}, + {file = "yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b"}, + {file = "yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e"}, + {file = "yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707"}, + {file = "yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03"}, + {file = "yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa"}, + {file = "yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9"}, + {file = "yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233"}, + {file = "yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b"}, + {file = "yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3"}, + {file = "yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371"}, + {file = "yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c"}, + {file = "yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee"}, + {file = "yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f"}, + {file = "yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd"}, + {file = "yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25"}, + {file = "yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3"}, + {file = "yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7"}, ] [package.dependencies] @@ -2043,4 +1685,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "215e859439a04fc2bed9ed8f4c394ed89148c8d8c49f85b1bc4b21985f408eb7" +content-hash = "cfb6427d67fa3bca54fc26233be501c3d55b8a933b58594a2d78eaf479a54d2c" diff --git a/pyproject.toml b/pyproject.toml index f6ab423..fc092ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,36 +1,89 @@ [project] name = "integration-zwave" version = "0.1.0" -description = "" -authors = [ - {name = "Ehor Lukianov",email = "ehorlukianov@gmail.com"} -] +description = "Zwave integration for MajorDom — bridges Zwave devices into the MajorDom language." +authors = [{ name = "Mark Parker", email = "mark@parker-programs.com" }] readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = ["majordom", "zwave", "zwave-js-server-python", "smart-home", "home-automation", "iot", "integration"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Home Automation", + "Programming Language :: Python :: 3", +] requires-python = ">=3.12" dependencies = [ - "majordom-integration-sdk (>=0.1.2,<0.2.0)", + "majordom-integration-sdk (>=0.1.5,<1.0)", "zwave-js-server-python (>=0.73.1,<0.74.0)", ] +[project.urls] +Homepage = "https://majordom.io" +Documentation = "https://docs.majordom.io/device-integration" +Repository = "https://github.com/MajorDom-Systems/integration-zwave" + +[dependency-groups] +dev = [ + "poethepoet (>=0.36.0,<1.0.0)", + "ty", # type checker; swap for mypy/pyright by editing the `check` task below + "ruff", + "pip-audit", # dependency vuln scan — gates release (not everyday CI); run locally with `poetry run pip-audit` + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-timeout", + "pytest-repeat", + "pytest-benchmark", + "pytest-profiling", + "pytest-resource-usage", + "coverage", + "pympler", # object memory introspection, leak hunting + "psutil", # process/system resource usage + "faker", # realistic fake data for tests +] + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -package-mode = true +requires-poetry = ">=2.0" packages = [{ include = "majordom_zwave" }] -[tool.poetry.group.dev.dependencies] -pytest = "^9.1.1" -pytest-asyncio = "^1.4.0" -ruff = "^0.16.6" -ty = "^0.0.78" -poethepoet = "^0.48.0" -pytest-cov = "^7.1.0" - [tool.pytest.ini_options] -pythonpath = ["."] asyncio_mode = "auto" +pythonpath = ["."] +filterwarnings = ["once"] + +[tool.coverage.run] +omit = ["tests/*"] + +[tool.ruff] +line-length = 120 +indent-width = 4 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "I", # isort +] +fixable = ["ALL"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" +docstring-code-format = true + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"tests/*" = ["E402"] [tool.poe.tasks.install] shell = """ @@ -43,19 +96,27 @@ echo "Pre-commit hook installed." [tool.poe.tasks.check] shell = """ code=0 + echo "\nRunning ruff lint:" poetry run ruff check --fix || code=1 + echo "\nRunning ruff format:" poetry run ruff format || code=1 + echo "\nRunning ty:" poetry run ty check || code=1 + echo "\nRunning pytest:" -poetry run pytest --cov=majordom_zwave --cov-report=xml || code=1 +poetry run pytest --cov=integration_template --cov-report=xml || code=1 + echo "\nRunning poetry build:" poetry build || code=1 + echo "\nRunning poetry check:" poetry check || code=1 + ${ci:+git diff --exit-code} + exit $code """ args = [{ name = "ci", type = "boolean" }] From 50c5011813f3ca8d3c922ebc190ad770933eb764 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:38:20 +0300 Subject: [PATCH 17/28] Poe fixed --- majordom_zwave/controller.py | 121 ++++++++------------------------- majordom_zwave/mapper.py | 19 ++---- tests/test_controller.py | 29 ++------ tests/virtual_zwave_network.py | 16 ++--- 4 files changed, 44 insertions(+), 141 deletions(-) diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 7e1927f..fe56573 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -99,18 +99,14 @@ async def start(self): await ready.wait() controller = self._get_controller() - controller.on( - "node added", lambda data: self._create_task(self._node_added(data["node"])) - ) + controller.on("node added", lambda data: self._create_task(self._node_added(data["node"]))) controller.on( "node removed", lambda data: self._create_task(self._node_removed(data["node"])), ) controller.on( "grant security classes", - lambda data: self._create_task( - self._grant_security_classes(data["requested"]) - ), + lambda data: self._create_task(self._grant_security_classes(data["requested"])), ) log.debug("[READY] connected to %s", zwave_server_url) @@ -155,10 +151,7 @@ async def start(self): controller.own_node_id, ) for node in controller.nodes.values(): - if ( - node.node_id == controller.own_node_id - or node.node_id in known_node_ids - ): + if node.node_id == controller.own_node_id or node.node_id in known_node_ids: continue await self._node_added(node) except Exception: @@ -183,38 +176,28 @@ async def stop(self): # Hub -> device operations # ------------------------------------------------------------------------- - async def start_pairing_window( - self, duration_sec: int, credentials: ProvidedCredentials | None = None - ) -> None: + async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCredentials | None = None) -> None: self._require_client() controller = self._get_controller() if credentials and credentials.type is CredentialsType.qr: if not credentials.value: raise ZwaveUnexpectedError("QR credentials provided without QR data") - provisioning_info = await async_parse_qr_code_string( - self._zwave_client, credentials.value - ) - await controller.async_begin_inclusion( - InclusionStrategy.SECURITY_S2, provisioning=provisioning_info - ) + provisioning_info = await async_parse_qr_code_string(self._zwave_client, credentials.value) + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, provisioning=provisioning_info) log.debug("[PAIRING] inclusion window opened for %ds via QR", duration_sec) elif credentials and credentials.type is CredentialsType.code: if not credentials.value: raise ZwaveUnexpectedError("PIN credentials provided without a value") - await controller.async_begin_inclusion( - InclusionStrategy.SECURITY_S2, dsk=credentials.value - ) + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, dsk=credentials.value) log.debug( "[PAIRING] inclusion window opened for %ds with pre-supplied PIN", duration_sec, ) elif credentials and credentials.type is CredentialsType.secret: - raise ZwaveUnexpectedError( - "Z-Wave does not support secret-based pairing credentials" - ) + raise ZwaveUnexpectedError("Z-Wave does not support secret-based pairing credentials") else: await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) @@ -222,17 +205,13 @@ async def start_pairing_window( self._create_task(self._close_pairing_window(duration_sec)) - async def pair_device( - self, discovery: Discovery, credentials: ProvidedCredentials | None - ) -> UUID: + async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: """Waits for the pending node's interview to finish, then builds and saves the device.""" self._require_client() node = self._awaiting_zw_discoveries.pop(discovery.id, None) if node is None: - raise ZwaveUnexpectedError( - f"No pending Z-Wave discovery for {discovery.id}" - ) + raise ZwaveUnexpectedError(f"No pending Z-Wave discovery for {discovery.id}") self._majordom_discoveries.pop(discovery.id, None) await self._wait_until_ready(node) @@ -248,21 +227,15 @@ async def pair_device( if device.integration_data: device.integration_data.node_id = node.node_id else: - device.integration_data = ZwaveDeviceIntegrationData( - node_id=node.node_id - ) + device.integration_data = ZwaveDeviceIntegrationData(node_id=node.node_id) parameters = self._build_parameters(device_id, node) device.parameters = parameters - main_parameter_id, default_value = self._mapper.get_main_parameter( - device_id, node - ) + main_parameter_id, default_value = self._mapper.get_main_parameter(device_id, node) device.main_parameter = main_parameter_id if main_parameter_id and default_value is not None: - main_parameter = next( - (p for p in parameters if p.id == main_parameter_id), None - ) + main_parameter = next((p for p in parameters if p.id == main_parameter_id), None) if main_parameter is None: device.main_parameter = None else: @@ -347,29 +320,18 @@ async def identify(self, device: ZwaveDevice): ( v for v in zwave_node.values.values() - if v.command_class - in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) + if v.command_class in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) and v.property_ == "targetValue" ), None, ) if switch_value is None: - raise ZwaveUnexpectedError( - f"Node {zwave_node.node_id} supports neither Indicator CC nor a switch to blink" - ) + raise ZwaveUnexpectedError(f"Node {zwave_node.node_id} supports neither Indicator CC nor a switch to blink") - current_value = zwave_node.values.get( - switch_value.value_id.replace("targetValue", "currentValue") - ) - restore_value = ( - current_value.value - if current_value and current_value.value is not None - else 0 - ) + current_value = zwave_node.values.get(switch_value.value_id.replace("targetValue", "currentValue")) + restore_value = current_value.value if current_value and current_value.value is not None else 0 - on_value = ( - 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 - ) + on_value = 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 for _ in range(10): await zwave_node.async_set_value(switch_value, 0) await asyncio.sleep(0.6) @@ -395,24 +357,16 @@ async def fetch(self, device: ZwaveDevice): await self.dependencies.output.controller_did_receive_events(self, parameters) - async def send_command( - self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter - ): + async def send_command(self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter): self._require_client() zwave_node = self._require_node(device) - zwave_value = self._require_value( - parameter.integration_data.value_id, zwave_node - ) + zwave_value = self._require_value(parameter.integration_data.value_id, zwave_node) if zwave_value.metadata.writeable is False: - raise ZwaveUnexpectedError( - f"Value {parameter.integration_data.value_id} is not writeable" - ) + raise ZwaveUnexpectedError(f"Value {parameter.integration_data.value_id} is not writeable") try: awake = zwave_node.status != NodeStatus.ASLEEP - result = await zwave_node.async_set_value( - zwave_value, command.value, wait_for_result=awake - ) + result = await zwave_node.async_set_value(zwave_value, command.value, wait_for_result=awake) log.info(result) except Exception as e: # noqa: BLE001 raise ZwaveUnexpectedError( @@ -428,11 +382,7 @@ async def send_command( await self.dependencies.output.controller_did_receive_events( self, - [ - DeviceParameterChange( - device_id=device.id, parameter_id=parameter.id, value=command.value - ) - ], + [DeviceParameterChange(device_id=device.id, parameter_id=parameter.id, value=command.value)], ) # ========================================================================= @@ -467,9 +417,7 @@ def _require_node(self, device: ZwaveDevice) -> Node: def _require_value(self, value_id: str, node: Node) -> Value: value = node.values.get(value_id) if not value: - raise ZwaveUnexpectedError( - f"Value with {value_id} id for node {node.node_id} not found" - ) + raise ZwaveUnexpectedError(f"Value with {value_id} id for node {node.node_id} not found") return value # ------------------------------------------------------------------------- @@ -526,19 +474,14 @@ async def _wait_until_ready(self, node: Node) -> None: finally: unsubscribe() - def _build_parameters( - self, device_id: UUID, node: Node - ) -> list[ZwaveParameterState]: + def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameterState]: """Maps every Z-Wave value on a node into a majordom parameter state.""" parameters: list[ZwaveParameterState] = [] for value_id, value in node.values.items(): metadata = value.metadata parameter = ZwaveParameterState( id=self._mapper.parameter_uuid(device_id, value_id), - name=metadata.label - or value.property_name - or value.property_key_name - or str(value.property_), + name=metadata.label or value.property_name or value.property_key_name or str(value.property_), data_type=self._mapper.parse_zwave_data_type(value), role=self._mapper.get_role(value.command_class, metadata), visibility=self._mapper.get_visibility(value.command_class, metadata), @@ -590,9 +533,7 @@ async def _node_added(self, node: Node): expiration=None, transport=NonEmptyStr("ZWAVE"), device_manufacturer=None, - device_name=NonEmptyStr( - node.name or node.device_config.description or "Unknown" - ), + device_name=NonEmptyStr(node.name or node.device_config.description or "Unknown"), device_category=None, device_icon=None, ) @@ -614,18 +555,14 @@ async def _set_availability(self, device_id: UUID, available: bool) -> None: return self._availability[device_id] = available if available: - await self.dependencies.output.controller_did_connect_device( - self, device_id - ) + await self.dependencies.output.controller_did_connect_device(self, device_id) else: await self.dependencies.output.controller_did_lose_device(self, device_id) def _subscribe(self, device_id: UUID, node: Node) -> None: node.on( "value updated", - lambda data: self._create_task( - self._value_updated(device_id, data["value"]) - ), + lambda data: self._create_task(self._value_updated(device_id, data["value"])), ) node.on( "dead", diff --git a/majordom_zwave/mapper.py b/majordom_zwave/mapper.py index 5608de7..566836b 100644 --- a/majordom_zwave/mapper.py +++ b/majordom_zwave/mapper.py @@ -69,9 +69,7 @@ def get_role(self, command_class: int, metadata: ValueMetadata) -> ParameterRole return ParameterRole.control return ParameterRole.sensor - def get_visibility( - self, command_class: int, metadata: ValueMetadata - ) -> ParameterVisibility: + def get_visibility(self, command_class: int, metadata: ValueMetadata) -> ParameterVisibility: """Readable -> user, writeable-only -> setting, else system; SYSTEM/DIAGNOSTIC command classes override this for plumbing/diagnostic values.""" if command_class in SYSTEM_COMMAND_CLASSES: @@ -116,9 +114,7 @@ def parse_zwave_data_type(self, value: Value) -> ParameterDataType: # Opaque CC-specific object — expose as data rather than guessing a shape. return ParameterDataType.data - def parse_zwave_valid_values( - self, metadata: ValueMetadata - ) -> dict[int | float | str, str] | None: + def parse_zwave_valid_values(self, metadata: ValueMetadata) -> dict[int | float | str, str] | None: """Converts states (`{"0": "Off"}`) to valid_values, coercing keys to int.""" if not metadata.states: return None @@ -130,9 +126,7 @@ def parse_zwave_valid_values( result[raw_key] = label return result - def get_main_parameter( - self, device_id: UUID, node: Node - ) -> tuple[UUID | None, bool | int | float | None]: + def get_main_parameter(self, device_id: UUID, node: Node) -> tuple[UUID | None, bool | int | float | None]: """Picks the value for the device's one-tap action, by MAIN_VALUE_BY_COMMAND_CLASS priority. Returns (None, None) if none applies.""" for command_class, spec in MAIN_VALUE_BY_COMMAND_CLASS.items(): @@ -140,13 +134,10 @@ def get_main_parameter( ( v for v in node.values.values() - if v.command_class == command_class - and v.property_name == spec.property_name + if v.command_class == command_class and v.property_name == spec.property_name ), None, ) if value is not None: - return self.parameter_uuid( - device_id, value.value_id - ), spec.default_value + return self.parameter_uuid(device_id, value.value_id), spec.default_value return None, None diff --git a/tests/test_controller.py b/tests/test_controller.py index 16e8b36..0f1f9bf 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -46,9 +46,7 @@ async def test_pairs_a_discovered_device( assert paired.integration_data is not None assert paired.integration_data.node_id == NODE_ID assert state is not None - assert state.parameters, ( - "Expected at least one parameter to be mapped during pairing" - ) + assert state.parameters, "Expected at least one parameter to be mapped during pairing" async def test_fetches_state( @@ -79,10 +77,7 @@ async def test_fetches_state( new_events = output.events[events_before:] assert any( - isinstance(e, DeviceParameterChange) - and e.device_id == device.id - and e.value is True - for e in new_events + isinstance(e, DeviceParameterChange) and e.device_id == device.id and e.value is True for e in new_events ), "fetch() never reported the current value back to the Hub" await controller.stop() @@ -99,20 +94,14 @@ async def test_sends_a_command( device = devices[0] state = await repo.state(device.id, as_=ZwaveDeviceState) assert state is not None - parameter_state = next( - p for p in state.parameters if p.id == state.main_parameter - ) + parameter_state = next(p for p in state.parameters if p.id == state.main_parameter) parameter = ZwaveParameter.model_validate(parameter_state.model_dump()) command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=0) await controller.send_command(command, device, parameter) await controller.stop() - sent = [ - c - for c in virtual_zwave.async_send_command.call_args_list - if c.args[0].get("command") == "node.set_value" - ] + sent = [c for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value"] assert sent, "Controller never sent a node.set_value command to the device" assert sent[-1].args[0]["value"] == 0 @@ -130,14 +119,8 @@ async def test_identifies( await controller.identify(device) await controller.stop() - sent = [ - c - for c in virtual_zwave.async_send_command.call_args_list - if c.args[0].get("command") == "node.set_value" - ] - assert sent, ( - "controller.identify() never sent a node.set_value command to the device" - ) + sent = [c for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value"] + assert sent, "controller.identify() never sent a node.set_value command to the device" async def test_unpairs( diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py index 2861154..77e0c60 100644 --- a/tests/virtual_zwave_network.py +++ b/tests/virtual_zwave_network.py @@ -121,9 +121,7 @@ def __init__(self, controller_state: dict | None = None) -> None: self._patches: list[Any] = [] async def _handle_send_command(self, message: dict, **_kwargs: Any) -> dict: - return self._command_responses.get( - message.get("command", ""), {"success": True} - ) + return self._command_responses.get(message.get("command", ""), {"success": True}) def set_response(self, command: str, response: dict) -> None: self._command_responses[command] = response @@ -136,9 +134,7 @@ async def fake_connect(client_self: Client) -> None: client_self._client = Mock(closed=False) async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: - client_self.driver = Driver( - client_self, network.controller_state, _LOG_CONFIG - ) + client_self.driver = Driver(client_self, network.controller_state, _LOG_CONFIG) network._client = client_self driver_ready.set() try: @@ -173,17 +169,13 @@ def __exit__(self, *exc_info: object) -> None: p.stop() def _connected_client(self) -> Client: - assert self._client is not None, ( - "virtual_zwave used before controller.start() ran" - ) + assert self._client is not None, "virtual_zwave used before controller.start() ran" return self._client @property def controller(self): client = self._connected_client() - assert client.driver is not None, ( - "virtual_zwave.controller accessed before controller.start() ran" - ) + assert client.driver is not None, "virtual_zwave.controller accessed before controller.start() ran" return client.driver.controller def node_found(self, node_id: int) -> None: From 37f53bcf6f363553c084ff307d27b27440f05630 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:45:34 +0300 Subject: [PATCH 18/28] Fixed pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc092ba..36eba47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "integration-zwave" +name = "majordom-zwave" version = "0.1.0" description = "Zwave integration for MajorDom — bridges Zwave devices into the MajorDom language." authors = [{ name = "Mark Parker", email = "mark@parker-programs.com" }] @@ -107,7 +107,7 @@ echo "\nRunning ty:" poetry run ty check || code=1 echo "\nRunning pytest:" -poetry run pytest --cov=integration_template --cov-report=xml || code=1 +poetry run pytest --cov=majordom_zwave --cov-report=xml || code=1 echo "\nRunning poetry build:" poetry build || code=1 From d578114447c46f5802dac7e13591baa89e57eac3 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:45:42 +0300 Subject: [PATCH 19/28] Fixed poe --- tests/virtual_zwave_network.py | 132 +++++++++++---------------------- 1 file changed, 45 insertions(+), 87 deletions(-) diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py index 77e0c60..f892a8d 100644 --- a/tests/virtual_zwave_network.py +++ b/tests/virtual_zwave_network.py @@ -2,10 +2,10 @@ Client the controller creates becomes an in-memory stub instead of hitting a real server. Everything else (Driver/Controller/Node/Value) is the real library. """ - from __future__ import annotations import asyncio +import contextlib from typing import Any, Self, cast from unittest.mock import AsyncMock, Mock, patch @@ -17,33 +17,18 @@ from zwave_js_server.model.node.data_model import NodeDataType _LOG_CONFIG: LogConfigDataType = { - "enabled": True, - "level": "info", - "logToFile": False, - "filename": "", - "forceConsole": False, + "enabled": True, "level": "info", "logToFile": False, + "filename": "", "forceConsole": False, } _DEFAULT_CONTROLLER_STATE: dict[str, Any] = { "controller": { - "sdkVersion": "Z-Wave 3.95", - "type": 1, - "homeId": 1, - "ownNodeId": 1, - "isSecondary": False, - "isUsingHomeIdFromOtherNetwork": False, - "isSISPresent": True, - "wasRealPrimary": True, - "isStaticUpdateController": True, - "isSlave": False, - "firmwareVersion": "1.0", - "manufacturerId": 1, - "productType": 1, - "productId": 1, - "supportedFunctionTypes": [], - "sucNodeId": 1, - "supportsTimers": False, - "isRebuildingRoutes": False, + "sdkVersion": "Z-Wave 3.95", "type": 1, "homeId": 1, "ownNodeId": 1, + "isSecondary": False, "isUsingHomeIdFromOtherNetwork": False, + "isSISPresent": True, "wasRealPrimary": True, "isStaticUpdateController": True, + "isSlave": False, "firmwareVersion": "1.0", "manufacturerId": 1, + "productType": 1, "productId": 1, "supportedFunctionTypes": [], + "sucNodeId": 1, "supportsTimers": False, "isRebuildingRoutes": False, "inclusionState": 0, }, "nodes": [], @@ -53,45 +38,23 @@ _DEFAULT_NODE_VALUES: list[dict[str, Any]] = [ { - "commandClass": 37, - "commandClassName": "Binary Switch", - "endpoint": 0, - "property": "currentValue", - "propertyName": "currentValue", - "value": False, - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": False, - "label": "Current value", - }, + "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, + "property": "currentValue", "propertyName": "currentValue", + "value": False, "ccVersion": 1, + "metadata": {"type": "boolean", "readable": True, "writeable": False, "label": "Current value"}, }, { - "commandClass": 37, - "commandClassName": "Binary Switch", - "endpoint": 0, - "property": "targetValue", - "propertyName": "targetValue", - "value": False, - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - }, + "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, + "property": "targetValue", "propertyName": "targetValue", + "value": False, "ccVersion": 1, + "metadata": {"type": "boolean", "readable": True, "writeable": True, "label": "Target value"}, }, ] _DEFAULT_NODE_ENDPOINTS: list[dict[str, Any]] = [ - { - "index": 0, - "deviceClass": None, - "commandClasses": [ - {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, - ], - }, + {"index": 0, "deviceClass": None, "commandClasses": [ + {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, + ]}, ] # Client.async_send_command's return value IS the server message's "result" @@ -137,10 +100,8 @@ async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: client_self.driver = Driver(client_self, network.controller_state, _LOG_CONFIG) network._client = client_self driver_ready.set() - try: + with contextlib.suppress(asyncio.CancelledError): await asyncio.Event().wait() - except asyncio.CancelledError: - pass async def fake_disconnect(client_self: Client) -> None: client_self.driver = None @@ -154,28 +115,28 @@ async def fake_disconnect(client_self: Client) -> None: patch.object(Client, "listen", new=fake_listen), patch.object(Client, "disconnect", new=fake_disconnect), patch.object(Client, "async_send_command", new=self.async_send_command), - patch.object( - Client, - "async_send_command_no_wait", - new=self.async_send_command_no_wait, - ), + patch.object(Client, "async_send_command_no_wait", new=self.async_send_command_no_wait), ] for p in self._patches: p.start() return self - def __exit__(self, *exc_info: object) -> None: + def __exit__(self, *exc_info: Any) -> None: for p in reversed(self._patches): p.stop() def _connected_client(self) -> Client: - assert self._client is not None, "virtual_zwave used before controller.start() ran" + assert self._client is not None, ( + "virtual_zwave used before controller.start() ran" + ) return self._client @property def controller(self): client = self._connected_client() - assert client.driver is not None, "virtual_zwave.controller accessed before controller.start() ran" + assert client.driver is not None, ( + "virtual_zwave.controller accessed before controller.start() ran" + ) return client.driver.controller def node_found(self, node_id: int) -> None: @@ -184,7 +145,7 @@ def node_found(self, node_id: int) -> None: def node_joined(self, node_data: dict) -> Node: payload: dict[str, Any] = { "status": 4, # NodeStatus.ALIVE, not 1 (ASLEEP) -- ASLEEP makes - # async_send_command silently switch to no_wait + # async_send_command silently switch to no_wait "ready": True, "interviewStage": "Complete", "endpoints": _DEFAULT_NODE_ENDPOINTS, @@ -207,7 +168,7 @@ def emit_value_updated( *, command_class: int, command_class_name: str, - property: int | str, + property: int | str, # noqa: A002 new_value: Any, prev_value: Any = None, property_name: str | None = None, @@ -215,22 +176,19 @@ def emit_value_updated( metadata: dict | None = None, ) -> None: node = self.controller.nodes[node_id] - event = Event( - "value updated", - { - "source": "node", - "event": "value updated", - "nodeId": node_id, - "args": { - "commandClass": command_class, - "commandClassName": command_class_name, - "endpoint": endpoint, - "property": property, - "propertyName": property_name or str(property), - "newValue": new_value, - "prevValue": prev_value, - "metadata": metadata or _DEFAULT_VALUE_METADATA, - }, + event = Event("value updated", { + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClass": command_class, + "commandClassName": command_class_name, + "endpoint": endpoint, + "property": property, + "propertyName": property_name or str(property), + "newValue": new_value, + "prevValue": prev_value, + "metadata": metadata or _DEFAULT_VALUE_METADATA, }, - ) + }) node.receive_event(event) From 564bd32de18612ae9d97c40c4bb5693d413729cd Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:52:54 +0300 Subject: [PATCH 20/28] Poe fix --- tests/virtual_zwave_network.py | 103 +++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py index f892a8d..d56e1d1 100644 --- a/tests/virtual_zwave_network.py +++ b/tests/virtual_zwave_network.py @@ -2,6 +2,7 @@ Client the controller creates becomes an in-memory stub instead of hitting a real server. Everything else (Driver/Controller/Node/Value) is the real library. """ + from __future__ import annotations import asyncio @@ -17,18 +18,33 @@ from zwave_js_server.model.node.data_model import NodeDataType _LOG_CONFIG: LogConfigDataType = { - "enabled": True, "level": "info", "logToFile": False, - "filename": "", "forceConsole": False, + "enabled": True, + "level": "info", + "logToFile": False, + "filename": "", + "forceConsole": False, } _DEFAULT_CONTROLLER_STATE: dict[str, Any] = { "controller": { - "sdkVersion": "Z-Wave 3.95", "type": 1, "homeId": 1, "ownNodeId": 1, - "isSecondary": False, "isUsingHomeIdFromOtherNetwork": False, - "isSISPresent": True, "wasRealPrimary": True, "isStaticUpdateController": True, - "isSlave": False, "firmwareVersion": "1.0", "manufacturerId": 1, - "productType": 1, "productId": 1, "supportedFunctionTypes": [], - "sucNodeId": 1, "supportsTimers": False, "isRebuildingRoutes": False, + "sdkVersion": "Z-Wave 3.95", + "type": 1, + "homeId": 1, + "ownNodeId": 1, + "isSecondary": False, + "isUsingHomeIdFromOtherNetwork": False, + "isSISPresent": True, + "wasRealPrimary": True, + "isStaticUpdateController": True, + "isSlave": False, + "firmwareVersion": "1.0", + "manufacturerId": 1, + "productType": 1, + "productId": 1, + "supportedFunctionTypes": [], + "sucNodeId": 1, + "supportsTimers": False, + "isRebuildingRoutes": False, "inclusionState": 0, }, "nodes": [], @@ -38,23 +54,35 @@ _DEFAULT_NODE_VALUES: list[dict[str, Any]] = [ { - "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, - "property": "currentValue", "propertyName": "currentValue", - "value": False, "ccVersion": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "value": False, + "ccVersion": 1, "metadata": {"type": "boolean", "readable": True, "writeable": False, "label": "Current value"}, }, { - "commandClass": 37, "commandClassName": "Binary Switch", "endpoint": 0, - "property": "targetValue", "propertyName": "targetValue", - "value": False, "ccVersion": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "value": False, + "ccVersion": 1, "metadata": {"type": "boolean", "readable": True, "writeable": True, "label": "Target value"}, }, ] _DEFAULT_NODE_ENDPOINTS: list[dict[str, Any]] = [ - {"index": 0, "deviceClass": None, "commandClasses": [ - {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, - ]}, + { + "index": 0, + "deviceClass": None, + "commandClasses": [ + {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, + ], + }, ] # Client.async_send_command's return value IS the server message's "result" @@ -126,17 +154,13 @@ def __exit__(self, *exc_info: Any) -> None: p.stop() def _connected_client(self) -> Client: - assert self._client is not None, ( - "virtual_zwave used before controller.start() ran" - ) + assert self._client is not None, "virtual_zwave used before controller.start() ran" return self._client @property def controller(self): client = self._connected_client() - assert client.driver is not None, ( - "virtual_zwave.controller accessed before controller.start() ran" - ) + assert client.driver is not None, "virtual_zwave.controller accessed before controller.start() ran" return client.driver.controller def node_found(self, node_id: int) -> None: @@ -145,7 +169,7 @@ def node_found(self, node_id: int) -> None: def node_joined(self, node_data: dict) -> Node: payload: dict[str, Any] = { "status": 4, # NodeStatus.ALIVE, not 1 (ASLEEP) -- ASLEEP makes - # async_send_command silently switch to no_wait + # async_send_command silently switch to no_wait "ready": True, "interviewStage": "Complete", "endpoints": _DEFAULT_NODE_ENDPOINTS, @@ -176,19 +200,22 @@ def emit_value_updated( metadata: dict | None = None, ) -> None: node = self.controller.nodes[node_id] - event = Event("value updated", { - "source": "node", - "event": "value updated", - "nodeId": node_id, - "args": { - "commandClass": command_class, - "commandClassName": command_class_name, - "endpoint": endpoint, - "property": property, - "propertyName": property_name or str(property), - "newValue": new_value, - "prevValue": prev_value, - "metadata": metadata or _DEFAULT_VALUE_METADATA, + event = Event( + "value updated", + { + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClass": command_class, + "commandClassName": command_class_name, + "endpoint": endpoint, + "property": property, + "propertyName": property_name or str(property), + "newValue": new_value, + "prevValue": prev_value, + "metadata": metadata or _DEFAULT_VALUE_METADATA, + }, }, - }) + ) node.receive_event(event) From a4514684041899c8ee59d9963bb12320966a1831 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sat, 19 Sep 2026 16:53:09 +0300 Subject: [PATCH 21/28] New poetry lock --- poetry.lock | 1161 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1157 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 04e9fdc..5a35b16 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -224,6 +224,367 @@ winrt-windows-storage-streams = {version = ">=3.1", markers = "sys_platform == \ [package.extras] pythonista = ["bleak-pythonista (>=0.1.1)"] +[[package]] +name = "cachecontrol" +version = "0.12.10" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "CacheControl-0.12.10-py2.py3-none-any.whl", hash = "sha256:b0d43d8f71948ef5ebdee5fe236b86c6ffc7799370453dccb0e894c20dfa487c"}, + {file = "CacheControl-0.12.10.tar.gz", hash = "sha256:d8aca75b82eec92d84b5d6eb8c8f66ea16f09d2adb09dbca27fe2d5fc8d3732d"}, +] + +[package.dependencies] +msgpack = ">=0.5.2" +requests = "*" + +[package.extras] +filecache = ["lockfile (>=0.9)"] +redis = ["redis (>=2.10.5)"] + +[[package]] +name = "certifi" +version = "2026.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.16.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853"}, + {file = "coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b"}, + {file = "coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594"}, + {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773"}, + {file = "coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc"}, + {file = "coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da"}, + {file = "coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e"}, + {file = "coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08"}, + {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572"}, + {file = "coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2"}, + {file = "coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7"}, + {file = "coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af"}, + {file = "coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360"}, + {file = "coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6"}, + {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0"}, + {file = "coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d"}, + {file = "coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e"}, + {file = "coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9"}, + {file = "coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41"}, + {file = "coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b"}, + {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0"}, + {file = "coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a"}, + {file = "coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa"}, + {file = "coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15"}, + {file = "coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab"}, + {file = "coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9"}, + {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361"}, + {file = "coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b"}, + {file = "coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b"}, + {file = "coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152"}, + {file = "coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8"}, + {file = "coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e"}, + {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae"}, + {file = "coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2"}, + {file = "coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d"}, + {file = "coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743"}, + {file = "coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de"}, + {file = "coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df"}, + {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040"}, + {file = "coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827"}, + {file = "coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d"}, + {file = "coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6"}, + {file = "coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b"}, + {file = "coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220"}, + {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0"}, + {file = "coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5"}, + {file = "coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db"}, + {file = "coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0"}, + {file = "coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4"}, + {file = "coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "dbus-fast" version = "5.0.22" @@ -284,6 +645,25 @@ files = [ {file = "dbus_fast-5.0.22.tar.gz", hash = "sha256:34dc67d7d21a12399828dd13e63b352750580beea54ea7c729e708f2d2905fef"}, ] +[[package]] +name = "faker" +version = "40.39.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "faker-40.39.0-py3-none-any.whl", hash = "sha256:c4c7ec2cddfaf602c5a8a2c960614946aa8c161250bd9d49a8846cd24d1ef028"}, + {file = "faker-40.39.0.tar.gz", hash = "sha256:52799ad96fcf92aab2fc96a9e03869bbf1a634300f7cd91c38dbfd51f639f6fa"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +image = ["pillow"] +tzdata = ["tzdata"] + [[package]] name = "frozenlist" version = "1.8.0" @@ -424,13 +804,47 @@ files = [ {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] +[[package]] +name = "gprof2dot" +version = "2025.4.14" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, + {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, +] + +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml ; platform_python_implementation == \"CPython\""] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml ; platform_python_implementation == \"CPython\""] + [[package]] name = "idna" version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, @@ -451,6 +865,30 @@ files = [ {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "lockfile" +version = "0.12.2" +description = "Platform-independent file locking module" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, + {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, +] + [[package]] name = "majordom-integration-sdk" version = "0.1.6" @@ -472,6 +910,104 @@ zeroconf = ">=0.130" [package.extras] cli = ["rich (>=13)", "typer-shell (>=0.2)"] +[[package]] +name = "msgpack" +version = "1.2.2" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970"}, + {file = "msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3"}, + {file = "msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c"}, + {file = "msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648"}, + {file = "msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563"}, + {file = "msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e"}, + {file = "msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba"}, + {file = "msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb"}, + {file = "msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f"}, + {file = "msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b"}, + {file = "msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c"}, + {file = "msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc"}, + {file = "msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923"}, + {file = "msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc"}, + {file = "msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3"}, + {file = "msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4"}, + {file = "msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3"}, + {file = "msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533"}, + {file = "msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9"}, + {file = "msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5"}, + {file = "msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab"}, + {file = "msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd"}, + {file = "msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f"}, + {file = "msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04"}, + {file = "msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f"}, + {file = "msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5"}, + {file = "msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54"}, + {file = "msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248"}, + {file = "msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc"}, + {file = "msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650"}, + {file = "msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3"}, + {file = "msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929"}, + {file = "msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7"}, + {file = "msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36"}, + {file = "msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5"}, + {file = "msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21"}, + {file = "msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5"}, + {file = "msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b"}, + {file = "msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf"}, + {file = "msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff"}, + {file = "msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8"}, + {file = "msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782"}, + {file = "msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23"}, + {file = "msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212"}, + {file = "msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d"}, + {file = "msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754"}, + {file = "msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a"}, +] + [[package]] name = "multidict" version = "6.9.0" @@ -653,6 +1189,128 @@ files = [ {file = "multidict-6.9.0.tar.gz", hash = "sha256:d7d32c0543494efbc9394e2b571725071d08e295993486bc9a43f6f89375ee01"}, ] +[[package]] +name = "packaging" +version = "26.3" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pip" +version = "26.2.1" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e"}, + {file = "pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f"}, +] + +[[package]] +name = "pip-api" +version = "0.0.35" +description = "An unofficial, importable pip API" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pip_api-0.0.35-py3-none-any.whl", hash = "sha256:66990d0da25378555cbb538db79384bab92d4617c6350dfd7a2126ba2a3a9894"}, + {file = "pip_api-0.0.35.tar.gz", hash = "sha256:14dea05d7b3b68f4c7582213e3ae8932bbba8e1f754bf06b95263d5fa6ab4363"}, +] + +[package.dependencies] +pip = "*" + +[[package]] +name = "pip-audit" +version = "0.0.5" +description = "A tool for scanning Python environments for known vulnerabilities" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pip-audit-0.0.5.tar.gz", hash = "sha256:5048871bcf9a8cf38422f2476caeb7489324ef794e9d7952b3cd72cf1f490985"}, + {file = "pip_audit-0.0.5-py3-none-any.whl", hash = "sha256:e037c67c32c80cb3613faa423ab8e0b53aa4b93ca3e4e942870c848107f23ccf"}, +] + +[package.dependencies] +CacheControl = "0.12.10" +html5lib = ">=1.1" +lockfile = ">=0.12.2" +packaging = ">=21.0.0" +pip-api = ">=0.0.23" +progress = ">=1.6" +resolvelib = ">=0.8.0" + +[package.extras] +dev = ["black", "bump", "coverage[toml]", "flake8", "interrogate", "isort", "mypy", "pretend", "pytest", "pytest-cov", "types-dataclasses", "types-html5lib", "types-requests"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.48.0" +description = "A task runner that works well with poetry and uv." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "poethepoet-0.48.0-py3-none-any.whl", hash = "sha256:98da6096d060f49b8d84034770265863fb7dc92a40233b7694b9d216ac68737d"}, + {file = "poethepoet-0.48.0.tar.gz", hash = "sha256:a06f49d244fadfc2e2e7faa78b54e64a9694727e4ce1d50e08f23cea3ded74f1"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.3,<7.0" + +[package.extras] +poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] + +[[package]] +name = "progress" +version = "1.6.1" +description = "Easy to use progress bars" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "progress-1.6.1-py3-none-any.whl", hash = "sha256:5239f22f305c12fdc8ce6e0e47f70f21622a935e16eafc4535617112e7c7ea0b"}, + {file = "progress-1.6.1.tar.gz", hash = "sha256:c1ba719f862ce885232a759eab47971fe74dfc7bb76ab8a51ef5940bad35086c"}, +] + [[package]] name = "propcache" version = "0.5.4" @@ -801,6 +1459,53 @@ files = [ {file = "propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558"}, ] +[[package]] +name = "psutil" +version = "7.2.2" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, + {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63"}, + {file = "psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312"}, + {file = "psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b"}, + {file = "psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00"}, + {file = "psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a"}, + {file = "psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf"}, + {file = "psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1"}, + {file = "psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486"}, + {file = "psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9"}, + {file = "psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8"}, + {file = "psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc"}, + {file = "psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988"}, + {file = "psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee"}, + {file = "psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] + +[[package]] +name = "py-cpuinfo2" +version = "10.1.1" +description = "Get CPU info with pure Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "py_cpuinfo2-10.1.1-py3-none-any.whl", hash = "sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d"}, + {file = "py_cpuinfo2-10.1.1.tar.gz", hash = "sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771"}, +] + [[package]] name = "pydantic" version = "2.13.5" @@ -956,6 +1661,36 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" +[[package]] +name = "pygments" +version = "2.21.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pympler" +version = "1.1" +description = "A development tool to measure, monitor and analyze the memory behavior of Python objects." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "Pympler-1.1-py3-none-any.whl", hash = "sha256:5b223d6027d0619584116a0cbc28e8d2e378f7a79c1e5e024f9ff3b673c58506"}, + {file = "pympler-1.1.tar.gz", hash = "sha256:1eaa867cb8992c218430f1708fdaccda53df064144d1c5656b1e6f1ee6000424"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + [[package]] name = "pyobjc-core" version = "12.2.2" @@ -1051,6 +1786,154 @@ files = [ pyobjc-core = ">=12.2.2" pyobjc-framework-Cocoa = ">=12.2.2" +[[package]] +name = "pytest" +version = "9.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, +] + +[package.dependencies] +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-benchmark" +version = "5.3.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_benchmark-5.3.0-py3-none-any.whl", hash = "sha256:920ab1dfcffa718d49aa15ba144c7e357bda59216a0dc308016cc1c7236f719d"}, + {file = "pytest_benchmark-5.3.0.tar.gz", hash = "sha256:358444d4e89be901ee2b6404fb043ac3d7684002ad7f3563cc153fca6339c965"}, +] + +[package.dependencies] +py-cpuinfo2 = ">=10.1" +pytest = ">=8.1" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs", "setuptools"] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-profiling" +version = "1.8.1" +description = "Profiling plugin for py.test" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d"}, + {file = "pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea"}, +] + +[package.dependencies] +gprof2dot = "*" +pytest = "*" +six = "*" + +[[package]] +name = "pytest-repeat" +version = "0.9.4" +description = "pytest plugin for repeating tests" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3"}, + {file = "pytest_repeat-0.9.4.tar.gz", hash = "sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485"}, +] + +[package.dependencies] +pytest = "*" + +[[package]] +name = "pytest-resource-usage" +version = "1.0.0" +description = "Pytest plugin for reporting running time and peak memory usage" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-resource-usage-1.0.0.tar.gz", hash = "sha256:3a200cee007fc3e7496ff34620b2841494356c92063aa6fa79e9201fd1e583cc"}, + {file = "pytest_resource_usage-1.0.0-py3-none-any.whl", hash = "sha256:94534f378c13ccdf1413d461f0d780ff2cebf45f07de1622d6e1c07f0a86154b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +report-uss = ["psutil (>=4.0.0)"] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, + {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "python-slugify" version = "9.1.0" @@ -1070,6 +1953,200 @@ text-unidecode = ">=1.3" anyascii = ["anyascii (>=0.3.2)"] unidecode = ["Unidecode (>=1.1.1)"] +[[package]] +name = "pywin32" +version = "312" +description = "Python for Windows Extensions" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, + {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, + {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, + {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, + {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, + {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, + {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, + {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, + {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, + {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, + {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, + {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, + {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, + {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, + {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, + {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, + {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, + {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, + {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, + {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, + {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "requests" +version = "2.34.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "resolvelib" +version = "1.2.1" +description = "Resolve abstract dependencies into concrete ones" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2"}, + {file = "resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c"}, +] + +[package.extras] +lint = ["mypy", "ruff", "types-requests"] +release = ["build", "towncrier", "twine"] +test = ["packaging", "pytest"] + +[[package]] +name = "ruff" +version = "0.16.8" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38"}, + {file = "ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e"}, + {file = "ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df"}, + {file = "ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e"}, + {file = "ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f"}, + {file = "ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2"}, + {file = "ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9"}, + {file = "ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773"}, + {file = "ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f"}, + {file = "ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa"}, + {file = "ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a"}, + {file = "ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "text-unidecode" version = "1.3" @@ -1082,17 +2159,46 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] +[[package]] +name = "ty" +version = "0.0.82" +description = "An extremely fast Python type checker, written in Rust." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ty-0.0.82-py3-none-linux_armv6l.whl", hash = "sha256:be58e089e317e1ada7a655216828e76d369386dc958f7a3b3d455ab6dc7f02b6"}, + {file = "ty-0.0.82-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c080ba9a816cfbb3e34c0d3a0071829c602d14bb992bc799d885d4b30008b4ab"}, + {file = "ty-0.0.82-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f08bd5b751c3b2c46eabe8aee890e7fa54e3bcd97ffa4b8338b0a2508d928c2"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f5ef0401743c2eeac8039ef9ff1bc5dd612bd17ed0259b9b0794e19696548a6"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c899643f2a4c1782c06a4bf4959909d1aa2d9d2cac7c1ac3de3161dfcb6909f8"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aa6e4e157c6e0b770c327a5d966371b2a7bc7ad4f22d662ac068623cba00eb"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28c141ab481748f1b889eb572e803172ea452af621e514c30a2f30884753b020"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16fff2729fbaa3998ba1cab536dc7d176aef28927b09200588e50982394c56df"}, + {file = "ty-0.0.82-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2024a33209b37525ffa85a29036985e9ef53a013c0ff321231bd8b23987cf71"}, + {file = "ty-0.0.82-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14a0ae8591e08aca5911b545d100e51c9b796350a85b2b36d82219820552d662"}, + {file = "ty-0.0.82-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2964202b09108c72232760119c2680cefc33191c428b09e6f620b6c0ab9c6b0f"}, + {file = "ty-0.0.82-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f4ed8fc714e35dc11ef4209832e73ad461cf29c9161099eb6c2c541c6b9a3e0e"}, + {file = "ty-0.0.82-py3-none-musllinux_1_2_i686.whl", hash = "sha256:32a3fe7b1a50f20679fe76c85c0087280419365d900300898a7b95cbcfe510fc"}, + {file = "ty-0.0.82-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f751044ae63ea9e73ce2eae4c23a839efa4a04d6487d6ea411a590fbe43360b6"}, + {file = "ty-0.0.82-py3-none-win32.whl", hash = "sha256:0eb66c5ce58434a48d2cae2780d25285c7ca19e07a0d38b1fe39d1a0f75d6fa3"}, + {file = "ty-0.0.82-py3-none-win_amd64.whl", hash = "sha256:1e4a6c1ca2dc1a6929517d73a47cd626989bb8bd67811425fbe022d06eb2bf73"}, + {file = "ty-0.0.82-py3-none-win_arm64.whl", hash = "sha256:a6f0e8a1f961518746edbccc5986945c01a56a917ebe37117aa028a038d54ae3"}, + {file = "ty-0.0.82.tar.gz", hash = "sha256:586e3bf784cece42113929bb64b4761bed5c2127ae6cea294bb82da670066356"}, +] + [[package]] name = "typing-extensions" version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] +markers = {dev = "python_version == \"3.12\""} [[package]] name = "typing-inspection" @@ -1109,6 +2215,53 @@ files = [ [package.dependencies] typing-extensions = ">=4.15.0" +[[package]] +name = "tzdata" +version = "2026.4" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["dev"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81"}, + {file = "tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79"}, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "webencodings" +version = "0.6.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b"}, + {file = "webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910"}, +] + +[package.extras] +doc = ["furo", "sphinx"] +test = ["pytest", "ruff"] + [[package]] name = "winrt-runtime" version = "3.2.1" @@ -1685,4 +2838,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "cfb6427d67fa3bca54fc26233be501c3d55b8a933b58594a2d78eaf479a54d2c" +content-hash = "3864f7091b4334f50dafa7e185e6cfd5c303072c4bd2279f7e018f1ea12bf155" From 575388381fe902c0a0f10fe9019d0e23b5455862 Mon Sep 17 00:00:00 2001 From: Ehor Lukianov Date: Sun, 20 Sep 2026 12:35:13 +0300 Subject: [PATCH 22/28] Changed readme, added notices by hardware testing --- README.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7b2b1cf..0661909 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ the same server Home Assistant's Z-Wave JS integration uses. `zwave-js-server` i the serial connection to the Z-Wave USB controller and speaks the Z-Wave protocol; this integration just talks JSON over a WebSocket to it. -- Run `zwave-js-server` yourself (commonly via Docker, `ghcr.io/zwave-js/zwave-js-server`) on - whatever machine has the Z-Wave USB controller attached. +- Run `zwave-js-server` yourself (commonly via Docker) on whatever machine has the Z-Wave USB + controller attached. - Point this integration at it with the `ZWAVE_SERVER_URL` environment variable (e.g. `ws://localhost:3000`) — see `majordom_zwave/config.py`. @@ -76,14 +76,16 @@ just talks JSON over a WebSocket to it. - **Transport(s):** Z-Wave (sub-GHz RF mesh). - **Supported devices:** any Z-Wave / Z-Wave Plus device exposing standard Command Classes (Binary/Multilevel Switch, Indicator, etc.) that Z-Wave JS supports. -- **Credentials needed to pair:** `none` (S0 / unauthenticated inclusion), `code` (S2 PIN), or - `qr` (S2 SmartStart / DSK QR code). `secret` is not supported. +- **Credentials needed to pair:** none for the Hub's pairing flow — `Discovery` always advertises + `CredentialsType.none`. S2 `code` (PIN) and `qr` (DSK) are supported, but must be supplied when + *opening the pairing window*, not at `pair_device` time — Z-Wave needs them during inclusion, + before the node exists as a discovery. `secret` is not supported. ### Required harness -- **Hardware adapters:** a Z-Wave USB controller (e.g. Aeotec Z-Stick, Zooz ZST10) — but it's - attached to whatever host runs `zwave-js-server`, not necessarily the Hub. This integration - doesn't use `dependencies.hardware_interfaces`; the radio is abstracted behind the server. +- **Hardware adapters:** a Z-Wave USB controller (e.g. Aeotec/Zooz Z-Stick) — but it's attached to + whatever host runs `zwave-js-server`, not necessarily the Hub. This integration doesn't use + `dependencies.hardware_interfaces`; the radio is abstracted behind the server. - **Third-party software services:** [`zwave-js-server`](https://github.com/zwave-js/zwave-js-server) (Node.js) must be running and reachable at `ZWAVE_SERVER_URL`. - **OS / permissions:** network reachability (TCP) to the `zwave-js-server` host:port. No special @@ -98,6 +100,16 @@ just talks JSON over a WebSocket to it. | Application | Z-Wave Command Classes (CC) | `zwave-js-server` (external Node.js service) | | Network / MAC / PHY | Z-Wave mesh, sub-GHz RF | Z-Wave USB controller + its driver (harness) | +### Manual testing (real hardware) + +Verified against real hardware, not just the virtual-device test suite: + +- **Z-Wave controller:** Z-Stick 7 (ZWA010) +- **Test device:** HKZW-RGB01 v1.0 (RGB bulb) + +Ran the same lifecycle as `tests/test_controller.py` — pairing, `fetch`, `send_command`, +`identify`, and `unpair` — all completed successfully. + ### Progress **Implementation** — makes the integration functional: @@ -107,7 +119,7 @@ just talks JSON over a WebSocket to it. `controller_did_receive_discovery` called - [ ] Discovery of already-paired devices on reconnect — currently `start()` marks known devices available internally on startup without going through `controller_did_connect_device` (see Notes) -- [x] `start_pairing_window` (default S0, S2 PIN, S2 QR/SmartStart, auto-grant of requested +- [x] `start_pairing_window` (default, S2 PIN, S2 QR/SmartStart, auto-grant of requested security classes with no user prompt) - [x] Device pairing - [x] Device schema mapped (via `ZwaveMapper`: values → parameters, role/visibility/units) @@ -120,7 +132,8 @@ just talks JSON over a WebSocket to it. `controller_did_connect_device`) — `last_error` isn't set/cleared on these specific transitions though (see Notes) - [x] Graceful shutdown in `stop` (tasks cancelled, client disconnected, session closed) -- [x] Tests pass against a virtual/simulated device (`tests/test_controller.py`) +- [x] Tests pass against a virtual/simulated device (`tests/test_controller.py`) and against real + hardware (see **Manual testing** above) **Quality** — makes it reliable and maintainable (the bar for release): @@ -136,14 +149,15 @@ just talks JSON over a WebSocket to it. - [x] **Stable identity** — device/parameter UUIDs derived via `ZwaveMapper`'s SDK-backed helpers - [x] **End-to-end tests** drive pair → command → fetch → events → `unpair` against a virtual device - [ ] **Failure paths tested** — offline device / transport error / rejected credentials not covered yet -- [ ] **Broad device coverage** — only a single virtual Binary Switch device is exercised in CI -- [ ] **Fully typed** (`ty`) and **clean** (`poe check`) — TODO: confirm with a clean `poe check` run +- [ ] **Broad device coverage** — one virtual device type in CI, one real device manually +- [x] **Fully typed** (`ty`) and **clean** (`poe check`) — ruff, ty, pytest, and + `poetry build`/`check` all pass - [x] **Readable & structured** — conversion logic lives in `ZwaveMapper`, models separated - [x] **Efficient** — subscription-based (`"value updated"`/`"dead"`/`"alive"`), not polling - [ ] **Diagnosable** — decent per-area log tags (`[PAIR]`, `[CMD]`, `[JOIN]`, …), but `send_command`'s exception handler re-raises with `from None`, discarding the original traceback - [x] **Rich parameter metadata** — role/visibility/`main_parameter` resolved per value via `ZwaveMapper` -- [ ] **Owned** — TODO: add a listed maintainer +- [ ] **Owned** — add a listed maintainer - [ ] _nice-to-have:_ localizable naming · firmware/software updates · user-facing supported-device docs ### Notes From 93dfd865d6b87a752c67ae5754837465a6949264 Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:51:07 +0200 Subject: [PATCH 23/28] Prepare the first release - License under PolyForm Noncommercial, like the other integrations - Bump zwave-js-server-python to 0.74 and dependencies with known vulnerabilities - Add the release workflow --- .github/workflows/release.yml | 38 ++++ LICENSE | 273 +++++++------------------- docker-compose.yml | 2 +- poetry.lock | 354 +++++++++++++++++----------------- pyproject.toml | 8 +- 5 files changed, 292 insertions(+), 383 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dd17d52 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + workflow_dispatch: + +permissions: + contents: write + id-token: write + +jobs: + release: + uses: ParkerIndustries/workflows/.github/workflows/release.yml@v1 + with: + pypi-package-name: majordom-zwave + python-version: "3.12" + stable-branch: master + # the QA run needs the mock Z-Wave network: see test.yml + setup-script: npm ci --prefix tests/mock --no-audit --no-fund + + # PyPI publish runs HERE, in the caller — Trusted Publishing can't match a reusable workflow. + publish-pypi: + name: Publish to PyPI + needs: release + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/p/majordom-zwave + permissions: + id-token: write + steps: + - name: Download distributions + uses: actions/download-artifact@v8 + with: + pattern: dist-* + merge-multiple: true + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/LICENSE b/LICENSE index d645695..5ecc88c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,73 @@ +# PolyForm Noncommercial License 1.0.0 - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + +## Acceptance + +In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. + +## Copyright License + +The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. + +**You** refers to the individual or entity agreeing to these terms. + +**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the software under these terms. + +**Use** means anything you do with the software requiring one of your licenses. diff --git a/docker-compose.yml b/docker-compose.yml index 54dd95e..e068c2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: stop_signal: SIGINT environment: - SESSION_SECRET=change_me_to_a_secure_string - - TZ=America/New-York + - TZ=America/New_York - KEY_S0_Legacy=${ZWAVE_KEY_S0_LEGACY} - KEY_S2_Unauthenticated=${ZWAVE_KEY_S2_UNAUTHENTICATED} - KEY_S2_Authenticated=${ZWAVE_KEY_S2_AUTHENTICATED} diff --git a/poetry.lock b/poetry.lock index 5a35b16..faa6ef0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1010,183 +1010,183 @@ files = [ [[package]] name = "multidict" -version = "6.9.0" +version = "6.9.1" description = "multidict implementation" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "multidict-6.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:63c9da87fa6d4284e57d2a6e549dfe8f2938ab578fddab3f8ed9742812c9d0f3"}, - {file = "multidict-6.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3663ce6cb19c5e3123c9aad5907804a42d384f8cd4b67e59291efc9480f8da9a"}, - {file = "multidict-6.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7f340a4a6ad4dacb0f866143452d794ea32a0d9ddf79d93ecb4c3ac5515d0d4"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf44632149302f7fdcf5baecc277d816ab77a47a268f2392dc5d2d01c56770cf"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:095b091158c89a03ad1237b4880f857d27628f93c23da1616ee5640011439c53"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3958fa87a161a6b9c6661606784abdd8f1afa21a0d6e99d1b99512e70e6b4f1"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f99e0eec05aca200b66735a38f8de5c9be03a6afd62ba7f28860203d936a03f"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d98efa7b004b67cb8cca26b218012efa50c7103cf4ae54898f2bbf89a3c62f"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a23926c06835ad71e6bc755ebc07ad34655847de63c01a5d06b6501997712c4c"}, - {file = "multidict-6.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:06723cd13ace09a0ce1ed010fc9e57cd762e57ee23691d6cdb13b7fcf1a35468"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:27dee31d6f3a0d2be018b71721adbe24aed9b08bdc69d133c88eead07beee600"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90d341b2c64eacef24fc0a35aaabc5227834ce93bb1da416d61601663df29456"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:986de92227e44b257fef664a69613a4346a090553c186d0ae3bf15ceab447f5e"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c8da6d6fc78f971ebb9e29563c891a429374861afda55e4692a7962e3822637d"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8903ce696f0aa9dc301c5621fa0f0e24e985d4e6cc247cd63bc9abcd5a081628"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bf345dacf96fd9c698ad30c620fbdbe6e1edadf83944203ce5b5688f52e3d141"}, - {file = "multidict-6.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e63e05228c1f1c1bef09c08abd2f97b6fe597c9ca14a1c94bec4e76b4664046c"}, - {file = "multidict-6.9.0-cp310-cp310-win32.whl", hash = "sha256:f25229fddbd6bb3d47f1141c319bead19fa3c9e62790c6d51201564fde05fc2a"}, - {file = "multidict-6.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:05f197459db34dee24ae7e3560affa5c6f93989805899c9bba13358b5116a12e"}, - {file = "multidict-6.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:d4464437dbbd7a5a71a5e86f2a9ec92ccbb8d96706f5a173ca985175f23adaf9"}, - {file = "multidict-6.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e46237ef18c8ee183dde740f0174d6ecf9c26d362b506b5d23fb57bc3634bc0"}, - {file = "multidict-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e33fabca25633cf0e4f9dd4495733d09b7f0628fc9424c5b5e58da644fd4ab94"}, - {file = "multidict-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e82873b56ed37b328501fb063962299ea6f7fc1caf1a5c47966028824bde0807"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e2169fdb886288fc6bf9d8e48abca9487f8b3ace1f5ab269b074864d212351e"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eefec2a6b0381846ce54d88f30de4b3a5c15ff0bddc5264cccba85b710a8785d"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c63013a210a09d4a8f30814b785450f1d0a2aaef8230c1e068e83f3afceb6030"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fd82f81dd0676059f9bc3a418a79f27feab19486100a255bf8488cccb3e60d3"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:deea26b6979ce46f2fb3b2db9b651e12eb7f94c26a1ee457b79f66ad36a64fd1"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58a4aee7188676c8317e3131bd844faf70e427979e1f20b8598c61f6cbd8b073"}, - {file = "multidict-6.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eee11705013394ebeeb383c92beb0fc9a3c449ac3382a6ee3aa23b38c6b06c3a"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51e77d089f82ed2e288d3a1cbf6cb7ffa927c8055d8ef61d57b27f4d8da557b6"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:79640bb8746b703971fb16569e7bfa82d90548137cf4094769aa45cbafda9d9a"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:092214366f729803010a5c260f4a4f44d223c688641dfc27da851588ac762ffe"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:da469a672bf3d343eb6757d53ffbac36f1a29b93a9fe76558b5d0fd7a7efa599"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4216a8b4214a1ee4511c551c59d51534637394e9433765fbd8face86db43c532"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f05f5b3171e23801e72fd818de236be9f239b10ba699f23e43bb9596651f8b34"}, - {file = "multidict-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0c69b098c85685ba39efef1d8a9711243df8a4c4b051a35d3cce7f323381f8bb"}, - {file = "multidict-6.9.0-cp311-cp311-win32.whl", hash = "sha256:855be03f39b97e352d54eaa7b14ba2a46846cf14719ddb689cdaaee811462440"}, - {file = "multidict-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a8040dde3fe35ead6b04adf3cd3fdd3bfc7c941505775d230b4501d18227c77b"}, - {file = "multidict-6.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:70b3097b838d9fc336f426c7aa7d45b7881ee9b06a10f4bcffd0dcda0cbd44e5"}, - {file = "multidict-6.9.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fdd484b84d3394e805689c56be3ab1f877ae7ff0eb9ff90a3ee7a755cbebab4f"}, - {file = "multidict-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f93c9058a0eceac0df2ce9d4c8823b84786ee598194753c2ca0c100224405e47"}, - {file = "multidict-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c7ab60b91e11b25e7682c5cd8763fdd17929ea83f234ba441091f1492e631ea3"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:640113258c5925a9eed2c12523410b25565ac5df2fa6735fbae88fb09bcdd212"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ea72901860ccbe94517421681c60b13533ce03ba2f7bd96293c3a4d16ac4ccb"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:408fac672931f2458be3bc8c89d9facd16dac2aad17c7cee2ca1693eee99f07e"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:67bed23e9803945b0760650ec3e903af772c49abe869f3bc03c66b2e7649d5ef"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb769ccc72e15d7d441e1a08f169d418376be77cdc387e813129c26b357fe50"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a5111a2bd824c821a3dd09da29680391b0caaa18fea7761358f4001e6898d1c"}, - {file = "multidict-6.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e991677c4bdc5d9f2e71c74717a4e32cfe98930ca05becdf722b5eae1329d6a"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb69b724c345420ba49187a17a894f146099f5b2e501df42ddb4452ac8be37fa"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cff3cff5a725bdb8359962de8d7429aea592d7693dbd197eeabbdfab9b6300e9"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fca5b74b5909c29041f857c40d51d9273636fb4221cf020e4c452deb1c448a40"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11e32ccf23cdbfcf8299a6a825930a858ec9a6aa05d6752d0f90f2bdf19489e1"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ec1e387b1f8a85ae5b94aa8c4e0576912ffa4d31bd0578f24c950d4f05ee476"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4826f6b56456fb1e98111d7bc20cbdc7fa0a41b1f9ad80ff2dd3f2f5b226fe1"}, - {file = "multidict-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e71a072c52c78b7f97cd4611df6cef10977e4f2367cd0654a7626192f931adde"}, - {file = "multidict-6.9.0-cp312-cp312-win32.whl", hash = "sha256:95d339c3b75b4a50c665bdcf8417428cd71c3e5cd48e194cb1d336fcb856beac"}, - {file = "multidict-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f06c4da5315a6f709b13408c3e13f3b475f8c559ec7608c3c67062512871235"}, - {file = "multidict-6.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:0db5bf96ec2ce45a8bc7fbbe8a486089969bb2791a66b6789ee3aed0d5dd562e"}, - {file = "multidict-6.9.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:662315f8621b3559268813b11134c1f9633edd4ab5f7b713688a243c13f4026d"}, - {file = "multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4bef8cb5edea9c9daeb8396a75eeb97f912c3fb3fa408fe659edfecf91e47424"}, - {file = "multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:75f7fc21ffce9a792cb919f0e0b0b52117bd672f2a55d1929d573b6fc437f374"}, - {file = "multidict-6.9.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bb6be697065cbf31051465f9939d65624573d2219c3da8316db9e7cf5e0f5a7"}, - {file = "multidict-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67bcff396d2ad62197c95488a716a88462792b384b5ea6e44bf7c1070eddb8fc"}, - {file = "multidict-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:beda95a6bd0a2e2265b941b90ff72543afbeaebb6a450e3a6a010d10d4a2ffca"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e78870909e9f9e3d672ba99f1eb75130d7e01c42e642e6a70f434df32ca0ee1"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91092d597fcf0940cd6a59e64ba7156ee22d6899993fd7c5b1897fda71482f8f"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be612f23990a261060ccd8f15e89dad1f9b7bb0c1021c5c3987f5fc57959391"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7aaa14f0b9ffa2780c5d3b21da0e9b58778ed47af3369df72e5fd6dc10ff8041"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97555ad30ad20a8eb90fa86522088eebbbd68aba03e56d53f4850b3535c65e61"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c51fd8d59e72e45c64907bf7f81fbd94f4c8dcd1c8b2f23f4c2d7ede788fcfc3"}, - {file = "multidict-6.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22067e88ff266e6a5dc59114529332692a01cc04b10dbdc2c14ab91217dee819"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e231de8ce43d4fd10bec4f67f71238ed88a974de30a9f20be7a4ab16c970a988"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b33e499a7b1f722547d57b4865ed68c03161643b9afe49fe097a0832078d183d"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1eb7939025bd9289d9dfe3a399102b1642f4fbb105a6af82f284052ee89e97a9"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:51a08dceed4b42ef25755ee2cbf50df325e90e6b415d00955ffdea2c394ad6c0"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb847cb4002e725f88ffe29883da8290fd0754fa0461ed8988735e20964a9f65"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:516fa4817cd070088f616a901380db56189f86daee9da79aada3c9b653f49ed7"}, - {file = "multidict-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:254e53be2ec70518bb82dfa9b0c7166baaf2bd0e918bd17b65caaf489d5a5522"}, - {file = "multidict-6.9.0-cp313-cp313-win32.whl", hash = "sha256:9869105ab61db13004f9ed610cd29ce4237b2c609f5f7b8bb8f2339fb9dad17d"}, - {file = "multidict-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:85ed0f3c3b01174aea5a8a8fcd456f64c2e9719c0f42c612c2694ecd35974a65"}, - {file = "multidict-6.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:1126782e3c3b1a7ccd990be3d3221709348e4b07d6ecf8b50965a93ae2624145"}, - {file = "multidict-6.9.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:1b34b10b615db5a3ed424b174a362a3cdf06220bd892d03d151a619bb157c4e9"}, - {file = "multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f7f0b996a20776018a253061a781ba983e7b944e17a0748822026048b8b0bb07"}, - {file = "multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b612842f7f286c42abc5dcbda1db50e9dbed39720dd15feb07dbc9974bfeaa7c"}, - {file = "multidict-6.9.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:71ee2b3198323cbff7a846538f01599f309dddddba79b5bf5a3dca2e49460ef6"}, - {file = "multidict-6.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0dc1f9c7dc07d86e5a4ed24f3a55b945106f3e7c70a984ff8dd9267270414ed"}, - {file = "multidict-6.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a26c8672d70d2c12c2f1c4df91fe92422e905efaf1d639bd4d7192868a21e711"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:186cc28558e3d6921ba797e433cd58ce64eb8b2f553e93f75dccbb9a604b7457"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebc02af6ac170631774219c26fd63b4404d1e836a1aa92b738a3127eaa2aebad"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62c4ffba9f81a2b17cf081bfe4e6b7eaf17193dc17d5870c01b2763e48efed24"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a7e2aaa0e9a6413b54cd90fa0fe5944487872a7130a1c07347627974e677a34"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26e34dbbd6c3438ee19e0db1f5b846a7ca957085d193ec312b488924c3941a0d"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d811799f821c06883404a171faa880660ef04c2687fc32fdaef22fb645fe80"}, - {file = "multidict-6.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fde003cec43783193360c27de2fbb0c8ea2db1cfebc193c3fc657c436f9d38"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9bfa4ba6d97ae6d9c1f5336c2abad5690bce1802a040f9aa5fa39cb0b3099c02"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a776fbaf700549943b647f21479d69191c238c84fb55d5a48eec1b4cf6cf6e00"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ed04614c47379bcdfa01d3e165149518017ea034cd5f48e203c287cc3d2ebd4b"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9c65d9912983991f368bd01f993191b1593c90ecde6be990d73add46084d235f"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4b89b102be78ed286123e75a30efda519e1ef6b9d4a4d01a08213f5e01a18e45"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e3ed6563ea143214c63bb4452ae5e8787a39226b22101c179fa4d2eda5972267"}, - {file = "multidict-6.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:448c05bd13f8675b3629fbc3fabb9e7bb19a5636a7a3eb677ea25a08d0544a56"}, - {file = "multidict-6.9.0-cp314-cp314-win32.whl", hash = "sha256:2b5db4f2ca34fce55d34b9c66a0eb5d0d01962e2420a1218ba836d8e46aa6ea3"}, - {file = "multidict-6.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccf9f7f07e9ab6e24f68ccb2b4da8ded65c1fbc5390ebd897a5fe55cdd4c064f"}, - {file = "multidict-6.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:64ef289ca3c6eaafaa705978686631cb5acc90a92a09bef39c95a810c5f4b804"}, - {file = "multidict-6.9.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f17f485e1837a7d31216be8be991a51265bcfc3cfd2457d5c94ade6086793ad7"}, - {file = "multidict-6.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3fccacf47e756a0f487b2b7e4afc9f167db0770ecc4e887b38ae2aec99dcb510"}, - {file = "multidict-6.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:527655c10fca73fd336cbcd5ccaaba5d8ded1512a3183ed7ebcc1569db5f1de3"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d3520f774e78dad488c97398e5c7253333baa4eea09785aca56d477c4dc05c7"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518a76bff2567fb0941603b9ccecdd830dd2d717605e7a28c1edc9e64688759e"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff308def558cbbff74ea889ec659d117ac550d4d554467ed6cb3bf6adadac7e6"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:875a77927cd7bc0bc1d0a0520a7ed2267bb169c5db03cc133eaa746526d4182b"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2182da6ffc9793dd66d7d3632eea2b131f1a1e26eaedd51956845501830d84fe"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e680a0b07e44b87a975eca63f84860a772667b0037066a6e93d998eb41ad2814"}, - {file = "multidict-6.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5066b7877e1e2ae2c3712c8037eff40a80d4cf35b393b4c42c81f1c4b73b346d"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fd0ec96e9877cb4dddf7942dffa777837809663f8e55d4f79a2bad06453446b0"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:98b0738ac1a1593f609a7b67d25e09c7de57c0f91c2879eb01c46b2dae55c0ba"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0548fdffa634c495b866d89e02ae58a9588cedaeeb972f6ced433e20dee1db5f"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7d0649ae61a341b9a47a2afab57424bd2a0ce98d36ec170ce9a51ce92c7284f6"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6d470d693ff45678b9ab237113546f6dc18918be55b3c852a69ed05ae7404a8"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ac05ce26d2c902d2eb5afe70f2012d19f3b9510ed5ef054bab27a3d43bc7f39c"}, - {file = "multidict-6.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e82ea3ca4409c20172074f27dc1ba5858174f143d0ee637dd058b8061ff024c6"}, - {file = "multidict-6.9.0-cp314-cp314t-win32.whl", hash = "sha256:d61680c1969b5b1b900fece510077e7732336eae02f68dda02162e04ff6ff321"}, - {file = "multidict-6.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a9af5d28024c1de9e0efe06a64b5a8561e6517daa66788169e60d2c588e15c5"}, - {file = "multidict-6.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:810ae53369d957fcc3e3855612e3a35a39960adaf4b3b1cb4cb4a2e94185bcfe"}, - {file = "multidict-6.9.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:b9bcbd708bb5e1ce8cf9c58908ae9777a90f777406315b046bb10cb3e87f8621"}, - {file = "multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27ed5aaee62eeff74786a3259de81ada4f2bf84b75122bc06d7d8eadc1be5643"}, - {file = "multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30ba80aa7ec7340e80140d29f563e33acb21f3756e42df13612f2a26c80c90ba"}, - {file = "multidict-6.9.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c26e56c68bde47f8028265b3829915450c944febc75a1e4d0365b8a75557861a"}, - {file = "multidict-6.9.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:f6705ae71e4b14c693877be3cc09fc62af10197ca08ee08bb57ffce6e0f9c5f4"}, - {file = "multidict-6.9.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:1fc7b1ce735737b7abf38935928729433ef2c81306dcab7f360ef3c3076bc8e9"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7731a591920d77bd9372a9a8ab3f447129dbf17d29b10928c94005e1606641c"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a9295177c33756d7a6f8da0f1db345ac951f2436be6ad4b90f53056526e767"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b4ae468e341cc84c1ae035ee12f032cc0716f618b1eb417fb14e8454a17c620"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb9f52b89e339007a06c88cb95068e9d612ce252baaaa5ece85de96e2d51fca9"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69190050f74fe139a7b3759c730e4e87484db9cf74b1786910a24496ab7a40f9"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5f04bbfb064b87455a8012f5829b20201d608cf4b3a1a0e50c7a14173a75109"}, - {file = "multidict-6.9.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d81d0ec43a73056458e844cf90239d2c3a7aeb4ef05fea8c0f64775d5111847"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b8aea3d54f3cc20a557fb951c68294d9aa88d614cf7151aa5629b1d5e4033b74"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:31fd2961bd754ac3177aae9438c01becc22eae029911f13c558e5767a10e5dc2"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:04e6f4eddeff93c778a9e7137568284f28856d960275085fcbd87cf77a53a057"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:319160c73795ce31e86494773e0f12c446d32dae7d1a76f4829007ef69eb80bf"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:0d7fbd38d52119423ed8c69672ca47eac121062c30c976e00ba732d114774874"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:981280a946256fc151584fb3b82b99db091fccac81cfecce530585db559ec62c"}, - {file = "multidict-6.9.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:fb3d1f3c63d49656a83e1604ef3ac573016cf2e62da84d1937f47107d5689c8b"}, - {file = "multidict-6.9.0-cp315-cp315-win32.whl", hash = "sha256:0c5388c41bc7c551548c4ce840349f0708f7ced6df877d82900bac82268160fc"}, - {file = "multidict-6.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:49e47de3823bee4896f1eedc17ce4fc4f42c433bab168ece7911f42e69a4f666"}, - {file = "multidict-6.9.0-cp315-cp315-win_arm64.whl", hash = "sha256:1ac339b944616d4061a9ee9571c89767543db38c19c8c7917a9c08b1462c4800"}, - {file = "multidict-6.9.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:cb6be563dd43b2d4d528afded1878b559ef1c2f45f89e94d273ef515485727f9"}, - {file = "multidict-6.9.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:76f0df301516563085342d9cdad03fc3c0301b36c3cfa8454e49ca73878aa59c"}, - {file = "multidict-6.9.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:34c00a534e948eb5f8b3c50cacea9daa4fc4dfd503f6edd114bb532f53742c9e"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cce5e33383904e32b58bef7765c38917899a9dc87058f75c421c82e7c11440ee"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c375408881861294207c1d038a711120776989c5e8c489b251a818cd74d39886"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f95940278766f273b668bbd9dd412ab5dd170ff40fc836b714c38de7852f1fc"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6009b58d6994881c53f56d2f2010f0b3cd30139c7b14156c2329d00c0d592e8e"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7d5ab831301decfb7c29d1c243823f6ccbb3766117c5957bdc5b9f283355d0f1"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17b74ffc94e63843cf34b7eb874ab1653afac0eda3edfa3de1207db7fc8c0ed9"}, - {file = "multidict-6.9.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62dc471198ac2f1085f6c6ba9aa5ad3343c0cbac0756d2fbe551659d55e14212"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:da97c0a501c5da00d9c3af6d481ed97dace148f9030f4f5578534981d58682bb"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:986f2a23330e8823b389872ae63b83b862a10e63e8cfbd11b42ab8664d1eaa7e"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ed96992cac94b32a9c3da5870b05967271397a813029d68197939397860b1470"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:94e767d30be457da0ef134e688cd2b6a438d19b85bf000b1c4a2b9b6f55b4211"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d1f98df09bc22d298d88dee07eb5365dba1fbf3b41e876a6edb2652a8b3a6ca1"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c5290038269f7d96037afc47b05b286e4791adfa7046911e2a0483b6220c84fb"}, - {file = "multidict-6.9.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8858d2ddd0bc21b4aaf189016c95840b29ddf0236a049af70a9cb4379bef9abc"}, - {file = "multidict-6.9.0-cp315-cp315t-win32.whl", hash = "sha256:f90724dade5865b1c9429d9b058f11096e44a7f1f0e0a21eff18d98ef32c22aa"}, - {file = "multidict-6.9.0-cp315-cp315t-win_amd64.whl", hash = "sha256:150f1e5bcc14234e99aacc931deafa52713157df947efeecc1a3b7601f2714ab"}, - {file = "multidict-6.9.0-cp315-cp315t-win_arm64.whl", hash = "sha256:734c985bbd60e84d0ea523bafe139394e46b8a3f35754a70111dbcbba243f7f4"}, - {file = "multidict-6.9.0-py3-none-any.whl", hash = "sha256:57c2445049f7d8e66306f712868219da7ff7168ef42263dc032401211bf1205c"}, - {file = "multidict-6.9.0.tar.gz", hash = "sha256:d7d32c0543494efbc9394e2b571725071d08e295993486bc9a43f6f89375ee01"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c"}, + {file = "multidict-6.9.1-cp310-cp310-win32.whl", hash = "sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9"}, + {file = "multidict-6.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0"}, + {file = "multidict-6.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13"}, + {file = "multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f"}, + {file = "multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e"}, + {file = "multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6"}, + {file = "multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb"}, + {file = "multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203"}, + {file = "multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc"}, + {file = "multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3"}, + {file = "multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382"}, + {file = "multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14"}, + {file = "multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40"}, + {file = "multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d"}, + {file = "multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4"}, + {file = "multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5"}, + {file = "multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9"}, + {file = "multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194"}, + {file = "multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841"}, + {file = "multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3"}, + {file = "multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e"}, + {file = "multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd"}, + {file = "multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9"}, + {file = "multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3"}, + {file = "multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1"}, + {file = "multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82"}, + {file = "multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d"}, + {file = "multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d"}, + {file = "multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f"}, + {file = "multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1"}, + {file = "multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1"}, + {file = "multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec"}, + {file = "multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3"}, + {file = "multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b"}, + {file = "multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2"}, ] [[package]] @@ -2821,14 +2821,14 @@ ifaddr = ">=0.1.7" [[package]] name = "zwave-js-server-python" -version = "0.73.1" +version = "0.74.0" description = "Python wrapper for zwave-js-server" optional = false python-versions = ">=3.12" groups = ["main"] files = [ - {file = "zwave_js_server_python-0.73.1-py3-none-any.whl", hash = "sha256:07ca62957392dff6fdf211e40dc36041c459f9943e78b613687d3aea6c316c46"}, - {file = "zwave_js_server_python-0.73.1.tar.gz", hash = "sha256:5f4bfe7268a7de093a8f107159e2d8d6828f00b442e3ec52b9d116ef514e4e20"}, + {file = "zwave_js_server_python-0.74.0-py3-none-any.whl", hash = "sha256:ab195dbc47cadca46e351ff29685093de8a4d753574535cf0b2404005517b2e7"}, + {file = "zwave_js_server_python-0.74.0.tar.gz", hash = "sha256:243160a2bfd4fce45458e73e26ad171a179e67b311b812fca303322eca4d4229"}, ] [package.dependencies] @@ -2838,4 +2838,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "3864f7091b4334f50dafa7e185e6cfd5c303072c4bd2279f7e018f1ea12bf155" +content-hash = "4be76b6f1bc679c04100082b84d1856185f272da8aac49d6d682091e4df2f289" diff --git a/pyproject.toml b/pyproject.toml index 36eba47..407193c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "majordom-zwave" version = "0.1.0" -description = "Zwave integration for MajorDom — bridges Zwave devices into the MajorDom language." +description = "Z-Wave integration for MajorDom — bridges Z-Wave devices into the MajorDom language." authors = [{ name = "Mark Parker", email = "mark@parker-programs.com" }] readme = "README.md" -license = "Apache-2.0" +license = "LicenseRef-PolyForm-Noncommercial-1.0.0" license-files = ["LICENSE"] -keywords = ["majordom", "zwave", "zwave-js-server-python", "smart-home", "home-automation", "iot", "integration"] +keywords = ["majordom", "zwave", "z-wave", "zwave-js", "zwave-js-server-python", "smart-home", "home-automation", "iot", "integration"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -16,7 +16,7 @@ classifiers = [ requires-python = ">=3.12" dependencies = [ "majordom-integration-sdk (>=0.1.5,<1.0)", - "zwave-js-server-python (>=0.73.1,<0.74.0)", + "zwave-js-server-python (>=0.74.0,<0.75.0)", ] [project.urls] From 92079361fa032918a350d9aad335038ca225a15e Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:51:10 +0200 Subject: [PATCH 24/28] Test against a mock Z-Wave network zwave-js's mock controller and devices behind a real zwave-js-server: pairing (S2 PIN/DSK), state, commands, identify, availability, reconnection and unpairing, including their failure paths. --- .github/workflows/test.yml | 47 +- .gitignore | 1 + tests/conftest.py | 133 +- tests/helpers.py | 156 +++ tests/mock/network.mjs | 377 ++++++ tests/mock/package-lock.json | 2239 ++++++++++++++++++++++++++++++++ tests/mock/package.json | 10 + tests/network.py | 116 ++ tests/test_commands.py | 95 ++ tests/test_controller.py | 147 --- tests/test_identify.py | 47 + tests/test_lifecycle.py | 170 +++ tests/test_pairing.py | 175 +++ tests/test_smoke.py | 5 - tests/test_state.py | 141 ++ tests/test_unpair.py | 68 + tests/virtual_zwave_network.py | 221 ---- 17 files changed, 3634 insertions(+), 514 deletions(-) create mode 100644 tests/helpers.py create mode 100644 tests/mock/network.mjs create mode 100644 tests/mock/package-lock.json create mode 100644 tests/mock/package.json create mode 100644 tests/network.py create mode 100644 tests/test_commands.py delete mode 100644 tests/test_controller.py create mode 100644 tests/test_identify.py create mode 100644 tests/test_lifecycle.py create mode 100644 tests/test_pairing.py create mode 100644 tests/test_state.py create mode 100644 tests/test_unpair.py delete mode 100644 tests/virtual_zwave_network.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7727265..e267598 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,55 +2,16 @@ name: QA & Tests on: push: - # Long-lived branches — keep whichever your repo uses (main or master) and develop. branches: - - main - master - develop pull_request: workflow_dispatch: jobs: - # Real integrations created from this template (any repo other than the template itself) - # run the full shared QA suite, including tests/test_controller.py — which stays red until - # the controller is implemented. test: - if: github.repository != 'MajorDom-Systems/integration-template' uses: ParkerIndustries/workflows/.github/workflows/test.yml@v1 - # Defaults to the team matrix ["3.12", "3.13"]. Uncomment to customize: - # with: - # python-versions: '["3.12", "3.13", "3.14"]' - # # Runs after `poetry install`, before checks (system deps, Cython build_ext, ...): - # setup-script: | - # sudo apt-get update && sudo apt-get install -y espeak-ng - # # Teardown, always runs after checks: - # post-run-script: docker compose down - - # ───────────────────────────────────────────────────────────────────────────── - # TEMPLATE-ONLY — delete this whole job AND the `if:` line on `test` above when you - # scaffold a real integration. - # - # tests/test_controller.py is prefilled to FAIL until the controller is implemented (no - # skip markers in the code or pytest config — deliberate, so a real integration's CI is - # red until it works). On the template repo itself we run everything *except* that file so - # the template stays green. This exclusion lives only here, only for the template's repo. - template-selfcheck: - if: github.repository == 'MajorDom-Systems/integration-template' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - name: Install poetry - run: pipx install poetry - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - # No `cache: poetry` — the template gitignores poetry.lock, so there's nothing to key on. - - name: Install dependencies - run: poetry install - - name: Lint & type-check (full tree, including the skeleton tests) - run: | - poetry run ruff check . - poetry run ruff format --check . - poetry run ty check - - name: Tests (excluding the prefilled, expected-to-fail controller suite) - run: poetry run pytest --ignore=tests/test_controller.py + with: + # The end-to-end tests run against a mock Z-Wave network (zwave-js's mock controller behind a real + # zwave-js-server, tests/mock): install it. Node.js comes with the runner. + setup-script: npm ci --prefix tests/mock --no-audit --no-fund diff --git a/.gitignore b/.gitignore index bffc3c9..734fcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ venv/ # OS .DS_Store Thumbs.db +node_modules/ diff --git a/tests/conftest.py b/tests/conftest.py index 6857c73..094b93b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,126 +1,63 @@ -from typing import cast -from uuid import uuid4 +"""Shared fixtures: a fresh controller and Hub doubles per test, and the mock Z-Wave network it connects to.""" + +from collections.abc import AsyncIterator, Awaitable, Callable +from typing import Any, cast import pytest from majordom_integration_sdk.controller import AbstractController -from majordom_integration_sdk.schemas.device import ( - CredentialsType, - Device, - DeviceState, - Discovery, -) -from majordom_integration_sdk.schemas.parameter import ( - ParameterDataType, - ParameterRole, - ParameterState, - ParameterVisibility, -) -from majordom_integration_sdk.testing import ( - RecordingControllerOutput, - build_test_dependencies, -) -from virtual_zwave_network import VirtualZwaveNetwork +from majordom_integration_sdk.testing import RecordingControllerOutput, build_test_dependencies from majordom_zwave import ZwaveController +from tests.network import MockNetwork -INTEGRATION = "Zwave" +INTEGRATION = "ZWave" -@pytest.fixture(scope="session") +@pytest.fixture def deps() -> AbstractController.Dependencies: return build_test_dependencies(integration=INTEGRATION) -@pytest.fixture(scope="session") +@pytest.fixture def output(deps: AbstractController.Dependencies) -> RecordingControllerOutput: - # deps.output is typed as the abstract ControllerOutput; build_test_dependencies() - # always wires the concrete RecordingControllerOutput, so this cast is safe and - # gives tests access to its recorded fields (.events, .connected_devices, ...). return cast(RecordingControllerOutput, deps.output) -@pytest.fixture(scope="session") -def controller(deps: AbstractController.Dependencies) -> ZwaveController: - return ZwaveController(deps) - - -@pytest.fixture(autouse=True) -def virtual_zwave(): - # autouse: any test calling controller.start() needs this active, even if - # it never touches nodes directly (otherwise it hits a real socket). - with VirtualZwaveNetwork() as net: - yield net - - -async def _seed(deps: AbstractController.Dependencies, device: DeviceState) -> None: - async with deps.make_device_repository() as repo: - await repo.save(device) - - @pytest.fixture -def discovery() -> Discovery: - return Discovery( - id=uuid4(), - integration=INTEGRATION, - expected_credentials_options=[CredentialsType.none], - transport="wifi", - device_manufacturer="ACME", - device_name="Zwave Lamp", - device_category=None, - device_icon=None, - ) +def fast(monkeypatch: pytest.MonkeyPatch) -> None: + """Shorten the waits meant for people and radios, so failure paths run in seconds.""" + monkeypatch.setattr(ZwaveController, "RECONNECT_DELAYS", (0.2, 0.5)) + monkeypatch.setattr(ZwaveController, "EXCLUSION_TIMEOUT", 2.0) + monkeypatch.setattr(ZwaveController, "READY_TIMEOUT", 3.0) + monkeypatch.setattr(ZwaveController, "IDENTIFY_BLINKS", 2) + monkeypatch.setattr(ZwaveController, "IDENTIFY_PERIOD", 0.05) @pytest.fixture -def make_provisional_device(deps: AbstractController.Dependencies): - async def _factory(discovery: Discovery) -> DeviceState: - # parameters=[] on purpose: a placeholder param here needs Zwave-specific - # integration_data (not None), or pair_device's re-read as ZwaveDeviceState - # fails validation. Pairing is what maps and persists real parameters. - device = DeviceState( - id=discovery.id, - name="Living Room Lamp", - room_id=uuid4(), - transport=discovery.transport, - integration=INTEGRATION, - manufacturer=discovery.device_manufacturer, - parameters=[], - ) - await _seed(deps, device) - return device - - return _factory +async def controller(deps: AbstractController.Dependencies, fast: None) -> AsyncIterator[ZwaveController]: + controller = ZwaveController(deps) + yield controller + await controller.stop() @pytest.fixture -async def device_state(deps: AbstractController.Dependencies) -> DeviceState: - power = ParameterState( - id=uuid4(), - name="Power", - data_type=ParameterDataType.bool, - role=ParameterRole.control, - visibility=ParameterVisibility.user, - integration_data=None, - value=True, - ) - state = DeviceState( - id=uuid4(), - name="Living Room Lamp", - room_id=uuid4(), - transport="wifi", - integration=INTEGRATION, - manufacturer="ACME", - parameters=[power], - ) - await _seed(deps, state) - return state +async def make_network(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[Callable[..., Awaitable[MockNetwork]]]: + """Start a mock network (with the given nodes already included) and point the integration at it.""" + networks: list[MockNetwork] = [] + async def make(nodes: list[dict[str, Any]] | None = None, serve: bool = True) -> MockNetwork: + network = MockNetwork(nodes or []) + networks.append(network) + await network.start(serve=serve) + monkeypatch.setenv("ZWAVE_SERVER_URL", network.url) + return network -@pytest.fixture -def device(device_state: DeviceState) -> Device: - return Device.model_validate(device_state.model_dump()) + yield make + for network in networks: + await network.stop() @pytest.fixture -def parameter(device_state: DeviceState) -> ParameterState: - return device_state.parameters[0] +async def network(make_network: Callable[..., Awaitable[MockNetwork]]) -> MockNetwork: + """An empty network: devices join it during the test.""" + return await make_network() diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..90d729d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,156 @@ +"""Helpers shared by the tests: what the Hub does around pairing, and waiting for what the integration reports.""" + +import asyncio +import time +from collections.abc import Callable +from typing import Any +from uuid import UUID + +from majordom_integration_sdk.controller import AbstractController +from majordom_integration_sdk.schemas import DeviceParameterChange +from majordom_integration_sdk.schemas.command import DeviceCommand +from majordom_integration_sdk.schemas.device import Device, Discovery, ProvidedCredentials +from majordom_integration_sdk.schemas.parameter import ParameterDataType +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from majordom_zwave.model import ZwaveDevice, ZwaveParameter +from tests.network import MockNetwork + +ROOM = UUID(int=42) + + +async def wait_until(predicate: Callable[[], Any], timeout: float = 10.0, message: str = "condition") -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if predicate(): + return + await asyncio.sleep(0.05) + raise TimeoutError(f"timed out after {timeout}s waiting for {message}") + + +async def hub_creates_device( + deps: AbstractController.Dependencies, discovery: Discovery, name: str = "Hall lamp" +) -> None: + """The Hub creates the device (name, room) before it asks to pair it (`DeviceProvider.create_device`).""" + async with deps.make_device_repository() as repo: + await repo.save( + Device( + id=discovery.id, + name=name, + room_id=ROOM, + transport=discovery.transport, + integration=discovery.integration, + manufacturer=discovery.device_manufacturer, + ) + ) + + +async def discover( + controller: ZwaveController, + output: RecordingControllerOutput, + network: MockNetwork, + node: int, + kind: str, + credentials: Callable[[dict[str, str]], ProvidedCredentials | None] = lambda _: None, + duration: int = 60, + **options: Any, +) -> Discovery: + """Open the pairing window and let a device join; returns its discovery.""" + label = await network.join(node, kind, **options) + seen = len(output.received_discoveries) + await controller.start_pairing_window(duration, credentials(label)) + await wait_until(lambda: len(output.received_discoveries) > seen, 20, f"discovery of node {node}") + return output.received_discoveries[-1] + + +async def pair( + controller: ZwaveController, + output: RecordingControllerOutput, + network: MockNetwork, + node: int = 5, + kind: str = "switch", + **options: Any, +) -> ZwaveDevice: + """Include a device and pair it as the Hub does; returns the stored device.""" + discovery = await discover(controller, output, network, node, kind, **options) + await hub_creates_device(controller.dependencies, discovery) + device_id = await controller.pair_device(discovery, None) + return await stored(controller, device_id or discovery.id) + + +async def stored(controller: ZwaveController, device_id: UUID) -> ZwaveDevice: + async with controller.dependencies.make_device_repository() as repo: + device = await repo.get(device_id, as_=ZwaveDevice) + assert device is not None, f"device {device_id} is not stored" + return device + + +def param(device: ZwaveDevice, key: str) -> ZwaveParameter: + """The parameter of a Z-Wave value, by its key without the node: `--[-]`.""" + matches = [p for p in device.parameters if p.integration_data.value_id.split("-", 1)[1] == key] + assert len(matches) == 1, ( + f"expected one parameter for {key}, got {[p.integration_data.value_id for p in device.parameters]}" + ) + return ZwaveParameter.model_validate(matches[0].model_dump()) + + +def changes(output: RecordingControllerOutput) -> list[DeviceParameterChange]: + return [e for e in output.events if isinstance(e, DeviceParameterChange)] + + +def values(output: RecordingControllerOutput, parameter_id: UUID) -> list[Any]: + return [e.value for e in changes(output) if e.parameter_id == parameter_id] + + +async def wait_for_value( + output: RecordingControllerOutput, + parameter: ZwaveParameter, + predicate: Callable[[Any], bool], + timeout: float = 10.0, +) -> Any: + await wait_until( + lambda: any(predicate(v) for v in values(output, parameter.id)), + timeout, + f"a value of {parameter.name} (seen: {values(output, parameter.id)})", + ) + return next(v for v in reversed(values(output, parameter.id)) if predicate(v)) + + +async def command(controller: ZwaveController, device: ZwaveDevice, parameter: ZwaveParameter, value: Any) -> None: + await controller.send_command( + DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=value), device, parameter + ) + + +PYTHON_TYPES: dict[ParameterDataType, tuple[type, ...]] = { + ParameterDataType.bool: (bool,), + ParameterDataType.integer: (int,), + ParameterDataType.decimal: (int, float), + ParameterDataType.enum: (int,), + ParameterDataType.string: (str,), +} + + +def assert_values_match_parameters(output: RecordingControllerOutput, device: ZwaveDevice) -> None: + """Every reported value has the type its parameter declares, and lies within its limits.""" + by_id = {p.id: p for p in device.parameters} + for change in changes(output): + parameter = by_id.get(change.parameter_id) + assert parameter is not None, f"event for unknown parameter {change.parameter_id}" + if change.value is None or parameter.data_type not in PYTHON_TYPES: + continue + assert isinstance(change.value, PYTHON_TYPES[parameter.data_type]), ( + f"{parameter.name}: {change.value!r} is not {parameter.data_type}" + ) + if parameter.data_type is not ParameterDataType.bool and isinstance(change.value, int | float): + assert parameter.min_value is None or change.value >= parameter.min_value, ( + f"{parameter.name}: {change.value}" + ) + assert parameter.max_value is None or change.value <= parameter.max_value, ( + f"{parameter.name}: {change.value}" + ) + if parameter.data_type is ParameterDataType.enum: + assert parameter.valid_values and change.value in parameter.valid_values, ( + f"{parameter.name}: {change.value}" + ) diff --git a/tests/mock/network.mjs b/tests/mock/network.mjs new file mode 100644 index 0000000..798abfc --- /dev/null +++ b/tests/mock/network.mjs @@ -0,0 +1,377 @@ +// Mock Z-Wave network for the end-to-end tests: zwave-js's own mock controller and mock nodes (the ones its +// integration tests use), a real zwave-js driver on top, and a real zwave-js-server in front of it. +// The nodes already on the network are given as argv; the tests drive it over stdin, one JSON command per line ({"id", "cmd", ...}); each reply is a JSON line on stdout. +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { createInterface } from "node:readline"; + +import { ZwavejsServer } from "@zwave-js/server"; +import { BinarySwitchCCReport, MultilevelSensorCCReport, SceneActivationCCSet } from "@zwave-js/cc"; +import { CommandClasses, SecurityClass, dskToString } from "@zwave-js/core"; +import { + AddNodeToNetworkRequest, + AddNodeType, + IsFailedNodeRequest, + IsFailedNodeResponse, + RemoveFailedNodeRequest, + RemoveFailedNodeRequestStatusReport, + RemoveFailedNodeResponse, + RemoveFailedNodeStartFlags, + RemoveFailedNodeStatus, + RemoveNodeFromNetworkRequest, + RemoveNodeFromNetworkRequestStatusReport, + RemoveNodeStatus, + RemoveNodeType, +} from "@zwave-js/serial/serialapi"; +import { FunctionType } from "@zwave-js/serial"; +import { + MockController, + MockNode, + ccCaps, + createMockZWaveRequestFrame, + getDefaultSupportedFunctionTypes, +} from "@zwave-js/testing"; +import { Bytes } from "@zwave-js/shared"; +import { createAndStartDriverWithMockPort, createDefaultMockControllerBehaviors, createDefaultMockNodeBehaviors } from "zwave-js/Testing"; + +// stdout carries the protocol only: the libraries' console output goes to stderr +console.log = console.info = console.debug = console.warn = console.error; + +const HOME_ID = 0x7e570001; +const OWN_NODE_ID = 1; + +// Every kind has the CCs a real device needs to be interviewed: identity (Manufacturer Specific, Version) and +// Z-Wave Plus Info. Manufacturer ids outside the device database, so no device file changes what is reported. +const BASE = [ + CommandClasses["Z-Wave Plus Info"], + CommandClasses["Manufacturer Specific"], + CommandClasses.Version, +]; + +let sensorValue = 21; // whole: a reading like this must still be a decimal + +const KINDS = { + switch: () => [ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, defaultValue: false })], + // confirms (or refuses) each command through Supervision CC + supervised: () => [ + CommandClasses.Supervision, + ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, defaultValue: false }), + ], + dimmer: () => [ccCaps({ ccId: CommandClasses["Multilevel Switch"], version: 4, defaultValue: 0, primarySwitchType: 2 })], + bulb: () => [ + ccCaps({ ccId: CommandClasses["Multilevel Switch"], version: 4, defaultValue: 0, primarySwitchType: 2 }), + ccCaps({ ccId: CommandClasses["Color Switch"], version: 3, colorComponents: { 2: 0, 3: 0, 4: 0 } }), + ], + identifiable: () => [ + ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, defaultValue: false }), + ccCaps({ ccId: CommandClasses.Indicator, version: 3, indicators: { 0x50: { properties: [3, 4, 5] } } }), + ], + sensor: () => [ + ccCaps({ + ccId: CommandClasses["Multilevel Sensor"], + version: 11, + sensors: { 1: { supportedScales: [0] } }, // air temperature, °C + getValue: () => sensorValue, + }), + ], + sensor_fahrenheit: () => [ + ccCaps({ + ccId: CommandClasses["Multilevel Sensor"], + version: 11, + sensors: { 1: { supportedScales: [1] } }, // air temperature, °F + getValue: () => 69.8, + }), + ], + scene: () => [{ ccId: CommandClasses["Scene Activation"], version: 1, isSupported: true }], + // a double wall switch: one relay per Multi Channel endpoint, each with the same CC and labels + two_switches: () => ({ + commandClasses: [CommandClasses["Multi Channel"], ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, defaultValue: false })], + endpoints: [1, 2].map(() => ({ + commandClasses: [ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, defaultValue: false })], + })), + }), + secure: () => [ + CommandClasses["Security 2"], + ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, secure: true, defaultValue: false }), + ], +}; + +function capabilities(kind, options = {}) { + const make = KINDS[kind]; + if (!make) throw new Error(`unknown node kind ${kind}`); + const made = make(); + const { commandClasses, endpoints } = Array.isArray(made) ? { commandClasses: made } : made; + return { + manufacturerId: 0xfff0, + productType: options.productType ?? 0x0001, + productId: options.productId ?? 0x0001, + commandClasses: [...BASE, ...commandClasses], + ...(endpoints ? { endpoints } : {}), + ...(kind === "secure" + ? { securityClasses: new Set([options.access ? SecurityClass.S2_AccessControl : SecurityClass.S2_Authenticated]) } + : {}), + }; +} + +function withPayload(message, bytes) { + message.payload = Bytes.from(bytes); + return message; +} + +class Network { + failed = new Set(); // node ids the controller reports as failed + silenced = new Set(); // node ids that stop responding (no ACK, no reply) + rejecting = new Set(); // node ids that refuse commands + removeCallbackId = undefined; // of the running exclusion, to complete it on request + + async start(nodes) { + this.cacheDir = await mkdtemp(path.join(tmpdir(), "zwave-mock-")); + const securityKeys = { + S0_Legacy: Bytes.from("0102030405060708090a0b0c0d0e0f10", "hex"), + S2_Unauthenticated: Bytes.from("11111111111111111111111111111111", "hex"), + S2_Authenticated: Bytes.from("22222222222222222222222222222222", "hex"), + S2_AccessControl: Bytes.from("33333333333333333333333333333333", "hex"), + }; + const { driver, continueStartup, mockPort, serial } = await createAndStartDriverWithMockPort({ + logConfig: { enabled: false }, + securityKeys, + storage: { cacheDir: this.cacheDir, lockDir: path.join(this.cacheDir, "locks") }, + testingHooks: { skipFirmwareIdentification: true }, + }); + this.driver = driver; + this.controller = await MockController.create({ + homeId: HOME_ID, + ownNodeId: OWN_NODE_ID, + mockPort, + serial, + securityKeys, // the network's keys: secure nodes bootstrap with them + capabilities: { + supportedFunctionTypes: [ + ...getDefaultSupportedFunctionTypes(), + FunctionType.IsFailedNode, + FunctionType.RemoveFailedNode, + ], + }, + }); + this.controller.defineBehavior(...createDefaultMockControllerBehaviors()); + this.controller.defineBehavior(...this.behaviors()); // defined later: checked before the defaults + for (const spec of nodes) this.controller.addNode(await this.createNode(spec.node, spec.kind, spec)); + const ready = new Promise((resolve) => driver.once("all nodes ready", resolve)); + continueStartup(); + await ready; + } + + // What the default mock controller lacks. These messages are normally only sent by the host or only parsed + // by it, so the mock reads some requests' fields from the payload, and sets the replies' payload itself. + behaviors() { + const network = this; + let adding = false; + return [ + { + async onHostMessage(controller, msg) { + if (msg instanceof IsFailedNodeRequest) { + const failed = network.failed.has(msg.payload[0]); + await controller.sendMessageToHost(withPayload(new IsFailedNodeResponse({ result: failed }), [failed ? 1 : 0])); + return true; + } + }, + }, + { + async onHostMessage(controller, msg) { + if (!(msg instanceof RemoveFailedNodeRequest)) return; + const nodeId = msg.failedNodeId; + const node = controller.nodes.get(nodeId); + const removable = node && network.failed.has(nodeId); + const started = removable ? RemoveFailedNodeStartFlags.OK : RemoveFailedNodeStartFlags.NodeNotFound; + await controller.sendMessageToHost(withPayload(new RemoveFailedNodeResponse({ removeStatus: started }), [started])); + if (removable) { + controller.removeNode(node); + network.failed.delete(nodeId); + const status = RemoveFailedNodeStatus.NodeRemoved; + const report = new RemoveFailedNodeRequestStatusReport({ callbackId: msg.callbackId, removeStatus: status }); + await controller.sendMessageToHost(withPayload(report, [msg.callbackId, status])); + } + return true; + }, + }, + { + // zwave-js stops inclusion twice (the second without callback); the default behavior takes a stop + // while idle for a start, and then refuses the next inclusion: ignore it + async onHostMessage(controller, msg) { + if (!(msg instanceof AddNodeToNetworkRequest)) return; + const stop = msg.addNodeType === AddNodeType.Stop; + if (stop && !adding) return true; + adding = !stop; + return false; + }, + }, + { + // The default behavior only enters the exclusion state; remember the callback to finish it in `exclude` + async onHostMessage(controller, msg) { + if (msg instanceof RemoveNodeFromNetworkRequest && msg.removeNodeType !== RemoveNodeType.Stop) { + network.removeCallbackId = msg.callbackId; + } + return false; + }, + }, + ]; + } + + nodeBehaviors(id) { + const network = this; + return [ + { + handleCC(controller, self, receivedCC) { + if (network.silenced.has(id)) return { action: "stop" }; // out of range: no answer + // rejects what it is told to do: a supervised command gets a Fail status back + if (network.rejecting.has(id) && receivedCC.constructor.name.endsWith("Set")) return { action: "fail" }; + }, + }, + ]; + } + + silence(id, silent) { + // out of range: frames to it are neither acknowledged nor answered + if (silent) this.silenced.add(id); + else this.silenced.delete(id); + const node = this.controller.nodes.get(id); + if (node) node.autoAckControllerFrames = !silent; + } + + async createNode(id, kind, options) { + const node = await MockNode.create({ id, controller: this.controller, capabilities: capabilities(kind, options) }); + node.defineBehavior(...createDefaultMockNodeBehaviors()); + node.defineBehavior(...this.nodeBehaviors(id)); // checked before the defaults + return node; + } + + async startServer(port) { + this.port = port ?? this.port; + this.server = new ZwavejsServer(this.driver, { port: this.port, host: "127.0.0.1" }); + await this.server.start(true); + } + + async handle(request) { + const { cmd } = request; + switch (cmd) { + case "server": // start zwave-js-server on the given port (also after `drop_server`) + await this.startServer(request.port); + return { port: this.port }; + case "drop_server": // the server goes away: every client loses its connection + await this.server.destroy(); + return {}; + case "join": { + // a node waiting to be included: it joins once the controller starts inclusion + const id = request.node; + const caps = capabilities(request.kind, request); + const network = this; + // The PIN and DSK come from the node's key pair, generated when the node is created at inclusion: + // generate it now, so the test knows them before including (as printed on a real device's label) + const probe = await MockNode.create({ id, controller: this.controller, capabilities: caps }); + if (request.silent) this.silenced.add(id); // joins, then never answers: its interview cannot finish + this.controller.nodePendingInclusion = { + id, + capabilities: caps, + setup(node) { + node.ecdhKeyPair = probe.ecdhKeyPair; + node.defineBehavior(...network.nodeBehaviors(id)); + node.autoAckControllerFrames = !network.silenced.has(id); + }, + }; + // as zwave-js reads it from the key the node sends: its first 16 bytes (MockNode.dsk skips one byte) + const dsk = dskToString(probe.ecdhKeyPair.publicKey.slice(0, 16)); + return { pin: dsk.slice(0, 5), dsk }; + } + case "fail": // the controller reports the node as failed (it can be removed without the device) + this.failed.add(request.node); + this.silence(request.node, true); + return {}; + case "silence": + this.silence(request.node, true); + return {}; + case "reject": + this.rejecting.add(request.node); + return {}; + case "revive": { + // the node answers again and announces itself with a report + this.silence(request.node, false); + this.failed.delete(request.node); + await this.report({ node: request.node, kind: "switch", value: request.value ?? false }); + return {}; + } + case "exclude": { + // the user puts the device into exclusion mode while the controller is excluding + const node = this.controller.nodes.get(request.node); + if (!node || this.removeCallbackId === undefined) throw new Error("no exclusion running for this node"); + const callbackId = this.removeCallbackId; + await this.controller.sendMessageToHost(new RemoveNodeFromNetworkRequestStatusReport({ callbackId, status: RemoveNodeStatus.NodeFound })); + await this.controller.sendMessageToHost( + new RemoveNodeFromNetworkRequestStatusReport({ callbackId, status: RemoveNodeStatus.RemovingSlave, nodeId: node.id }), + ); + this.controller.removeNode(node); + await this.controller.sendMessageToHost( + new RemoveNodeFromNetworkRequestStatusReport({ callbackId, status: RemoveNodeStatus.Done, nodeId: node.id }), + ); + return {}; + } + case "report": + await this.report(request); + return {}; + case "frames": { + // the CC commands a node received, by class name, for assertions on what was sent + const node = this.controller.nodes.get(request.node); + return { frames: (node?.receivedControllerFrames ?? []).map((f) => f.payload?.constructor?.name).filter(Boolean) }; + } + case "nodes": // the node ids the controller knows + return { nodes: [...this.controller.nodes.keys()].filter((id) => id !== OWN_NODE_ID) }; + case "stop": + setImmediate(async () => { + await this.stop(); + process.exit(0); + }); + return {}; + default: + throw new Error(`unknown command ${cmd}`); + } + } + + async report({ node: id, kind, value }) { + const node = this.controller.nodes.get(id); + if (!node) throw new Error(`no node ${id}`); + const address = { nodeId: OWN_NODE_ID }; + let cc; + if (kind === "switch") cc = new BinarySwitchCCReport({ ...address, currentValue: value }); + else if (kind === "sensor") { + sensorValue = value; + cc = new MultilevelSensorCCReport({ ...address, type: 1, scale: 0, value }); + } else if (kind === "scene") cc = new SceneActivationCCSet({ ...address, sceneId: value }); + else throw new Error(`unknown report ${kind}`); + await node.sendToController(createMockZWaveRequestFrame(cc, { ackRequested: false })); + } + + async stop() { + await this.server?.destroy().catch(() => {}); + await this.driver?.destroy().catch(() => {}); + await rm(this.cacheDir, { recursive: true, force: true }); + } +} + +// argv: {"nodes": [{"node": 5, "kind": "switch"}, ...]}, the nodes already included when the network starts +const { nodes = [] } = JSON.parse(process.argv[2] ?? "{}"); +const network = new Network(); +await network.start(nodes); +const print = (message) => process.stdout.write(`${JSON.stringify(message)}\n`); +print({ ready: true }); + +const lines = createInterface({ input: process.stdin }); +for await (const line of lines) { + if (!line.trim()) continue; + const request = JSON.parse(line); + try { + print({ id: request.id, ok: true, result: await network.handle(request) }); + } catch (error) { + print({ id: request.id, ok: false, error: String(error?.stack ?? error) }); + } +} +await network.stop(); diff --git a/tests/mock/package-lock.json b/tests/mock/package-lock.json new file mode 100644 index 0000000..69b2642 --- /dev/null +++ b/tests/mock/package-lock.json @@ -0,0 +1,2239 @@ +{ + "name": "majordom-zwave-mock-network", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "majordom-zwave-mock-network", + "dependencies": { + "@zwave-js/server": "3.11.0", + "zwave-js": "15.31.0" + } + }, + "node_modules/@alcalzone/jsonl-db": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@alcalzone/jsonl-db/-/jsonl-db-4.0.2.tgz", + "integrity": "sha512-GUs0Q2UYdOw3mtWD4NOePxRAvCEp6yMjpzRkbAOPxlBn3YQ0tuUzUe70RW0CIEy0Q6glLR2flyLFZPnPJyZIoQ==", + "license": "MIT", + "dependencies": { + "@alcalzone/proper-lockfile": "^4.1.3-0", + "alcalzone-shared": "^5.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@alcalzone/proper-lockfile": { + "version": "4.1.3-0", + "resolved": "https://registry.npmjs.org/@alcalzone/proper-lockfile/-/proper-lockfile-4.1.3-0.tgz", + "integrity": "sha512-8mlX3l5Xc+pYyiK9G156NyMosNuvvukL+TtNMqw7ti2zgVpz+WqPMPb2J1WU8I03Jbm4cXF+Q0D53hWvQqLQ0Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.4.tgz", + "integrity": "sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==", + "license": "MIT" + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.9.tgz", + "integrity": "sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@homebridge/ciao": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.12.tgz", + "integrity": "sha512-84/0u0kqKy4qcKupIDaTtbkmk+3qFDuMzxTC0NPfVlHcUT+jHGdP+QCqwEw94/GJxRezDeBLwxaclkBZgCdeUg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1" + }, + "bin": { + "ciao-bcs": "lib/bonjour-conformance-testing.js" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", + "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "12.0.0", + "debug": "4.4.0", + "node-addon-api": "8.3.0", + "node-gyp-build": "4.8.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "12.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "license": "MIT", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", + "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", + "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", + "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", + "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", + "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", + "license": "MIT", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", + "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "13.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", + "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", + "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", + "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", + "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", + "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@zwave-js/cc": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/@zwave-js/cc/-/cc-15.31.0.tgz", + "integrity": "sha512-G727OUvGQSySp0XA3u1Vz7yC8Rcfmx3o+b5NBH01liyLSR3cx0vCWxOQpMW/t7NHbQSqLCVmUkNo5oG8/iYHIw==", + "license": "MIT", + "dependencies": { + "@zwave-js/core": "15.29.0", + "@zwave-js/host": "15.31.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "ansi-colors": "^4.1.3", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/config": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/@zwave-js/config/-/config-15.31.0.tgz", + "integrity": "sha512-YaZ40jRGIdU+f0tR8tnTWgAZVmwPqu0ID+/QGtlOeh5L3kwvweC9iaKt3bj3aDGpBKiSLuOVaDlV5qGqrZ0Swg==", + "license": "MIT", + "dependencies": { + "@zwave-js/core": "15.29.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "ansi-colors": "^4.1.3", + "eslint": "^10.3.0", + "json-logic-js": "^2.0.5", + "json5": "^2.2.3", + "pathe": "^2.0.3", + "semver": "^7.8.5", + "tslib": "^2.8.1", + "winston": "^3.19.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/core": { + "version": "15.29.0", + "resolved": "https://registry.npmjs.org/@zwave-js/core/-/core-15.29.0.tgz", + "integrity": "sha512-CGh3wpWeX4KSZvuhRiFeTfnz8Grg8WMzuD/+Zta8jWTb1rAE+55Z20L5p3pi+elJZg4GJc1uFPD5s8jX3WaZ3A==", + "license": "MIT", + "dependencies": { + "@alcalzone/jsonl-db": "^4.0.2", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "ansi-colors": "^4.1.3", + "dayjs": "^1.11.21", + "fflate": "0.8.3", + "logform": "^2.7.0", + "nrf-intel-hex": "^1.4.0", + "pathe": "^2.0.3", + "reflect-metadata": "^0.2.2", + "semver": "^7.8.5", + "triple-beam": "*", + "tslib": "^2.8.1", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/host": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/@zwave-js/host/-/host-15.31.0.tgz", + "integrity": "sha512-cR+1BPE3/u8ZyXRnFA/BSOPEs0wPxCWxgx1JmyARLA+69LQXMLyAaNCTCJnoOaYPNt66ifg1CXTl/hYKKnmoEA==", + "license": "MIT", + "dependencies": { + "@zwave-js/config": "15.31.0", + "@zwave-js/core": "15.29.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/nvmedit": { + "version": "15.29.0", + "resolved": "https://registry.npmjs.org/@zwave-js/nvmedit/-/nvmedit-15.29.0.tgz", + "integrity": "sha512-u70k/g4+nTg4QhsvMMAZrAgf2DA5P5ttZVjHl2RvXW/VyVtSW+S6lBMrPAmK170CmNdbkm5Hp+n1xKawDXDapA==", + "license": "MIT", + "dependencies": { + "@zwave-js/core": "15.29.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "reflect-metadata": "^0.2.2", + "semver": "^7.8.5", + "tslib": "^2.8.1", + "yargs": "^18.0.0" + }, + "bin": { + "nvmedit": "bin/nvmedit.mjs" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/serial": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/@zwave-js/serial/-/serial-15.31.0.tgz", + "integrity": "sha512-qO35XvyLmsd36j1YKL/PO7l7OZq2AY13giaMNVc8XiON3u/DTDpLHQWdG9QcG1zOWI+fwNUDGGEqerjC8p7ztw==", + "license": "MIT", + "dependencies": { + "@serialport/stream": "^13.0.0", + "@zwave-js/cc": "15.31.0", + "@zwave-js/core": "15.29.0", + "@zwave-js/host": "15.31.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "serialport": "^13.0.0", + "tslib": "^2.8.1", + "winston": "^3.19.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/server": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@zwave-js/server/-/server-3.11.0.tgz", + "integrity": "sha512-qOSHm7agzHKHAdWLTH0LtQeRK61fA+nR/6z/slgWwL5pG1RudfsJS3fhHTqEKqSICUjdAwIqO7wzo6Mod00tpw==", + "license": "Apache-2.0", + "dependencies": { + "@homebridge/ciao": "^1.1.7", + "minimist": "^1.2.8", + "ws": "^8.18.0" + }, + "bin": { + "zwave-client": "dist-esm/bin/client.js", + "zwave-server": "dist-esm/bin/server.js" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "zwave-js": "^15.31.0" + } + }, + "node_modules/@zwave-js/shared": { + "version": "15.29.0", + "resolved": "https://registry.npmjs.org/@zwave-js/shared/-/shared-15.29.0.tgz", + "integrity": "sha512-NK1eAUxGuRw+RqU4/vQ/GEbd9xJ6yDyXTzoeWs0szIjcs7ew3MqWSmDQq6tdckZuADMAI20/W+PPo1fzv38gjA==", + "license": "MIT", + "dependencies": { + "alcalzone-shared": "^5.0.0", + "pathe": "^2.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/testing": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/@zwave-js/testing/-/testing-15.31.0.tgz", + "integrity": "sha512-YD1e1ISTxmAUK9I4vcR2aiqqepvh64dJThh6YGq6ZbZ2e7cQh8yQgI4P9PgeYBqS/5GqfiwWYFlBgkTUt1w4kQ==", + "license": "MIT", + "dependencies": { + "@zwave-js/core": "15.29.0", + "@zwave-js/host": "15.31.0", + "@zwave-js/serial": "15.31.0", + "@zwave-js/shared": "15.29.0", + "alcalzone-shared": "^5.0.0", + "ansi-colors": "^4.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + }, + "node_modules/@zwave-js/waddle": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@zwave-js/waddle/-/waddle-1.3.1.tgz", + "integrity": "sha512-HYbXtH5GnJ8YXgkbaRmclXFalFxAfIWVohtUVocaXa8H2aYjtUTVTCSHhe39IE51/gh0Y2zWngeShB4ptdoP0A==", + "license": "MIT", + "dependencies": { + "alcalzone-shared": "^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alcalzone-shared": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/alcalzone-shared/-/alcalzone-shared-5.0.0.tgz", + "integrity": "sha512-X73hgVWcrIKUUB6jZgHj5flRbTft8AAoJ2MqRKEcAX1whW3OeGkxsQ6ol4nd4/rKxd1eoCRXUGW3cIhXrXU4Sg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-logic-js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.5.tgz", + "integrity": "sha512-rTT2+lqcuUmj4DgWfmzupZqQDA64AdmYqizzMPWj3DxGdfFNsxPpcNVSaTj4l8W2tG/+hg7/mQhxjU3aPacO6g==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/mdns-server": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/mdns-server/-/mdns-server-1.0.11.tgz", + "integrity": "sha512-luzHnhQYmxaqrvYb8akwH2V/lSSyAumDoAK1zmogvpsj7sd4l04AF/LchBYH44NQVm7oo/KBVVFg3AbsJXr12w==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nrf-intel-hex": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz", + "integrity": "sha512-q3+GGRIpe0VvCjUP1zaqW5rk6IpCZzhD0lu7Sguo1bgWwFcA9kZRjsaKUb0jBQMnefyOl5o0BBGAxvqMqYx8Sg==", + "license": "BSD", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialport": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", + "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "13.0.0", + "@serialport/parser-byte-length": "13.0.0", + "@serialport/parser-cctalk": "13.0.0", + "@serialport/parser-delimiter": "13.0.0", + "@serialport/parser-inter-byte-timeout": "13.0.0", + "@serialport/parser-packet-length": "13.0.0", + "@serialport/parser-readline": "13.0.0", + "@serialport/parser-ready": "13.0.0", + "@serialport/parser-regex": "13.0.0", + "@serialport/parser-slip-encoder": "13.0.0", + "@serialport/parser-spacepacket": "13.0.0", + "@serialport/stream": "13.0.0", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/serialport/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwave-js": { + "version": "15.31.0", + "resolved": "https://registry.npmjs.org/zwave-js/-/zwave-js-15.31.0.tgz", + "integrity": "sha512-eRHBpPHFGaEBaqbZDt2ef5kkRW2pfn3g5tp8rMYi5kGIKXgDubPG/siILlhaJm4uhZvq6899ni4aUCADrlf7CQ==", + "license": "MIT", + "dependencies": { + "@alcalzone/jsonl-db": "^4.0.2", + "@andrewbranch/untar.js": "^1.0.3", + "@homebridge/ciao": "^1.3.8", + "@zwave-js/cc": "15.31.0", + "@zwave-js/config": "15.31.0", + "@zwave-js/core": "15.29.0", + "@zwave-js/host": "15.31.0", + "@zwave-js/nvmedit": "15.29.0", + "@zwave-js/serial": "15.31.0", + "@zwave-js/shared": "15.29.0", + "@zwave-js/testing": "15.31.0", + "@zwave-js/waddle": "^1.2.3", + "alcalzone-shared": "^5.0.0", + "ansi-colors": "^4.1.3", + "ky": "^1.8.1", + "mdns-server": "^1.0.11", + "p-queue": "^9.1.0", + "pathe": "^2.0.3", + "reflect-metadata": "^0.2.2", + "semver": "^7.8.5", + "serialport": "^13.0.0", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "winston": "^3.19.0" + }, + "bin": { + "mock-server": "bin/mock-server.cjs" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + } + } + } +} diff --git a/tests/mock/package.json b/tests/mock/package.json new file mode 100644 index 0000000..76f7e28 --- /dev/null +++ b/tests/mock/package.json @@ -0,0 +1,10 @@ +{ + "name": "majordom-zwave-mock-network", + "private": true, + "description": "Mock Z-Wave network for the end-to-end tests: zwave-js's mock controller and nodes behind a real zwave-js-server", + "type": "module", + "dependencies": { + "@zwave-js/server": "3.11.0", + "zwave-js": "15.31.0" + } +} diff --git a/tests/network.py b/tests/network.py new file mode 100644 index 0000000..d786b7b --- /dev/null +++ b/tests/network.py @@ -0,0 +1,116 @@ +"""Runs the mock Z-Wave network (tests/mock/network.mjs) as a subprocess and talks to it. + +The network is zwave-js's own mock controller and mock nodes, with a real zwave-js driver and a real +zwave-js-server in front: the integration connects to it exactly as to a real server. Needs Node.js and +`npm ci --prefix tests/mock`. +""" + +import asyncio +import contextlib +import itertools +import json +import socket +from pathlib import Path +from typing import Any, Self + +MOCK_DIR = Path(__file__).parent / "mock" +HOME_ID = 0x7E570001 # the mock controller's home id + + +def free_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +class MockNetwork: + def __init__(self, nodes: list[dict[str, Any]]) -> None: + self.nodes = nodes + self.port = free_port() + self.url = f"ws://127.0.0.1:{self.port}" + self._process: asyncio.subprocess.Process | None = None + self._ids = itertools.count(1) + self._lock = asyncio.Lock() + + async def start(self, serve: bool = True) -> Self: + if not (MOCK_DIR / "node_modules").exists(): + raise RuntimeError("mock network not installed: run `npm ci --prefix tests/mock`") + self._process = await asyncio.create_subprocess_exec( + "node", + str(MOCK_DIR / "network.mjs"), + json.dumps({"nodes": self.nodes}), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + limit=2**20, + ) + line = await asyncio.wait_for(self._stdout.readline(), 60) + if json.loads(line or b"{}").get("ready") is not True: + raise RuntimeError(f"mock network did not start: {line!r}") + if serve: + await self.serve() + return self + + @property + def _stdout(self) -> asyncio.StreamReader: + assert self._process is not None and self._process.stdout is not None + return self._process.stdout + + async def call(self, cmd: str, **args: Any) -> dict[str, Any]: + assert self._process is not None and self._process.stdin is not None + async with self._lock: + request_id = next(self._ids) + self._process.stdin.write(json.dumps({"id": request_id, "cmd": cmd, **args}).encode() + b"\n") + await self._process.stdin.drain() + reply = json.loads(await asyncio.wait_for(self._stdout.readline(), 30)) + if not reply.get("ok"): + raise RuntimeError(f"mock network {cmd} failed: {reply.get('error')}") + return reply["result"] + + async def serve(self) -> None: + """Start (or restart) zwave-js-server on this network's port.""" + await self.call("server", port=self.port) + + async def drop_server(self) -> None: + """Stop zwave-js-server: connected clients lose their connection, the network itself keeps running.""" + await self.call("drop_server") + + async def join(self, node: int, kind: str, **options: Any) -> dict[str, str]: + """Make a node wait for inclusion; returns its S2 `pin` and `dsk`, as printed on a real device's label.""" + return await self.call("join", node=node, kind=kind, **options) + + async def report(self, node: int, kind: str, value: Any) -> None: + await self.call("report", node=node, kind=kind, value=value) + + async def silence(self, node: int) -> None: + await self.call("silence", node=node) + + async def reject(self, node: int) -> None: + """The node refuses the commands it gets (a supervised command is answered with a Fail status).""" + await self.call("reject", node=node) + + async def fail(self, node: int) -> None: + await self.call("fail", node=node) + + async def revive(self, node: int) -> None: + await self.call("revive", node=node) + + async def exclude(self, node: int) -> None: + await self.call("exclude", node=node) + + async def node_ids(self) -> list[int]: + return (await self.call("nodes"))["nodes"] + + async def frames(self, node: int) -> list[str]: + """The CC commands the node received, by class name (e.g. `IndicatorCCSet`).""" + return (await self.call("frames", node=node))["frames"] + + async def stop(self) -> None: + if self._process is None or self._process.returncode is not None: + return + with contextlib.suppress(RuntimeError, ConnectionError, TimeoutError, json.JSONDecodeError): + await self.call("stop") + try: + await asyncio.wait_for(self._process.wait(), 10) + except TimeoutError: + self._process.kill() + await self._process.wait() diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..f60327b --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,95 @@ +"""Commands: Hub → device, and what is reported back.""" + +import pytest +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import command, pair, param, stored, values, wait_for_value +from tests.network import MockNetwork + +SWITCH = "37-0-targetValue" +LEVEL = "38-0-targetValue" + + +async def test_a_command_switches_the_device_and_its_report_comes_back( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + + await command(controller, device, switch, True) + + assert await wait_for_value(output, switch, lambda v: v is True) is True + assert "BinarySwitchCCSet" in await network.frames(5) + + +async def test_a_level_command_is_a_percentage( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "dimmer") + level = param(device, LEVEL) + + await command(controller, device, level, 100) # Z-Wave's top level is 99 + assert await wait_for_value(output, level, lambda v: v == 100) == 100 + await command(controller, device, level, 40) + assert await wait_for_value(output, level, lambda v: v == 40) == 40 + + +async def test_a_failed_command_is_surfaced_and_not_reported_as_applied( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + await network.silence(5) + output.events.clear() + + with pytest.raises(ConnectionError): + await command(controller, device, switch, True) + + assert True not in values(output, switch.id) + assert (await stored(controller, device.id)).last_error + + +async def test_a_command_the_device_refuses_is_surfaced_and_not_reported_as_applied( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "supervised") + switch = param(device, SWITCH) + await network.reject(5) + output.events.clear() + + with pytest.raises(RuntimeError): + await command(controller, device, switch, True) + + assert True not in values(output, switch.id) + assert (await stored(controller, device.id)).last_error + + +async def test_a_successful_command_clears_the_last_error( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + await network.silence(5) + with pytest.raises(ConnectionError): + await command(controller, device, switch, True) + + await network.revive(5) + await command(controller, device, switch, True) + + assert (await stored(controller, device.id)).last_error is None + + +async def test_a_read_only_parameter_cannot_be_commanded( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sensor") + + with pytest.raises(ValueError): + await command(controller, device, param(device, "49-0-Air temperature"), 30) diff --git a/tests/test_controller.py b/tests/test_controller.py deleted file mode 100644 index 0f1f9bf..0000000 --- a/tests/test_controller.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Run order matters: `controller`/`deps` are session-scoped, so pairing happens -once in test_pairs_a_discovered_device and later tests read that same device -back from the shared repo. `virtual_zwave` is function-scoped though (fresh, -empty node registry each test), so any test that needs to reach the "device" -re-creates the node with the same NODE_ID. -""" - -import asyncio - -from majordom_integration_sdk.schemas.command import DeviceCommand -from majordom_integration_sdk.schemas.event import DeviceParameterChange -from majordom_integration_sdk.testing import RecordingControllerOutput -from virtual_zwave_network import VirtualZwaveNetwork - -from majordom_zwave import ZwaveController -from majordom_zwave.model import ZwaveDevice, ZwaveDeviceState, ZwaveParameter - -NODE_ID = 5 - - -async def test_pairs_a_discovered_device( - controller: ZwaveController, - output: RecordingControllerOutput, - virtual_zwave: VirtualZwaveNetwork, - make_provisional_device, -) -> None: - await controller.start() - await controller.start_pairing_window(10) - - virtual_zwave.node_found(NODE_ID) - virtual_zwave.node_joined({"nodeId": NODE_ID}) - await asyncio.sleep(0.05) - - assert controller.discoveries, "No device was discovered during the pairing window" - discovery = next(iter(controller.discoveries.values())) - await make_provisional_device(discovery) - await controller.pair_device(discovery, credentials=None) - await controller.stop() - - assert discovery.id in output.connected_devices - - async with controller.dependencies.make_device_repository() as repo: - paired = await repo.get(discovery.id, as_=ZwaveDevice) - state = await repo.state(discovery.id, as_=ZwaveDeviceState) - assert paired is not None - assert paired.integration_data is not None - assert paired.integration_data.node_id == NODE_ID - assert state is not None - assert state.parameters, "Expected at least one parameter to be mapped during pairing" - - -async def test_fetches_state( - controller: ZwaveController, - output: RecordingControllerOutput, - virtual_zwave: VirtualZwaveNetwork, -) -> None: - await controller.start() - virtual_zwave.node_joined({"nodeId": NODE_ID}) - await asyncio.sleep(0.05) - - async with controller.dependencies.make_device_repository() as repo: - devices = await repo.get_all(as_=ZwaveDevice) - device = devices[0] - - virtual_zwave.emit_value_updated( - NODE_ID, - command_class=37, - command_class_name="Binary Switch", - property="currentValue", - new_value=True, - prev_value=False, - ) - # fetch() doesn't touch the repo -- it reports straight to output.events - # (which accumulates for the whole session), so only check what this call added. - events_before = len(output.events) - await controller.fetch(device) - new_events = output.events[events_before:] - - assert any( - isinstance(e, DeviceParameterChange) and e.device_id == device.id and e.value is True for e in new_events - ), "fetch() never reported the current value back to the Hub" - await controller.stop() - - -async def test_sends_a_command( - controller: ZwaveController, - virtual_zwave: VirtualZwaveNetwork, -) -> None: - await controller.start() - virtual_zwave.node_joined({"nodeId": NODE_ID}) - - async with controller.dependencies.make_device_repository() as repo: - devices = await repo.get_all(as_=ZwaveDevice) - device = devices[0] - state = await repo.state(device.id, as_=ZwaveDeviceState) - assert state is not None - parameter_state = next(p for p in state.parameters if p.id == state.main_parameter) - parameter = ZwaveParameter.model_validate(parameter_state.model_dump()) - - command = DeviceCommand(device_id=device.id, parameter_id=parameter.id, value=0) - await controller.send_command(command, device, parameter) - await controller.stop() - - sent = [c for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value"] - assert sent, "Controller never sent a node.set_value command to the device" - assert sent[-1].args[0]["value"] == 0 - - -async def test_identifies( - controller: ZwaveController, - virtual_zwave: VirtualZwaveNetwork, -) -> None: - await controller.start() - virtual_zwave.node_joined({"nodeId": NODE_ID}) - - async with controller.dependencies.make_device_repository() as repo: - devices = await repo.get_all(as_=ZwaveDevice) - device = devices[0] - await controller.identify(device) - await controller.stop() - - sent = [c for c in virtual_zwave.async_send_command.call_args_list if c.args[0].get("command") == "node.set_value"] - assert sent, "controller.identify() never sent a node.set_value command to the device" - - -async def test_unpairs( - controller: ZwaveController, - virtual_zwave: VirtualZwaveNetwork, -) -> None: - await controller.start() - virtual_zwave.node_joined({"nodeId": NODE_ID}) - - async with controller.dependencies.make_device_repository() as repo: - devices = await repo.get_all(as_=ZwaveDevice) - device = devices[0] - - # unpair() waits (up to 60s) for a real "node removed" event, so it has - # to run concurrently with the simulated confirmation below. - unpair_task = asyncio.create_task(controller.unpair(device)) - await asyncio.sleep(0.05) - virtual_zwave.node_removed(NODE_ID) - await unpair_task - - # Must check before stop(): stop() disconnects the virtual client, after - # which virtual_zwave.controller is torn down. - assert NODE_ID not in virtual_zwave.controller.nodes - await controller.stop() diff --git a/tests/test_identify.py b/tests/test_identify.py new file mode 100644 index 0000000..2714361 --- /dev/null +++ b/tests/test_identify.py @@ -0,0 +1,47 @@ +"""Identify: make the device show itself, without switching what it controls.""" + +import pytest +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import pair +from tests.network import MockNetwork + + +async def test_a_device_with_the_identify_indicator_identifies_itself( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "identifiable") + + await controller.identify(device) + + frames = await network.frames(5) + assert "IndicatorCCSet" in frames + assert "BinarySwitchCCSet" not in frames + + +async def test_a_light_without_the_indicator_blinks_and_returns_to_its_level( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "bulb") + + await controller.identify(device) + + frames = await network.frames(5) + assert frames.count("MultilevelSwitchCCSet") >= 3 # off/on blinks, then back to the level it had + + +@pytest.mark.parametrize("kind", ["switch", "dimmer"]) +async def test_a_device_not_known_to_be_a_light_is_not_toggled( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork, kind: str +): + await controller.start() + device = await pair(controller, output, network, 5, kind) # a relay or a motor: a heater, a pump, a blind + + await controller.identify(device) + + frames = await network.frames(5) + assert "BinarySwitchCCSet" not in frames + assert "MultilevelSwitchCCSet" not in frames diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py new file mode 100644 index 0000000..71f5199 --- /dev/null +++ b/tests/test_lifecycle.py @@ -0,0 +1,170 @@ +"""Lifecycle: start, restart, availability, losing the server, and devices that change behind the Hub's back.""" + +import asyncio +import contextlib + +import aiohttp +from majordom_integration_sdk.controller import AbstractController +from majordom_integration_sdk.testing import RecordingControllerOutput +from zwave_js_server.client import Client + +from majordom_zwave import ZwaveController, config +from tests.helpers import command, pair, param, stored, wait_for_value, wait_until +from tests.network import MockNetwork + +SWITCH = "37-0-targetValue" + + +def test_the_server_url_comes_from_the_documented_variable(monkeypatch): + monkeypatch.setenv("ZWAVE_SERVER_URL", "ws://zwave.local:3000") + assert config.server_url() == "ws://zwave.local:3000" + monkeypatch.delenv("ZWAVE_SERVER_URL") + assert config.server_url() == "ws://localhost:3000" + + +async def restart(controller: ZwaveController, deps: AbstractController.Dependencies) -> ZwaveController: + """Stop and start the integration again, with the Hub's data kept (a Hub restart).""" + await controller.stop() + again = ZwaveController(deps) + await again.start() + return again + + +async def test_paired_devices_are_reconnected_after_a_restart( + controller: ZwaveController, deps, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + output.connected_devices.clear() + + again = await restart(controller, deps) + try: + assert device.id in output.connected_devices + await network.report(5, "switch", True) + assert await wait_for_value(output, param(device, SWITCH), lambda v: v is True) + assert device.id not in again.discoveries # paired, not offered again + finally: + await again.stop() + + +async def test_a_device_that_stops_answering_is_lost_and_found_again( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + + await network.silence(5) + with contextlib.suppress(ConnectionError): # it does not answer + await command(controller, device, switch, True) + await wait_until(lambda: device.id in output.lost_devices, 10, "the device to be lost") + lost = await stored(controller, device.id) + assert lost.available is False + assert lost.last_error + + output.connected_devices.clear() + await network.revive(5) + await wait_until(lambda: device.id in output.connected_devices, 10, "the device to come back") + back = await stored(controller, device.id) + assert back.available is True + assert back.last_error is None + + +async def test_a_dead_device_is_reported_unavailable_after_a_restart( + controller: ZwaveController, deps, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + await network.silence(5) + with contextlib.suppress(ConnectionError): + await command(controller, device, param(device, SWITCH), True) + await wait_until(lambda: device.id in output.lost_devices, 10, "the device to be lost") + output.lost_devices.clear() + output.connected_devices.clear() + + again = await restart(controller, deps) + try: + assert device.id in output.lost_devices + assert device.id not in output.connected_devices + finally: + await again.stop() + + +async def test_losing_the_server_loses_the_devices_and_it_reconnects_when_back( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + await network.drop_server() + await wait_until(lambda: device.id in output.lost_devices, 10, "the devices to be lost with the server") + + output.connected_devices.clear() + await network.serve() + await wait_until(lambda: device.id in output.connected_devices, 15, "the reconnection") + await network.report(5, "switch", True) + assert await wait_for_value(output, param(device, SWITCH), lambda v: v is True) + + +async def test_starting_without_the_server_does_not_fail_and_connects_once_it_is_up( + controller: ZwaveController, output: RecordingControllerOutput, make_network +): + network = await make_network(serve=False) + + await controller.start() # the server is not up yet: reported, not raised + assert output.errors and output.errors[-1][1] is True # still running + + await network.serve() + await wait_until(lambda: controller.connected, 15, "the connection") + + +async def test_stop_is_clean_without_a_connection(controller: ZwaveController, make_network): + await make_network(serve=False) + await controller.start() + await controller.stop() + await controller.stop() + + +async def test_a_device_excluded_elsewhere_is_lost_with_a_reason( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + # removed with another tool (e.g. Z-Wave JS UI, another client of the same server) while the integration runs + async with aiohttp.ClientSession() as session: + other_tool = Client(network.url, session) + await other_tool.connect() + ready = asyncio.Event() + listening = asyncio.create_task(other_tool.listen(ready)) + await ready.wait() + assert other_tool.driver is not None + await other_tool.driver.controller.async_begin_exclusion() + await network.exclude(5) + await other_tool.disconnect() + listening.cancel() + + await wait_until(lambda: device.id in output.lost_devices, 10, "the excluded device to be lost") + assert (await stored(controller, device.id)).last_error + + +async def test_another_device_reusing_a_node_id_is_not_taken_for_the_paired_one( + controller: ZwaveController, deps, output: RecordingControllerOutput, make_network +): + network = await make_network() + await controller.start() + device = await pair(controller, output, network) + await controller.stop() + await network.stop() + + # while the Hub was off, the device was replaced: another product now has node id 5 + await make_network([{"node": 5, "kind": "switch", "productId": 2}]) + output.connected_devices.clear() + again = ZwaveController(deps) + await again.start() + try: + assert device.id not in output.connected_devices + assert device.id in output.lost_devices + assert (await stored(again, device.id)).last_error + finally: + await again.stop() diff --git a/tests/test_pairing.py b/tests/test_pairing.py new file mode 100644 index 0000000..4b547de --- /dev/null +++ b/tests/test_pairing.py @@ -0,0 +1,175 @@ +"""Pairing: inclusion through the pairing window, S2 credentials, and what pairing leaves in the Hub.""" + +import asyncio + +import pytest +from majordom_integration_sdk.schemas.device import CredentialsType, ProvidedCredentials +from majordom_integration_sdk.schemas.parameter import ParameterRole, ParameterVisibility +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import ROOM, discover, hub_creates_device, pair, param, stored, wait_until +from tests.network import HOME_ID, MockNetwork + +BINARY_SWITCH = "37-0-targetValue" + + +async def test_a_device_included_in_the_pairing_window_is_discovered( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + discovery = await discover(controller, output, network, 5, "switch") + + assert discovery.integration == controller.name + assert discovery.transport == "ZWAVE" + assert discovery.expected_credentials_options == [CredentialsType.none] + assert discovery.id in controller.discoveries + + +async def test_pairing_completes_the_device_the_hub_created( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + assert (device.name, device.room_id) == ("Hall lamp", ROOM) # kept from the Hub + assert device.available is True + assert device.last_error is None + assert device.integration_data is not None + assert (device.integration_data.home_id, device.integration_data.node_id) == (HOME_ID, 5) + assert device.parameters + assert device.id in output.connected_devices + assert device.id not in controller.discoveries + + +async def test_identity_is_derived_through_the_sdk_helpers( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + # the home id scopes the node id: node ids repeat across networks and are reused after a device leaves + assert device.id == controller.device_uuid(f"{HOME_ID:08x}-5") + for parameter in device.parameters: + assert parameter.id == controller.parameter_uuid(device.id, parameter.integration_data.value_id) + + +async def test_the_switch_is_one_parameter_and_the_main_one( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + switch = param(device, BINARY_SWITCH) # commands go to targetValue, the state comes from currentValue + assert switch.integration_data.state_value_id == "5-37-0-currentValue" + assert (switch.role, switch.visibility) == (ParameterRole.control, ParameterVisibility.user) + assert not [p for p in device.parameters if p.integration_data.value_id.endswith("-37-0-currentValue")] + assert device.main_parameter == switch.id + assert switch.default_value is None # a bool: a tap toggles it (a single value would be an always-on button) + + +async def test_pairing_without_the_hub_device_fails_and_is_reported( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + discovery = await discover(controller, output, network, 5, "switch") + + with pytest.raises(LookupError): + await controller.pair_device(discovery, None) + + assert output.updated_discoveries[-1].id == discovery.id + assert output.updated_discoveries[-1].last_error + assert discovery.id in controller.discoveries # retryable + + +async def test_pairing_an_unknown_discovery_fails( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + discovery = await discover(controller, output, network, 5, "switch") + stale = discovery.model_copy(update={"id": controller.device_uuid("nothing")}) + + with pytest.raises(ValueError): + await controller.pair_device(stale, None) + + +async def test_a_device_that_never_becomes_ready_fails_pairing_and_stays_discovered( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + discovery = await discover(controller, output, network, 5, "switch", silent=True) # joins, then never answers + await hub_creates_device(controller.dependencies, discovery) + + with pytest.raises(TimeoutError): + await controller.pair_device(discovery, None) + + assert output.updated_discoveries[-1].last_error + assert discovery.id in controller.discoveries + + +async def test_the_s2_pin_entered_with_the_pairing_window_includes_securely( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + pin = lambda label: ProvidedCredentials(type=CredentialsType.code, value=label["pin"]) # noqa: E731 + device = await pair(controller, output, network, 5, "secure", credentials=pin) + + param(device, BINARY_SWITCH) # only supported securely: present only if S2 bootstrapping succeeded + assert output.received_discoveries[-1].last_error is None + + +async def test_the_full_dsk_works_as_the_pin( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + dsk = lambda label: ProvidedCredentials(type=CredentialsType.code, value=label["dsk"]) # noqa: E731 + device = await pair(controller, output, network, 5, "secure", credentials=dsk) + + param(device, BINARY_SWITCH) + + +async def test_without_the_pin_a_secure_device_joins_with_lower_security_and_says_so( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + discovery = await asyncio.wait_for(discover(controller, output, network, 5, "secure"), 30) # no waiting for a PIN + + assert discovery.last_error and "PIN" in discovery.last_error + + +async def test_a_malformed_qr_code_is_rejected(controller: ZwaveController, network: MockNetwork): + await controller.start() + + with pytest.raises(ValueError): + await controller.start_pairing_window(60, ProvidedCredentials(type=CredentialsType.qr, value="not a code")) + + +async def test_secret_credentials_are_rejected(controller: ZwaveController, network: MockNetwork): + await controller.start() + + with pytest.raises(ValueError): + await controller.start_pairing_window(60, ProvidedCredentials(type=CredentialsType.secret, value="key")) + + +async def test_closing_the_window_keeps_live_unclaimed_devices_and_removes_failed_ones( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + alive = await discover(controller, output, network, 5, "switch", duration=1) + await asyncio.sleep(1.5) + assert alive.id in controller.discoveries + + failed = await discover(controller, output, network, 6, "switch", duration=1) + await network.fail(6) + await wait_until(lambda: failed.id in output.lost_discoveries, 10, "the failed node's discovery to be dropped") + assert 6 not in await network.node_ids() + + +async def test_a_paired_device_can_be_found_again_after_its_discovery( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + assert (await stored(controller, device.id)).id == device.id + assert not output.lost_devices diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 72a18de..3915d1d 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,11 +1,6 @@ from majordom_zwave import ZwaveController -async def test_starts_and_stops(controller: ZwaveController) -> None: - await controller.start() - await controller.stop() - - async def test_name_and_slug(controller: ZwaveController) -> None: assert controller.name == "ZWave" assert controller.name_slug == "zwave" diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..55a94b3 --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,141 @@ +"""State: what a paired device reports, in which shape, and how it maps Z-Wave values to parameters.""" + +from majordom_integration_sdk.schemas.parameter import ( + ParameterDataType, + ParameterRole, + ParameterUnit, + ParameterVisibility, +) +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import assert_values_match_parameters, changes, pair, param, values, wait_for_value +from tests.network import MockNetwork + +SWITCH = "37-0-targetValue" +LEVEL = "38-0-targetValue" +TEMPERATURE = "49-0-Air temperature" + + +async def test_pairing_reports_the_current_value_of_every_readable_parameter( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sensor") + + temperature = param(device, TEMPERATURE) + assert values(output, temperature.id) == [21] + assert_values_match_parameters(output, device) + + +async def test_changes_made_on_the_device_are_reported( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + + await network.report(5, "switch", True) # e.g. toggled by hand + + assert await wait_for_value(output, switch, lambda v: v is True) is True + + +async def test_a_sensor_reading_is_a_decimal_with_its_unit( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sensor") # paired while reading a whole 21 + temperature = param(device, TEMPERATURE) + assert (temperature.data_type, temperature.unit, temperature.role) == ( + ParameterDataType.decimal, + ParameterUnit.celsius, + ParameterRole.sensor, + ) + + await network.report(5, "sensor", 23.25) + await wait_for_value(output, temperature, lambda v: v == 23.25) + assert_values_match_parameters(output, device) + + +async def test_fahrenheit_readings_are_reported_in_celsius( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sensor_fahrenheit") + temperature = param(device, TEMPERATURE) + + assert temperature.unit == ParameterUnit.celsius + assert values(output, temperature.id) == [21] # 69.8 °F + + +async def test_a_level_is_a_percentage( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "dimmer") + level = param(device, LEVEL) + + assert (level.data_type, level.unit, level.min_value, level.max_value) == ( + ParameterDataType.integer, + ParameterUnit.percentage, + 0, + 100, + ) + assert level.integration_data.state_value_id is not None + assert device.main_parameter == level.id + # a tap toggles off / fully on, within the declared range (a set: the SDK's repository round-trip returns a list) + assert level.default_value is not None and set(level.default_value) == {0, 100} + + +async def test_scene_activations_are_events( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "scene") + + await network.report(5, "scene", 3) # a scene button pressed: stateless, sent as a value notification + + scene = next(p for p in device.parameters if p.role == ParameterRole.event) + assert await wait_for_value(output, scene, lambda v: v == 3) == 3 + + +async def test_compatibility_and_transition_values_are_not_shown( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sensor") + switch = await pair(controller, output, network, 6, "switch") + + # Basic CC mirrors the real CCs for old controllers; a transition's remaining time is not a state of its own + basic = [p for p in device.parameters if p.integration_data.value_id.split("-")[1] == "32"] + durations = [p for p in switch.parameters if p.integration_data.value_id.endswith("-duration")] + assert basic and durations + assert {p.visibility for p in basic + durations} == {ParameterVisibility.system} + + +async def test_parameters_of_another_endpoint_are_told_apart( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "two_switches") + + names = [p.name for p in device.parameters] + assert len(names) == len(set(names)), names + assert {"37-1-targetValue", "37-2-targetValue"} <= { + p.integration_data.value_id.split("-", 1)[1] for p in device.parameters + } + + +async def test_fetch_reports_the_current_values( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + switch = param(device, SWITCH) + output.events.clear() + + await controller.fetch(device) + + assert values(output, switch.id) == [False] + assert {c.parameter_id for c in changes(output)} <= {p.id for p in device.parameters} + assert_values_match_parameters(output, device) diff --git a/tests/test_unpair.py b/tests/test_unpair.py new file mode 100644 index 0000000..f868299 --- /dev/null +++ b/tests/test_unpair.py @@ -0,0 +1,68 @@ +"""Unpairing: removing the device from the Z-Wave network.""" + +import asyncio + +import pytest +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import pair, stored +from tests.network import MockNetwork + + +async def test_a_failed_device_is_removed_without_the_device( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + await network.fail(5) + + await controller.unpair(device) + + assert 5 not in await network.node_ids() + + +async def test_a_live_device_is_removed_once_it_confirms_the_exclusion( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + unpairing = asyncio.create_task(controller.unpair(device)) + await asyncio.sleep(0.3) + assert not unpairing.done() # waits for the user to press the device's button + await network.exclude(5) + await asyncio.wait_for(unpairing, 10) + + assert 5 not in await network.node_ids() + + +async def test_excluding_another_device_does_not_count_as_removing_this_one( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "switch") + other = await pair(controller, output, network, 6, "switch") + + unpairing = asyncio.create_task(controller.unpair(device)) + await asyncio.sleep(0.3) + await network.exclude(6) # the user pressed the button of the wrong device + + with pytest.raises(LookupError, match="another device"): + await asyncio.wait_for(unpairing, 10) + assert other.id in output.lost_devices + assert 5 in await network.node_ids() + + +async def test_an_unconfirmed_exclusion_times_out_with_a_reason( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network) + + with pytest.raises(TimeoutError): + await controller.unpair(device) + + assert (await stored(controller, device.id)).last_error + assert 5 in await network.node_ids() + await controller.start_pairing_window(1) # the exclusion was stopped: inclusion can start diff --git a/tests/virtual_zwave_network.py b/tests/virtual_zwave_network.py deleted file mode 100644 index d56e1d1..0000000 --- a/tests/virtual_zwave_network.py +++ /dev/null @@ -1,221 +0,0 @@ -"""In-memory zwave-js-server stub. Patches zwave_js_server.client.Client so any -Client the controller creates becomes an in-memory stub instead of hitting a -real server. Everything else (Driver/Controller/Node/Value) is the real library. -""" - -from __future__ import annotations - -import asyncio -import contextlib -from typing import Any, Self, cast -from unittest.mock import AsyncMock, Mock, patch - -from zwave_js_server.client import Client -from zwave_js_server.event import Event -from zwave_js_server.model.driver import Driver -from zwave_js_server.model.log_config import LogConfigDataType -from zwave_js_server.model.node import Node -from zwave_js_server.model.node.data_model import NodeDataType - -_LOG_CONFIG: LogConfigDataType = { - "enabled": True, - "level": "info", - "logToFile": False, - "filename": "", - "forceConsole": False, -} - -_DEFAULT_CONTROLLER_STATE: dict[str, Any] = { - "controller": { - "sdkVersion": "Z-Wave 3.95", - "type": 1, - "homeId": 1, - "ownNodeId": 1, - "isSecondary": False, - "isUsingHomeIdFromOtherNetwork": False, - "isSISPresent": True, - "wasRealPrimary": True, - "isStaticUpdateController": True, - "isSlave": False, - "firmwareVersion": "1.0", - "manufacturerId": 1, - "productType": 1, - "productId": 1, - "supportedFunctionTypes": [], - "sucNodeId": 1, - "supportsTimers": False, - "isRebuildingRoutes": False, - "inclusionState": 0, - }, - "nodes": [], -} - -_DEFAULT_VALUE_METADATA = {"type": "boolean", "readable": True, "writeable": True} - -_DEFAULT_NODE_VALUES: list[dict[str, Any]] = [ - { - "commandClass": 37, - "commandClassName": "Binary Switch", - "endpoint": 0, - "property": "currentValue", - "propertyName": "currentValue", - "value": False, - "ccVersion": 1, - "metadata": {"type": "boolean", "readable": True, "writeable": False, "label": "Current value"}, - }, - { - "commandClass": 37, - "commandClassName": "Binary Switch", - "endpoint": 0, - "property": "targetValue", - "propertyName": "targetValue", - "value": False, - "ccVersion": 1, - "metadata": {"type": "boolean", "readable": True, "writeable": True, "label": "Target value"}, - }, -] - -_DEFAULT_NODE_ENDPOINTS: list[dict[str, Any]] = [ - { - "index": 0, - "deviceClass": None, - "commandClasses": [ - {"id": 37, "name": "Binary Switch", "version": 1, "isSecure": False}, - ], - }, -] - -# Client.async_send_command's return value IS the server message's "result" -# field already unwrapped once -- each command needs the exact key(s) its -# caller reads off that dict (e.g. is_failed_node reads data["failed"]). -_DEFAULT_COMMAND_RESPONSES: dict[str, dict[str, Any]] = { - "controller.begin_inclusion": {"success": True}, - "controller.stop_inclusion": {"success": True}, - "controller.begin_exclusion": {"success": True}, - "controller.stop_exclusion": {"success": True}, - "controller.is_failed_node": {"failed": False}, - "controller.remove_failed_node": {}, - "controller.replace_failed_node": {"success": True}, - "node.set_value": {"result": {"status": 255}}, - "node.refresh_info": {}, - "node.refresh_values": {}, -} - - -class VirtualZwaveNetwork: - def __init__(self, controller_state: dict | None = None) -> None: - self.controller_state = controller_state or _DEFAULT_CONTROLLER_STATE - self._command_responses = dict(_DEFAULT_COMMAND_RESPONSES) - self.async_send_command = AsyncMock(side_effect=self._handle_send_command) - self.async_send_command_no_wait = AsyncMock(return_value=None) - self._client: Client | None = None - self._patches: list[Any] = [] - - async def _handle_send_command(self, message: dict, **_kwargs: Any) -> dict: - return self._command_responses.get(message.get("command", ""), {"success": True}) - - def set_response(self, command: str, response: dict) -> None: - self._command_responses[command] = response - - def __enter__(self) -> Self: - network = self - - async def fake_connect(client_self: Client) -> None: - # Client.connected checks `_client is not None and not _client.closed`. - client_self._client = Mock(closed=False) - - async def fake_listen(client_self: Client, driver_ready: asyncio.Event) -> None: - client_self.driver = Driver(client_self, network.controller_state, _LOG_CONFIG) - network._client = client_self - driver_ready.set() - with contextlib.suppress(asyncio.CancelledError): - await asyncio.Event().wait() - - async def fake_disconnect(client_self: Client) -> None: - client_self.driver = None - # Don't touch `_client.closed` (read-only on the real - # ClientWebSocketResponse) -- dropping the reference entirely - # makes `.connected` False too, which is what matters here. - client_self._client = None - - self._patches = [ - patch.object(Client, "connect", new=fake_connect), - patch.object(Client, "listen", new=fake_listen), - patch.object(Client, "disconnect", new=fake_disconnect), - patch.object(Client, "async_send_command", new=self.async_send_command), - patch.object(Client, "async_send_command_no_wait", new=self.async_send_command_no_wait), - ] - for p in self._patches: - p.start() - return self - - def __exit__(self, *exc_info: Any) -> None: - for p in reversed(self._patches): - p.stop() - - def _connected_client(self) -> Client: - assert self._client is not None, "virtual_zwave used before controller.start() ran" - return self._client - - @property - def controller(self): - client = self._connected_client() - assert client.driver is not None, "virtual_zwave.controller accessed before controller.start() ran" - return client.driver.controller - - def node_found(self, node_id: int) -> None: - self.controller.emit("node found", {"node": {"nodeId": node_id}}) - - def node_joined(self, node_data: dict) -> Node: - payload: dict[str, Any] = { - "status": 4, # NodeStatus.ALIVE, not 1 (ASLEEP) -- ASLEEP makes - # async_send_command silently switch to no_wait - "ready": True, - "interviewStage": "Complete", - "endpoints": _DEFAULT_NODE_ENDPOINTS, - "values": _DEFAULT_NODE_VALUES, - **node_data, - } - node = Node(self._connected_client(), cast(NodeDataType, payload)) - self.controller.nodes[node.node_id] = node - self.controller.emit("node added", {"node": node}) - return node - - def node_removed(self, node_id: int, reason: int = 0) -> None: - node = self.controller.nodes.pop(node_id, None) - if node is not None: - self.controller.emit("node removed", {"node": node, "reason": reason}) - - def emit_value_updated( - self, - node_id: int, - *, - command_class: int, - command_class_name: str, - property: int | str, # noqa: A002 - new_value: Any, - prev_value: Any = None, - property_name: str | None = None, - endpoint: int = 0, - metadata: dict | None = None, - ) -> None: - node = self.controller.nodes[node_id] - event = Event( - "value updated", - { - "source": "node", - "event": "value updated", - "nodeId": node_id, - "args": { - "commandClass": command_class, - "commandClassName": command_class_name, - "endpoint": endpoint, - "property": property, - "propertyName": property_name or str(property), - "newValue": new_value, - "prevValue": prev_value, - "metadata": metadata or _DEFAULT_VALUE_METADATA, - }, - }, - ) - node.receive_event(event) From c3c79ae60e5d7def5f27a248c7ef603f63a4dcb6 Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:51:11 +0200 Subject: [PATCH 25/28] Fix the integration against the mock network --- README.md | 136 +++--- majordom_zwave/config.py | 5 +- majordom_zwave/controller.py | 897 ++++++++++++++++++----------------- majordom_zwave/exceptions.py | 6 - majordom_zwave/mapper.py | 297 +++++++----- majordom_zwave/model.py | 38 +- majordom_zwave/zwave_spec.py | 83 ++-- 7 files changed, 787 insertions(+), 675 deletions(-) delete mode 100644 majordom_zwave/exceptions.py diff --git a/README.md b/README.md index 0661909..82dc6f6 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,16 @@ poetry install && poetry run poe install | `poe check` | Full quality pipeline (ruff, ty, pytest, poetry build/check) | | `poe check --ci` | Same, plus `git diff --exit-code` | -Work lands on `develop`; `master` is protected and released via **Actions → Release**. Tests drive -the controller with the SDK's test doubles against an in-memory `zwave-js-server-python` -stub — no real server, radio, or Z-Wave stick required (see `tests/`). +Work lands on `develop`; `master` is protected and released via **Actions → Release**. + +Tests drive the controller with the SDK's test doubles against a **mock Z-Wave network**: zwave-js's own +mock controller and mock devices, with a real zwave-js driver and a real `zwave-js-server` in front +(`tests/mock`) — the integration connects to it exactly as to a real server, no radio or stick required. +It needs Node.js 20+: + +```sh +npm ci --prefix tests/mock && poetry run pytest +``` ## Run it standalone (without the Hub) @@ -67,7 +74,9 @@ just talks JSON over a WebSocket to it. - Run `zwave-js-server` yourself (commonly via Docker) on whatever machine has the Z-Wave USB controller attached. - Point this integration at it with the `ZWAVE_SERVER_URL` environment variable - (e.g. `ws://localhost:3000`) — see `majordom_zwave/config.py`. + (default `ws://localhost:3000`) — see `majordom_zwave/config.py`. +- The integration keeps reconnecting (1 → 30 s backoff) while the server is unreachable; paired devices are + reported unavailable meanwhile, with the reason as their `last_error`. ## About this integration @@ -76,10 +85,16 @@ just talks JSON over a WebSocket to it. - **Transport(s):** Z-Wave (sub-GHz RF mesh). - **Supported devices:** any Z-Wave / Z-Wave Plus device exposing standard Command Classes (Binary/Multilevel Switch, Indicator, etc.) that Z-Wave JS supports. -- **Credentials needed to pair:** none for the Hub's pairing flow — `Discovery` always advertises - `CredentialsType.none`. S2 `code` (PIN) and `qr` (DSK) are supported, but must be supplied when - *opening the pairing window*, not at `pair_device` time — Z-Wave needs them during inclusion, - before the node exists as a discovery. `secret` is not supported. +- **Credentials needed to pair:** given when *opening the pairing window* (`start_pairing_window`), not + at `pair_device` time — Z-Wave needs them during inclusion, before the device exists as a discovery; + the discovery itself always expects `CredentialsType.none`. + - `code` — the S2 PIN (the first 5 digits of the DSK on the device's label), or the whole DSK. + - `qr` — the device's S2 QR code (`90…`); malformed codes are rejected. + - none — the device joins, but without the S2 Authenticated / Access Control classes (they need the + PIN); its discovery's `last_error` says so. + - `secret` — not supported. + + The Hub does not pass credentials to `start_pairing_window` yet (the SDK's `develop` has the parameter). ### Required harness @@ -102,73 +117,78 @@ just talks JSON over a WebSocket to it. ### Manual testing (real hardware) -Verified against real hardware, not just the virtual-device test suite: +Verified against real hardware, on the version before the rework of 2026-10 (the rework is tested against +the mock network only; to be re-run on hardware before release): - **Z-Wave controller:** Z-Stick 7 (ZWA010) - **Test device:** HKZW-RGB01 v1.0 (RGB bulb) -Ran the same lifecycle as `tests/test_controller.py` — pairing, `fetch`, `send_command`, -`identify`, and `unpair` — all completed successfully. +### Mapping + +| Z-Wave | MajorDom | +|--------|----------| +| Device | `device_uuid("-")` (SDK helper): node ids repeat across networks and are reused after a device leaves | +| Value | One parameter, `parameter_uuid(device, value id)`; a current/target pair (`currentValue`/`targetValue`, `currentMode`/`targetMode`, …) is **one** parameter: commanded through the target, state from the current one | +| Multilevel Switch / Basic level (0–99) | Percentage 0–100 (99 is 100) | +| Multilevel Sensor, Meter, setpoints | `decimal` (scaled decimals by spec), whatever the first reading | +| `states` | `enum` with labels in `valid_values` (`integer` when the device allows other values too) | +| Units | SDK units by the zwave-js scale string; °F → °C and kPa → Pa converted, unknown units `plain` | +| Central Scene, Scene Activation, `stateful: false` | `event` role (sent by zwave-js as "value notification") | +| Basic CC (when other CCs exist), durations, secrets, opaque data, protocol CCs | `system` visibility | +| Battery, Version, Configuration, Wake Up, Protection, … | `setting` visibility | +| Main parameter | Binary Switch (toggle) › Multilevel Switch (off / 100 %) › Barrier (closed / open) › Door Lock (unsecured / secured) | ### Progress **Implementation** — makes the integration functional: -- [x] Discovery of joining devices via `zwave-js-server`'s `"node added"` event; also reconciles - nodes already on the network but not yet claimed (e.g. after a Hub restart) — - `controller_did_receive_discovery` called -- [ ] Discovery of already-paired devices on reconnect — currently `start()` marks known devices - available internally on startup without going through `controller_did_connect_device` (see Notes) -- [x] `start_pairing_window` (default, S2 PIN, S2 QR/SmartStart, auto-grant of requested - security classes with no user prompt) -- [x] Device pairing -- [x] Device schema mapped (via `ZwaveMapper`: values → parameters, role/visibility/units) -- [x] Hub → Device control (`send_command`) -- [x] Device → Hub event subscription (`controller_did_receive_events` on `"value updated"`) -- [x] `identify` (Indicator CC, falls back to blinking a Binary/Multilevel Switch) -- [x] `unpair` (failed-node removal, or live interactive exclusion confirmed by the device) +- [x] Discovery of joining devices (`"node added"`), and of nodes already on the network but not paired +- [x] Discovery of already-paired devices on (re)connect — matched by home id and node id, and checked against + the device's fingerprint (a node id reused by another device is not taken for the paired one) +- [x] `start_pairing_window` (default, S2 PIN or DSK, S2 QR) +- [x] Device pairing (completes the device the Hub created; fails cleanly and stays discovered when the device + does not finish its interview) +- [x] Device schema mapped (see **Mapping**) +- [x] Hub → Device control (`send_command`); the state comes back as the device's report, never echoed +- [x] Device → Hub events (`"value updated"`, `"value notification"`) +- [x] `identify` — Indicator CC v3 identify; a light blinks; any other device is left alone (a relay or a motor + may drive a heater, a pump or a blind) +- [x] `unpair` — a failed node is removed without the device; a live one when *this* device confirms the exclusion - [x] `fetch` -- [x] Availability tracking while running (`"dead"`/`"alive"` → `controller_did_lose_device` / - `controller_did_connect_device`) — `last_error` isn't set/cleared on these specific - transitions though (see Notes) -- [x] Graceful shutdown in `stop` (tasks cancelled, client disconnected, session closed) -- [x] Tests pass against a virtual/simulated device (`tests/test_controller.py`) and against real - hardware (see **Manual testing** above) +- [x] Availability — `"dead"`/`"alive"`, the server connection, a device removed with another tool +- [x] Graceful shutdown in `stop` +- [x] Tests pass against a simulated network (mock zwave-js controller and devices) **Quality** — makes it reliable and maintainable (the bar for release): -- [ ] **Recovers automatically** from connection loss / offline device / restarted backend — no - reconnect/backoff around `Client.connect()`/`listen()` yet -- [ ] **No exception escapes the controller** — event handlers (`_node_added`, `_value_updated`, - `_set_availability`, `_grant_security_classes`) run via fire-and-forget tasks with no - individual `try`/`except`; an exception there surfaces only as an unretrieved task exception -- [ ] **Failures are surfaced, not raised** — done in a couple of places (reconnect-missing, - pending exclusion) but not consistently across all paths -- [ ] **Re-authenticates automatically** — not applicable to Z-Wave (no expiring credential concept) -- [x] **Fully asynchronous** — no blocking I/O found on the event loop -- [x] **Stable identity** — device/parameter UUIDs derived via `ZwaveMapper`'s SDK-backed helpers -- [x] **End-to-end tests** drive pair → command → fetch → events → `unpair` against a virtual device -- [ ] **Failure paths tested** — offline device / transport error / rejected credentials not covered yet -- [ ] **Broad device coverage** — one virtual device type in CI, one real device manually -- [x] **Fully typed** (`ty`) and **clean** (`poe check`) — ruff, ty, pytest, and - `poetry build`/`check` all pass -- [x] **Readable & structured** — conversion logic lives in `ZwaveMapper`, models separated -- [x] **Efficient** — subscription-based (`"value updated"`/`"dead"`/`"alive"`), not polling -- [ ] **Diagnosable** — decent per-area log tags (`[PAIR]`, `[CMD]`, `[JOIN]`, …), but - `send_command`'s exception handler re-raises with `from None`, discarding the original traceback -- [x] **Rich parameter metadata** — role/visibility/`main_parameter` resolved per value via `ZwaveMapper` +- [x] **Recovers automatically** from a lost or restarted `zwave-js-server` (reconnects with backoff) +- [x] **No exception escapes background work** — event handlers run as logged tasks; Hub calls raise after + recording the reason +- [x] **Failures are surfaced, not swallowed** — the reason is the device's or discovery's `last_error`; an + unreachable server at start is reported through `controller_did_encounter_error` +- [x] **Re-authenticates automatically** — not applicable (Z-Wave has no expiring credential) +- [x] **Fully asynchronous** +- [x] **Stable identity** — the SDK helpers, scoped by the network's home id +- [x] **End-to-end tests** drive pair → command → fetch → events → `unpair` against the mock network +- [x] **Failure paths tested** — unreachable or lost server, unresponsive and refusing devices, interview timeout, + exclusion timeout, the wrong device excluded, a reused node id, a missing PIN +- [ ] **Broad device coverage** — mocked: switch, supervised switch, dimmer, RGB light, °C/°F sensor, scene + controller, double switch (endpoints), S2 device, identify indicator. Not yet: locks, thermostats, + meters, covers, sleeping (battery) devices +- [x] **Fully typed** (`ty`) and **clean** (`poe check`) +- [x] **Readable & structured** — mapping in `mapper.py`, static tables in `zwave_spec.py` +- [x] **Efficient** — subscription-based, not polling +- [x] **Diagnosable** — per-area log tags (`[PAIR]`, `[CONNECT]`, …), reasons in `last_error`, causes chained +- [x] **Rich parameter metadata** — role, visibility, units, limits, enum labels, main parameter - [ ] **Owned** — add a listed maintainer -- [ ] _nice-to-have:_ localizable naming · firmware/software updates · user-facing supported-device docs +- [ ] _nice-to-have:_ localizable naming · firmware updates · SmartStart provisioning · user-facing supported-device docs ### Notes -- Z-Wave discovery doesn't use `zeroconf`/`ssdp`/`ble` — joined/reconciled nodes are surfaced - directly from `zwave-js-server`'s own `"node added"` event. -- On startup, devices already known to the Hub are matched against the live node list and marked - available/subscribed directly, without a corresponding `controller_did_connect_device` call — - worth revisiting if the Hub relies on that call to refresh its own state after a restart. -- `last_error` is set explicitly when a paired device isn't found on the network at startup and - while an exclusion is pending, but isn't cleared/set on ordinary `"dead"`/`"alive"` transitions. +- Z-Wave discovery doesn't use `zeroconf`/`ssdp`/`ble`: nodes are surfaced from `zwave-js-server`'s own events. +- QR inclusion is not covered end to end (the mock network has no QR flow); malformed codes are tested. +- A main parameter's set `default_value` (e.g. `{0, 100}`) comes back from the SDK's repository as a list: an SDK + issue (`set[V] | V` with an unbound `V`), not this integration's. ## License diff --git a/majordom_zwave/config.py b/majordom_zwave/config.py index b0993d3..d9698d7 100644 --- a/majordom_zwave/config.py +++ b/majordom_zwave/config.py @@ -1,3 +1,6 @@ import os -zwave_server_url = os.getenv("ZWAVE-SERVER-URL", "ws://localhost:3000") + +def server_url() -> str: + """The zwave-js-server to connect to, read when the integration starts.""" + return os.getenv("ZWAVE_SERVER_URL", "ws://localhost:3000") diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index fe56573..3fff432 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -1,71 +1,68 @@ import asyncio import logging -from typing import ClassVar, override +import re +from collections.abc import Callable, Coroutine +from typing import Any, ClassVar, override from uuid import UUID from aiohttp import ClientSession from majordom_integration_sdk.controller import AbstractController -from majordom_integration_sdk.schemas import ( - DeviceCommand, - Discovery, - ProvidedCredentials, -) +from majordom_integration_sdk.schemas import DeviceCommand, Discovery, ProvidedCredentials from majordom_integration_sdk.schemas.device import CredentialsType, NonEmptyStr from majordom_integration_sdk.schemas.event import DeviceParameterChange -from majordom_integration_sdk.schemas.parameter import ParameterUnit +from majordom_integration_sdk.schemas.parameter import ParameterRole from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import ( - CommandClass, - InclusionStrategy, - NodeStatus, - SecurityClass, -) +from zwave_js_server.const import CommandClass, InclusionStrategy, NodeStatus, SecurityClass, SetValueStatus from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.inclusion_and_provisioning import InclusionGrant from zwave_js_server.model.node import Node -from zwave_js_server.model.utils import async_parse_qr_code_string from zwave_js_server.model.value import Value -from .config import zwave_server_url -from .exceptions import ZwaveConnectionError, ZwaveUnexpectedError -from .mapper import ZwaveMapper +from . import config, mapper from .model import ( ZwaveDevice, ZwaveDeviceIntegrationData, - ZwaveDeviceState, ZwaveParameter, ZwaveParameterIntegrationData, - ZwaveParameterState, ) -from .zwave_spec import IDENTIFY_INDICATOR_ID log = logging.getLogger(__name__) +PIN = re.compile(r"\d{5}") # the S2 PIN: the first 5 digits of the DSK, printed on the device +DSK = re.compile(r"\d{5}(-\d{5}){7}") # the full DSK +APPLIED = {SetValueStatus.SUCCESS, SetValueStatus.SUCCESS_UNSUPERVISED, SetValueStatus.WORKING} +# Security classes that need the device's PIN; granted only when the PIN (or DSK) was given +AUTHENTICATED = {SecurityClass.S2_AUTHENTICATED, SecurityClass.S2_ACCESS_CONTROL} + class ZwaveController(AbstractController): """Bridges the Hub to Z-Wave devices through zwave-js-server.""" name: ClassVar[str] = "ZWave" - _zwave_session: ClientSession - _zwave_client: ZwaveClient - - _mapper: ZwaveMapper - - _majordom_discoveries: dict[UUID, Discovery] - _awaiting_zw_discoveries: dict[UUID, Node] - _connected_devices: dict[UUID, Node] - _availability: dict[UUID, bool] - _tasks: set[asyncio.Task] + RECONNECT_DELAYS: ClassVar[tuple[float, ...]] = (1, 2, 5, 10, 30) # then 30 s between attempts + READY_TIMEOUT: ClassVar[float] = 120 # a device's interview; a sleeping device finishes it when it wakes up + EXCLUSION_TIMEOUT: ClassVar[float] = 60 # for the user to put the device into exclusion mode + IDENTIFY_BLINKS: ClassVar[int] = 5 + IDENTIFY_PERIOD: ClassVar[float] = 0.6 def __init__(self, dependencies: AbstractController.Dependencies): super().__init__(dependencies) - self._mapper = ZwaveMapper() - self._majordom_discoveries = {} - self._awaiting_zw_discoveries = {} - self._connected_devices = {} - self._availability = {} - self._tasks = set() + self._session: ClientSession | None = None + self._zwave_client: ZwaveClient | None = None + self._supervisor: asyncio.Task | None = None + self._first_attempt = asyncio.Event() + self._tasks: set[asyncio.Task] = set() + self._discoveries: dict[UUID, Discovery] = {} + self._joined: dict[UUID, Node] = {} # nodes behind the discoveries + self._nodes: dict[UUID, Node] = {} # nodes of paired devices + self._routes: dict[UUID, dict[str, str | None]] = {} # value id → value id of the parameter it reports + self._unsubscribe: dict[UUID, list[Callable[[], None]]] = {} # per paired device + self._listeners: list[Callable[[], None]] = [] # on the driver's controller, gone with the connection + self._available: dict[UUID, bool] = {} + self._credentials: ProvidedCredentials | None = None # of the open pairing window + self._closing_window: asyncio.Task | None = None + self._removal: asyncio.Future[int] | None = None # the node id the running exclusion removed # ------------------------------------------------------------------------- # AbstractController interface @@ -73,7 +70,7 @@ def __init__(self, dependencies: AbstractController.Dependencies): @property def discoveries(self) -> dict[UUID, Discovery]: - return self._majordom_discoveries + return self._discoveries @property @override @@ -85,499 +82,539 @@ def device_type(self) -> type[ZwaveDevice]: def parameter_type(self) -> type[ZwaveParameter]: return ZwaveParameter + @property + def connected(self) -> bool: + return self._zwave_client is not None and self._zwave_client.connected + # ------------------------------------------------------------------------- # Lifecycle # ------------------------------------------------------------------------- async def start(self): - self._zwave_session = ClientSession() - self._zwave_client = ZwaveClient(zwave_server_url, self._zwave_session) - await self._zwave_client.connect() - - ready = asyncio.Event() - self._create_task(self._zwave_client.listen(ready)) - await ready.wait() - - controller = self._get_controller() - controller.on("node added", lambda data: self._create_task(self._node_added(data["node"]))) - controller.on( - "node removed", - lambda data: self._create_task(self._node_removed(data["node"])), - ) - controller.on( - "grant security classes", - lambda data: self._create_task(self._grant_security_classes(data["requested"])), - ) - - log.debug("[READY] connected to %s", zwave_server_url) - - async with self.dependencies.make_device_repository() as device_repository: - try: - known_node_ids: set[int] = set() - for raw_device in await device_repository.get_all(): - try: - device = ZwaveDevice.model_validate(raw_device.model_dump()) - except Exception: # noqa: BLE001 - log.debug( - "[SKIP] device_id=%s not paired yet, skipping", - raw_device.id, - ) - continue - - if device.integration_data is None: - continue - - node = controller.nodes.get(device.integration_data.node_id) - if node is None: - device.available = False - device.last_error = f"Device {device.name} is no longer connected to the Z-Wave network" - await device_repository.save(device, device.id) - log.debug( - "[MISSING] device_id=%s node_id=%s not on network", - device.id, - device.integration_data.node_id, - ) - continue - known_node_ids.add(node.node_id) - self._connected_devices[device.id] = node - self._availability[device.id] = bool(node.ready) - self._subscribe(device.id, node) - log.debug("[KNOWN] node_id=%s", node.node_id) - - # Nodes already on the network but not ours yet (Hub restart, pre-existing node). - log.debug( - "[RECONCILE] %d node(s) on network, own_node_id=%s", - len(controller.nodes), - controller.own_node_id, - ) - for node in controller.nodes.values(): - if node.node_id == controller.own_node_id or node.node_id in known_node_ids: - continue - await self._node_added(node) - except Exception: - log.exception("[START] node reconciliation failed") - raise + """Connects to zwave-js-server and keeps reconnecting; an unreachable server is reported, not raised.""" + self._session = ClientSession() + self._first_attempt.clear() + self._supervisor = asyncio.create_task(self._run(config.server_url(), self._session)) + await self._first_attempt.wait() async def stop(self): - self._require_client() - for task in self._tasks: - task.cancel() - if self._tasks: - await asyncio.gather(*self._tasks, return_exceptions=True) + for task in [self._supervisor, *self._tasks]: + if task is not None: + task.cancel() + await asyncio.gather(*[t for t in [self._supervisor, *self._tasks] if t is not None], return_exceptions=True) + self._supervisor = None self._tasks.clear() - await self._zwave_client.disconnect() - await self._zwave_session.close() - - self._majordom_discoveries.clear() - self._awaiting_zw_discoveries.clear() - self._connected_devices.clear() + await self._disconnect() + if self._session is not None: + await self._session.close() + self._session = None + self._discoveries.clear() + self._joined.clear() + self._available.clear() + + async def _run(self, url: str, session: ClientSession) -> None: + attempt = 0 + while True: + listening: asyncio.Task | None = None + try: + client = ZwaveClient(url, session) + await client.connect() + ready = asyncio.Event() + listening = asyncio.create_task(client.listen(ready)) + waiting = asyncio.create_task(ready.wait()) + await asyncio.wait({listening, waiting}, return_when=asyncio.FIRST_COMPLETED) + waiting.cancel() + if listening.done(): + listening.result() # raises what made it stop + raise ConnectionError("the connection closed before the driver was ready") + self._zwave_client = client + await self._attach() + log.info("[READY] connected to %s", url) + attempt = 0 + self._first_attempt.set() + await listening + raise ConnectionError("the connection to zwave-js-server closed") + except asyncio.CancelledError: + raise + except Exception as exc: # noqa: BLE001 any failure means: reconnect + reason = f"zwave-js-server at {url} is unreachable: {exc or type(exc).__name__}" + log.warning("[CONNECT] %s", reason) + await self._detach(reason) + if not self._first_attempt.is_set(): + await self.dependencies.output.controller_did_encounter_error(self, reason, True) + self._first_attempt.set() + finally: + if listening is not None: + listening.cancel() + await asyncio.sleep(self.RECONNECT_DELAYS[min(attempt, len(self.RECONNECT_DELAYS) - 1)]) + attempt += 1 + + async def _disconnect(self) -> None: + client, self._zwave_client = self._zwave_client, None + if client is not None and client.connected: + try: + await client.disconnect() + except Exception: # noqa: BLE001 + log.debug("[CONNECT] disconnect failed", exc_info=True) + + async def _attach(self) -> None: + """Binds the paired devices to the (new) driver's nodes and offers the other nodes as discoveries.""" + controller = self._controller() + self._available.clear() # report every device's availability afresh + self._track(controller.on("node added", lambda data: self._spawn(self._node_added(data["node"], data)))) + self._track(controller.on("node removed", lambda data: self._spawn(self._node_removed(data["node"])))) + self._track( + controller.on("grant security classes", lambda data: self._spawn(self._grant(data["requested_grant"]))) + ) + self._track(controller.on("validate dsk and enter pin", lambda data: self._spawn(self._enter_pin()))) + + async with self.dependencies.make_device_repository() as repo: + devices = [d for d in await repo.get_all(as_=ZwaveDevice) if d.integration_data is not None] + claimed: set[int] = set() + for device in devices: + data = device.integration_data + assert data is not None + node = controller.nodes.get(data.node_id) if data.home_id == controller.home_id else None + if data.home_id == controller.home_id: + claimed.add(data.node_id) # its id is taken even when another device now has the node + if node is None: + await self._set_available(device.id, False, "The device is no longer on this Z-Wave network") + elif data.fingerprint and (found := self._fingerprint(node)) and found != data.fingerprint: + await self._set_available( + device.id, False, f"Node {data.node_id} is now another device ({found}): pair that one anew" + ) + else: + self._bind(device.id, node) + await self._update_availability(device.id, node) + if node.status != NodeStatus.DEAD: + await self._report_snapshot(device.id, node) + for node in controller.nodes.values(): # on the network, not paired (yet): offered for pairing + if node.node_id != controller.own_node_id and node.node_id not in claimed: + await self._offer(node) + + async def _detach(self, reason: str) -> None: + for device_id in list(self._nodes): + self._unbind(device_id) + await self._set_available(device_id, False, reason) + for unsubscribe in self._listeners: + unsubscribe() + self._listeners.clear() + self._joined.clear() + await self._disconnect() # ------------------------------------------------------------------------- # Hub -> device operations # ------------------------------------------------------------------------- async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCredentials | None = None) -> None: - self._require_client() - controller = self._get_controller() - - if credentials and credentials.type is CredentialsType.qr: - if not credentials.value: - raise ZwaveUnexpectedError("QR credentials provided without QR data") - provisioning_info = await async_parse_qr_code_string(self._zwave_client, credentials.value) - await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, provisioning=provisioning_info) - log.debug("[PAIRING] inclusion window opened for %ds via QR", duration_sec) - - elif credentials and credentials.type is CredentialsType.code: - if not credentials.value: - raise ZwaveUnexpectedError("PIN credentials provided without a value") - await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, dsk=credentials.value) - log.debug( - "[PAIRING] inclusion window opened for %ds with pre-supplied PIN", - duration_sec, - ) - - elif credentials and credentials.type is CredentialsType.secret: - raise ZwaveUnexpectedError("Z-Wave does not support secret-based pairing credentials") - - else: - await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) - log.debug("[PAIRING] inclusion window opened for %ds", duration_sec) - - self._create_task(self._close_pairing_window(duration_sec)) - - async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: - """Waits for the pending node's interview to finish, then builds and saves the device.""" - self._require_client() - - node = self._awaiting_zw_discoveries.pop(discovery.id, None) - if node is None: - raise ZwaveUnexpectedError(f"No pending Z-Wave discovery for {discovery.id}") - self._majordom_discoveries.pop(discovery.id, None) - - await self._wait_until_ready(node) - - device_id = self._mapper.device_uuid_from_node_id(node.node_id) - self._connected_devices[device_id] = node - - async with self.dependencies.make_device_repository() as device_repository: - device = await device_repository.state(discovery.id, ZwaveDeviceState) - assert device - device.id = device_id - - if device.integration_data: - device.integration_data.node_id = node.node_id + controller = self._controller() + kind = credentials.type if credentials else CredentialsType.none + value = (credentials.value if credentials else None) or "" + if kind == CredentialsType.secret: + raise ValueError("Z-Wave devices are paired with their PIN, DSK or QR code, not a secret") + if kind == CredentialsType.code and not DSK.fullmatch(value) and not PIN.fullmatch(value): + raise ValueError("The PIN is the first 5 digits of the device's DSK (or give the whole DSK)") + self._credentials = credentials # before inclusion starts: a device can ask for its grant right away + try: + if kind == CredentialsType.qr: + # a malformed code is rejected here (ValueError); the server parses the rest + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, provisioning=value) + elif kind == CredentialsType.code and DSK.fullmatch(value): + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2, dsk=value) + elif kind == CredentialsType.code: + await controller.async_begin_inclusion(InclusionStrategy.SECURITY_S2) # the PIN is entered when asked else: - device.integration_data = ZwaveDeviceIntegrationData(node_id=node.node_id) - - parameters = self._build_parameters(device_id, node) - device.parameters = parameters - - main_parameter_id, default_value = self._mapper.get_main_parameter(device_id, node) - device.main_parameter = main_parameter_id - if main_parameter_id and default_value is not None: - main_parameter = next((p for p in parameters if p.id == main_parameter_id), None) - if main_parameter is None: - device.main_parameter = None - else: - main_parameter.default_value = default_value + await controller.async_begin_inclusion(InclusionStrategy.DEFAULT) + except Exception: + self._credentials = None + raise + log.debug("[PAIRING] inclusion window opened for %ds (%s)", duration_sec, kind) + if self._closing_window is not None: + self._closing_window.cancel() + self._closing_window = self._spawn(self._close_pairing_window(duration_sec)) + async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentials | None) -> UUID: + known = self._discoveries.get(discovery.id) + node = self._joined.get(discovery.id) + if known is None or node is None: + raise ValueError("Unknown device: it is no longer discovered") + try: + if credentials is not None and credentials.type != CredentialsType.none: + raise ValueError("Z-Wave credentials are given when opening the pairing window") + await self._wait_until_ready(node) + async with self.dependencies.make_device_repository() as repo: + hub_device = await repo.get(discovery.id, as_=ZwaveDevice) # the Hub creates it before pairing + if hub_device is None: + raise LookupError("The Hub has not created a device for this discovery") + + parameters = self._build_parameters(discovery.id, node) + main = mapper.main_value(node) + main_id = self._parameter_id(discovery.id, main[0].value_id) if main else None + for parameter in parameters: + if main and parameter.id == main_id: + parameter.default_value = main[1] + device = hub_device.model_copy( + update={ + "parameters": parameters, + "main_parameter": main_id if any(p.id == main_id for p in parameters) else None, + "available": True, + "last_error": None, + "integration_data": ZwaveDeviceIntegrationData( + home_id=self._controller().home_id or 0, + node_id=node.node_id, + fingerprint=self._fingerprint(node), + ), + } + ) + async with self.dependencies.make_device_repository() as repo: + await repo.save(device) log.debug( - f"[PAIR] node_id={node.node_id} mapped schema\n\t" - + "\n\t".join( - f" {p.role.value:8} {p.visibility.value:8} {p.data_type.value:10} {p.name} id={p.id}" + "[PAIR] node_id=%s mapped schema\n\t%s", + node.node_id, + "\n\t".join( + f"{p.role:8} {p.visibility:8} {p.data_type:8} {p.name} ({p.integration_data.value_id})" for p in parameters ), ) - - await device_repository.save(device, discovery.id) - - self._subscribe(device_id, node) - await self._set_availability(device_id, True) - await self.dependencies.output.controller_did_connect_device(self, device_id) - return device_id + except Exception as exc: + log.warning("[PAIR] pairing %s failed: %s", discovery.id, exc) + failed = known.model_copy(update={"last_error": str(exc)}) + self._discoveries[discovery.id] = failed + await self.dependencies.output.controller_did_update_discovery(self, failed) + raise + + self._discoveries.pop(discovery.id, None) + self._joined.pop(discovery.id, None) + self._bind(discovery.id, node) + await self._set_available(discovery.id, True) + await self._report_snapshot(discovery.id, node) + return discovery.id async def unpair(self, device: ZwaveDevice): - self._require_client() - controller = self._get_controller() - zwave_node = self._require_node(device) - - if await controller.async_is_failed_node(zwave_node): - await controller.async_remove_failed_node(zwave_node) + controller = self._controller() + node = self._nodes.get(device.id) + if node is None: # not on the network anymore: nothing to remove + self._unbind(device.id) + return + if await controller.async_is_failed_node(node): + await controller.async_remove_failed_node(node) else: - # Live node: requires interactive exclusion, confirmed by the device itself. - async with self.dependencies.make_device_repository() as device_repository: - stored = await device_repository.get(device.id, as_=ZwaveDevice) - if stored: - stored.last_error = ( - "Trigger exclusion mode on the device manually " - "(see manufacturer instructions) to complete removal" - ) - await device_repository.save(stored, device.id) - - removed = asyncio.Event() - - def _on_node_removed(data: dict) -> None: - if data["node"].node_id == zwave_node.node_id: - removed.set() - - unsubscribe = controller.on("node removed", _on_node_removed) + # A live node leaves only when the device itself confirms (its button, per the manufacturer) + await self._update_device(device.id, last_error="Put the device into exclusion mode to remove it") + self._removal = asyncio.get_running_loop().create_future() try: await controller.async_begin_exclusion() - try: - await asyncio.wait_for(removed.wait(), timeout=60) - except TimeoutError: - raise ZwaveUnexpectedError( - f"Node {zwave_node.node_id} was not excluded in time — make sure the " - f"device was put into exclusion mode" - ) from None + removed = await asyncio.wait_for(self._removal, self.EXCLUSION_TIMEOUT) + except TimeoutError: + reason = "The device was not put into exclusion mode in time: it is still on the network" + await self._update_device(device.id, last_error=reason) + raise TimeoutError(reason) from None finally: - unsubscribe() - await controller.async_stop_exclusion() - - self._connected_devices.pop(device.id, None) - self._availability.pop(device.id, None) + self._removal = None + try: + await controller.async_stop_exclusion() + except Exception: # noqa: BLE001 already stopped by the removal + log.debug("[UNPAIR] stop exclusion failed", exc_info=True) + if removed != node.node_id: + reason = f"another device (node {removed}) was excluded instead: it left the network" + await self._update_device(device.id, last_error=f"Not removed: {reason}") + raise LookupError(reason) + self._unbind(device.id) + self._available.pop(device.id, None) async def identify(self, device: ZwaveDevice): - self._require_client() - zwave_node = self._require_node(device) - - indicator_value = next( + node = self._require_node(device) + indicator = next( # Indicator CC v3 "identify": the device's own identify pattern, cross-vendor ( v - for v in zwave_node.values.values() - if v.command_class == CommandClass.INDICATOR - and v.property_ == "value" - and v.property_key == IDENTIFY_INDICATOR_ID + for v in node.values.values() + if v.command_class == CommandClass.INDICATOR and v.property_ == "identify" ), None, ) - if indicator_value is not None: - await zwave_node.async_set_value(indicator_value, 0xFF) - log.debug("[IDENTIFY] node_id=%s via Indicator CC", zwave_node.node_id) + if indicator is not None: + await node.async_set_value(indicator, True) + log.debug("[IDENTIFY] node_id=%s via Indicator CC", node.node_id) return + if any(v.command_class == CommandClass.SWITCH_COLOR for v in node.values.values()): + await self._blink(node) # a light: blinking it is harmless, unlike toggling a relay's load + return + log.info("[IDENTIFY] node_id=%s has no identify indicator and is not a light: nothing to show", node.node_id) - switch_value = next( + async def _blink(self, node: Node) -> None: + level = next( ( v - for v in zwave_node.values.values() - if v.command_class in (CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL) - and v.property_ == "targetValue" + for v in node.values.values() + if v.command_class == CommandClass.SWITCH_MULTILEVEL and v.property_ == "targetValue" ), None, ) - if switch_value is None: - raise ZwaveUnexpectedError(f"Node {zwave_node.node_id} supports neither Indicator CC nor a switch to blink") - - current_value = zwave_node.values.get(switch_value.value_id.replace("targetValue", "currentValue")) - restore_value = current_value.value if current_value and current_value.value is not None else 0 - - on_value = 0xFF if switch_value.command_class == CommandClass.SWITCH_BINARY else 99 - for _ in range(10): - await zwave_node.async_set_value(switch_value, 0) - await asyncio.sleep(0.6) - await zwave_node.async_set_value(switch_value, on_value) - await asyncio.sleep(0.6) - - await zwave_node.async_set_value(switch_value, restore_value) - log.debug("[IDENTIFY] node_id=%s via switch blink fallback", zwave_node.node_id) + if level is None: + return + current = node.values.get(level.value_id.replace("targetValue", "currentValue")) + restore = current.value if current is not None and isinstance(current.value, int) else 0 + for _ in range(self.IDENTIFY_BLINKS): + await node.async_set_value(level, 99 if restore == 0 else 0) + await asyncio.sleep(self.IDENTIFY_PERIOD) + await node.async_set_value(level, restore) + await asyncio.sleep(self.IDENTIFY_PERIOD) + log.debug("[IDENTIFY] node_id=%s blinked", node.node_id) async def fetch(self, device: ZwaveDevice): - self._require_client() - - zwave_node = self._require_node(device) - parameters: list[DeviceParameterChange] = [] - for value_id, value in zwave_node.values.items(): - parameters.append( - DeviceParameterChange( - device_id=device.id, - parameter_id=self._mapper.parameter_uuid(device.id, value_id), - value=self._mapper.format_zwave_value(value), - ) - ) - - await self.dependencies.output.controller_did_receive_events(self, parameters) + await self._report_snapshot(device.id, self._require_node(device)) async def send_command(self, command: DeviceCommand, device: ZwaveDevice, parameter: ZwaveParameter): - self._require_client() - - zwave_node = self._require_node(device) - zwave_value = self._require_value(parameter.integration_data.value_id, zwave_node) - if zwave_value.metadata.writeable is False: - raise ZwaveUnexpectedError(f"Value {parameter.integration_data.value_id} is not writeable") + node = self._nodes.get(device.id) + if node is None: + reason = f"{parameter.name} could not be set: the device is not connected" + await self._update_device(device.id, last_error=reason) + raise ConnectionError(reason) + value = node.values.get(parameter.integration_data.value_id) + if value is None: + raise ValueError(f"{parameter.name} is not a value of this device") + if not value.metadata.writeable: + raise ValueError(f"{parameter.name} is read-only") + + awake = node.status != NodeStatus.ASLEEP # a sleeping device gets the command when it wakes up try: - awake = zwave_node.status != NodeStatus.ASLEEP - result = await zwave_node.async_set_value(zwave_value, command.value, wait_for_result=awake) - log.info(result) - except Exception as e: # noqa: BLE001 - raise ZwaveUnexpectedError( - f"Failed to set value {parameter.integration_data.value_id} on node {zwave_node.node_id}: {e}" - ) from None - if awake and result is not None and not result.status: - log.warning( - "[CMD] node_id=%s value_id=%s result=%s", - zwave_node.node_id, - parameter.integration_data.value_id, - result, - ) - - await self.dependencies.output.controller_did_receive_events( - self, - [DeviceParameterChange(device_id=device.id, parameter_id=parameter.id, value=command.value)], - ) + result = await node.async_set_value(value, mapper.to_device(value, command.value), wait_for_result=awake) + except Exception as exc: + reason = f"{parameter.name} could not be set: {exc or type(exc).__name__}" + await self._update_device(device.id, last_error=reason) + raise ConnectionError(reason) from exc + if awake and (result is None or result.status not in APPLIED): + reason = f"{parameter.name} could not be set: the device refused it ({result})" + await self._update_device(device.id, last_error=reason) + raise RuntimeError(reason) + # no echo: the device's report (or zwave-js's verification of it) comes back as "value updated" + await self._update_device(device.id, last_error=None) # ========================================================================= # Private helpers # ========================================================================= - def _get_controller(self) -> Controller: - if self._zwave_client.driver is None: - raise ZwaveUnexpectedError("Z-Wave driver is not connected") + def _controller(self) -> Controller: + if self._zwave_client is None or self._zwave_client.driver is None: + raise ConnectionError("Not connected to zwave-js-server") return self._zwave_client.driver.controller - def _create_task(self, coro) -> asyncio.Task: + def _spawn(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task: + """Runs a handler in the background; its failure is logged, never lost or raised into the library.""" task = asyncio.create_task(coro) self._tasks.add(task) - task.add_done_callback(self._tasks.discard) + task.add_done_callback(self._done) return task - def _require_client(self) -> ZwaveClient: - if self._zwave_client and self._zwave_client.connected: - return self._zwave_client - raise ZwaveConnectionError("Zwave client is not started") + def _done(self, task: asyncio.Task) -> None: + self._tasks.discard(task) + if not task.cancelled() and (exc := task.exception()) is not None: + log.error("[TASK] %s failed", task.get_coro(), exc_info=exc) + + def _track(self, unsubscribe: Callable[[], None]) -> None: + self._listeners.append(unsubscribe) def _require_node(self, device: ZwaveDevice) -> Node: - if device.integration_data is None: - raise ZwaveUnexpectedError(f"Device {device.id} missing integration_data") - controller = self._get_controller() - node = controller.nodes.get(device.integration_data.node_id) - if not node: - raise ZwaveUnexpectedError(f"Node for device {device.id} not found") + node = self._nodes.get(device.id) + if node is None: + raise ConnectionError(f"Device {device.name} is not connected") return node - def _require_value(self, value_id: str, node: Node) -> Value: - value = node.values.get(value_id) - if not value: - raise ZwaveUnexpectedError(f"Value with {value_id} id for node {node.node_id} not found") - return value + def _parameter_id(self, device_id: UUID, value_id: str) -> UUID: + return self.parameter_uuid(device_id, value_id) + + @staticmethod + def _fingerprint(node: Node) -> str | None: + ids = (node.manufacturer_id, node.product_type, node.product_id) + return ":".join(f"{i:04x}" for i in ids) if all(i is not None for i in ids) else None + + async def _update_device(self, device_id: UUID, **changes: Any) -> None: + async with self.dependencies.make_device_repository() as repo: + device = await repo.get(device_id, as_=ZwaveDevice) + if device is not None and any(getattr(device, k) != v for k, v in changes.items()): + await repo.save(device.model_copy(update=changes)) # ------------------------------------------------------------------------- # Pairing # ------------------------------------------------------------------------- + async def _offer(self, node: Node, low_security: bool = False) -> None: + device_id = self.device_uuid(f"{self._controller().home_id or 0:08x}-{node.node_id}") + self._joined[device_id] = node + if device_id in self._discoveries: + return + discovery = Discovery( + id=device_id, + integration=NonEmptyStr(self.name), + expected_credentials_options=[CredentialsType.none], + expiration=None, + transport=NonEmptyStr("ZWAVE"), + device_manufacturer=None, + device_name=NonEmptyStr(node.name or node.device_config.description or f"Z-Wave node {node.node_id}"), + device_category=None, + device_icon=None, + last_error=( + "Joined without S2 authentication: open the pairing window with the device's PIN to include it securely" + if low_security + else None + ), + ) + self._discoveries[device_id] = discovery + log.debug("[DISCOVERY] node_id=%s discovery_id=%s", node.node_id, device_id) + await self.dependencies.output.controller_did_receive_discovery(self, discovery) + async def _close_pairing_window(self, duration_sec: int) -> None: await asyncio.sleep(duration_sec) - controller = self._get_controller() + self._credentials = None + controller = self._controller() await controller.async_stop_inclusion() log.debug("[PAIRING] inclusion window closed") - # Anything discovered but never claimed via pair_device: try to undo the - # inclusion. Only forget the discovery if the node actually left the - # network -- otherwise it's still a real Z-Wave node and the user must - # still be able to pair it (or explicitly remove it) later. - for device_id in list(self._majordom_discoveries): - node = self._awaiting_zw_discoveries.get(device_id) - if node is None: - # Nothing to reconcile against -- drop the stale discovery entry. - self._majordom_discoveries.pop(device_id, None) - continue - - if not await controller.async_is_failed_node(node): - log.warning( - "[PAIRING] node_id=%s joined but was never paired and is still " - "alive on the network -- keeping the discovery so the user can " - "pair or remove it", - node.node_id, - ) - continue - - await controller.async_remove_failed_node(node) - self._majordom_discoveries.pop(device_id, None) - self._awaiting_zw_discoveries.pop(device_id, None) - log.debug( - "[PAIRING] node_id=%s force-removed after failing to pair in time", - node.node_id, - ) + # Joined but never paired: a node that failed is removed (it can't be paired anymore); a live one stays a + # discovery: it is a real Z-Wave node, and the user must still be able to pair or remove it + for node in list(self._joined.values()): + if await controller.async_is_failed_node(node): + await controller.async_remove_failed_node(node) + log.debug("[PAIRING] node_id=%s removed after failing before it was paired", node.node_id) async def _wait_until_ready(self, node: Node) -> None: - """Waits for the background interview to populate node.values.""" + """Waits for the interview that fills node.values.""" if node.ready: return became_ready = asyncio.Event() - unsubscribe = node.on("ready", lambda data: became_ready.set()) + unsubscribe = node.on("ready", lambda _: became_ready.set()) try: - await asyncio.wait_for(became_ready.wait(), timeout=120) + await asyncio.wait_for(became_ready.wait(), self.READY_TIMEOUT) except TimeoutError: - log.warning( - "[PAIR] node_id=%s not ready after 120s, pairing with partial values", - node.node_id, - ) + raise TimeoutError( + "The device did not finish its interview: wake it up (or move it closer) and pair it again" + ) from None finally: unsubscribe() - def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameterState]: - """Maps every Z-Wave value on a node into a majordom parameter state.""" - parameters: list[ZwaveParameterState] = [] - for value_id, value in node.values.items(): - metadata = value.metadata - parameter = ZwaveParameterState( - id=self._mapper.parameter_uuid(device_id, value_id), - name=metadata.label or value.property_name or value.property_key_name or str(value.property_), - data_type=self._mapper.parse_zwave_data_type(value), - role=self._mapper.get_role(value.command_class, metadata), - visibility=self._mapper.get_visibility(value.command_class, metadata), - min_value=metadata.min, - max_value=metadata.max, - min_step=self._mapper.get_min_step(value), - unit=self._mapper.get_unit(metadata.unit) or ParameterUnit.plain, - valid_values=self._mapper.parse_zwave_valid_values(metadata), - integration_data=ZwaveParameterIntegrationData(value_id=value_id), - value=self._mapper.format_zwave_value(value), + def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameter]: + """The device's parameters; their values reach the Hub as events (the snapshot after pairing).""" + parameters: list[ZwaveParameter] = [] + for value, state in mapper.parameter_values(node): + low, high, step = mapper.limits(value) + parameters.append( + ZwaveParameter( + id=self._parameter_id(device_id, value.value_id), + name=mapper.name(value, state), + data_type=mapper.data_type(value), + role=mapper.role(value), + visibility=mapper.visibility(value, node), + min_value=low, + max_value=high, + min_step=step, + unit=mapper.unit(value), + valid_values=mapper.valid_values(value), + integration_data=ZwaveParameterIntegrationData( + value_id=value.value_id, state_value_id=state.value_id if state else None + ), + ) ) - parameters.append(parameter) return parameters - async def _grant_security_classes(self, requested: dict) -> None: - """Grants exactly what the joining device requested — no user choice involved.""" - grant = InclusionGrant( - security_classes=[SecurityClass(c) for c in requested["securityClasses"]], - client_side_auth=requested["clientSideAuth"], - ) - controller = self._get_controller() - await controller.async_grant_security_classes(grant) - log.debug("[S2] granted security classes=%s", requested["securityClasses"]) + async def _grant(self, requested: InclusionGrant) -> None: + """Grants what the joining device requested, except what needs a PIN nobody gave: then it joins with less + security, and says so, instead of waiting for a PIN that never comes.""" + classes = requested.security_classes + if self._credentials is None: + classes = [c for c in classes if c not in AUTHENTICATED] + grant = InclusionGrant(security_classes=classes, client_side_auth=requested.client_side_auth) + await self._controller().async_grant_security_classes(grant) + log.debug("[S2] granted %s of %s", classes, requested.security_classes) + + async def _enter_pin(self) -> None: + credentials = self._credentials + if credentials is None or credentials.type != CredentialsType.code or not credentials.value: + log.warning("[S2] the device asks for its PIN, but none was given") + return + await self._controller().async_validate_dsk_and_enter_pin(credentials.value[:5]) # ------------------------------------------------------------------------- # Device <-> Hub: Z-Wave network events & availability # ------------------------------------------------------------------------- - async def _node_added(self, node: Node): - """New node joined — surfaces a discovery unless it's already known.""" + async def _node_added(self, node: Node, data: dict) -> None: log.debug("[JOIN] node_id=%s", node.node_id) + result = data.get("result") or {} + await self._offer(node, low_security=bool(result.get("lowSecurity"))) - device_id = self._mapper.device_uuid_from_node_id(node.node_id) - - if device_id in self._connected_devices: - self._connected_devices[device_id] = node - self._subscribe(device_id, node) - await self._set_availability(device_id, True) - return - - if device_id in self._majordom_discoveries: - self._awaiting_zw_discoveries[device_id] = node - return - - discovery = Discovery( - id=device_id, - integration=NonEmptyStr(self.name), - expected_credentials_options=[CredentialsType.none], - expiration=None, - transport=NonEmptyStr("ZWAVE"), - device_manufacturer=None, - device_name=NonEmptyStr(node.name or node.device_config.description or "Unknown"), - device_category=None, - device_icon=None, - ) - self._majordom_discoveries[device_id] = discovery - self._awaiting_zw_discoveries[device_id] = node - log.debug("[DISCOVERY] node_id=%s discovery_id=%s", node.node_id, device_id) - await self.dependencies.output.controller_did_receive_discovery(self, discovery) - - async def _node_removed(self, node: Node): - device_id = self._mapper.device_uuid_from_node_id(node.node_id) + async def _node_removed(self, node: Node) -> None: log.debug("[REMOVED] node_id=%s", node.node_id) - if device_id in self._connected_devices: - self._connected_devices.pop(device_id, None) - await self._set_availability(device_id, False) + if self._removal is not None and not self._removal.done(): + self._removal.set_result(node.node_id) + for device_id, joined in list(self._joined.items()): + if joined.node_id == node.node_id: + self._joined.pop(device_id) + if self._discoveries.pop(device_id, None) is not None: + await self.dependencies.output.controller_did_lose_discovery(self, device_id) + for device_id, bound in list(self._nodes.items()): + if bound.node_id == node.node_id: + self._unbind(device_id) + await self._set_available(device_id, False, "The device was removed from the Z-Wave network") + + def _bind(self, device_id: UUID, node: Node) -> None: + self._unbind(device_id) + self._nodes[device_id] = node + self._routes[device_id] = mapper.reported_parameters(node) + self._unsubscribe[device_id] = [ + node.on("value updated", lambda data: self._spawn(self._value_updated(device_id, data["value"]))), + node.on( + "value notification", + lambda data: self._spawn(self._value_updated(device_id, data["value_notification"])), + ), + node.on("dead", lambda _: self._spawn(self._update_availability(device_id, node))), + node.on("alive", lambda _: self._spawn(self._update_availability(device_id, node))), + ] + + def _unbind(self, device_id: UUID) -> None: + self._nodes.pop(device_id, None) + self._routes.pop(device_id, None) + for unsubscribe in self._unsubscribe.pop(device_id, []): + unsubscribe() + + async def _update_availability(self, device_id: UUID, node: Node) -> None: + dead = node.status == NodeStatus.DEAD + await self._set_available(device_id, not dead, "The device does not respond" if dead else None) - async def _set_availability(self, device_id: UUID, available: bool) -> None: - """Only reports on actual change.""" - if self._availability.get(device_id) == available: + async def _set_available(self, device_id: UUID, available: bool, reason: str | None = None) -> None: + """Reports a change of availability (with its reason as the device's last error).""" + if self._available.get(device_id) == available: return - self._availability[device_id] = available + self._available[device_id] = available + await self._update_device(device_id, available=available, last_error=None if available else reason) if available: await self.dependencies.output.controller_did_connect_device(self, device_id) else: await self.dependencies.output.controller_did_lose_device(self, device_id) - def _subscribe(self, device_id: UUID, node: Node) -> None: - node.on( - "value updated", - lambda data: self._create_task(self._value_updated(device_id, data["value"])), - ) - node.on( - "dead", - lambda data: self._create_task(self._set_availability(device_id, False)), - ) - node.on( - "alive", - lambda data: self._create_task(self._set_availability(device_id, True)), - ) - async def _value_updated(self, device_id: UUID, value: Value) -> None: - parameter_id = self._mapper.parameter_uuid(device_id, value.value_id) + routes = self._routes.get(device_id, {}) + parameter_value_id = routes.get(value.value_id, value.value_id) + if parameter_value_id is None: # a target: the state is its current value + return + if value.value_id not in routes: # appeared after pairing: no parameter for it + log.debug("[VALUE] %s has no parameter", value.value_id) + return event = DeviceParameterChange( device_id=device_id, - parameter_id=parameter_id, - value=self._mapper.format_zwave_value(value), + parameter_id=self._parameter_id(device_id, parameter_value_id), + value=mapper.to_hub(value, value.value), ) await self.dependencies.output.controller_did_receive_events(self, [event]) + + async def _report_snapshot(self, device_id: UUID, node: Node) -> None: + events = [ + DeviceParameterChange( + device_id=device_id, + parameter_id=self._parameter_id(device_id, value.value_id), + value=mapper.to_hub(state or value, (state or value).value), + ) + for value, state in mapper.parameter_values(node) + if mapper.role(value) != ParameterRole.event and (value.metadata.readable or state is not None) + ] + if events: + await self.dependencies.output.controller_did_receive_events(self, events) diff --git a/majordom_zwave/exceptions.py b/majordom_zwave/exceptions.py deleted file mode 100644 index e8f2bd8..0000000 --- a/majordom_zwave/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -class ZwaveConnectionError(Exception): - """Raised when Matter client is not started or not available.""" - - -class ZwaveUnexpectedError(Exception): - """Raised when an unexpected internal error occurs.""" diff --git a/majordom_zwave/mapper.py b/majordom_zwave/mapper.py index 566836b..87987bc 100644 --- a/majordom_zwave/mapper.py +++ b/majordom_zwave/mapper.py @@ -1,5 +1,11 @@ +"""Z-Wave values → MajorDom parameters, and value conversion both ways. + +A parameter is one Z-Wave value, except for current/target pairs (`currentValue`/`targetValue`, +`currentMode`/`targetMode`, ...): those are one parameter, commanded through the target and reporting the +current one, so the user sees one switch rather than a "Current value" sensor next to a "Target value" control. +""" + from typing import Any -from uuid import NAMESPACE_DNS, UUID, uuid5 from majordom_integration_sdk.schemas.parameter import ( ParameterDataType, @@ -7,137 +13,188 @@ ParameterUnit, ParameterVisibility, ) +from zwave_js_server.const import CommandClass from zwave_js_server.model.duration import Duration from zwave_js_server.model.node import Node -from zwave_js_server.model.value import ( - AllowedRangeValue, - Value, - ValueMetadata, - ValueType, -) +from zwave_js_server.model.value import AllowedRangeValue, Value, ValueType from .zwave_spec import ( + CONVERTED_UNITS, + DECIMAL_COMMAND_CLASSES, DIAGNOSTIC_COMMAND_CLASSES, EVENT_COMMAND_CLASSES, + LEVEL_COMMAND_CLASSES, + LEVEL_PROPERTIES, MAIN_VALUE_BY_COMMAND_CLASS, SYSTEM_COMMAND_CLASSES, UNIT_MAP, ) -class ZwaveMapper: - def device_uuid_from_node_id(self, node_id: int) -> UUID: - return uuid5(NAMESPACE_DNS, f"device-{node_id}") - - def parameter_uuid(self, device_id: UUID, value_id: str) -> UUID: - return uuid5(NAMESPACE_DNS, f"parameter-{device_id}-{value_id}") - - # ------------------------------------------------------------------------- - # Value metadata -> SDK parameter schema - # ------------------------------------------------------------------------- - - def get_unit(self, raw_unit: str | None) -> ParameterUnit | None: - """Maps a raw unit string to ParameterUnit; None if not in UNIT_MAP.""" - if not raw_unit: - return None - return UNIT_MAP.get(raw_unit) - - def _is_duration_property(self, value: Value) -> bool: - """True for CC-specific "duration" properties, matched by name since - zwave-js-server-python doesn't wrap them in its Duration class.""" - return value.property_name == "duration" or value.property_ == "duration" - - def format_zwave_value(self, value: Value) -> Any: - """Formats a raw value to match parse_zwave_data_type() — keep in sync.""" - if self._is_duration_property(value): - # None means "unknown" here; Duration itself expects the string "unknown". - return str(Duration(value.value)) if value.value is not None else "unknown" - return value.value - - def get_min_step(self, value: Value) -> int | float | None: - """Step size if zwave-js reported one; most devices don't, so no default.""" - for entry in value.metadata.allowed or []: - if isinstance(entry, AllowedRangeValue) and entry.step: - return entry.step - return None +def parameter_values(node: Node) -> list[tuple[Value, Value | None]]: + """(value commanded and identifying the parameter, value its state comes from when another one) per parameter.""" + states = {target.value_id: current for target in node.values.values() if (current := _current_of(node, target))} + merged = {current.value_id for current in states.values()} + return [(value, states.get(value.value_id)) for value in node.values.values() if value.value_id not in merged] - def get_role(self, command_class: int, metadata: ValueMetadata) -> ParameterRole: - """Sensor/control/event from command class + read/write access.""" - if command_class in EVENT_COMMAND_CLASSES: - return ParameterRole.event - if metadata.writeable: - return ParameterRole.control - return ParameterRole.sensor - - def get_visibility(self, command_class: int, metadata: ValueMetadata) -> ParameterVisibility: - """Readable -> user, writeable-only -> setting, else system; SYSTEM/DIAGNOSTIC - command classes override this for plumbing/diagnostic values.""" - if command_class in SYSTEM_COMMAND_CLASSES: - return ParameterVisibility.system - if command_class in DIAGNOSTIC_COMMAND_CLASSES: - return ParameterVisibility.setting - if metadata.readable or metadata.writeable: - return ParameterVisibility.user + +def reported_parameters(node: Node) -> dict[str, str | None]: + """Value id → the value id of the parameter it reports, None for a target whose state comes from its current.""" + routes: dict[str, str | None] = {} + for value, state in parameter_values(node): + routes[value.value_id] = None if state else value.value_id + if state: + routes[state.value_id] = value.value_id + return routes + + +def _current_of(node: Node, target: Value) -> Value | None: + prop = target.property_ + if not isinstance(prop, str) or not prop.startswith("target") or prop == "target": + return None + current = f"current{prop.removeprefix('target')}" + return next( + ( + v + for v in node.values.values() + if v.command_class == target.command_class + and v.endpoint == target.endpoint + and v.property_ == current + and v.property_key == target.property_key + ), + None, + ) + + +def name(value: Value, state: Value | None) -> str: + base = value.command_class_name if state else value.metadata.label or value.property_name or str(value.property_) + return f"{base} {value.endpoint}" if value.endpoint else base + + +def _is_level(value: Value) -> bool: + return value.command_class in LEVEL_COMMAND_CLASSES and value.property_ in LEVEL_PROPERTIES + + +def _is_duration(value: Value) -> bool: + # zwave-js-server-python's ValueType has no "duration": it is told by the metadata type string or the property + return value.metadata.type == "duration" or value.property_ == "duration" + + +def data_type(value: Value) -> ParameterDataType: + metadata = value.metadata + if metadata.type == ValueType.BOOLEAN: + return ParameterDataType.bool + if metadata.type == ValueType.STRING or metadata.type == "color" or _is_duration(value): + return ParameterDataType.string + if metadata.type == ValueType.NUMBER: + if metadata.states and not metadata.allow_manual_entry: + return ParameterDataType.enum + return ( + ParameterDataType.decimal if value.command_class in DECIMAL_COMMAND_CLASSES else ParameterDataType.integer + ) + raw = value.value # ValueType.ANY: only the value itself tells + if isinstance(raw, bool): + return ParameterDataType.bool + if isinstance(raw, int): + return ParameterDataType.integer + if isinstance(raw, float): + return ParameterDataType.decimal + if isinstance(raw, str): + return ParameterDataType.string + return ParameterDataType.data # an opaque CC-specific object: exposed as data rather than a guessed shape + + +def role(value: Value) -> ParameterRole: + if value.command_class in EVENT_COMMAND_CLASSES or value.metadata.stateful is False: + return ParameterRole.event + return ParameterRole.control if value.metadata.writeable else ParameterRole.sensor + + +def visibility(value: Value, node: Node) -> ParameterVisibility: + command_class = value.command_class + if ( + command_class in SYSTEM_COMMAND_CLASSES + or value.metadata.secret + or _is_duration(value) + or data_type(value) == ParameterDataType.data + or (command_class == CommandClass.BASIC and _has_other_application_values(node)) + ): return ParameterVisibility.system + if command_class in DIAGNOSTIC_COMMAND_CLASSES: + return ParameterVisibility.setting + return ParameterVisibility.user - def parse_zwave_data_type(self, value: Value) -> ParameterDataType: - """Maps a Value to ParameterDataType. ValueType alone can't tell integer from - decimal/enum, or resolve ValueType.ANY, so those fall back to the raw value.""" - metadata = value.metadata - - if metadata.type == ValueType.BOOLEAN: - return ParameterDataType.bool - if metadata.type == ValueType.STRING: - return ParameterDataType.string - if metadata.type == ValueType.NUMBER: - if metadata.states: - return ParameterDataType.enum - raw = value.value - if isinstance(raw, float) and not raw.is_integer(): - return ParameterDataType.decimal - return ParameterDataType.integer - - # ValueType.ANY: keep in sync with format_zwave_value's duration handling. - if self._is_duration_property(value): - return ParameterDataType.string - raw = value.value - if isinstance(raw, bool): - return ParameterDataType.bool - if isinstance(raw, int): - return ParameterDataType.integer - if isinstance(raw, float): - return ParameterDataType.decimal - if isinstance(raw, str): - return ParameterDataType.string - if isinstance(raw, dict): - return ParameterDataType.struct - # Opaque CC-specific object — expose as data rather than guessing a shape. - return ParameterDataType.data - - def parse_zwave_valid_values(self, metadata: ValueMetadata) -> dict[int | float | str, str] | None: - """Converts states (`{"0": "Off"}`) to valid_values, coercing keys to int.""" - if not metadata.states: - return None - result: dict[int | float | str, str] = {} - for raw_key, label in metadata.states.items(): - try: - result[int(raw_key)] = label - except ValueError: - result[raw_key] = label - return result - - def get_main_parameter(self, device_id: UUID, node: Node) -> tuple[UUID | None, bool | int | float | None]: - """Picks the value for the device's one-tap action, by MAIN_VALUE_BY_COMMAND_CLASS - priority. Returns (None, None) if none applies.""" - for command_class, spec in MAIN_VALUE_BY_COMMAND_CLASS.items(): - value = next( - ( - v - for v in node.values.values() - if v.command_class == command_class and v.property_name == spec.property_name - ), - None, - ) - if value is not None: - return self.parameter_uuid(device_id, value.value_id), spec.default_value - return None, None + +def _has_other_application_values(node: Node) -> bool: + """Basic CC only mirrors the device's real CCs (for old controllers): shown only when nothing else is.""" + ignored = SYSTEM_COMMAND_CLASSES | DIAGNOSTIC_COMMAND_CLASSES | {CommandClass.BASIC} + return any(v.command_class not in ignored for v in node.values.values()) + + +def unit(value: Value) -> ParameterUnit: + if _is_level(value): + return ParameterUnit.percentage + raw = value.metadata.unit or "" + if raw in CONVERTED_UNITS: + return CONVERTED_UNITS[raw][0] + return UNIT_MAP.get(raw, ParameterUnit.plain) + + +def limits(value: Value) -> tuple[Any, Any, Any]: + """(min, max, step) in MajorDom's units.""" + if data_type(value) not in (ParameterDataType.integer, ParameterDataType.decimal): + return None, None, None + if _is_level(value): + return 0, 100, 1 + step = next((a.step for a in value.metadata.allowed or [] if isinstance(a, AllowedRangeValue) and a.step), None) + return to_hub(value, value.metadata.min), to_hub(value, value.metadata.max), step + + +def valid_values(value: Value) -> dict[int | float | str, str] | None: + """Enum labels: zwave-js `states` (`{"0": "Off"}`), keyed by the integer the value takes.""" + if data_type(value) != ParameterDataType.enum or not value.metadata.states: + return None + result: dict[int | float | str, str] = {} + for key, label in value.metadata.states.items(): + try: + result[int(key)] = label + except ValueError: + result[key] = label + return result + + +def to_hub(value: Value, raw: Any) -> Any: + """A value as the device reports it → as MajorDom expresses it.""" + if _is_duration(value): + return str(Duration(raw)) if raw is not None else "unknown" + if raw is None or isinstance(raw, bool) or not isinstance(raw, int | float): + return raw + if _is_level(value): + return 100 if raw == 99 else int(raw) # 99 is fully on; above it are special values (255: last level) + converted = CONVERTED_UNITS.get(value.metadata.unit or "") + if converted: + _, factor, offset = converted + return round(raw * factor + offset, 2) + return raw + + +def to_device(value: Value, hub: Any) -> Any: + """A value as MajorDom commands it → as the device takes it.""" + if hub is None or isinstance(hub, bool) or not isinstance(hub, int | float): + return hub + if _is_level(value): + return 99 if hub >= 99 else int(hub) + converted = CONVERTED_UNITS.get(value.metadata.unit or "") + if converted: + _, factor, offset = converted + return round((hub - offset) / factor, 2) + return hub + + +def main_value(node: Node) -> tuple[Value, Any] | None: + """The value of the device's one-tap action and what a tap sends, by MAIN_VALUE_BY_COMMAND_CLASS priority.""" + for command_class, spec in MAIN_VALUE_BY_COMMAND_CLASS.items(): + for value in node.values.values(): + if value.command_class == command_class and value.property_ == spec.property_name: + return value, spec.default_value + return None diff --git a/majordom_zwave/model.py b/majordom_zwave/model.py index f7d7bb3..da1ebdf 100644 --- a/majordom_zwave/model.py +++ b/majordom_zwave/model.py @@ -1,38 +1,28 @@ -from majordom_integration_sdk.schemas import ( - Device, - DeviceState, - Parameter, - ParameterState, -) -from pydantic import BaseModel +from majordom_integration_sdk.schemas import Device, Parameter +from pydantic import BaseModel, Field, field_validator class ZwaveDeviceIntegrationData(BaseModel): - node_id: int = 0 + home_id: int + node_id: int + # manufacturer:product type:product id: tells a node id reused by another device from the paired one + fingerprint: str | None = None class ZwaveParameterIntegrationData(BaseModel): - value_id: str + value_id: str # the value commands go to (and the parameter's identity) + state_value_id: str | None = None # the value the state is read from, when it is another one (current vs target) class ZwaveDevice(Device): - integration_data: ZwaveDeviceIntegrationData | None = None + integration_data: ZwaveDeviceIntegrationData | None = None # None until paired: the Hub creates the device first + parameters: list["ZwaveParameter"] = Field(default_factory=list) - @property - def node_id(self) -> int: - assert self.integration_data - if isinstance(self.integration_data, dict): - return self.integration_data.get("node_id", -1) - return self.integration_data.node_id + @field_validator("integration_data", mode="before") + @classmethod + def _empty_is_unpaired(cls, value: object) -> object: + return value or None # the device the Hub creates before pairing carries `{}` class ZwaveParameter(Parameter): integration_data: ZwaveParameterIntegrationData - - -class ZwaveParameterState(ParameterState): - integration_data: ZwaveParameterIntegrationData - - -class ZwaveDeviceState(ZwaveDevice, DeviceState): - parameters: list[ZwaveParameterState] diff --git a/majordom_zwave/zwave_spec.py b/majordom_zwave/zwave_spec.py index d405b3f..5423756 100644 --- a/majordom_zwave/zwave_spec.py +++ b/majordom_zwave/zwave_spec.py @@ -2,10 +2,10 @@ Unlike Zigbee, zwave-js already reports `min`/`max`/`unit`/`states` directly on `ValueMetadata`, so this module only needs to cover what the wire format *doesn't* give us: which command classes -are protocol plumbing or diagnostics-only, how to normalize zwave-js's freeform unit strings, -and which property is a device's one-tap "main" action. +are protocol plumbing or diagnostics-only, which report decimals or levels, how to normalize zwave-js's +unit strings, and which property is a device's one-tap "main" action. -Pure data only — the functions/methods that use these tables live on ZwaveMapper in mapper.py. +Pure data only — the functions that use these tables live in mapper.py. """ from dataclasses import dataclass @@ -13,12 +13,6 @@ from majordom_integration_sdk.schemas.parameter import ParameterUnit from zwave_js_server.const import CommandClass -# The standard, cross-vendor Indicator CC value for "make the device blink so I can find it" -# (Indicator CC spec, indicator id 0x50 "Node Identify"). Setting it triggers the device's own -# built-in identify blink pattern and it self-resets — there's nothing to turn back off. -IDENTIFY_INDICATOR_ID = 0x50 - - # Protocol/network plumbing: never shown to the user, regardless of read/write access. # (association & multi channel wiring, security/transport encapsulation, inclusion, S2, etc.) SYSTEM_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( @@ -81,62 +75,79 @@ } ) -# CCs that report one-shot occurrences rather than a persistent state (button presses, -# scene activations, alarms) — modeled as ParameterRole.event regardless of read/write flags. +# CCs that report one-shot occurrences (button presses, scene activations) rather than a state — modeled as +# ParameterRole.event. zwave-js marks such values `stateful: false` and sends them as "value notification". +# Notification CC is not one: zwave-js models its variables as states with an idle value. EVENT_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( { CommandClass.CENTRAL_SCENE, CommandClass.SCENE_ACTIVATION, - CommandClass.NOTIFICATION, } ) -# zwave-js's ValueMetadata.unit is a freeform string straight from the device's CC report, not a -# controlled vocabulary — map the ones we recognize, leave the rest unmapped rather than guessed. -# Extend this as new unit strings show up in the field. +# Values these CCs report are scaled decimals by spec (a reading of 21.0 is still a decimal), so the type does not +# depend on the value seen at pairing +DECIMAL_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( + { + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.THERMOSTAT_SETPOINT, + CommandClass.ENERGY_PRODUCTION, + CommandClass.HUMIDITY_CONTROL_SETPOINT, + } +) + +# Levels 0-99 (99 is fully on) are percentages in MajorDom, as with Matter, Zigbee and ESPHome: 0-98 stay, 99 is 100 +LEVEL_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({CommandClass.SWITCH_MULTILEVEL, CommandClass.BASIC}) +LEVEL_PROPERTIES: frozenset[str] = frozenset({"currentValue", "targetValue"}) + +# zwave-js's ValueMetadata.unit is the unit string of the CC's scale (packages/core/src/registries/Scales.ts and +# Meters.ts) — map the ones MajorDom has a unit for, leave the rest `plain` rather than guessed. UNIT_MAP: dict[str, ParameterUnit] = { "%": ParameterUnit.percentage, "s": ParameterUnit.second, "Hz": ParameterUnit.hertz, "kg": ParameterUnit.kilogram, "°": ParameterUnit.arcdegree, - "deg": ParameterUnit.arcdegree, "m": ParameterUnit.meters, "m/s": ParameterUnit.mps, - "m/s2": ParameterUnit.mps2, + "m/s²": ParameterUnit.mps2, "rpm": ParameterUnit.rpm, "N": ParameterUnit.newton, "J": ParameterUnit.joule, + "kWh": ParameterUnit.kwh, "W": ParameterUnit.watt, "°C": ParameterUnit.celsius, "K": ParameterUnit.kelvin, "V": ParameterUnit.volt, "A": ParameterUnit.ampere, "lx": ParameterUnit.lux, + "Lux": ParameterUnit.lux, "Pa": ParameterUnit.pascal, "ppm": ParameterUnit.ppm, + "µg/m³": ParameterUnit.ugm3, "b": ParameterUnit.bytes, "B": ParameterUnit.bytes, - # Deliberately unmapped (no ParameterUnit equivalent yet — falls back to `plain`): - # "kWh" (energy-over-time, not instantaneous joule), "dB"/"dBm" (log scale), "UV index". +} + +# Units MajorDom has no unit for, converted to one it has: (unit, factor, offset), hub value = raw * factor + offset +CONVERTED_UNITS: dict[str, tuple[ParameterUnit, float, float]] = { + "°F": (ParameterUnit.celsius, 5 / 9, -32 * 5 / 9), + "kPa": (ParameterUnit.pascal, 1000, 0), } @dataclass(frozen=True) class MainValueSpec: - """Identifies a device's one-tap main parameter and the value to send for it. - - Z-Wave has no invokable "commands" the way Zigbee/Matter clusters do — every action is a - property get/set — so every Z-Wave main parameter is attribute-like (the same shape as - Matter's FanControl case, the one cluster where Matter's own main parameter is attribute-type - too, rather than a command). `default_value` is what a tap sends when the app doesn't already - know the device's current state (e.g. right after pairing); once telemetry is flowing, the - app is expected to flip between the parameter's known states itself rather than always - sending this same value on every tap. + """Identifies a device's one-tap main parameter and what a tap does (`Parameter.default_value`). + + Z-Wave has no invokable "commands" the way Zigbee/Matter clusters do — every action is a property set — so + every main parameter is a value. `default_value` follows the SDK: None lets the data type decide (a bool + toggles), a set is cycled through (two values: a toggle), a single value is a button that always sends it. """ property_name: str - default_value: bool | int | float + default_value: set[int] | None # Priority order matters: the first command class present on the node wins, same as @@ -144,11 +155,11 @@ class MainValueSpec: # listed — e.g. Thermostat Mode is deliberately left out, since there's no single mode that's # the obviously-correct one-tap action across vendors. MAIN_VALUE_BY_COMMAND_CLASS: dict[CommandClass, MainValueSpec] = { - CommandClass.SWITCH_BINARY: MainValueSpec("targetValue", True), - # 255 = "restore last non-zero level" per the Multilevel Switch CC spec, i.e. "turn on". - CommandClass.SWITCH_MULTILEVEL: MainValueSpec("targetValue", 255), - # BarrierState.OPEN = 255. - CommandClass.BARRIER_OPERATOR: MainValueSpec("targetState", 255), - # DoorLockMode.SECURED = 255 — lock, not unlock, is the safer default one-tap action. - CommandClass.DOOR_LOCK: MainValueSpec("targetMode", 255), + CommandClass.SWITCH_BINARY: MainValueSpec("targetValue", None), + # Off and fully on, as percentages (see LEVEL_COMMAND_CLASSES). + CommandClass.SWITCH_MULTILEVEL: MainValueSpec("targetValue", {0, 100}), + # BarrierState CLOSED = 0, OPEN = 255. + CommandClass.BARRIER_OPERATOR: MainValueSpec("targetState", {0, 255}), + # DoorLockMode UNSECURED = 0, SECURED = 255: not a cycle through every mode the lock has. + CommandClass.DOOR_LOCK: MainValueSpec("targetMode", {0, 255}), } From 8d4d9e534ed655a4677d96e0fb12fed1511ab111 Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:02:20 +0200 Subject: [PATCH 26/28] Require the SDK that keeps a set default --- README.md | 2 - poetry.lock | 153 ++++++++++++++++++++++---------------------- pyproject.toml | 2 +- tests/test_state.py | 4 +- 4 files changed, 79 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 82dc6f6..cc80722 100644 --- a/README.md +++ b/README.md @@ -187,8 +187,6 @@ the mock network only; to be re-run on hardware before release): - Z-Wave discovery doesn't use `zeroconf`/`ssdp`/`ble`: nodes are surfaced from `zwave-js-server`'s own events. - QR inclusion is not covered end to end (the mock network has no QR flow); malformed codes are tested. -- A main parameter's set `default_value` (e.g. `{0, 100}`) comes back from the SDK's repository as a list: an SDK - issue (`set[V] | V` with an unbound `V`), not this integration's. ## License diff --git a/poetry.lock b/poetry.lock index faa6ef0..d157de3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -891,21 +891,21 @@ files = [ [[package]] name = "majordom-integration-sdk" -version = "0.1.6" +version = "0.1.7" description = "Models, protocols, and tooling for building MajorDom integrations." optional = false python-versions = ">=3.12" groups = ["main"] files = [ - {file = "majordom_integration_sdk-0.1.6-py3-none-any.whl", hash = "sha256:a35e39c1ee2c6b254803d8967b20e0c90722514fea54fd51d72463ab9d7d5d3a"}, - {file = "majordom_integration_sdk-0.1.6.tar.gz", hash = "sha256:cdd3bd5d30c1e004f2b2784995182838f12e5c6b0c0764a5f8e9d558024512f9"}, + {file = "majordom_integration_sdk-0.1.7-py3-none-any.whl", hash = "sha256:a9aaf85b7ed48eaccf845ca5a8ac9672adc4497a7430f7ff3be65597d45896e2"}, + {file = "majordom_integration_sdk-0.1.7.tar.gz", hash = "sha256:6df59b26fe0138a880110cb7510b1268d99e31811a61d030014cee8c28b2a131"}, ] [package.dependencies] bleak = ">=0.22" -pydantic = ">=2.0,<3.0" +pydantic = ">=2.13.5,<3.0" python-slugify = "*" -zeroconf = ">=0.130" +zeroconf = ">=0.151.5" [package.extras] cli = ["rich (>=13)", "typer-shell (>=0.2)"] @@ -2738,82 +2738,81 @@ propcache = ">=0.2.1" [[package]] name = "zeroconf" -version = "0.151.3" +version = "0.151.5" description = "A pure python implementation of multicast DNS service discovery" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "zeroconf-0.151.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b5ac9625b008e13a0571d12d9650afbf66c7d01b35cc986894fa591e7d606a2"}, - {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb9b7933f4e002131710855d79556f7860e617316de969742c3d38ad06ff4722"}, - {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:871e72adae8ae206445debfff0b2f049c7c1f984408346574ed38a2e7f0de172"}, - {file = "zeroconf-0.151.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c33836e55d7f8edacac0dc2283bb60b33aa81e176207cc5f52813c41f52191c0"}, - {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e979f2f565719590d19c4d8313f69ea040cc74b4c12658b8ecd5e51fc7266cd"}, - {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5061fbb4052411c1e23d1164e885a7b83846f4823b069e985a74895cd34a8e6c"}, - {file = "zeroconf-0.151.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3ca29e53e3e64e6bac06c4132db45d01060cfdf16a3070925a6a49a5d1278cbf"}, - {file = "zeroconf-0.151.3-cp310-cp310-win32.whl", hash = "sha256:13dfca4530a1fddca4ff18cc2794c2fea0a7c5a4c66cbcbe7ad51e968edaf5e7"}, - {file = "zeroconf-0.151.3-cp310-cp310-win_amd64.whl", hash = "sha256:b0c9aed238135f9adcf8c686ca49c479a6715ef3fc1ab826b24232d2dda7b761"}, - {file = "zeroconf-0.151.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:675c559d287fe7385d7aa87b4f649de49ec8431396d70f7ad25ae05b0ef0c0ea"}, - {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35edcd3d61c945b4d5e98a04a3d2e2ee2bd7ab92a588b0475356e144ccc2680"}, - {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b80006e2f9ca9f7d094209438688b1b7ab1f8231ecd41b1917669d09fdd0f62f"}, - {file = "zeroconf-0.151.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8872c85242c46286849c7cb1c0d44d0c75db9fc654ccce351b3dcf26d580b04"}, - {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c74baa9873150f5a7ebf0d7800a22171ec75ae76942f2464b951cb2b3fbcf00d"}, - {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1eee2348d9863a8821ba6cbdc9af97bfb8a79109c8b400cce59679d4d100eb52"}, - {file = "zeroconf-0.151.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:657797cc284ab1da4feb20de476806db9a39f1535c5f8e40e5f2ebe149335eea"}, - {file = "zeroconf-0.151.3-cp311-cp311-win32.whl", hash = "sha256:fb544b545c725a13d208cb67f5f8a168b18a568ad0a78e07894f3b913f8225f9"}, - {file = "zeroconf-0.151.3-cp311-cp311-win_amd64.whl", hash = "sha256:4c01d8ca4fc85c107448c2802edfbc39845b06c3a1cb72a3f4337c030f4a9ff3"}, - {file = "zeroconf-0.151.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbec6e928e75629063bd29775611c390bbd3737e9a5b6546545bcf6cebbc85d0"}, - {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8193a17d718ae5a9059c4fbbaefbf937c9bb5eeccb9dd66f9aafcacfec4cefc"}, - {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b76cdefeb7ebe5a3cd8cdd0b1761113d5a64e3031c38c1de873a289ced574eff"}, - {file = "zeroconf-0.151.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbd2b73902a44426080190d653fa35638a93b6c6725c2be76888b4b614bc0b48"}, - {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5307ca6ad418200e37dcb12ff5fe5a689f3e559da6342442a82ca98ac703e60"}, - {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b69c26aca186d2b3e4f4b7789a74d9ea81202ecae04105a663939dd85e561d05"}, - {file = "zeroconf-0.151.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e573682524d71e8e44b6cdbadbeb9c472a8c3af21de7eead794661b250e04fa1"}, - {file = "zeroconf-0.151.3-cp312-cp312-win32.whl", hash = "sha256:c883d1d64c92df375a6bbee909e65fb76fc47ae92c88261e338bc79b5b505fd5"}, - {file = "zeroconf-0.151.3-cp312-cp312-win_amd64.whl", hash = "sha256:fb1880444668e2a384e925490f1d969ae702bda4472c112a18255446f98ad86b"}, - {file = "zeroconf-0.151.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7bba8a4e7639c1ae5aee9bcf18d994a8b5fcc00b862ca5e82a52a1134b4bf517"}, - {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45bd0e92fcbb6f8d5851f5575792b5f18a85856ae69fee601da4ed3d7cdcfa09"}, - {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:07a21a694d9496ab68aba2aa7d9d628e24d6f3f89766a8cac526395cf7bbb4ae"}, - {file = "zeroconf-0.151.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b514f47729ae5b9b42f066163ad509d020c34cacdda3c5efb972f3336213318"}, - {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1aa55ec953ffd0c393d59260a7e26dfd57d6d0347c13c95f9987ff5cfe0d98d"}, - {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9bac55a6aa70797b436c2fc0f25435389bc6443bc7ba1b39a459df3173152b63"}, - {file = "zeroconf-0.151.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d542814f914ee78876276db7417f515f53d33a61b84b68f7256a7a603dc73e59"}, - {file = "zeroconf-0.151.3-cp313-cp313-win32.whl", hash = "sha256:2e54bcec212e030b06d9af35b19d5f8144427b52d3fb2c0e06f015a25fe50f08"}, - {file = "zeroconf-0.151.3-cp313-cp313-win_amd64.whl", hash = "sha256:aab19746b9878ed64f7631c0fbe336cec65679c6c31c1489aaea933adf66b2d6"}, - {file = "zeroconf-0.151.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1aa300ba3cc084264b9be6fc2659f003577a1c83bf67920c7cdb4de9b571543a"}, - {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d6cf45c64be5fe4e42ea08f27edc63293357ccc04f850911a96e0fa5647dd4f"}, - {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a91538270ab1e10e0df10c66373e68f6e3e884701593c943e67f82a73ad728f1"}, - {file = "zeroconf-0.151.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b903d00e9689a103d453554d794b8ed0f13148366cee08fa8c802e5d85f1ae51"}, - {file = "zeroconf-0.151.3-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:a154a96a84b04b868596ab17eb45c16bf2af25fda8356ea36da35463b25b31bb"}, - {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59bde1d42ca1e9aa665787d97b0a9d38cfb1466d01bcd28733c21507146e7d85"}, - {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9e260a6f2f4441b83c86981fe29191076b4350d5868b630a6545b386dd4223c4"}, - {file = "zeroconf-0.151.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e42be39f72e06feaadc40509b081ab7c468fed1f19d1dc3714bc0f3a29be2dd"}, - {file = "zeroconf-0.151.3-cp314-cp314-win32.whl", hash = "sha256:df3b7c3166646a941c151da5981640e930658e96829dc6177834e763d8327d8d"}, - {file = "zeroconf-0.151.3-cp314-cp314-win_amd64.whl", hash = "sha256:688d0282e94376aec608505266a04b8d36b1f026eabc9237ce2b31e7b8c0f2ee"}, - {file = "zeroconf-0.151.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed1a629cbed1d040229b0a3fc3447f4c996e184cc4a98afa43e8ab675de07841"}, - {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3a404649c8dd1ba3cedbb56807dd2b8339ec27b2864df333f90a98f931a5286"}, - {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b97d3eb3343627a5b3ce18600f819001fbb8f29271e19beb586c10df8c32654"}, - {file = "zeroconf-0.151.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:022470d54a941805ea30ab57ca2abf443dd6e6a3d79e178149e427864756cd88"}, - {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab58e7a159b0cc91db41783e2790371bb0ba4a701ab215b3ccce12cdc48a3f81"}, - {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fef170e0c995ddeb8828caf07921077f940d634ab6fafd6955f1df88922ee29d"}, - {file = "zeroconf-0.151.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ea24e790f9f4601c7b591395e2b6a0d418345854e395ec65a6f9b4b58da7af3"}, - {file = "zeroconf-0.151.3-cp314-cp314t-win32.whl", hash = "sha256:38d1373c0e0dee5941cccef5550e0de34a49c2abbd83199b8ef42a42fbc549c6"}, - {file = "zeroconf-0.151.3-cp314-cp314t-win_amd64.whl", hash = "sha256:6bb60b8e80101cc749ff517f0d3fa77c54e6b06f29cba10faea75f32f253cbbd"}, - {file = "zeroconf-0.151.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:98c752236f6e2415b33d24e99ae4dee9f36181bc322f2103053f23dc8d699cca"}, - {file = "zeroconf-0.151.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa644376895d00b7a556370c7cc7979af94053c012e61df44d431c25db1ade99"}, - {file = "zeroconf-0.151.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:702f94d6d1a61c01ab68dd3a6e7a0ee9128302d1f79683707e92e9dab46a8e21"}, - {file = "zeroconf-0.151.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ebdacd288faddd519cef12c87e02f6af1d37ee99e15ad6cd2fd91a8043cb9832"}, - {file = "zeroconf-0.151.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:89914b9ba91604e9f6c67d115631e74aa03161e415780d8f5dabe0b4e59b31ca"}, - {file = "zeroconf-0.151.3-cp315-cp315-win32.whl", hash = "sha256:ec78ae3d72cf6050fcc4179a0c9ecda935b495396336560a26c49f2c0f8bb0ad"}, - {file = "zeroconf-0.151.3-cp315-cp315-win_amd64.whl", hash = "sha256:2904563547eb34368b9b988e657672bad2e7732a4a9990732a9666ae82970c0c"}, - {file = "zeroconf-0.151.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:04f0801e0f9955b44971cd9aac4204c2f3bcca779f4e4bb27d6ef7db5d85f22d"}, - {file = "zeroconf-0.151.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8d855f783936b7b50a72c30d74330229bbbeda799dfcd1981e0d38973b1ce69"}, - {file = "zeroconf-0.151.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13e3f70fcd64a75eccadfa91baafc9a5e498736299af7511b02421a0f3bcc7a1"}, - {file = "zeroconf-0.151.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:e226fd343924b685c09f9e20cadf4e50693f08824846feef6c6cb05173e407ad"}, - {file = "zeroconf-0.151.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c3e87bb627e469c6e901c47ab9fab3a38e98250dc4032d505cf7f83c2e2eee2f"}, - {file = "zeroconf-0.151.3-cp315-cp315t-win32.whl", hash = "sha256:facffd6e4392869b3a53b674df502f9f90846a6ccb70045eebe8e40306f5b0d9"}, - {file = "zeroconf-0.151.3-cp315-cp315t-win_amd64.whl", hash = "sha256:397a824856fad1aea642640bad40e9a52a6ea9fe5ee2954b5ec7f46d9985bf08"}, - {file = "zeroconf-0.151.3.tar.gz", hash = "sha256:ce6c548e665759b6150cef4db9ab9d7bdd89857e90c513abd6b7340bdd7dbd6a"}, + {file = "zeroconf-0.151.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a561c0d95c96aea326bbfd8577044b22499a6154c4ee6a74bc70cd0cc1185743"}, + {file = "zeroconf-0.151.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b15a9ff99c33622daf4a9b3c0bd0aeac7532b86d62e80e03fc3fab7e154140c"}, + {file = "zeroconf-0.151.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:effc02f3ac1b39828b47db5f7c386b57699e2f2292d9d773879a9d1f9ef137b3"}, + {file = "zeroconf-0.151.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5e17c8b93fa9da4e6b125e2dd844cec3c5fe60af223ab6e179ad8ef0674fdbd"}, + {file = "zeroconf-0.151.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:841b16e18846132fd555cba136fbca2ff33ccd5b332d2c54293ceba242851e7e"}, + {file = "zeroconf-0.151.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:470ec9d7056d8e4d22e22c699a6e2e544c3e0b7bb2d49b700ef516e18d5b67a4"}, + {file = "zeroconf-0.151.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:50f67bf880c28ddc1d8c5717d759ef955a04066e5c9dbfd2dabb0489d01b7239"}, + {file = "zeroconf-0.151.5-cp310-cp310-win32.whl", hash = "sha256:a941c82401ae04d8548cf3567b22347509b589a867adede2646eee9bdfd4f446"}, + {file = "zeroconf-0.151.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ff72dd4ba1549814dcd87104997f324989fc2319558e6f4461931c4fc7dacec"}, + {file = "zeroconf-0.151.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d702be8e8b456470af59fb268b4756c96b4754c7c4ad8c38035c7f0336ac35c5"}, + {file = "zeroconf-0.151.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d81e8e2c70cc590573069fad9036d1ded7f135c92ada79c89fe547978acf2f90"}, + {file = "zeroconf-0.151.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:11cc483a2fb4a08c8f3456d574c4b64dd8413028fd0bafc8eae77987a63f04c7"}, + {file = "zeroconf-0.151.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bfb0efd3d9259e3a5e9b2373f895900ea3951ca843be84772b6726b8f16dd60"}, + {file = "zeroconf-0.151.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6715218b4348b306ec11799cbc6e365fb078b60e0e5a384c5a6d5f48a495025"}, + {file = "zeroconf-0.151.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:35b908297f4a36b5a934aa25dbcf5078bd26fedd7203bf92506041e0bfe32616"}, + {file = "zeroconf-0.151.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92e9a1494c5938ca4e426150a3ad575e9b2ca14cc8af9372124d8800d791b1b8"}, + {file = "zeroconf-0.151.5-cp311-cp311-win32.whl", hash = "sha256:c967096138b7a38ef51b54d71966ec234e1c0353485ab1925b845106db938e47"}, + {file = "zeroconf-0.151.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab67bb2233a0d9ce7d71d01b6791dac50d56c5a814ef1553ac97ae9c43e7eabf"}, + {file = "zeroconf-0.151.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f13dfaa9091daf9c30198149b5d414bc1e92f25aceb0d8d9c9ca3f7ea7a239f8"}, + {file = "zeroconf-0.151.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8905f5ccf8cfa7e767dc9b0dd009e2787f8d8e5595f2a6d9c3e956cbb1a397cb"}, + {file = "zeroconf-0.151.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:137f3c5cf600fccd100513e6d22a265d1b0301278607b84b9eb2de1e54c72fde"}, + {file = "zeroconf-0.151.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e843fdb7249cbf612cf0bdb729d4581d057b78c45423bd46a6bf377832b4ffc"}, + {file = "zeroconf-0.151.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:849c312c7c2f23f1e681ad8d3d2ca4189fb4df9cf426ad1933b3ad0403a5b4f8"}, + {file = "zeroconf-0.151.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:be010daad6e60fbdbbd085c6873dd9b491b2cd1e93f64e4a58722b2db72c2b9b"}, + {file = "zeroconf-0.151.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92dc32ac2ce69967d288091106581da063136441bd22ad8efd5bb611d692b890"}, + {file = "zeroconf-0.151.5-cp312-cp312-win32.whl", hash = "sha256:b2c0ec31fd0195b3f32dcfb4b6133189278ad18fb27f88a20857adaa8d1589ad"}, + {file = "zeroconf-0.151.5-cp312-cp312-win_amd64.whl", hash = "sha256:244d853ec76620cd31c5c4657cff22fb34e181826d833134244135202f92888f"}, + {file = "zeroconf-0.151.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44b34921217c4387cdb0b4b08ae510d9adbf76e80a3b84ae3f7c1f8cdc60c436"}, + {file = "zeroconf-0.151.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f346b414b882ac0dee5beb55bc8bd6ad8c7415f26d6ab4629d91bfd6c5f30f62"}, + {file = "zeroconf-0.151.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56060316f3464c86c1fbd32287ffab28fbb9c539ffd6a6e070251c455252c50c"}, + {file = "zeroconf-0.151.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92d5f219f09a45bebf28ce40776dbc1f8a0697871ceda8b6b9f2ffe8209826fe"}, + {file = "zeroconf-0.151.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a48fedd887c3bafe45cf24e08aaa06a1b641ec3403696f4a01701ae64c9088a0"}, + {file = "zeroconf-0.151.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:23f5d281c130af8f4976a611a1db196d17395d69815aeff3537a599430b9ca5a"}, + {file = "zeroconf-0.151.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:667d8fb8f6cb02d9e28085759f5c336b26fc596ce427a36dbb20ecd440dfe029"}, + {file = "zeroconf-0.151.5-cp313-cp313-win32.whl", hash = "sha256:475e527d371fdc8d29d10cd2300140e735a8562df041041465704cff63452d9f"}, + {file = "zeroconf-0.151.5-cp313-cp313-win_amd64.whl", hash = "sha256:05cdec63c6bc2fde2086a174339b40947bc30eead775effaab90c20558835f89"}, + {file = "zeroconf-0.151.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cba88b0fb77ea1b4dceaebae577c8245852159c525201b2453cd3bd5715cf56"}, + {file = "zeroconf-0.151.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b9f3f49860cb60dd5d1453c681dbcced4cac96960935f1b9f7c24d7f3993ce"}, + {file = "zeroconf-0.151.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2304c3fd4717422242cc78315b37d48613109963900e0c9a2b784a91fd6e9c0e"}, + {file = "zeroconf-0.151.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5846416ae2bddacb1bbb755233dd13c4bd15d94dfd0feb23a4b1abe1e2312302"}, + {file = "zeroconf-0.151.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c1f45df00d20918fc7a0c2a6229ae1f9b44fbbef6a161df96c7710098c82e019"}, + {file = "zeroconf-0.151.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f8f7ec289a07acb9e272d0f2998759b76ec81dd23b15a7506e05689f630fafd1"}, + {file = "zeroconf-0.151.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dc7d125bd01eca9f63e7c65a3e52a81e385ef6ac25721bc67e15b99efc3ce6b0"}, + {file = "zeroconf-0.151.5-cp314-cp314-win32.whl", hash = "sha256:965d9e92ad34896a2537352a37affcff2daeb8c7af2072451220e753f0de990d"}, + {file = "zeroconf-0.151.5-cp314-cp314-win_amd64.whl", hash = "sha256:3d3d2f0eb51e605f2010e83ab49db34c70c51304ee88a980ed1c59c0ea20b82f"}, + {file = "zeroconf-0.151.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a4bde05914a6dc6a4de83fb9f85ebcbf7fb1c89d9021122b689ebb6aa9d717c"}, + {file = "zeroconf-0.151.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5daadc4306a6def67187dd8a4ee15978d0c4d425bad4dfef7b2131e909edce02"}, + {file = "zeroconf-0.151.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5cfb9d625a12bb6788c142bd238479444ba43b8cbb56d6e0fd2becc17ef8e4a3"}, + {file = "zeroconf-0.151.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd773d22ddd60f696e0be063a925a9d34f8de7bcc40e4b9c9360c9e081f97ab0"}, + {file = "zeroconf-0.151.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99729ee6109f08ebee6ce7ef7bd4280747848550462d40275b059e3eaca57"}, + {file = "zeroconf-0.151.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:faecc5a1f318a920a4f7052a14391ae6def8fd6035313886e88f53705ad9dc8e"}, + {file = "zeroconf-0.151.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b124e30e18f29690907f7f7ed8b79ab34fa8f40fe300a6f5a8f21b04148497e5"}, + {file = "zeroconf-0.151.5-cp314-cp314t-win32.whl", hash = "sha256:6385a69307ee9ccd71d0d2374f0a822590e2468919acbd4ae6bf3ab5f501a1f3"}, + {file = "zeroconf-0.151.5-cp314-cp314t-win_amd64.whl", hash = "sha256:9583f52f51f08f7b14e9f09d99cc0e5ed29afd02bfef6f9d89f175a3e3582a33"}, + {file = "zeroconf-0.151.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09a933cd3a4bb6a5a0eaf84b58aa0d32af330aa81aa75fc0e300551ccb9b775b"}, + {file = "zeroconf-0.151.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:532e28946f6b384369747771e71094f76aa75b4bc2dfba6c099286d90111caf0"}, + {file = "zeroconf-0.151.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:720a872df2924faedd33c737c2b8a4ac43c045b0b278c58d8b32e1eadff4fa9f"}, + {file = "zeroconf-0.151.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c6f98dafac6bacb715fd6644921a9220c09499fcaedc308d27e6fd4fd157837e"}, + {file = "zeroconf-0.151.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:bf19169739ee8608748db3533670f1f8622727e4cf189a7619eadf0895ccf2dd"}, + {file = "zeroconf-0.151.5-cp315-cp315-win32.whl", hash = "sha256:422592b2d7dbe8c82fb0ec38f33898fa4a580587a8ee9a1554d1d5c477cf7903"}, + {file = "zeroconf-0.151.5-cp315-cp315-win_amd64.whl", hash = "sha256:1ab36f829be85f6546476be525463879e11ce67c6eb18d0b01b1c2b989ba35d0"}, + {file = "zeroconf-0.151.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3da652fc2d7ba788651b8713340b6470e7218f6fd8392755666a49636d9d3383"}, + {file = "zeroconf-0.151.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c24366d4e60b72adbf6fc954fb8df6165dae44d137f5c8afd8f7272995580681"}, + {file = "zeroconf-0.151.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99162e67862c2f17aeb5a432c70757419573e33cfab925eed566f8bc2a346bcb"}, + {file = "zeroconf-0.151.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c3f34b0e863f20af95fca7ef5871536015c964ce07462b6462c3246f6a385177"}, + {file = "zeroconf-0.151.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:37f8a073825a208878e994300c0feb7d6b253e2af3d258678a8beec6b020506a"}, + {file = "zeroconf-0.151.5-cp315-cp315t-win32.whl", hash = "sha256:a382205700aa8ea63634b4a92b41176a6033bef7ec2380bfd0503e3e11e6d598"}, + {file = "zeroconf-0.151.5-cp315-cp315t-win_amd64.whl", hash = "sha256:00ee9d456519c45c94201334d5cbd2699d5de9cc9ceeb647203b85295640abab"}, + {file = "zeroconf-0.151.5.tar.gz", hash = "sha256:28c2ec9d772007eedf11b41a9c9fd3d5c684c17b00721ff8f1ee31b20ad286a1"}, ] [package.dependencies] @@ -2838,4 +2837,4 @@ pydantic = ">=2.0.0" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "4be76b6f1bc679c04100082b84d1856185f272da8aac49d6d682091e4df2f289" +content-hash = "87dac081b9f3c7c0321c40ed36fef854f936dd032aa52453980adc4cd22f401c" diff --git a/pyproject.toml b/pyproject.toml index 407193c..0a6733c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ ] requires-python = ">=3.12" dependencies = [ - "majordom-integration-sdk (>=0.1.5,<1.0)", + "majordom-integration-sdk (>=0.1.7,<1.0)", "zwave-js-server-python (>=0.74.0,<0.75.0)", ] diff --git a/tests/test_state.py b/tests/test_state.py index 55a94b3..66c9be8 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -83,8 +83,8 @@ async def test_a_level_is_a_percentage( ) assert level.integration_data.state_value_id is not None assert device.main_parameter == level.id - # a tap toggles off / fully on, within the declared range (a set: the SDK's repository round-trip returns a list) - assert level.default_value is not None and set(level.default_value) == {0, 100} + assert level.default_value == {0, 100} # a tap toggles off / fully on, within the declared range + assert level.main_cycle == [0, 100] async def test_scene_activations_are_events( From 8efc404b1b95c3e6fa95d9540acac248357bfe97 Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:12:13 +0200 Subject: [PATCH 27/28] Add the command-class catalogue and its canary Every command class zwave-js knows is hosted by a mock device, protocol plumbing, or explained; each hosted one must map exactly as reviewed against the parameter UX docs (names, buckets, units, main tile), pass the SDK's parameter audit and round-trip its writable values. Mock devices with every sensor, meter, notification, setpoint and mode variant; drift checks for value types and units; sleeping-device, QR, SmartStart, keypad and notification scenarios; a monthly canary on the latest zwave-js, zwave-js-server and zwave-js-server-python. --- .github/workflows/canary.yml | 45 +++ tests/catalogue.py | 523 +++++++++++++++++++++++++++++++++++ tests/conftest.py | 6 + tests/mock/catalogue.mjs | 510 ++++++++++++++++++++++++++++++++++ tests/mock/network.mjs | 127 ++++++++- tests/mock/registry.mjs | 29 ++ tests/network.py | 11 + tests/test_catalogue.py | 273 ++++++++++++++++++ tests/test_notifications.py | 133 +++++++++ tests/test_pairing.py | 16 +- tests/test_sleeping.py | 56 ++++ tests/test_smart_start.py | 92 ++++++ 12 files changed, 1807 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/canary.yml create mode 100644 tests/catalogue.py create mode 100644 tests/mock/catalogue.mjs create mode 100644 tests/mock/registry.mjs create mode 100644 tests/test_catalogue.py create mode 100644 tests/test_notifications.py create mode 100644 tests/test_sleeping.py create mode 100644 tests/test_smart_start.py diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 0000000..f098a1f --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,45 @@ +name: Command-class catalogue canary + +# Monthly canary, like the Matter and ESPHome integrations'. +# +# PURPOSE: catch zwave-js / zwave-js-server / zwave-js-server-python changes this integration does not support yet. +# Each run installs their LATEST releases (not the locked ones) and runs the whole suite, whose catalogue +# (tests/test_catalogue.py) hosts one mock device per command class. It goes RED when upstream ships something +# unsupported: a new command class, value metadata type or unit (the fast checks), or a new or changed value of a +# hosted command class (the sweep). That is the signal to add support, or to explain why not. + +on: + schedule: + - cron: "0 4 1 * *" # monthly + workflow_dispatch: + +permissions: + contents: read + +jobs: + canary: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install the project + run: (command -v poetry || pipx install poetry) && poetry install + + - name: Upgrade to the latest zwave-js, zwave-js-server and zwave-js-server-python + run: | + npm ci --prefix tests/mock --no-audit --no-fund + npm install --prefix tests/mock --no-save --no-audit --no-fund zwave-js@latest @zwave-js/server@latest + poetry run pip install --upgrade zwave-js-server-python + npm ls --prefix tests/mock zwave-js @zwave-js/server + poetry run pip show zwave-js-server-python | head -2 + + - name: Whole suite, catalogue included + run: poetry run pytest -v diff --git a/tests/catalogue.py b/tests/catalogue.py new file mode 100644 index 0000000..a66bbd1 --- /dev/null +++ b/tests/catalogue.py @@ -0,0 +1,523 @@ +"""The command-class catalogue: every CC zwave-js knows is hosted by a mock device (a recipe, tests/mock/catalogue.mjs), +is protocol plumbing, or is explained here — and what each hosted CC must map to. + +Spec terms: "plumbing" CCs carry no user-facing value (they map to `system`); "unhostable" ones have no zwave-js mock +(their values still go through the generic mapping, checked shape by shape in test_value_shapes). +""" + +from majordom_integration_sdk.schemas.parameter import ( + ParameterDataType as T, +) +from majordom_integration_sdk.schemas.parameter import ( + ParameterRole as R, +) +from majordom_integration_sdk.schemas.parameter import ( + ParameterUnit as U, +) +from majordom_integration_sdk.schemas.parameter import ( + ParameterVisibility as V, +) + +from majordom_zwave.zwave_spec import SYSTEM_COMMAND_CLASSES + +# recipe → the command classes it hosts (beyond the ones every mock device has, see BASE) +RECIPES: dict[str, set[int]] = { + "basic": {0x20}, + "binary_sensor": {0x30}, + "binary_switch": {0x25}, + "color_switch": {0x33, 0x26}, + "configuration": {0x70}, + "door_lock": {0x62}, + "energy_production": {0x90}, + "indicator": {0x87}, + "lock": {0x76}, + "meter": {0x32}, + "multilevel_sensor": {0x31}, + "multilevel_switch": {0x26}, + "node_naming": {0x77}, + "notification": {0x71}, + "scene_activation": {0x2B}, + "schedule_entry_lock": {0x4E, 0x63}, + "sound_switch": {0x79}, + "thermostat_mode": {0x40}, + "thermostat_setback": {0x47}, + "thermostat_setpoint": {0x43}, + "user_code": {0x63}, + "user_credential": {0x83}, + "window_covering": {0x6A}, + # not mocked by zwave-js: the harness answers for them (BEHAVIORS in catalogue.mjs) + "central_scene": {0x5B}, + "thermostat_fan_mode": {0x44}, + "thermostat_fan_state": {0x45}, + "thermostat_operating_state": {0x42}, + "barrier_operator": {0x66}, + "protection": {0x75, 0x25}, + "humidity_control": {0x64, 0x6D, 0x6E}, + "entry_control": {0x6F}, + "alarm_sensor": {0x9C}, + "powerlevel": {0x73, 0x25}, + "sleeping": {0x80, 0x84, 0x25}, # Battery and Wake Up, on a switch +} + +# On every mock device (identity, interview), or used by the scenario tests (endpoints, supervision, S2) +BASE = {0x5E, 0x72, 0x86} +INFRASTRUCTURE = BASE | {0x60, 0x6C, 0x9F} + +# Not in zwave-js-server-python's CommandClass yet: newer than the library +NEWER_THAN_THE_LIBRARY = { + 0x01: "Z-Wave Protocol: the protocol's own frames, no values", + 0x04: "Z-Wave Long Range: the protocol's own frames, no values", + 0x83: "User Credential: hosted (recipe user_credential); compared by its number", +} + +PLUMBING = {int(cc) for cc in SYSTEM_COMMAND_CLASSES} | {0x01, 0x04} + +_NO_MOCK = "zwave-js has no mock for it; its values go through the generic mapping (test_value_shapes)" +_RARE = "no mock, and rare in homes (commercial metering, IP gateways, AV): generic mapping (test_value_shapes)" +UNHOSTABLE: dict[int, str] = { + **dict.fromkeys( + [ + 0x27, # All Switch + 0x28, # Binary Toggle Switch + 0x29, # Multilevel Toggle Switch + 0x2C, # Scene Actuator Configuration + 0x2D, # Scene Controller Configuration + 0x46, # Climate Control Schedule + 0x4C, # Door Lock Logging + 0x50, # Basic Window Covering + 0x51, # Move To Position Window Covering + 0x53, # Schedule + 0x5D, # Anti-Theft + 0x6B, # Irrigation + 0x7E, # Anti-Theft Unlock + 0x82, # Hail + 0x89, # Language + 0x9D, # Alarm Silence + 0x9E, # Sensor Configuration + 0xA3, # Generic Schedule + ], + _NO_MOCK, + ), + **dict.fromkeys( + [ + 0x35, # Pulse Meter + 0x36, # Basic Tariff Information + 0x37, # HRV Status + 0x39, # HRV Control + 0x3A, # Demand Control Plan Configuration + 0x3B, # Demand Control Plan Monitor + 0x3C, # Meter Table Configuration + 0x3D, # Meter Table Monitor + 0x3E, # Meter Table Push Configuration + 0x3F, # Prepayment + 0x41, # Prepayment Encapsulation + 0x48, # Rate Table Configuration + 0x49, # Rate Table Monitor + 0x4A, # Tariff Table Configuration + 0x4B, # Tariff Table Monitor + 0x4F, # Z/IP 6LoWPAN + 0x58, # Z/IP ND + 0x5F, # Z/IP Gateway + 0x61, # Z/IP Portal + 0x68, # Z/IP Naming and Location + 0x5C, # IP Association + 0x69, # Mailbox + 0x94, # Simple AV Control + 0x9A, # IP Configuration + 0x9B, # Association Command Configuration + 0xA0, # IR Repeater + 0xA1, # Authentication + 0xA2, # Authentication Media Write + ], + _RARE, + ), +} + +# What each hosted CC maps to, reviewed value by value against the docs' parameter UX (docs/device-integration/ +# parameter-ux.md): everyday controls and live readings `user` (the main tile among them), a CC's configuration and +# diagnostics `setting`, wiring, metadata, secrets and durations `system`; names for people, not zwave-js's labels. +Row = tuple[str, T, R, V, U] # (name, data type, role, visibility, unit) + +# Every device's identity values (Manufacturer Specific, Version): checked once, the same on every recipe +COMMON: dict[str, Row] = { + "114-0-productId": ("Product ID", T.integer, R.sensor, V.setting, U.plain), + "114-0-productType": ("Product type", T.integer, R.sensor, V.setting, U.plain), + "114-0-manufacturerId": ("Manufacturer ID", T.integer, R.sensor, V.setting, U.plain), + "134-0-firmwareVersions": ("Firmware version", T.string, R.sensor, V.setting, U.plain), + "134-0-protocolVersion": ("Z-Wave protocol version", T.string, R.sensor, V.setting, U.plain), + "134-0-libraryType": ("Z-Wave library type", T.enum, R.sensor, V.setting, U.plain), +} + +# key (value id without the node: --[-]) → (name, data type, role, visibility, unit) +EXPECTED: dict[str, dict[str, Row]] = { + "alarm_sensor": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "156-0-state-1": ("Smoke state", T.bool, R.sensor, V.user, U.plain), + "156-0-severity-1": ("Smoke severity", T.integer, R.sensor, V.setting, U.percentage), + "156-0-duration-1": ("Smoke duration", T.integer, R.sensor, V.setting, U.second), + "156-0-state-2": ("CO state", T.bool, R.sensor, V.user, U.plain), + "156-0-severity-2": ("CO severity", T.integer, R.sensor, V.setting, U.percentage), + "156-0-duration-2": ("CO duration", T.integer, R.sensor, V.setting, U.second), + }, + "barrier_operator": { + "102-0-signalingState-1": ("Audible warning", T.enum, R.control, V.setting, U.plain), + "102-0-signalingState-2": ("Visual warning", T.enum, R.control, V.setting, U.plain), + "102-0-position": ("Position", T.integer, R.sensor, V.user, U.percentage), + "102-0-targetState": ("Door", T.enum, R.control, V.user, U.plain), + }, + "basic": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + }, + "binary_sensor": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "48-0-General Purpose": ("General purpose", T.bool, R.sensor, V.user, U.plain), + "48-0-Door/Window": ("Door/Window", T.bool, R.sensor, V.user, U.plain), + "48-0-Motion": ("Motion", T.bool, R.sensor, V.user, U.plain), + }, + "binary_switch": { + "37-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "37-0-targetValue": ("Power", T.bool, R.control, V.user, U.plain), + }, + "central_scene": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "91-0-scene-001": ("Scene 1", T.enum, R.event, V.user, U.plain), + "91-0-scene-002": ("Scene 2", T.enum, R.event, V.user, U.plain), + "91-0-slowRefresh": ("Slow refresh while held", T.bool, R.control, V.setting, U.plain), + }, + "color_switch": { + "38-0-duration": ("Remaining duration (Multilevel Switch)", T.string, R.sensor, V.system, U.plain), + "38-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "38-0-Up": ("Level up", T.bool, R.control, V.setting, U.plain), + "38-0-Down": ("Level down", T.bool, R.control, V.setting, U.plain), + "38-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + "51-0-hexColor": ("Color", T.string, R.control, V.user, U.plain), + "51-0-targetColor-0": ("Warm white", T.integer, R.control, V.setting, U.plain), + "51-0-targetColor-1": ("Cold white", T.integer, R.control, V.setting, U.plain), + "51-0-targetColor-2": ("Red", T.integer, R.control, V.setting, U.plain), + "51-0-targetColor-3": ("Green", T.integer, R.control, V.setting, U.plain), + "51-0-targetColor-4": ("Blue", T.integer, R.control, V.setting, U.plain), + "51-0-targetColor": ("Color Switch", T.data, R.control, V.system, U.plain), + "51-0-duration": ("Remaining duration (Color Switch)", T.string, R.sensor, V.system, U.plain), + "38-0-levelChange": ("Level change", T.enum, R.event, V.system, U.plain), + }, + "configuration": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + "112-0-1": ("Brightness on power-up", T.integer, R.control, V.setting, U.plain), + "112-0-2": ("LED mode", T.integer, R.control, V.setting, U.plain), + "112-0-3": ("Offset", T.integer, R.control, V.setting, U.plain), + "112-0-4": ("Firmware flags", T.integer, R.sensor, V.setting, U.plain), + }, + "door_lock": { + "98-0-targetMode": ("Lock", T.enum, R.control, V.user, U.plain), + "98-0-operationType": ("Operation type", T.enum, R.control, V.setting, U.plain), + "98-0-doorStatus": ("Door", T.string, R.sensor, V.user, U.plain), + "98-0-latchStatus": ("Latch", T.string, R.sensor, V.user, U.plain), + "98-0-boltStatus": ("Bolt", T.string, R.sensor, V.user, U.plain), + "98-0-insideHandlesCanOpenDoorConfiguration": ( + "Inside handles that open the door", + T.data, + R.control, + V.system, + U.plain, + ), + "98-0-outsideHandlesCanOpenDoorConfiguration": ( + "Outside handles that open the door", + T.data, + R.control, + V.system, + U.plain, + ), + "98-0-autoRelockTime": ("Auto-relock time", T.integer, R.control, V.setting, U.second), + "98-0-holdAndReleaseTime": ("Hold and release time", T.integer, R.control, V.setting, U.second), + "98-0-twistAssist": ("Twist assist", T.bool, R.control, V.setting, U.plain), + "98-0-blockToBlock": ("Block to block", T.bool, R.control, V.setting, U.plain), + "98-0-insideHandlesCanOpenDoor": ("Inside handles open the door", T.data, R.sensor, V.system, U.plain), + "98-0-outsideHandlesCanOpenDoor": ("Outside handles open the door", T.data, R.sensor, V.system, U.plain), + "98-0-duration": ("Remaining duration until target lock mode", T.string, R.sensor, V.system, U.plain), + "98-0-lockTimeoutConfiguration": ("Timed mode duration", T.integer, R.control, V.setting, U.second), + "98-0-lockTimeout": ("Time until relock", T.integer, R.sensor, V.setting, U.second), + }, + "energy_production": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "144-0-value-0": ("Power", T.decimal, R.sensor, V.user, U.watt), + "144-0-value-1": ("Total production", T.decimal, R.sensor, V.user, U.kwh), + "144-0-value-2": ("Production today", T.decimal, R.sensor, V.user, U.kwh), + "144-0-value-3": ("Operating time", T.decimal, R.sensor, V.user, U.second), + }, + "entry_control": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "111-0-keyCacheSize": ("Key cache size", T.integer, R.control, V.setting, U.plain), + "111-0-keyCacheTimeout": ("Key cache timeout", T.integer, R.control, V.setting, U.second), + "111-0-event": ("Keypad", T.enum, R.event, V.user, U.plain), + "111-0-code": ("Keypad code", T.string, R.event, V.system, U.plain), + }, + "humidity_control": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "100-0-setpointScale-1": ("Setpoint scale (humidifier)", T.enum, R.sensor, V.system, U.plain), + "100-0-setpoint-1": ("Humidifier setpoint", T.decimal, R.control, V.user, U.percentage), + "100-0-setpointScale-2": ("Setpoint scale (de-humidifier)", T.enum, R.sensor, V.system, U.plain), + "100-0-setpoint-2": ("De-humidifier setpoint", T.decimal, R.control, V.user, U.percentage), + "109-0-mode": ("Humidity mode", T.enum, R.control, V.user, U.plain), + "110-0-state": ("Humidity operating state", T.enum, R.sensor, V.user, U.plain), + }, + "indicator": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + "135-0-67-2": ("Button 1 indication", T.bool, R.control, V.setting, U.plain), + "135-0-value": ("Indicator value", T.integer, R.control, V.setting, U.plain), + "135-0-identify": ("Identify", T.none, R.control, V.setting, U.plain), + }, + "lock": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "118-0-locked": ("Locked", T.bool, R.control, V.user, U.plain), + }, + "meter": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "50-0-value-65537": ("Electric energy", T.decimal, R.sensor, V.user, U.kwh), + "50-0-value-66049": ("Electric power", T.decimal, R.sensor, V.user, U.watt), + "50-0-value-66561": ("Electric voltage", T.decimal, R.sensor, V.user, U.volt), + "50-0-value-66817": ("Electric current", T.decimal, R.sensor, V.user, U.ampere), + "50-0-reset-65537": ("Reset electric energy", T.none, R.control, V.setting, U.plain), + "50-0-reset": ("Reset all meters", T.none, R.control, V.setting, U.plain), + }, + "multilevel_sensor": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "49-0-Air temperature": ("Air temperature", T.decimal, R.sensor, V.user, U.celsius), + "49-0-Illuminance": ("Illuminance", T.decimal, R.sensor, V.user, U.lux), + "49-0-Humidity": ("Humidity", T.decimal, R.sensor, V.user, U.percentage), + }, + "multilevel_switch": { + "38-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "38-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "38-0-Up": ("Level up", T.bool, R.control, V.setting, U.plain), + "38-0-Down": ("Level down", T.bool, R.control, V.setting, U.plain), + "38-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + "38-0-levelChange": ("Level change", T.enum, R.event, V.system, U.plain), + }, + "node_naming": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + "119-0-name": ("Node name", T.string, R.control, V.system, U.plain), + "119-0-location": ("Node location", T.string, R.control, V.system, U.plain), + }, + "notification": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "113-0-Water Alarm-Sensor status": ("Sensor status (Water alarm)", T.enum, R.sensor, V.user, U.plain), + "113-0-Home Security-Cover status": ("Cover status (Home security)", T.enum, R.sensor, V.user, U.plain), + "113-0-Home Security-Motion sensor status": ("Motion sensor status", T.enum, R.sensor, V.user, U.plain), + "113-0-alarmType": ("Alarm type", T.integer, R.sensor, V.setting, U.plain), + "113-0-alarmLevel": ("Alarm level", T.integer, R.sensor, V.setting, U.plain), + "113-0-event-6": ("Access control event", T.enum, R.event, V.user, U.plain), + "113-0-details-6": ("Access control event details", T.string, R.event, V.system, U.json), + }, + "powerlevel": { + "37-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "37-0-targetValue": ("Power", T.bool, R.control, V.user, U.plain), + "115-0-test": ("Powerlevel test", T.enum, R.event, V.setting, U.plain), + }, + "protection": { + "37-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "37-0-targetValue": ("Power", T.bool, R.control, V.user, U.plain), + "117-0-local": ("Local protection", T.enum, R.control, V.setting, U.plain), + "117-0-rf": ("Remote protection", T.enum, R.control, V.setting, U.plain), + "117-0-exclusiveControlNodeId": ("Exclusive control", T.integer, R.control, V.setting, U.plain), + "117-0-timeout": ("Remote protection timeout", T.string, R.control, V.setting, U.plain), + }, + "scene_activation": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "43-0-sceneId": ("Scene", T.integer, R.event, V.user, U.plain), + "43-0-dimmingDuration": ("Scene dimming duration", T.string, R.event, V.system, U.plain), + }, + "schedule_entry_lock": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "99-0-adminCode": ("Admin code", T.string, R.control, V.system, U.plain), + "99-0-userIdStatus-1": ("User 1 status", T.enum, R.control, V.setting, U.plain), + "99-0-userCode-1": ("User 1 code", T.string, R.control, V.system, U.plain), + "99-0-userIdStatus-2": ("User 2 status", T.enum, R.control, V.setting, U.plain), + "99-0-userCode-2": ("User 2 code", T.string, R.control, V.system, U.plain), + "99-0-keypadMode": ("Keypad mode", T.integer, R.sensor, V.setting, U.plain), + }, + "sleeping": { + "37-0-duration": ("Remaining duration", T.string, R.sensor, V.system, U.plain), + "37-0-targetValue": ("Power", T.bool, R.control, V.user, U.plain), + "128-0-level": ("Battery level", T.integer, R.sensor, V.user, U.percentage), + "132-0-wakeUpInterval": ("Wake-up interval", T.integer, R.control, V.setting, U.second), + "132-0-controllerNodeId": ("Wake-up destination", T.integer, R.sensor, V.system, U.plain), + "128-0-replacement": ("Battery replacement", T.enum, R.event, V.user, U.plain), + }, + "sound_switch": { + "121-0-defaultToneId": ("Default tone", T.enum, R.control, V.setting, U.plain), + "121-0-defaultVolume": ("Default volume", T.integer, R.control, V.setting, U.percentage), + "121-0-toneId": ("Play tone", T.enum, R.control, V.user, U.plain), + "121-0-volume": ("Tone volume", T.integer, R.control, V.setting, U.percentage), + }, + "thermostat_fan_mode": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "68-0-mode": ("Fan mode", T.enum, R.control, V.user, U.plain), + "68-0-off": ("Fan off", T.bool, R.control, V.setting, U.plain), + }, + "thermostat_fan_state": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "69-0-state": ("Fan state", T.enum, R.sensor, V.user, U.plain), + }, + "thermostat_mode": { + "64-0-mode": ("Thermostat mode", T.enum, R.control, V.user, U.plain), + "64-0-manufacturerData": ("Manufacturer data", T.data, R.sensor, V.system, U.plain), + }, + "thermostat_operating_state": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "66-0-state": ("Operating state", T.enum, R.sensor, V.user, U.plain), + }, + "thermostat_setback": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + }, + "thermostat_setpoint": { + "67-0-setpoint-1": ("Heating setpoint", T.decimal, R.control, V.user, U.celsius), + "67-0-setpoint-2": ("Cooling setpoint", T.decimal, R.control, V.user, U.celsius), + }, + "user_code": { + "32-0-targetValue": ("Level", T.integer, R.control, V.system, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.system, U.plain), + "99-0-adminCode": ("Admin code", T.string, R.control, V.system, U.plain), + "99-0-userIdStatus-1": ("User 1 status", T.enum, R.control, V.setting, U.plain), + "99-0-userCode-1": ("User 1 code", T.string, R.control, V.system, U.plain), + "99-0-userIdStatus-2": ("User 2 status", T.enum, R.control, V.setting, U.plain), + "99-0-userCode-2": ("User 2 code", T.string, R.control, V.system, U.plain), + "99-0-keypadMode": ("Keypad mode", T.integer, R.sensor, V.setting, U.plain), + }, + "user_credential": { + "32-0-targetValue": ("Level", T.integer, R.control, V.user, U.percentage), + "32-0-restorePrevious": ("Restore last level", T.none, R.control, V.setting, U.plain), + }, + "window_covering": { + "106-0-targetValue-13": ("Outbound bottom", T.integer, R.control, V.user, U.percentage), + "106-0-duration-13": ("Outbound bottom: remaining duration", T.string, R.sensor, V.system, U.plain), + "106-0-targetValue-23": ("Horizontal slats angle", T.integer, R.control, V.user, U.percentage), + "106-0-duration-23": ("Horizontal slats angle: remaining duration", T.string, R.sensor, V.system, U.plain), + "106-0-levelChangeUp-13": ("Outbound bottom: open", T.bool, R.control, V.setting, U.plain), + "106-0-levelChangeDown-13": ("Outbound bottom: close", T.bool, R.control, V.setting, U.plain), + "106-0-levelChangeUp-23": ( + "Horizontal slats angle: change tilt (down inside)", + T.bool, + R.control, + V.setting, + U.plain, + ), + "106-0-levelChangeDown-23": ( + "Horizontal slats angle: change tilt (up inside)", + T.bool, + R.control, + V.setting, + U.plain, + ), + }, +} + +# The device's one-tap main parameter (None: nothing has an unambiguous one-tap action) +MAIN: dict[str, str | None] = { + "barrier_operator": "102-0-targetState", + "basic": "32-0-targetValue", + "binary_switch": "37-0-targetValue", + "color_switch": "38-0-targetValue", + "configuration": "32-0-targetValue", + "door_lock": "98-0-targetMode", + "indicator": "32-0-targetValue", + "lock": "118-0-locked", + "multilevel_switch": "38-0-targetValue", + "node_naming": "32-0-targetValue", + "powerlevel": "37-0-targetValue", + "protection": "37-0-targetValue", + "sleeping": "37-0-targetValue", + "thermostat_setback": "32-0-targetValue", + "user_credential": "32-0-targetValue", + "window_covering": "106-0-targetValue-13", +} + + +# Writable values whose new value does not come back as a report from the mock device, and why +NO_REPORT: dict[str, str] = { + "32-0-targetValue": "Basic Set gets no report back from the mock (zwave-js verifies with a Get later)", + "51-0-hexColor": "a combined view of the channels: the channels report, not this", + **dict.fromkeys( + [f"51-0-targetColor-{c}" for c in range(5)], + "zwave-js's mock cannot decode a Color Switch Set (TypeError in its parser): the mock drops it, unanswered", + ), + "98-0-insideHandlesCanOpenDoorConfiguration": "an object value: not writable from MajorDom (system)", + "98-0-outsideHandlesCanOpenDoorConfiguration": "an object value: not writable from MajorDom (system)", + "119-0-name": "system: the Hub keeps the name", + "119-0-location": "system: the Hub keeps the location", + "99-0-adminCode": "a secret: never written from MajorDom", + "99-0-userCode-1": "a secret", + "99-0-userCode-2": "a secret", + "121-0-toneId": "playing a tone: the mock reports it finished (0), not the tone", + "121-0-volume": "only used with a tone being played", + "135-0-67-2": "zwave-js's Indicator mock takes the Set but does not keep the indicator's state", + "135-0-value": "zwave-js's Indicator mock takes the Set but does not keep the indicator's state", + "117-0-exclusiveControlNodeId": "the device does not support exclusive control (as many do not)", + "98-0-operationType": "zwave-js sets the lock configuration as a whole: timed mode needs its timeout with it", + "98-0-lockTimeoutConfiguration": "zwave-js sets the lock configuration as a whole: a timeout needs timed mode", + "99-0-userIdStatus-1": "a user slot is enabled together with its code (a secret): zwave-js rejects it alone", + "99-0-userIdStatus-2": "a user slot is enabled together with its code (a secret): zwave-js rejects it alone", +} + +# Every variant zwave-js knows (recipes *_all): too many values to list, so each value of the CC must follow its rule +# (key pattern → (data type, role, visibilities, unit; None: any, the drift check covers units)), at least `minimum` +VARIANTS: dict[str, tuple[int, dict[str, tuple[T, R, set[V], U | None]]]] = { + "binary_sensor_all": (13, {r"48-0-.+": (T.bool, R.sensor, {V.user}, U.plain)}), + "multilevel_sensor_all": (88, {r"49-0-.+": (T.decimal, R.sensor, {V.user}, None)}), + "multilevel_sensor_scales": (40, {r"49-0-.+": (T.decimal, R.sensor, {V.user}, None)}), + "meter_all": ( + 36, + { + r"50-\d-value-\d+": (T.decimal, R.sensor, {V.user}, None), + r"50-\d-reset(-\d+)?": (T.none, R.control, {V.setting}, U.plain), + }, + ), + "notification_all": ( + 120, + { + r"113-0-alarm(Type|Level)": (T.integer, R.sensor, {V.setting}, U.plain), + r"113-0-event-\d+": (T.enum, R.event, {V.user}, U.plain), + r"113-0-details-\d+": (T.string, R.event, {V.system}, U.json), + # diagnostics, not states people look at + r"113-0-.+-(Maintenance|Periodic inspection|Dust in device|Test) status": ( + T.enum, + R.sensor, + {V.setting}, + U.plain, + ), + r"113-0-.+": (T.enum, R.sensor, {V.user, V.setting}, U.plain), # states; diagnostics as settings + }, + ), + "thermostat_setpoint_all": (11, {r"67-0-setpoint-\d+": (T.decimal, R.control, {V.user}, U.celsius)}), + "thermostat_mode_all": (1, {r"64-0-mode": (T.enum, R.control, {V.user}, U.plain)}), +} + +# `user` parameters whose names are alike on purpose (the SDK's audit flags near-duplicates) +SIMILAR_NAMES: list[tuple[str, str]] = [ + ("Humidifier setpoint", "De-humidifier setpoint"), + ("Heating setpoint", "Cooling setpoint"), + ("Smoke state", "CO state"), + ("Scene 1", "Scene 2"), + ("Electric energy", "Electric power"), + ("Production today", "Total production"), +] diff --git a/tests/conftest.py b/tests/conftest.py index 094b93b..f811388 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,12 @@ def fast(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ZwaveController, "IDENTIFY_PERIOD", 0.05) +@pytest.fixture +def patient(fast: None, monkeypatch: pytest.MonkeyPatch) -> None: + """Interviewing every capability of a CC takes the mock a few seconds (Notification, User Code).""" + monkeypatch.setattr(ZwaveController, "READY_TIMEOUT", 90.0) + + @pytest.fixture async def controller(deps: AbstractController.Dependencies, fast: None) -> AsyncIterator[ZwaveController]: controller = ZwaveController(deps) diff --git a/tests/mock/catalogue.mjs b/tests/mock/catalogue.mjs new file mode 100644 index 0000000..e347283 --- /dev/null +++ b/tests/mock/catalogue.mjs @@ -0,0 +1,510 @@ +// The entity catalogue of the mock network: one device kind per command class zwave-js's mock can host, each with +// every capability of that CC switched on; behaviors for common CCs zwave-js does not mock (scene buttons, +// thermostat fan and state, garage doors, and Battery and Wake Up for a sleeping device); and a builder for S2 QR +// codes. tests/catalogue.py lists the same recipes, and why every other CC is not one. +import { createHash } from "node:crypto"; + +import { + AlarmSensorCCGet, + AlarmSensorCCReport, + AlarmSensorCCSupportedGet, + AlarmSensorCCSupportedReport, + BarrierOperatorCCEventSignalingGet, + BarrierOperatorCCEventSignalingReport, + BarrierOperatorCCEventSignalingSet, + BarrierOperatorCCGet, + BarrierOperatorCCReport, + BarrierOperatorCCSet, + BarrierOperatorCCSignalingCapabilitiesGet, + BarrierOperatorCCSignalingCapabilitiesReport, + BatteryCCGet, + BatteryCCReport, + CentralSceneCCConfigurationGet, + CentralSceneCCConfigurationReport, + CentralSceneCCConfigurationSet, + CentralSceneCCSupportedGet, + CentralSceneCCSupportedReport, + EntryControlCCConfigurationGet, + EntryControlCCConfigurationReport, + EntryControlCCConfigurationSet, + EntryControlCCEventSupportedGet, + EntryControlCCEventSupportedReport, + EntryControlCCKeySupportedGet, + EntryControlCCKeySupportedReport, + HumidityControlModeCCGet, + HumidityControlModeCCReport, + HumidityControlModeCCSet, + HumidityControlModeCCSupportedGet, + HumidityControlModeCCSupportedReport, + HumidityControlOperatingStateCCGet, + HumidityControlOperatingStateCCReport, + HumidityControlSetpointCCCapabilitiesGet, + HumidityControlSetpointCCCapabilitiesReport, + HumidityControlSetpointCCGet, + HumidityControlSetpointCCReport, + HumidityControlSetpointCCScaleSupportedGet, + HumidityControlSetpointCCScaleSupportedReport, + HumidityControlSetpointCCSet, + HumidityControlSetpointCCSupportedGet, + HumidityControlSetpointCCSupportedReport, + ProtectionCCGet, + ProtectionCCReport, + ProtectionCCSet, + ProtectionCCSupportedGet, + ProtectionCCSupportedReport, + ThermostatFanModeCCGet, + ThermostatFanModeCCReport, + ThermostatFanModeCCSet, + ThermostatFanModeCCSupportedGet, + ThermostatFanModeCCSupportedReport, + ThermostatFanStateCCGet, + ThermostatFanStateCCReport, + ThermostatOperatingStateCCGet, + ThermostatOperatingStateCCReport, + ThermostatSetpointCCSet, + WakeUpCCIntervalCapabilitiesGet, + WakeUpCCIntervalCapabilitiesReport, + WakeUpCCIntervalGet, + WakeUpCCIntervalReport, + WakeUpCCIntervalSet, + WakeUpCCNoMoreInformation, +} from "@zwave-js/cc"; +import { BinarySensorType, ThermostatMode, ThermostatSetpointType } from "@zwave-js/cc"; +import { CommandClasses, getAllMeters, getAllNotifications, getAllSensors } from "@zwave-js/core"; +import { ccCaps } from "@zwave-js/testing"; + +const cc = (ccId, version, capabilities = {}) => ccCaps({ ccId, version, ...capabilities }); +const members = (enumeration) => Object.values(enumeration).filter((value) => typeof value === "number"); +const level = () => cc(CommandClasses["Multilevel Switch"], 4, { defaultValue: 0, primarySwitchType: 2 }); + +export const RECIPES = { + basic: () => [cc(CommandClasses.Basic, 2)], + binary_sensor: () => [cc(CommandClasses["Binary Sensor"], 2, { supportedSensorTypes: [1, 10, 12] })], // general, door/window, motion + binary_switch: () => [cc(CommandClasses["Binary Switch"], 2, { defaultValue: false })], + color_switch: () => [ + level(), + cc(CommandClasses["Color Switch"], 2, { colorComponents: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 } }), // white + RGB + ], + configuration: () => [ + cc(CommandClasses.Configuration, 4, { + parameters: [ + { "#": 1, valueSize: 1, name: "Brightness on power-up", minValue: 0, maxValue: 99, defaultValue: 50, format: 1 }, + { "#": 2, valueSize: 1, name: "LED mode", minValue: 0, maxValue: 2, defaultValue: 0, format: 2 }, // enumerated + { "#": 3, valueSize: 2, name: "Offset", minValue: -100, maxValue: 100, defaultValue: 0, format: 0 }, // signed + { "#": 4, valueSize: 1, name: "Firmware flags", minValue: 0, maxValue: 255, defaultValue: 7, format: 1, readonly: true }, + ], + }), + ], + door_lock: () => [ + cc(CommandClasses["Door Lock"], 4, { + supportedOperationTypes: [1, 2], // constant, timed + supportedDoorLockModes: [0, 1, 255], // unsecured, unsecured with timeout, secured + autoRelockSupported: true, + holdAndReleaseSupported: true, + blockToBlockSupported: true, + twistAssistSupported: true, + }), + ], + energy_production: () => [cc(CommandClasses["Energy Production"], 1)], + indicator: () => [cc(CommandClasses.Indicator, 3, { indicators: { 0x50: { properties: [3, 4, 5] }, 0x43: { properties: [2] } } })], + lock: () => [cc(CommandClasses.Lock, 1)], + meter: () => [ + cc(CommandClasses.Meter, 6, { meterType: 1, supportedScales: [0, 2, 4, 5], supportedRateTypes: [1], supportsReset: true }), + ], // electric: kWh, W, V, A + multilevel_sensor: () => [ + cc(CommandClasses["Multilevel Sensor"], 11, { + sensors: { 1: { supportedScales: [0] }, 3: { supportedScales: [1] }, 5: { supportedScales: [0] } }, // °C, lux, % + getValue: (type) => ({ 1: 21, 3: 350, 5: 40 })[type], + }), + ], + multilevel_switch: () => [level()], + node_naming: () => [cc(CommandClasses["Node Naming and Location"], 1, { name: "Hall", location: "Upstairs" })], + notification: () => [ + cc(CommandClasses.Notification, 8, { + // water, access (manual and keypad lock/unlock: events), security + notificationTypesAndEvents: { 0x05: [0x02], 0x06: [0x01, 0x02, 0x05, 0x06], 0x07: [0x03, 0x08] }, + }), + ], + scene_activation: () => [{ ccId: CommandClasses["Scene Activation"], version: 1, isSupported: true }], + schedule_entry_lock: () => [ + cc(CommandClasses["User Code"], 2, { numUsers: 2 }), + cc(CommandClasses["Schedule Entry Lock"], 3, { numWeekDaySlots: 1, numYearDaySlots: 1, numDailyRepeatingSlots: 1 }), + ], + sound_switch: () => [ + cc(CommandClasses["Sound Switch"], 2, { + defaultToneId: 1, + defaultVolume: 50, + tones: [ + { name: "Beep", duration: 1 }, + { name: "Chime", duration: 3 }, + ], + }), + ], + thermostat_mode: () => [cc(CommandClasses["Thermostat Mode"], 3, { supportedModes: [0, 1, 2, 3] })], // off, heat, cool, auto + thermostat_setback: () => [cc(CommandClasses["Thermostat Setback"], 1)], + thermostat_setpoint: () => [ + cc(CommandClasses["Thermostat Setpoint"], 3, { + setpoints: { + 1: { minValue: 5, maxValue: 30, defaultValue: 20, scale: "°C" }, // heating + 2: { minValue: 50, maxValue: 95, defaultValue: 77, scale: "°F" }, // cooling, in °F + }, + }), + ], + user_code: () => [cc(CommandClasses["User Code"], 2, { numUsers: 2 })], + user_credential: () => [cc(CommandClasses["User Credential"], 1, { numberOfSupportedUsers: 2 })], + window_covering: () => [ + cc(CommandClasses["Window Covering"], 1, { supportedParameters: [13, 23], travelTime: 0 }), // outbound bottom, slats angle + ], + // Every variant zwave-js knows of the CCs with many (its registries): sensor types, meters and their scales, + // notification types and their states and events, setpoint types, thermostat modes + binary_sensor_all: () => [ + cc(CommandClasses["Binary Sensor"], 2, { supportedSensorTypes: members(BinarySensorType).filter((t) => t !== 0xff) }), + ], + multilevel_sensor_all: () => [ + cc(CommandClasses["Multilevel Sensor"], 11, { + sensors: Object.fromEntries(getAllSensors().map((s) => [s.key, { supportedScales: [Number(Object.keys(s.scales)[0])] }])), + getValue: () => 1, + }), + ], + multilevel_sensor_scales: () => [ + // the other scales of each sensor (a sensor reports in one scale at a time): every unit zwave-js knows + cc(CommandClasses["Multilevel Sensor"], 11, { + sensors: Object.fromEntries( + getAllSensors() + .filter((s) => Object.keys(s.scales).length > 1) + .map((s) => [s.key, { supportedScales: [Number(Object.keys(s.scales).at(-1))] }]), + ), + getValue: () => 1, + }), + ], + meter_all: () => ({ + // one meter per endpoint; Multi Channel v4 discovers endpoints the root does not mirror (v1 only counts those) + commandClasses: [cc(CommandClasses["Multi Channel"], 4)], + endpoints: getAllMeters().map((meter) => ({ + commandClasses: [ + cc(CommandClasses.Meter, 6, { + meterType: meter.key, + supportedScales: Object.keys(meter.scales).map(Number), + supportedRateTypes: [1], + supportsReset: true, + }), + ], + })), + }), + notification_all: () => [ + cc(CommandClasses.Notification, 8, { + notificationTypesAndEvents: Object.fromEntries( + getAllNotifications().map((n) => [ + n.type, + [...n.variables.flatMap((variable) => [...variable.states.keys()]), ...n.events.keys()], + ]), + ), + }), + ], + thermostat_setpoint_all: () => [ + cc(CommandClasses["Thermostat Setpoint"], 3, { + setpoints: Object.fromEntries( + members(ThermostatSetpointType) + .filter((t) => t !== 0) + .map((t) => [t, { minValue: 5, maxValue: 35, defaultValue: 20, scale: "°C" }]), + ), + }), + ], + thermostat_mode_all: () => [cc(CommandClasses["Thermostat Mode"], 3, { supportedModes: members(ThermostatMode) })], + // Not mocked by zwave-js: see BEHAVIORS + central_scene: () => [{ ccId: CommandClasses["Central Scene"], version: 3, isSupported: true }], + thermostat_fan_mode: () => [{ ccId: CommandClasses["Thermostat Fan Mode"], version: 3, isSupported: true }], + thermostat_fan_state: () => [{ ccId: CommandClasses["Thermostat Fan State"], version: 1, isSupported: true }], + thermostat_operating_state: () => [{ ccId: CommandClasses["Thermostat Operating State"], version: 1, isSupported: true }], + barrier_operator: () => [{ ccId: CommandClasses["Barrier Operator"], version: 1, isSupported: true }], + protection: () => [ + cc(CommandClasses["Binary Switch"], 2, { defaultValue: false }), // a plug with a child lock + { ccId: CommandClasses.Protection, version: 2, isSupported: true }, + ], + humidity_control: () => [ + { ccId: CommandClasses["Humidity Control Setpoint"], version: 2, isSupported: true }, + { ccId: CommandClasses["Humidity Control Mode"], version: 2, isSupported: true }, + { ccId: CommandClasses["Humidity Control Operating State"], version: 1, isSupported: true }, + ], + entry_control: () => [{ ccId: CommandClasses["Entry Control"], version: 1, isSupported: true }], // a keypad + alarm_sensor: () => [{ ccId: CommandClasses["Alarm Sensor"], version: 2, isSupported: true }], // older smoke / CO + powerlevel: () => [ + cc(CommandClasses["Binary Switch"], 2, { defaultValue: false }), + { ccId: CommandClasses.Powerlevel, version: 1, isSupported: true }, // a radio test's results arrive as notifications + ], + // Not a CC of its own: a battery device that sleeps between wake-ups (Wake Up CC), here a sleeping switch + sleeping: () => ({ + commandClasses: [ + cc(CommandClasses["Binary Switch"], 2, { defaultValue: false }), + { ccId: CommandClasses.Battery, version: 1, isSupported: true }, + { ccId: CommandClasses["Wake Up"], version: 2, isSupported: true }, + ], + isListening: false, + isFrequentListening: false, + }), +}; + +// A device's state between commands, for the behaviors below +const state = (self, key, initial) => (self.state.has(key) ? self.state.get(key) : initial); + +/** Behaviors zwave-js's mock lacks, by device kind: each answers its CC's Get/Set like a real device would. */ +export const BEHAVIORS = { + // zwave-js 15.31's mock refuses every setpoint above the minimum (`value > min || value > max`): a correct Set, + // stored under the mock's own state keys so its Get reports it + thermostat_setpoint: () => ({ + handleCC(controller, self, receivedCC) { + if (!(receivedCC instanceof ThermostatSetpointCCSet)) return; + const { setpoints } = self.getCCCapabilities(CommandClasses["Thermostat Setpoint"], receivedCC.endpointIndex); + const limits = setpoints?.[receivedCC.setpointType]; + if (!limits || receivedCC.value < limits.minValue || receivedCC.value > limits.maxValue) return { action: "fail" }; + self.state.set(`ThermostatSetpoint_setpoint_${receivedCC.setpointType}`, receivedCC.value); + self.state.set(`ThermostatSetpoint_scale_${receivedCC.setpointType}`, receivedCC.scale); + return { action: "ok" }; + }, + }), + central_scene: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof CentralSceneCCSupportedGet) { + const keys = [0, 1, 2, 3]; // pressed, released, held down, pressed twice + const report = new CentralSceneCCSupportedReport({ + ...address, + sceneCount: 2, + supportsSlowRefresh: true, + supportedKeyAttributes: { 1: keys, 2: keys }, + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof CentralSceneCCConfigurationGet) { + const slowRefresh = state(self, "slowRefresh", false); + return { action: "sendCC", cc: new CentralSceneCCConfigurationReport({ ...address, slowRefresh }) }; + } + if (receivedCC instanceof CentralSceneCCConfigurationSet) { + self.state.set("slowRefresh", receivedCC.slowRefresh); + return { action: "ok" }; + } + }, + }), + thermostat_fan_mode: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof ThermostatFanModeCCSupportedGet) { + const report = new ThermostatFanModeCCSupportedReport({ ...address, supportedModes: [0, 1, 3, 6] }); + return { action: "sendCC", cc: report }; // auto low, low, high, circulation + } + if (receivedCC instanceof ThermostatFanModeCCGet) { + const report = new ThermostatFanModeCCReport({ ...address, mode: state(self, "fanMode", 0), off: state(self, "fanOff", false) }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof ThermostatFanModeCCSet) { + self.state.set("fanMode", receivedCC.mode); + if (receivedCC.off !== undefined) self.state.set("fanOff", receivedCC.off); + return { action: "ok" }; + } + }, + }), + thermostat_fan_state: () => ({ + handleCC(controller, self, receivedCC) { + if (receivedCC instanceof ThermostatFanStateCCGet) { + return { action: "sendCC", cc: new ThermostatFanStateCCReport({ nodeId: controller.ownNodeId, state: 1 }) }; // running + } + }, + }), + thermostat_operating_state: () => ({ + handleCC(controller, self, receivedCC) { + if (receivedCC instanceof ThermostatOperatingStateCCGet) { + return { action: "sendCC", cc: new ThermostatOperatingStateCCReport({ nodeId: controller.ownNodeId, state: 1 }) }; // heating + } + }, + }), + barrier_operator: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + const report = () => { + const open = state(self, "barrierOpen", false); + return new BarrierOperatorCCReport({ ...address, currentState: open ? 255 : 0, position: open ? 100 : 0 }); + }; + if (receivedCC instanceof BarrierOperatorCCGet) return { action: "sendCC", cc: report() }; + if (receivedCC instanceof BarrierOperatorCCSet) { + self.state.set("barrierOpen", receivedCC.targetState === 255); // a garage door that opens at once + return { action: "sendCC", cc: report() }; + } + if (receivedCC instanceof BarrierOperatorCCSignalingCapabilitiesGet) { + const capabilities = new BarrierOperatorCCSignalingCapabilitiesReport({ ...address, supportedSubsystemTypes: [1, 2] }); + return { action: "sendCC", cc: capabilities }; // audible, visual + } + if (receivedCC instanceof BarrierOperatorCCEventSignalingGet) { + const subsystemType = receivedCC.subsystemType; + const subsystemState = state(self, `signaling${subsystemType}`, 255); + return { action: "sendCC", cc: new BarrierOperatorCCEventSignalingReport({ ...address, subsystemType, subsystemState }) }; + } + if (receivedCC instanceof BarrierOperatorCCEventSignalingSet) { + self.state.set(`signaling${receivedCC.subsystemType}`, receivedCC.subsystemState); + return { action: "ok" }; + } + }, + }), + protection: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof ProtectionCCSupportedGet) { + const report = new ProtectionCCSupportedReport({ + ...address, + supportsTimeout: false, + supportsExclusiveControl: false, + supportedLocalStates: [0, 1, 2], // unprotected, by sequence, no operation possible + supportedRFStates: [0, 1], // unprotected, no control + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof ProtectionCCGet) { + const report = new ProtectionCCReport({ ...address, local: state(self, "local", 0), rf: state(self, "rf", 0) }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof ProtectionCCSet) { + self.state.set("local", receivedCC.local); + if (receivedCC.rf !== undefined) self.state.set("rf", receivedCC.rf); + return { action: "ok" }; + } + }, + }), + humidity_control: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof HumidityControlSetpointCCSupportedGet) { + const report = new HumidityControlSetpointCCSupportedReport({ ...address, supportedSetpointTypes: [1, 2] }); + return { action: "sendCC", cc: report }; // humidifier, de-humidifier + } + if (receivedCC instanceof HumidityControlSetpointCCScaleSupportedGet) { + return { action: "sendCC", cc: new HumidityControlSetpointCCScaleSupportedReport({ ...address, supportedScales: [0] }) }; // % + } + if (receivedCC instanceof HumidityControlSetpointCCCapabilitiesGet) { + const report = new HumidityControlSetpointCCCapabilitiesReport({ + ...address, + type: receivedCC.setpointType, + minValue: 10, + maxValue: 90, + minValueScale: 0, + maxValueScale: 0, + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof HumidityControlSetpointCCGet) { + const type = receivedCC.setpointType; + const value = state(self, `humidity${type}`, 50); + return { action: "sendCC", cc: new HumidityControlSetpointCCReport({ ...address, type, scale: 0, value }) }; + } + if (receivedCC instanceof HumidityControlSetpointCCSet) { + self.state.set(`humidity${receivedCC.setpointType}`, receivedCC.value); + return { action: "ok" }; + } + if (receivedCC instanceof HumidityControlModeCCSupportedGet) { + const report = new HumidityControlModeCCSupportedReport({ ...address, supportedModes: [0, 1, 2, 3] }); + return { action: "sendCC", cc: report }; // off, humidify, de-humidify, auto + } + if (receivedCC instanceof HumidityControlModeCCGet) { + return { action: "sendCC", cc: new HumidityControlModeCCReport({ ...address, mode: state(self, "humidityMode", 0) }) }; + } + if (receivedCC instanceof HumidityControlModeCCSet) { + self.state.set("humidityMode", receivedCC.mode); + return { action: "ok" }; + } + if (receivedCC instanceof HumidityControlOperatingStateCCGet) { + return { action: "sendCC", cc: new HumidityControlOperatingStateCCReport({ ...address, state: 0 }) }; // idle + } + }, + }), + entry_control: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof EntryControlCCKeySupportedGet) { + const digits = [...Array(10).keys()].map((d) => 0x30 + d); // the keypad's 0-9 + return { action: "sendCC", cc: new EntryControlCCKeySupportedReport({ ...address, supportedKeys: digits }) }; + } + if (receivedCC instanceof EntryControlCCEventSupportedGet) { + const report = new EntryControlCCEventSupportedReport({ + ...address, + supportedDataTypes: [2], // ASCII + supportedEventTypes: [2, 3, 5, 6, 25], // enter, disarm all, arm away, arm home, cancel + minKeyCacheSize: 1, + maxKeyCacheSize: 32, + minKeyCacheTimeout: 1, + maxKeyCacheTimeout: 10, + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof EntryControlCCConfigurationGet) { + const report = new EntryControlCCConfigurationReport({ + ...address, + keyCacheSize: state(self, "keyCacheSize", 4), + keyCacheTimeout: state(self, "keyCacheTimeout", 5), + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof EntryControlCCConfigurationSet) { + self.state.set("keyCacheSize", receivedCC.keyCacheSize); + self.state.set("keyCacheTimeout", receivedCC.keyCacheTimeout); + return { action: "ok" }; + } + }, + }), + alarm_sensor: () => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof AlarmSensorCCSupportedGet) { + return { action: "sendCC", cc: new AlarmSensorCCSupportedReport({ ...address, supportedSensorTypes: [1, 2] }) }; // smoke, CO + } + if (receivedCC instanceof AlarmSensorCCGet) { + const report = new AlarmSensorCCReport({ ...address, sensorType: receivedCC.sensorType, state: false, severity: 0, duration: 0 }); + return { action: "sendCC", cc: report }; + } + }, + }), + sleeping: (network, id) => ({ + handleCC(controller, self, receivedCC) { + const address = { nodeId: controller.ownNodeId }; + if (receivedCC instanceof BatteryCCGet) { + return { action: "sendCC", cc: new BatteryCCReport({ ...address, level: 87, isLow: false }) }; + } + if (receivedCC instanceof WakeUpCCIntervalCapabilitiesGet) { + const report = new WakeUpCCIntervalCapabilitiesReport({ + ...address, + minWakeUpInterval: 300, + maxWakeUpInterval: 86400, + defaultWakeUpInterval: 3600, + wakeUpIntervalSteps: 60, + wakeUpOnDemandSupported: false, + }); + return { action: "sendCC", cc: report }; + } + if (receivedCC instanceof WakeUpCCIntervalGet) { + const interval = self.state.get("wakeUpInterval") ?? 3600; + return { + action: "sendCC", + cc: new WakeUpCCIntervalReport({ ...address, wakeUpInterval: interval, controllerNodeId: 1 }), + }; + } + if (receivedCC instanceof WakeUpCCIntervalSet) { + self.state.set("wakeUpInterval", receivedCC.wakeUpInterval); + return { action: "ok" }; + } + if (receivedCC instanceof WakeUpCCNoMoreInformation) { + network.silence(id, true); // back to sleep: the radio is off until the next wake-up + return { action: "stop" }; + } + }, + }), +}; + +/** An S2 QR code for a device (version 0, or 1 for SmartStart): the format zwave-js parses (packages/core/src/qr). */ +export function qrCode({ dsk, requestedKeys, manufacturerId, productType, productId, smartStart = false }) { + const d5 = (n) => String(n).padStart(5, "0"); + const tlv = (type, data) => `${String(type << 1).padStart(2, "0")}${String(data.length).padStart(2, "0")}${data}`; + const body = + String(requestedKeys).padStart(3, "0") + + dsk.replaceAll("-", "") + + tlv(0, d5((0x10 << 8) | 0x01) + d5(0x0700)) + // product type: binary switch, power switch; installer icon + tlv(1, d5(manufacturerId) + d5(productType) + d5(productId) + d5(0x0100)); // product id, app version 1.0 + const checksum = createHash("sha1").update(body).digest().readUInt16BE(0); + return `90${smartStart ? "01" : "00"}${d5(checksum)}${body}`; +} diff --git a/tests/mock/network.mjs b/tests/mock/network.mjs index 798abfc..4b3a323 100644 --- a/tests/mock/network.mjs +++ b/tests/mock/network.mjs @@ -7,11 +7,25 @@ import path from "node:path"; import { createInterface } from "node:readline"; import { ZwavejsServer } from "@zwave-js/server"; -import { BinarySwitchCCReport, MultilevelSensorCCReport, SceneActivationCCSet } from "@zwave-js/cc"; -import { CommandClasses, SecurityClass, dskToString } from "@zwave-js/core"; import { + BatteryCCReport, + BinarySwitchCCReport, + EntryControlCCNotification, + MultilevelSensorCCReport, + MultilevelSwitchCCStartLevelChange, + MultilevelSwitchCCStopLevelChange, + NotificationCCReport, + PowerlevelCCTestNodeReport, + SceneActivationCCSet, + WakeUpCCWakeUpNotification, +} from "@zwave-js/cc"; +import { CommandClasses, SecurityClass, dskFromString, dskToString, nwiHomeIdFromDSK } from "@zwave-js/core"; +import { + AddNodeStatus, AddNodeToNetworkRequest, + AddNodeToNetworkRequestStatusReport, AddNodeType, + ApplicationUpdateRequestSmartStartHomeIDReceived, IsFailedNodeRequest, IsFailedNodeResponse, RemoveFailedNodeRequest, @@ -35,6 +49,8 @@ import { import { Bytes } from "@zwave-js/shared"; import { createAndStartDriverWithMockPort, createDefaultMockControllerBehaviors, createDefaultMockNodeBehaviors } from "zwave-js/Testing"; +import { BEHAVIORS, RECIPES, qrCode } from "./catalogue.mjs"; + // stdout carries the protocol only: the libraries' console output goes to stderr console.log = console.info = console.debug = console.warn = console.error; @@ -95,19 +111,20 @@ const KINDS = { CommandClasses["Security 2"], ccCaps({ ccId: CommandClasses["Binary Switch"], version: 2, secure: true, defaultValue: false }), ], + ...RECIPES, // the catalogue: one kind per command class (catalogue.mjs) }; function capabilities(kind, options = {}) { const make = KINDS[kind]; if (!make) throw new Error(`unknown node kind ${kind}`); const made = make(); - const { commandClasses, endpoints } = Array.isArray(made) ? { commandClasses: made } : made; + const { commandClasses, ...node } = Array.isArray(made) ? { commandClasses: made } : made; // node: endpoints, flags return { manufacturerId: 0xfff0, productType: options.productType ?? 0x0001, productId: options.productId ?? 0x0001, commandClasses: [...BASE, ...commandClasses], - ...(endpoints ? { endpoints } : {}), + ...node, ...(kind === "secure" ? { securityClasses: new Set([options.access ? SecurityClass.S2_AccessControl : SecurityClass.S2_Authenticated]) } : {}), @@ -124,6 +141,9 @@ class Network { silenced = new Set(); // node ids that stop responding (no ACK, no reply) rejecting = new Set(); // node ids that refuse commands removeCallbackId = undefined; // of the running exclusion, to complete it on request + adding = false; // an inclusion is running (see the AddNodeToNetworkRequest behaviors) + pendingDsk = undefined; // of the device waiting to join + sequence = 0; // of the keypad's notifications async start(nodes) { this.cacheDir = await mkdtemp(path.join(tmpdir(), "zwave-mock-")); @@ -134,7 +154,10 @@ class Network { S2_AccessControl: Bytes.from("33333333333333333333333333333333", "hex"), }; const { driver, continueStartup, mockPort, serial } = await createAndStartDriverWithMockPort({ - logConfig: { enabled: false }, + // ZWAVE_MOCK_LOG=: the driver's debug log, to see what zwave-js made of the mock (stdout is the protocol) + logConfig: process.env.ZWAVE_MOCK_LOG + ? { enabled: true, level: "debug", logToFile: true, filename: process.env.ZWAVE_MOCK_LOG } + : { enabled: false }, securityKeys, storage: { cacheDir: this.cacheDir, lockDir: path.join(this.cacheDir, "locks") }, testingHooks: { skipFirmwareIdentification: true }, @@ -166,8 +189,22 @@ class Network { // by it, so the mock reads some requests' fields from the payload, and sets the replies' payload itself. behaviors() { const network = this; - let adding = false; return [ + { + // SmartStart, which the mock does not know: zwave-js listens for provisioned devices while it has some, + // and includes one by its DSK when it asks to join (`power_on`) + async onHostMessage(controller, msg) { + if (!(msg instanceof AddNodeToNetworkRequest)) return; + if (msg.addNodeType === AddNodeType.SmartStartListen) return true; // nothing to answer + if (msg.addNodeType !== AddNodeType.SmartStartDSK || !controller.nodePendingInclusion) return; + controller.state.set("inclusionState", 1); // the mock's AddingNode: its closing Stop reports Done + network.adding = true; + const ready = new AddNodeToNetworkRequestStatusReport({ callbackId: msg.callbackId, status: AddNodeStatus.Ready }); + await controller.sendMessageToHost(ready); + void network.includePending(controller, msg.callbackId); + return true; + }, + }, { async onHostMessage(controller, msg) { if (msg instanceof IsFailedNodeRequest) { @@ -201,8 +238,8 @@ class Network { async onHostMessage(controller, msg) { if (!(msg instanceof AddNodeToNetworkRequest)) return; const stop = msg.addNodeType === AddNodeType.Stop; - if (stop && !adding) return true; - adding = !stop; + if (stop && !network.adding) return true; + network.adding = !stop; return false; }, }, @@ -218,9 +255,10 @@ class Network { ]; } - nodeBehaviors(id) { + nodeBehaviors(id, kind) { const network = this; return [ + ...(BEHAVIORS[kind] ? [BEHAVIORS[kind](network, id)] : []), // what zwave-js's mock lacks for this kind { handleCC(controller, self, receivedCC) { if (network.silenced.has(id)) return { action: "stop" }; // out of range: no answer @@ -231,6 +269,26 @@ class Network { ]; } + /** The inclusion the default AddNode behavior runs, for a SmartStart inclusion (the device in nodePendingInclusion). */ + async includePending(controller, callbackId) { + const { setup, ...options } = controller.nodePendingInclusion; + const node = await MockNode.create({ controller, ...options }); + node.defineBehavior(...createDefaultMockNodeBehaviors()); + setup?.(node); + const supportedCCs = [...node.implementedCCs].filter(([, info]) => info.isSupported && info.version > 0).map(([id]) => id); + const { basicDeviceClass, genericDeviceClass, specificDeviceClass } = node.capabilities; + const nodeInfo = { nodeId: node.id, basicDeviceClass, genericDeviceClass, specificDeviceClass, supportedCCs }; + const report = (status, extra = {}) => + controller.sendMessageToHost(new AddNodeToNetworkRequestStatusReport({ callbackId, status, ...extra })); + await new Promise((resolve) => setTimeout(resolve, 10)); + await report(AddNodeStatus.NodeFound); + await new Promise((resolve) => setTimeout(resolve, 10)); + await report(AddNodeStatus.AddingSlave, { nodeInfo }); + await new Promise((resolve) => setTimeout(resolve, 10)); + controller.addNode(node); + await report(AddNodeStatus.ProtocolDone); + } + silence(id, silent) { // out of range: frames to it are neither acknowledged nor answered if (silent) this.silenced.add(id); @@ -242,7 +300,7 @@ class Network { async createNode(id, kind, options) { const node = await MockNode.create({ id, controller: this.controller, capabilities: capabilities(kind, options) }); node.defineBehavior(...createDefaultMockNodeBehaviors()); - node.defineBehavior(...this.nodeBehaviors(id)); // checked before the defaults + node.defineBehavior(...this.nodeBehaviors(id, kind)); // checked before the defaults return node; } @@ -275,13 +333,16 @@ class Network { capabilities: caps, setup(node) { node.ecdhKeyPair = probe.ecdhKeyPair; - node.defineBehavior(...network.nodeBehaviors(id)); + node.defineBehavior(...network.nodeBehaviors(id, request.kind)); node.autoAckControllerFrames = !network.silenced.has(id); }, }; // as zwave-js reads it from the key the node sends: its first 16 bytes (MockNode.dsk skips one byte) const dsk = dskToString(probe.ecdhKeyPair.publicKey.slice(0, 16)); - return { pin: dsk.slice(0, 5), dsk }; + const keys = [...(caps.securityClasses ?? [])].reduce((mask, c) => mask | (1 << c), 0); // S2 classes 0..2 + const qr = qrCode({ dsk, requestedKeys: keys, ...caps, smartStart: Boolean(request.smartStart) }); + this.pendingDsk = dsk; + return { pin: dsk.slice(0, 5), dsk, qr }; } case "fail": // the controller reports the node as failed (it can be removed without the device) this.failed.add(request.node); @@ -293,6 +354,27 @@ class Network { case "reject": this.rejecting.add(request.node); return {}; + case "wake": { + // a sleeping device wakes up (its interval elapsed, or its button): it takes queued commands, then sleeps + const node = this.controller.nodes.get(request.node); + this.silence(request.node, false); + const notification = new WakeUpCCWakeUpNotification({ nodeId: OWN_NODE_ID }); + await node.sendToController(createMockZWaveRequestFrame(notification, { ackRequested: false })); + return {}; + } + case "power_on": { + // a provisioned (SmartStart) device is powered on: it asks the controller to include it + if (!this.pendingDsk) throw new Error("no device waiting to join"); + const nwiHomeId = nwiHomeIdFromDSK(dskFromString(this.pendingDsk)); + const classes = { basicDeviceClass: 4, genericDeviceClass: 0x10, specificDeviceClass: 0x01 }; + const request = new ApplicationUpdateRequestSmartStartHomeIDReceived({ remoteNodeId: 0, nwiHomeId, ...classes, supportedCCs: [] }); + // remote node, rx status, NWI home id, CC list length, device classes (the update type is prepended) + const payload = [0, 0, ...nwiHomeId, 0, classes.basicDeviceClass, classes.genericDeviceClass, classes.specificDeviceClass]; + await this.controller.sendMessageToHost(withPayload(request, payload)); + return {}; + } + case "awake": + return { awake: !this.silenced.has(request.node) }; case "revive": { // the node answers again and announces itself with a report this.silence(request.node, false); @@ -346,7 +428,26 @@ class Network { sensorValue = value; cc = new MultilevelSensorCCReport({ ...address, type: 1, scale: 0, value }); } else if (kind === "scene") cc = new SceneActivationCCSet({ ...address, sceneId: value }); - else throw new Error(`unknown report ${kind}`); + // what devices send as notifications (zwave-js "notification" events), `value` shaped per kind + else if (kind === "keypad") { + const { eventType, code } = value; // e.g. 2 (enter) and "1234" + const data = code === undefined ? { dataType: 0 } : { dataType: 2, eventData: code }; // none, or ASCII + cc = new EntryControlCCNotification({ ...address, sequenceNumber: ++this.sequence, eventType, ...data }); + } else if (kind === "notification") { + const { type, event, parameters } = value; // e.g. 6 (access control), 5 (keypad lock), [3] (user 3) + const extra = parameters ? { eventParameters: Bytes.from(parameters) } : {}; + const report = { notificationType: type, notificationEvent: event, notificationStatus: 0xff, ...extra }; + cc = new NotificationCCReport({ ...address, ...report }); + } else if (kind === "level_change") { + cc = + value === "stop" + ? new MultilevelSwitchCCStopLevelChange(address) + : new MultilevelSwitchCCStartLevelChange({ ...address, direction: value, ignoreStartLevel: true }); + } else if (kind === "battery_low") { + cc = new BatteryCCReport({ ...address, level: "low" }); + } else if (kind === "powerlevel") { + cc = new PowerlevelCCTestNodeReport({ ...address, testNodeId: 1, status: value, acknowledgedFrames: 10 }); + } else throw new Error(`unknown report ${kind}`); await node.sendToController(createMockZWaveRequestFrame(cc, { ackRequested: false })); } diff --git a/tests/mock/registry.mjs b/tests/mock/registry.mjs new file mode 100644 index 0000000..d066087 --- /dev/null +++ b/tests/mock/registry.mjs @@ -0,0 +1,29 @@ +// What zwave-js knows, for the drift checks (tests/test_catalogue.py): its command classes, the value metadata types, +// the units of its sensor, meter and named scales, and the harness's catalogue recipes. Prints one JSON object. +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +import * as core from "@zwave-js/core"; + +import { RECIPES } from "./catalogue.mjs"; + +const commandClasses = Object.fromEntries( + Object.entries(core.CommandClasses).filter(([, id]) => typeof id === "number").map(([name, id]) => [id, name]), +); + +// ValueType is a TypeScript type only: read it from the declarations +const require = createRequire(import.meta.url); +const coreDir = path.dirname(require.resolve("@zwave-js/core/package.json")); +const declarations = readFileSync(path.join(coreDir, "build/esm/values/Metadata.d.ts"), "utf8"); +const union = declarations.match(/export type ValueType = ([^;]+);/)?.[1] ?? ""; +const valueTypes = [...union.matchAll(/"([^"]+)"/g)].map((m) => m[1]); + +const units = new Set(); +const add = (scale) => scale?.unit && units.add(scale.unit); +for (const sensor of core.getAllSensors()) Object.values(sensor.scales ?? {}).forEach(add); +for (const group of Object.values(core.getAllNamedScaleGroups())) Object.values(group).forEach(add); +for (const meter of core.getAllMeters()) Object.values(meter.scales ?? {}).forEach(add); + +const recipes = Object.keys(RECIPES); +process.stdout.write(JSON.stringify({ commandClasses, valueTypes, units: [...units].sort(), recipes })); diff --git a/tests/network.py b/tests/network.py index d786b7b..d3520fc 100644 --- a/tests/network.py +++ b/tests/network.py @@ -84,6 +84,17 @@ async def report(self, node: int, kind: str, value: Any) -> None: async def silence(self, node: int) -> None: await self.call("silence", node=node) + async def wake(self, node: int) -> None: + """A sleeping device wakes up: it takes its queued commands, then sleeps again.""" + await self.call("wake", node=node) + + async def power_on(self, node: int) -> None: + """A provisioned (SmartStart) device is powered on: it asks to join, and is included by its DSK.""" + await self.call("power_on", node=node) + + async def awake(self, node: int) -> bool: + return (await self.call("awake", node=node))["awake"] + async def reject(self, node: int) -> None: """The node refuses the commands it gets (a supervised command is answered with a Fail status).""" await self.call("reject", node=node) diff --git a/tests/test_catalogue.py b/tests/test_catalogue.py new file mode 100644 index 0000000..e81bc8e --- /dev/null +++ b/tests/test_catalogue.py @@ -0,0 +1,273 @@ +"""The catalogue: every command class zwave-js knows is hosted, plumbing, or explained (fast checks); one mock device +per hosted CC maps exactly as expected and round-trips every writable value (the sweep). The monthly canary runs it +against the latest zwave-js, zwave-js-server and zwave-js-server-python: new CCs, values, value types and units fail +here until they are supported or explained. +""" + +import json +import logging +import re +import subprocess +from collections import Counter +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from majordom_integration_sdk.parameter_audit import audit_device_parameters +from majordom_integration_sdk.schemas.parameter import ParameterDataType, ParameterRole, ParameterVisibility +from majordom_integration_sdk.testing import RecordingControllerOutput +from zwave_js_server.const import CommandClass +from zwave_js_server.model.value import Value, ValueMetadata + +from majordom_zwave import ZwaveController, mapper +from majordom_zwave.zwave_spec import CONVERTED_UNITS, PLAIN_UNITS, UNIT_MAP +from tests.catalogue import ( + COMMON, + EXPECTED, + INFRASTRUCTURE, + MAIN, + NEWER_THAN_THE_LIBRARY, + NO_REPORT, + PLUMBING, + RECIPES, + SIMILAR_NAMES, + UNHOSTABLE, + VARIANTS, +) +from tests.helpers import command, pair, values, wait_for_value +from tests.network import MOCK_DIR, MockNetwork + + +@pytest.fixture(scope="module") +def registry() -> dict[str, Any]: + """What the installed zwave-js knows (tests/mock/registry.mjs).""" + result = subprocess.run(["node", str(MOCK_DIR / "registry.mjs")], capture_output=True, text=True, check=True) + return json.loads(result.stdout) + + +pytestmark = pytest.mark.usefixtures("patient") + + +def key(value_id: str) -> str: + return value_id.split("-", 1)[1] + + +# Fast checks: what zwave-js knows is supported or explained --------------------------------------------------------- + + +def test_every_command_class_is_hosted_plumbing_or_explained(registry: dict[str, Any]): + known = {int(cc): name for cc, name in registry["commandClasses"].items()} + hosted = set().union(*RECIPES.values()) | INFRASTRUCTURE + unexplained = { + f"{name} (0x{cc:02x})" for cc, name in known.items() if cc not in hosted | PLUMBING | UNHOSTABLE.keys() + } + assert not unexplained, ( + f"new command classes {sorted(unexplained)}: write a recipe (tests/mock/catalogue.mjs, tests/catalogue.py), " + "or say in tests/catalogue.py why the mock network cannot host it" + ) + assert not hosted & UNHOSTABLE.keys(), "a hosted CC is listed as unhostable" + + +def test_command_classes_newer_than_the_library_are_known(registry: dict[str, Any]): + library = {int(cc) for cc in CommandClass} + newer = {int(cc) for cc in registry["commandClasses"]} - library + assert newer <= NEWER_THAN_THE_LIBRARY.keys(), ( + f"zwave-js knows CCs zwave-js-server-python does not: {sorted(hex(cc) for cc in newer)}" + ) + + +def test_every_value_type_is_mapped(registry: dict[str, Any]): + assert set(registry["valueTypes"]) == mapper.KNOWN_VALUE_TYPES, ( + "zwave-js's value metadata types changed: map the new ones in mapper.data_type" + ) + + +def test_every_unit_is_mapped_converted_or_deliberately_plain(registry: dict[str, Any]): + handled = UNIT_MAP.keys() | CONVERTED_UNITS.keys() | PLAIN_UNITS + assert set(registry["units"]) <= handled, ( + f"new units {sorted(set(registry['units']) - handled)}: map, convert, or list them in zwave_spec.PLAIN_UNITS" + ) + + +def test_the_catalogue_tables_agree(registry: dict[str, Any]): + assert set(registry["recipes"]) >= RECIPES.keys() | VARIANTS.keys(), "a recipe is missing in catalogue.mjs" + assert EXPECTED.keys() == RECIPES.keys() + assert MAIN.keys() <= RECIPES.keys() + + +# Value shapes, for the CCs the mock network cannot host -------------------------------------------------------------- + + +def shape(metadata: dict[str, Any], value: Any = None, command_class: int = 0x99) -> Value: + """A value of a CC with no mock, with the given metadata: only what the mapper reads.""" + stand_in = SimpleNamespace( + value_id=f"5-{command_class}-0-value", + command_class=command_class, + endpoint=0, + property_="value", + property_key=None, + property_name="value", + value=value, + metadata=ValueMetadata(cast(Any, {"readable": True, "writeable": False, **metadata})), + ) + return cast(Value, stand_in) + + +@pytest.mark.parametrize( + ("metadata", "value", "data_type"), + [ + ({"type": "number"}, 4, ParameterDataType.integer), + ({"type": "number", "states": {"0": "Off", "1": "On"}}, 0, ParameterDataType.enum), + ({"type": "number", "states": {"0": "Off"}, "allowManualEntry": True}, 0, ParameterDataType.integer), + ({"type": "boolean"}, True, ParameterDataType.bool), + ( + {"type": "boolean", "readable": False, "writeable": True, "states": {"true": "Go"}}, + None, + ParameterDataType.none, + ), + ({"type": "string"}, "text", ParameterDataType.string), + ({"type": "color"}, "ff0000", ParameterDataType.string), + ({"type": "duration"}, {"value": 2, "unit": "seconds"}, ParameterDataType.string), + ({"type": "timeout"}, {"value": 2, "unit": "seconds"}, ParameterDataType.string), + ({"type": "number[]"}, [1, 2], ParameterDataType.data), + ({"type": "boolean[]"}, [True], ParameterDataType.data), + ({"type": "string[]"}, ["a"], ParameterDataType.string), # shown as text: "1.0, 2.3" + ({"type": "buffer"}, "AQI=", ParameterDataType.data), + ({"type": "any"}, 1.5, ParameterDataType.decimal), + ({"type": "any"}, {"a": 1}, ParameterDataType.data), + ], +) +def test_every_value_shape_maps(metadata: dict[str, Any], value: Any, data_type: ParameterDataType): + mapped = shape(metadata, value) + assert mapper.data_type(mapped) == data_type + if data_type == ParameterDataType.data: + assert mapper.visibility(mapped, cast(Any, SimpleNamespace(values={}))) == ParameterVisibility.system + + +def test_a_command_class_nobody_reviewed_is_hidden_but_kept(): + """ "When in doubt, hide it": a value of a CC not in the catalogue is `system` (automations and the API still see + it); the same value of a reviewed CC is a `setting`.""" + node = cast(Any, SimpleNamespace(values={})) + reading = {"type": "number", "label": "Reading"} + irrigation, configuration = 0x6B, 0x70 + assert mapper.visibility(shape(reading, 4, command_class=irrigation), node) == ParameterVisibility.system + assert mapper.visibility(shape(reading, 4, command_class=configuration), node) == ParameterVisibility.setting + + +def test_an_unknown_value_type_is_hidden_and_logged(caplog: pytest.LogCaptureFixture): + mapped = shape({"type": "quaternion"}, [1, 0, 0, 0]) + with caplog.at_level(logging.WARNING): + assert mapper.data_type(mapped) == ParameterDataType.data + assert "quaternion" in caplog.text + + +def test_an_unknown_unit_is_plain_and_logged(caplog: pytest.LogCaptureFixture): + with caplog.at_level(logging.WARNING): + assert mapper.unit(shape({"type": "number", "unit": "furlongs"}, 1)) == "plain" + assert "furlongs" in caplog.text + + +# The sweep: one mock device per hosted CC ---------------------------------------------------------------------------- + + +@pytest.mark.parametrize("recipe", sorted(RECIPES)) +async def test_every_hosted_command_class_maps_as_expected( + recipe: str, controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, recipe) + + mapped = { + key(p.integration_data.value_id): (p.name, p.data_type, p.role, p.visibility, p.unit) for p in device.parameters + } + expected = {**COMMON, **EXPECTED[recipe]} + differences = { + k: (mapped.get(k), expected.get(k)) for k in mapped.keys() | expected.keys() if mapped.get(k) != expected.get(k) + } + assert not differences, "value: (mapped, expected)\n" + "\n".join(f" {k}: {v}" for k, v in differences.items()) + + main = next((key(p.integration_data.value_id) for p in device.parameters if p.id == device.main_parameter), None) + assert main == MAIN.get(recipe) + # the SDK's advisory audit: no over-exposed device screen, no near-duplicate user names + assert not audit_device_parameters(recipe, device.parameters, ignore_similar_pairs=SIMILAR_NAMES) + assert_readable(device) + + +@pytest.mark.parametrize("recipe", sorted(VARIANTS)) +async def test_every_variant_maps_by_its_rules( + recipe: str, controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, recipe) + minimum, rules = VARIANTS[recipe] + + checked = 0 + for parameter in device.parameters: + k = key(parameter.integration_data.value_id) + rule = next((r for pattern, r in rules.items() if re.fullmatch(pattern, k)), None) + if rule is None: + continue + data_type, role, visibilities, unit = rule + assert (parameter.data_type, parameter.role) == (data_type, role), f"{k}: {parameter}" + assert parameter.visibility in visibilities, f"{k}: {parameter.visibility}" + assert unit is None or parameter.unit == unit, f"{k}: {parameter.unit}" + checked += 1 + assert checked >= minimum + assert_readable(device) + + +def assert_readable(device: Any) -> None: + """Names and enum labels are for people: unique, without zwave-js's protocol wording, words not camelCase.""" + names = Counter(p.name for p in device.parameters) + assert not [n for n, count in names.items() if count > 1], f"parameters share a name: {names}" + for parameter in device.parameters: + assert parameter.name.strip(), parameter + assert not re.search(r"\[|Consumption|Sensor state|Setpoint \(|\(Notification\)", parameter.name), ( + parameter.name + ) + assert not re.fullmatch(r"\S*[a-z][A-Z]\S*", parameter.name), parameter.name + for label in (parameter.valid_values or {}).values(): + assert not re.fullmatch(r"\S*([a-z][A-Z]|[a-z][0-9])\S*", label), label + assert not label[:1].islower(), label + + +def new_value(parameter: Any, current: Any) -> Any: + """A valid value other than the current one.""" + if parameter.data_type == ParameterDataType.bool: + return not current + if parameter.data_type == ParameterDataType.enum: + return next(v for v in sorted(parameter.valid_values, reverse=True) if v != current) + middle = (parameter.min_value + parameter.max_value) / 2 # inside the range: some devices refuse its ends + if parameter.data_type == ParameterDataType.integer: + middle = round(middle) + return middle if middle != current else middle + 1 + + +@pytest.mark.parametrize("recipe", sorted(RECIPES)) +async def test_every_writable_value_round_trips_through_the_device( + recipe: str, controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, recipe) + + for parameter in device.parameters: + k = key(parameter.integration_data.value_id) + if parameter.role != ParameterRole.control or parameter.visibility == ParameterVisibility.system: + continue + if parameter.data_type == ParameterDataType.none: # a button: it is sent, nothing comes back + await command(controller, device, parameter, None) + continue + if k in NO_REPORT or parameter.data_type not in ( + ParameterDataType.bool, + ParameterDataType.integer, + ParameterDataType.decimal, + ParameterDataType.enum, + ): + continue + reported = values(output, parameter.id) + if not reported: # write-only (start/stop a movement): no state to come back + continue + target = new_value(parameter, reported[-1]) + await command(controller, device, parameter, target) + # the device's report, or zwave-js's own check of the value ~5 s after an unsupervised set + await wait_for_value(output, parameter, lambda v, t=target: v == t, timeout=10) diff --git a/tests/test_notifications.py b/tests/test_notifications.py new file mode 100644 index 0000000..338673e --- /dev/null +++ b/tests/test_notifications.py @@ -0,0 +1,133 @@ +"""Notifications: what devices send as one-off events rather than values (zwave-js "notification" events) — a keypad's +entries, Notification CC events, a remote's dimming, a low battery, a radio test's result.""" + +import json + +import pytest +from majordom_integration_sdk.schemas.parameter import ParameterRole +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import pair, param, values, wait_for_value +from tests.network import MockNetwork +from tests.test_sleeping import wait_for_asleep + +pytestmark = pytest.mark.usefixtures("patient") + + +async def test_a_keypad_entry_reports_its_event_and_the_code( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "entry_control") + event, code = param(device, "111-0-event"), param(device, "111-0-code") + assert event.role == code.role == ParameterRole.event + assert event.valid_values == {2: "Enter", 3: "Disarm all", 5: "Away", 6: "Home", 25: "Cancel"} # what it supports + + await network.report(5, "keypad", {"eventType": 2, "code": "1234"}) # 1234, then Enter + + assert await wait_for_value(output, event, lambda v: v == 2) == 2 + assert await wait_for_value(output, code, lambda v: v == "1234") == "1234" + + +async def test_a_keypad_event_without_a_code_reports_no_code( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "entry_control") + event, code = param(device, "111-0-event"), param(device, "111-0-code") + + await network.report(5, "keypad", {"eventType": 5}) # the Away button + + assert await wait_for_value(output, event, lambda v: v == 5) == 5 + assert values(output, code.id) == [] + + +async def test_a_notification_event_reports_the_event_and_its_details( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 6, "notification") + event, details = param(device, "113-0-event-6"), param(device, "113-0-details-6") # Access Control + assert event.name == "Access control event" + assert event.valid_values == { + 1: "Manual lock operation", + 2: "Manual unlock operation", + 5: "Keypad lock operation", + 6: "Keypad unlock operation", + } + + await network.report(6, "notification", {"type": 6, "event": 5, "parameters": [3]}) # locked with user 3's code + + assert await wait_for_value(output, event, lambda v: v == 5) == 5 + reported = await wait_for_value(output, details, lambda v: v is not None) + assert json.loads(reported) == {"userId": 3} + + +async def test_a_notification_event_without_details_reports_none( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 6, "notification") + event, details = param(device, "113-0-event-6"), param(device, "113-0-details-6") + + await network.report(6, "notification", {"type": 6, "event": 1}) # locked by hand + + assert await wait_for_value(output, event, lambda v: v == 1) == 1 + assert values(output, details.id) == [] + + +async def test_an_event_the_device_did_not_announce_is_not_reported( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 6, "notification") + event = param(device, "113-0-event-6") + + await network.report(6, "notification", {"type": 6, "event": 3}) # auto lock: not in its supported events + await network.report(6, "notification", {"type": 6, "event": 2}) + + await wait_for_value(output, event, lambda v: v == 2) + assert values(output, event.id) == [2] # a value outside the enum would not be a valid state + + +async def test_dimming_from_the_device_reports_start_and_stop( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 7, "multilevel_switch") + change = param(device, "38-0-levelChange") + assert change.valid_values == {0: "Stopped", 1: "Started up", 2: "Started down"} + + await network.report(7, "level_change", "up") + await wait_for_value(output, change, lambda v: v == 1) + await network.report(7, "level_change", "stop") + await wait_for_value(output, change, lambda v: v == 0) + await network.report(7, "level_change", "down") + await wait_for_value(output, change, lambda v: v == 2) + + +async def test_a_low_battery_reports_that_it_needs_replacing( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 8, "sleeping") + replacement = param(device, "128-0-replacement") + await wait_for_asleep(network, 8) + + await network.wake(8) + await network.report(8, "battery_low", None) + + assert await wait_for_value(output, replacement, lambda v: v == 1) == 1 # soon + + +async def test_a_radio_test_result_is_reported( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 9, "powerlevel") + result = param(device, "115-0-test") + + await network.report(9, "powerlevel", 1) # success + + assert await wait_for_value(output, result, lambda v: v == 1) == 1 diff --git a/tests/test_pairing.py b/tests/test_pairing.py index 4b547de..e88bb1a 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -128,6 +128,16 @@ async def test_the_full_dsk_works_as_the_pin( param(device, BINARY_SWITCH) +async def test_the_s2_qr_code_includes_securely( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + qr = lambda label: ProvidedCredentials(type=CredentialsType.qr, value=label["qr"]) # noqa: E731 + device = await pair(controller, output, network, 5, "secure", credentials=qr) + + param(device, BINARY_SWITCH) # only supported securely: present only if S2 bootstrapping succeeded + + async def test_without_the_pin_a_secure_device_joins_with_lower_security_and_says_so( controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork ): @@ -159,7 +169,11 @@ async def test_closing_the_window_keeps_live_unclaimed_devices_and_removes_faile await asyncio.sleep(1.5) assert alive.id in controller.discoveries - failed = await discover(controller, output, network, 6, "switch", duration=1) + failed = await discover(controller, output, network, 6, "switch", duration=3) + for _ in range(100): # joined and interviewed (its state read) — then it fails, before anyone pairs it + if "BinarySwitchCCGet" in await network.frames(6): + break + await asyncio.sleep(0.05) await network.fail(6) await wait_until(lambda: failed.id in output.lost_discoveries, 10, "the failed node's discovery to be dropped") assert 6 not in await network.node_ids() diff --git a/tests/test_sleeping.py b/tests/test_sleeping.py new file mode 100644 index 0000000..59bb0bd --- /dev/null +++ b/tests/test_sleeping.py @@ -0,0 +1,56 @@ +"""A sleeping (battery) device: awake only at its wake-ups, so commands wait for the next one.""" + +import asyncio + +from majordom_integration_sdk.testing import RecordingControllerOutput + +from majordom_zwave import ZwaveController +from tests.helpers import command, pair, param, stored, values, wait_for_value +from tests.network import MockNetwork + +SWITCH = "37-0-targetValue" + + +async def test_a_sleeping_device_pairs_and_reports_its_battery( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sleeping") # awake while it is included + + assert values(output, param(device, "128-0-level").id) == [87] + + +async def test_a_command_to_a_sleeping_device_waits_for_its_wake_up( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sleeping") + switch = param(device, SWITCH) + await wait_for_asleep(network) + + await command(controller, device, switch, True) # queued by zwave-js: no error, and not applied yet + + assert True not in values(output, switch.id) + assert (await stored(controller, device.id)).last_error is None + await network.wake(5) + assert await wait_for_value(output, switch, lambda v: v is True) is True + + +async def test_a_sleeping_device_is_not_lost( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + device = await pair(controller, output, network, 5, "sleeping") + await wait_for_asleep(network) + + assert device.id not in output.lost_devices + assert (await stored(controller, device.id)).available is True + + +async def wait_for_asleep(network: MockNetwork, node: int = 5) -> None: + """zwave-js sends the device back to sleep once its interview is done.""" + for _ in range(100): + if not await network.awake(node): + return + await asyncio.sleep(0.05) + raise TimeoutError("the device did not go back to sleep") diff --git a/tests/test_smart_start.py b/tests/test_smart_start.py new file mode 100644 index 0000000..9d9b4d7 --- /dev/null +++ b/tests/test_smart_start.py @@ -0,0 +1,92 @@ +"""SmartStart: a device provisioned from its QR code joins by itself when it is powered on, securely, without a PIN.""" + +import asyncio + +import aiohttp +from majordom_integration_sdk.schemas.device import CredentialsType, ProvidedCredentials +from majordom_integration_sdk.testing import RecordingControllerOutput +from zwave_js_server.client import Client + +from majordom_zwave import ZwaveController +from tests.helpers import hub_creates_device, param, stored, wait_until +from tests.network import MockNetwork + +SWITCH = "37-0-targetValue" + + +async def provisioned(network: MockNetwork) -> list[str]: + """The DSKs zwave-js keeps in its SmartStart provisioning list (read as another client of the server would).""" + async with aiohttp.ClientSession() as session: + client = Client(network.url, session) + await client.connect() + ready = asyncio.Event() + listening = asyncio.create_task(client.listen(ready)) + await ready.wait() + assert client.driver is not None + entries = await client.driver.controller.async_get_provisioning_entries() + await client.disconnect() + listening.cancel() + return [entry.dsk for entry in entries] + + +async def provision(controller: ZwaveController, network: MockNetwork, duration: int = 60) -> dict[str, str]: + label = await network.join(5, "secure", smartStart=True) + await controller.start_pairing_window(duration, ProvidedCredentials(type=CredentialsType.qr, value=label["qr"])) + return label + + +async def test_a_provisioned_device_joins_when_powered_on_and_pairs_securely( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + label = await provision(controller, network) + assert label["dsk"] in await provisioned(network) + assert not output.received_discoveries # nothing joins before the device is powered on + + await network.power_on(5) + await wait_until(lambda: output.received_discoveries, 20, "the device to join") + discovery = output.received_discoveries[-1] + assert discovery.last_error is None # S2 from its QR code: no PIN needed + await hub_creates_device(controller.dependencies, discovery) + device = await stored(controller, await controller.pair_device(discovery, None)) + + param(device, SWITCH) # only supported securely: present only if S2 bootstrapping succeeded + + +async def test_provisioning_outlives_the_pairing_window( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + await provision(controller, network, duration=1) + await asyncio.sleep(1.5) # the window closed long before the device is installed and powered + + await network.power_on(5) + await wait_until(lambda: output.received_discoveries, 20, "the device to join") + + +async def test_unpairing_a_smart_start_device_removes_its_provisioning( + controller: ZwaveController, output: RecordingControllerOutput, network: MockNetwork +): + await controller.start() + label = await provision(controller, network) + await network.power_on(5) + await wait_until(lambda: output.received_discoveries, 20, "the device to join") + discovery = output.received_discoveries[-1] + await hub_creates_device(controller.dependencies, discovery) + device = await stored(controller, await controller.pair_device(discovery, None)) + + unpairing = asyncio.create_task(controller.unpair(device)) + await asyncio.sleep(0.3) + await network.exclude(5) + await asyncio.wait_for(unpairing, 10) + + assert label["dsk"] not in await provisioned(network) # or zwave-js would include it again at its next power-up + + +async def test_an_s2_qr_code_is_not_provisioned(controller: ZwaveController, network: MockNetwork): + await controller.start() + label = await network.join(5, "secure") # an S2 code: included now, in the window + + await controller.start_pairing_window(60, ProvidedCredentials(type=CredentialsType.qr, value=label["qr"])) + + assert label["dsk"] not in await provisioned(network) From 896b7a4c7a00f086b8955cfba6a49e33f0bdcc10 Mon Sep 17 00:00:00 2001 From: Mark Parker <34688010+MarkParker5@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:12:14 +0200 Subject: [PATCH 28/28] Support SmartStart and notifications, review parameter UX - Provision SmartStart devices, unprovision them when unpaired - Report keypad entries and codes, Notification CC events, remote dimming, low battery and radio tests as event parameters - Curate the user bucket, hide unreviewed CCs, name values for people - Write-only actions are buttons; settings of everyday CCs are settings - Every zwave-js unit is mapped, converted or deliberately plain - Read values back after an unconfirmed set --- README.md | 61 +++++-- majordom_zwave/controller.py | 91 +++++++++- majordom_zwave/mapper.py | 331 +++++++++++++++++++++++++++++++++-- majordom_zwave/zwave_spec.py | 267 +++++++++++++++++++++++++++- 4 files changed, 707 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index cc80722..67b5ac2 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,21 @@ It needs Node.js 20+: npm ci --prefix tests/mock && poetry run pytest ``` +### Command-class catalogue and canary + +`tests/test_catalogue.py` accounts for every command class zwave-js knows (125 today): each is hosted by a mock +device (a recipe in `tests/mock/catalogue.mjs`: 34 of them, all their capabilities switched on), is protocol plumbing, +or is listed in `tests/catalogue.py` with the reason the mock cannot host it. Each hosted one must map exactly as +`EXPECTED` says — name, type, role, visibility, unit, main parameter, reviewed value by value against the docs' +[parameter UX](https://docs.majordom.io/device-integration/parameter-ux) — pass the SDK's `parameter_audit`, and +round-trip every writable value through the device. Seven more mock devices carry *every* variant zwave-js knows +(88 sensor types and their scales, 14 binary sensors, 5 meters with all their scales, 22 notification types with all +their states and events, 12 setpoints, 16 thermostat modes), checked against rules. Fast checks fail on a new command +class, value metadata type or unit. + +The monthly **canary** (`.github/workflows/canary.yml`) runs all of it against the latest zwave-js, zwave-js-server +and zwave-js-server-python: red means upstream shipped something to support, or to explain. + ## Run it standalone (without the Hub) `majordom-zwave` is a standalone library — import it into your own app, or run **just this @@ -89,7 +104,10 @@ just talks JSON over a WebSocket to it. at `pair_device` time — Z-Wave needs them during inclusion, before the device exists as a discovery; the discovery itself always expects `CredentialsType.none`. - `code` — the S2 PIN (the first 5 digits of the DSK on the device's label), or the whole DSK. - - `qr` — the device's S2 QR code (`90…`); malformed codes are rejected. + - `qr` — the device's QR code (`90…`); malformed codes are rejected. An S2 code includes the device in the + window; a **SmartStart** code provisions it instead: zwave-js includes it securely, with no PIN, whenever it is + powered on (hours later, after installation), and it then arrives as a discovery. Unpairing removes it from + the provisioning list, or it would join again. - none — the device joins, but without the S2 Authenticated / Access Control classes (they need the PIN); its discovery's `last_error` says so. - `secret` — not supported. @@ -129,14 +147,17 @@ the mock network only; to be re-run on hardware before release): |--------|----------| | Device | `device_uuid("-")` (SDK helper): node ids repeat across networks and are reused after a device leaves | | Value | One parameter, `parameter_uuid(device, value id)`; a current/target pair (`currentValue`/`targetValue`, `currentMode`/`targetMode`, …) is **one** parameter: commanded through the target, state from the current one | -| Multilevel Switch / Basic level (0–99) | Percentage 0–100 (99 is 100) | -| Multilevel Sensor, Meter, setpoints | `decimal` (scaled decimals by spec), whatever the first reading | -| `states` | `enum` with labels in `valid_values` (`integer` when the device allows other values too) | -| Units | SDK units by the zwave-js scale string; °F → °C and kPa → Pa converted, unknown units `plain` | -| Central Scene, Scene Activation, `stateful: false` | `event` role (sent by zwave-js as "value notification") | -| Basic CC (when other CCs exist), durations, secrets, opaque data, protocol CCs | `system` visibility | -| Battery, Version, Configuration, Wake Up, Protection, … | `setting` visibility | -| Main parameter | Binary Switch (toggle) › Multilevel Switch (off / 100 %) › Barrier (closed / open) › Door Lock (unsecured / secured) | +| Multilevel Switch / Basic level, Window Covering position (0–99) | Percentage 0–100 (99 is 100) | +| Multilevel Sensor, Meter, setpoints, Energy Production | `decimal` (scaled decimals by spec), whatever the first reading | +| `states` | `enum` with labels in `valid_values` (`integer` when the device allows other values too, or when they only label special values: sound volume, wake-up interval) | +| Write-only action (`"Reset"`, `"Identify"`, `"Restore previous value"`) | `none`: a button | +| Units | Every zwave-js scale unit is mapped, converted (°F → °C, imperial → metric, Wh → kWh, mV → V, …) or deliberately `plain`; a few CCs give units only in labels (Door Lock times, wake-up interval) | +| Central Scene, Scene Activation, `stateful: false` | `event` role (sent by zwave-js as "value notification"); their settings stay controls | +| Notifications (zwave-js "notification" events) | Read-only `event` parameters: **Keypad** (the event type: Enter, Away, …) and **Keypad code** (what was typed, `system`); one **`` event** per Notification CC type with events (Access control, Home security, …) and its **details** (user id…, JSON, `system`); a remote's **Level change**; **Battery replacement**; **Powerlevel test**. Enum labels are what the device announced; events it did not are dropped | +| Visibility | "When in doubt, hide it": a curated list of everyday controls and live readings is `user` (switches, levels, colour, sensors, meters, locks, thermostats, covers, garage doors, battery, …); every other value of a reviewed CC is a `setting`; wiring, metadata, secrets, durations and opaque data are `system`, and so is every value of a CC nobody reviewed (still usable by automations and the API) | +| Names | Written for people, not zwave-js's labels: `Power`, `Level`, `Color`, `Lock`, `Electric energy` (not `Electric Consumption [kWh]`), `Motion` (not `Sensor state (Motion)`), `Heating setpoint`, `Scene 1`; a name several values share gets what tells them apart (`Sensor status (Water alarm)`); camelCase enum labels become words (`Unsecured with timeout`) | +| Unsupervised set (delivered, not confirmed) | Read back from the device ~6 s later unless zwave-js already did: the Hub sees what the device did | +| Main parameter | Binary Switch (toggle) › Multilevel Switch (off / 100 %) › Barrier (closed / open) › Door Lock (unsecured / secured) › Lock (toggle) › Window Covering (closed / open) › Basic, when it is the only control (off / 100 %) | ### Progress @@ -145,12 +166,13 @@ the mock network only; to be re-run on hardware before release): - [x] Discovery of joining devices (`"node added"`), and of nodes already on the network but not paired - [x] Discovery of already-paired devices on (re)connect — matched by home id and node id, and checked against the device's fingerprint (a node id reused by another device is not taken for the paired one) -- [x] `start_pairing_window` (default, S2 PIN or DSK, S2 QR) +- [x] `start_pairing_window` (default, S2 PIN or DSK, S2 QR, SmartStart) - [x] Device pairing (completes the device the Hub created; fails cleanly and stays discovered when the device does not finish its interview) - [x] Device schema mapped (see **Mapping**) - [x] Hub → Device control (`send_command`); the state comes back as the device's report, never echoed -- [x] Device → Hub events (`"value updated"`, `"value notification"`) +- [x] Device → Hub events (`"value updated"`, `"value notification"`, `"notification"`: keypads, Notification CC + events, remote dimming, low battery, radio tests) - [x] `identify` — Indicator CC v3 identify; a light blinks; any other device is left alone (a relay or a motor may drive a heater, a pump or a blind) - [x] `unpair` — a failed node is removed without the device; a live one when *this* device confirms the exclusion @@ -172,21 +194,26 @@ the mock network only; to be re-run on hardware before release): - [x] **End-to-end tests** drive pair → command → fetch → events → `unpair` against the mock network - [x] **Failure paths tested** — unreachable or lost server, unresponsive and refusing devices, interview timeout, exclusion timeout, the wrong device excluded, a reused node id, a missing PIN -- [ ] **Broad device coverage** — mocked: switch, supervised switch, dimmer, RGB light, °C/°F sensor, scene - controller, double switch (endpoints), S2 device, identify indicator. Not yet: locks, thermostats, - meters, covers, sleeping (battery) devices +- [x] **Broad device coverage** — the catalogue hosts 33 command classes (switches, dimmers, lights, sensors, meters, + locks, thermostats, covers, garage doors, scene controllers, sirens, user codes, child locks, humidifiers, + keypads, smoke/CO alarms, sleeping devices, …); S2 by PIN, DSK, QR and SmartStart. The others are plumbing or + explained (see **Command-class catalogue and canary**) - [x] **Fully typed** (`ty`) and **clean** (`poe check`) - [x] **Readable & structured** — mapping in `mapper.py`, static tables in `zwave_spec.py` - [x] **Efficient** — subscription-based, not polling - [x] **Diagnosable** — per-area log tags (`[PAIR]`, `[CONNECT]`, …), reasons in `last_error`, causes chained -- [x] **Rich parameter metadata** — role, visibility, units, limits, enum labels, main parameter +- [x] **Rich parameter metadata** — role, visibility, units, limits, enum labels, main parameter; names and buckets + reviewed against the docs' parameter UX for every hosted CC, and audited (`parameter_audit`) - [ ] **Owned** — add a listed maintainer -- [ ] _nice-to-have:_ localizable naming · firmware updates · SmartStart provisioning · user-facing supported-device docs +- [ ] _nice-to-have:_ localizable naming · firmware updates · user-facing supported-device docs ### Notes - Z-Wave discovery doesn't use `zeroconf`/`ssdp`/`ble`: nodes are surfaced from `zwave-js-server`'s own events. -- QR inclusion is not covered end to end (the mock network has no QR flow); malformed codes are tested. +- Command classes zwave-js's mock cannot host (rare ones: tariffs, IP gateways, AV, schedules, irrigation) are not + reviewed: their values map by the generic rules and are hidden (`system`), still usable by automations and the API. +- What zwave-js's mock cannot do, so the catalogue does not round-trip it: decode a Color Switch Set, keep an + Indicator's state; its Thermostat Setpoint Set refuses every value above the minimum, so the harness replaces it. ## License diff --git a/majordom_zwave/controller.py b/majordom_zwave/controller.py index 3fff432..5e0c284 100644 --- a/majordom_zwave/controller.py +++ b/majordom_zwave/controller.py @@ -30,6 +30,7 @@ PIN = re.compile(r"\d{5}") # the S2 PIN: the first 5 digits of the DSK, printed on the device DSK = re.compile(r"\d{5}(-\d{5}){7}") # the full DSK +SMART_START_QR = re.compile(r"9001\d+") # a QR code's version digits: 00 is S2, 01 is SmartStart APPLIED = {SetValueStatus.SUCCESS, SetValueStatus.SUCCESS_UNSUPERVISED, SetValueStatus.WORKING} # Security classes that need the device's PIN; granted only when the PIN (or DSK) was given AUTHENTICATED = {SecurityClass.S2_AUTHENTICATED, SecurityClass.S2_ACCESS_CONTROL} @@ -45,6 +46,9 @@ class ZwaveController(AbstractController): EXCLUSION_TIMEOUT: ClassVar[float] = 60 # for the user to put the device into exclusion mode IDENTIFY_BLINKS: ClassVar[int] = 5 IDENTIFY_PERIOD: ClassVar[float] = 0.6 + # After an unsupervised set (delivered, not confirmed), zwave-js re-reads some values ~5 s later; past this delay + # the value is read back here when it has not changed, so the Hub learns what the device actually did + VERIFY_DELAY: ClassVar[float] = 6 def __init__(self, dependencies: AbstractController.Dependencies): super().__init__(dependencies) @@ -57,6 +61,7 @@ def __init__(self, dependencies: AbstractController.Dependencies): self._joined: dict[UUID, Node] = {} # nodes behind the discoveries self._nodes: dict[UUID, Node] = {} # nodes of paired devices self._routes: dict[UUID, dict[str, str | None]] = {} # value id → value id of the parameter it reports + self._events: dict[UUID, dict[str, Any]] = {} # event parameters' value ids → their enum labels (or None) self._unsubscribe: dict[UUID, list[Callable[[], None]]] = {} # per paired device self._listeners: list[Callable[[], None]] = [] # on the driver's controller, gone with the connection self._available: dict[UUID, bool] = {} @@ -184,7 +189,12 @@ async def _attach(self) -> None: device.id, False, f"Node {data.node_id} is now another device ({found}): pair that one anew" ) else: - self._bind(device.id, node) + events = { + p.integration_data.value_id: p.valid_values + for p in device.parameters + if p.integration_data.value_id not in node.values # an event parameter (of a notification) + } + self._bind(device.id, node, events) await self._update_availability(device.id, node) if node.status != NodeStatus.DEAD: await self._report_snapshot(device.id, node) @@ -214,6 +224,12 @@ async def start_pairing_window(self, duration_sec: int, credentials: ProvidedCre raise ValueError("Z-Wave devices are paired with their PIN, DSK or QR code, not a secret") if kind == CredentialsType.code and not DSK.fullmatch(value) and not PIN.fullmatch(value): raise ValueError("The PIN is the first 5 digits of the device's DSK (or give the whole DSK)") + if kind == CredentialsType.qr and SMART_START_QR.fullmatch(value): + # SmartStart: no window to open. zwave-js keeps the device on its provisioning list and includes it, + # securely, whenever it is powered on — hours later, after installation; it then joins as a discovery + await controller.async_provision_smart_start_node(value) + log.debug("[PAIRING] SmartStart device provisioned: it joins when powered on") + return self._credentials = credentials # before inclusion starts: a device can ask for its grant right away try: if kind == CredentialsType.qr: @@ -247,7 +263,8 @@ async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentia if hub_device is None: raise LookupError("The Hub has not created a device for this discovery") - parameters = self._build_parameters(discovery.id, node) + events = mapper.event_parameters(node, await self._supported_notification_events(node)) + parameters = self._build_parameters(discovery.id, node, events) main = mapper.main_value(node) main_id = self._parameter_id(discovery.id, main[0].value_id) if main else None for parameter in parameters: @@ -256,7 +273,9 @@ async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentia device = hub_device.model_copy( update={ "parameters": parameters, - "main_parameter": main_id if any(p.id == main_id for p in parameters) else None, + "main_parameter": main_id + if any(p.id == main_id and p.can_be_main_parameter for p in parameters) + else None, "available": True, "last_error": None, "integration_data": ZwaveDeviceIntegrationData( @@ -285,7 +304,7 @@ async def pair_device(self, discovery: Discovery, credentials: ProvidedCredentia self._discoveries.pop(discovery.id, None) self._joined.pop(discovery.id, None) - self._bind(discovery.id, node) + self._bind(discovery.id, node, {event.value_id: event.valid_values for event in events}) await self._set_available(discovery.id, True) await self._report_snapshot(discovery.id, node) return discovery.id @@ -296,6 +315,8 @@ async def unpair(self, device: ZwaveDevice): if node is None: # not on the network anymore: nothing to remove self._unbind(device.id) return + # A SmartStart device stays provisioned unless removed from the list: it would join again when powered on + provisioning = await controller.async_get_provisioning_entry(node.node_id) if await controller.async_is_failed_node(node): await controller.async_remove_failed_node(node) else: @@ -319,6 +340,8 @@ async def unpair(self, device: ZwaveDevice): reason = f"another device (node {removed}) was excluded instead: it left the network" await self._update_device(device.id, last_error=f"Not removed: {reason}") raise LookupError(reason) + if provisioning is not None: + await controller.async_unprovision_smart_start_node(provisioning.dsk) self._unbind(device.id) self._available.pop(device.id, None) @@ -389,6 +412,15 @@ async def send_command(self, command: DeviceCommand, device: ZwaveDevice, parame raise RuntimeError(reason) # no echo: the device's report (or zwave-js's verification of it) comes back as "value updated" await self._update_device(device.id, last_error=None) + if awake and result is not None and result.status == SetValueStatus.SUCCESS_UNSUPERVISED: + state = node.values.get(parameter.integration_data.state_value_id or value.value_id, value) + self._spawn(self._verify(node, state, mapper.to_device(value, command.value))) + + async def _verify(self, node: Node, state: Value, sent: Any) -> None: + """Reads a value back after an unconfirmed set, unless zwave-js already did (its value then is the sent one).""" + await asyncio.sleep(self.VERIFY_DELAY) + if state.metadata.readable and state.value != sent and node.status != NodeStatus.DEAD: + await node.async_poll_value(state) # ========================================================================= # Private helpers @@ -492,15 +524,30 @@ async def _wait_until_ready(self, node: Node) -> None: finally: unsubscribe() - def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameter]: + async def _supported_notification_events(self, node: Node) -> list[dict[str, Any]]: + """The keypad and Notification CC events the node supports, as zwave-js knows them from its interview.""" + assert self._zwave_client is not None + try: + result = await self._zwave_client.async_send_command( + {"command": "node.get_supported_notification_events", "nodeId": node.node_id}, require_schema=43 + ) + except Exception: # noqa: BLE001 an older server: no keypad or Notification CC event parameters + log.warning("[PAIR] node_id=%s: supported notification events unknown", node.node_id, exc_info=True) + return [] + return result.get("events", []) + + def _build_parameters( + self, device_id: UUID, node: Node, events: list[mapper.EventParameter] + ) -> list[ZwaveParameter]: """The device's parameters; their values reach the Hub as events (the snapshot after pairing).""" parameters: list[ZwaveParameter] = [] - for value, state in mapper.parameter_values(node): + pairs = mapper.parameter_values(node) + for (value, state), name in zip(pairs, mapper.names(pairs), strict=True): low, high, step = mapper.limits(value) parameters.append( ZwaveParameter( id=self._parameter_id(device_id, value.value_id), - name=mapper.name(value, state), + name=name, data_type=mapper.data_type(value), role=mapper.role(value), visibility=mapper.visibility(value, node), @@ -514,6 +561,19 @@ def _build_parameters(self, device_id: UUID, node: Node) -> list[ZwaveParameter] ), ) ) + parameters += [ + ZwaveParameter( + id=self._parameter_id(device_id, event.value_id), + name=event.name, + data_type=event.data_type, + role=ParameterRole.event, + visibility=event.visibility, + unit=event.unit, + valid_values=event.valid_values, + integration_data=ZwaveParameterIntegrationData(value_id=event.value_id), + ) + for event in events + ] return parameters async def _grant(self, requested: InclusionGrant) -> None: @@ -556,11 +616,13 @@ async def _node_removed(self, node: Node) -> None: self._unbind(device_id) await self._set_available(device_id, False, "The device was removed from the Z-Wave network") - def _bind(self, device_id: UUID, node: Node) -> None: + def _bind(self, device_id: UUID, node: Node, events: dict[str, Any]) -> None: self._unbind(device_id) self._nodes[device_id] = node self._routes[device_id] = mapper.reported_parameters(node) + self._events[device_id] = events self._unsubscribe[device_id] = [ + node.on("notification", lambda data: self._spawn(self._notified(device_id, data["notification"]))), node.on("value updated", lambda data: self._spawn(self._value_updated(device_id, data["value"]))), node.on( "value notification", @@ -573,6 +635,7 @@ def _bind(self, device_id: UUID, node: Node) -> None: def _unbind(self, device_id: UUID) -> None: self._nodes.pop(device_id, None) self._routes.pop(device_id, None) + self._events.pop(device_id, None) for unsubscribe in self._unsubscribe.pop(device_id, []): unsubscribe() @@ -606,6 +669,18 @@ async def _value_updated(self, device_id: UUID, value: Value) -> None: ) await self.dependencies.output.controller_did_receive_events(self, [event]) + async def _notified(self, device_id: UUID, notification: Any) -> None: + known = self._events.get(device_id, {}) + changes = [] + for value_id, value in mapper.notification_events(notification): + if value_id not in known or (known[value_id] and value not in known[value_id]): + log.debug("[EVENT] %s=%r: not an event the device announced", value_id, value) + continue + parameter_id = self._parameter_id(device_id, value_id) + changes.append(DeviceParameterChange(device_id=device_id, parameter_id=parameter_id, value=value)) + if changes: + await self.dependencies.output.controller_did_receive_events(self, changes) + async def _report_snapshot(self, device_id: UUID, node: Node) -> None: events = [ DeviceParameterChange( diff --git a/majordom_zwave/mapper.py b/majordom_zwave/mapper.py index 87987bc..d244389 100644 --- a/majordom_zwave/mapper.py +++ b/majordom_zwave/mapper.py @@ -5,6 +5,11 @@ current one, so the user sees one switch rather than a "Current value" sensor next to a "Target value" control. """ +import json +import logging +import re +from collections import Counter +from dataclasses import dataclass from typing import Any from majordom_integration_sdk.schemas.parameter import ( @@ -14,8 +19,16 @@ ParameterVisibility, ) from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.multilevel_switch import MultilevelSwitchCommand from zwave_js_server.model.duration import Duration from zwave_js_server.model.node import Node +from zwave_js_server.model.notification import ( + BatteryNotification, + EntryControlNotification, + MultilevelSwitchNotification, + NotificationNotification, + PowerLevelNotification, +) from zwave_js_server.model.value import AllowedRangeValue, Value, ValueType from .zwave_spec import ( @@ -26,8 +39,38 @@ LEVEL_COMMAND_CLASSES, LEVEL_PROPERTIES, MAIN_VALUE_BY_COMMAND_CLASS, + METER_QUANTITIES, + NOTIFICATION_SETTING_VARIABLES, + NOTIFICATION_SHARED_VARIABLES, + NUMERIC_VALUES, + PLAIN_UNITS, + REVIEWED_COMMAND_CLASSES, + SETTING_VALUES, SYSTEM_COMMAND_CLASSES, + SYSTEM_VALUES, UNIT_MAP, + USER_VALUES, + VALUE_NAMES, + VALUE_UNITS, +) + +log = logging.getLogger(__name__) + +# zwave-js's value metadata types (ValueType in @zwave-js/core); test_catalogue fails when it gains one +KNOWN_VALUE_TYPES = frozenset( + { + "number", + "boolean", + "string", + "number[]", + "boolean[]", + "string[]", + "duration", + "timeout", + "color", + "buffer", + "any", + } ) @@ -66,27 +109,121 @@ def _current_of(node: Node, target: Value) -> Value | None: ) +def _sentence(text: str) -> str: + """'Warm White' → 'Warm white': the words after the first in lower case, acronyms (CO, RGB) as they are.""" + first, *rest = text.split(" ") + return " ".join([first, *(word if sum(c.isupper() for c in word) > 1 else word.lower() for word in rest)]) + + +def _readable(text: str) -> str: + """A camelCase word ("wakeUpInterval", "UnsecuredWithTimeout") → words; anything else as it is.""" + if " " in text or not re.search(r"[a-z][A-Z]", text): + return text + words = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[a-z])(?=[0-9])", " ", text) + return _sentence(words[0].upper() + words[1:]) + + +def _clean(label: str) -> str: + """zwave-js labels written for its UI or logs → a parameter's name.""" + if match := re.fullmatch(r"Sensor state \((.+)\)", label): # Binary Sensor: "Sensor state (Motion)" + return _sentence(match[1]) + if label == "Reset accumulated values": # Meter: every scale's reset at once + return "Reset all meters" + if match := re.fullmatch(r"Setpoint \((.+)\)", label): # "Setpoint (Heating)" + return f"{match[1]} setpoint" + if match := re.fullmatch(r"(Reset )?(.+) Consumption \[(.+)\]", label): # Meter: "Electric Consumption [kWh]" + reset, meter, scale = match.groups() + named = f"{meter} {METER_QUANTITIES.get(scale, scale)}" + return f"Reset {named.lower()}" if reset else named + if match := re.fullmatch(r"(.+) - (.+)", label): # Window Covering: "Open - Outbound Bottom" + return f"{_sentence(match[2])}: {match[1][0].lower()}{match[1][1:]}" + if match := re.fullmatch(r"Scene 0*(\d+)", label): # Central Scene: "Scene 001" + return f"Scene {match[1]}" + if match := re.fullmatch(r"Signaling State \((.+)\)", label): # Barrier Operator: "Signaling State (Audible)" + return f"{match[1]} warning" + return _sentence(_readable(label)) + + def name(value: Value, state: Value | None) -> str: - base = value.command_class_name if state else value.metadata.label or value.property_name or str(value.property_) + key = value.property_key_name or ("" if value.property_key is None else str(value.property_key)) + template = VALUE_NAMES.get((value.command_class, value.property_, value.property_key)) or VALUE_NAMES.get( + (value.command_class, value.property_) + ) + if template is not None and "{key}" in template and not key: # the CC's keyless value (all channels at once) + template = None + label = value.metadata.label or value.property_name or str(value.property_) + variable = _notification_variable(value) + if template is not None: + base = template.format(key=_sentence(key), label=_clean(label)) + elif state: # a pair with no name of its own: named by its CC + base = value.command_class_name + elif variable and variable[1] in NOTIFICATION_SHARED_VARIABLES: # "Sensor status": of which notification type + base = f"{variable[1]} ({_sentence(variable[0])})" + else: + base = _clean(label) return f"{base} {value.endpoint}" if value.endpoint else base +def names(pairs: list[tuple[Value, Value | None]]) -> list[str]: + """Each parameter's name. A name several values share gets what tells them apart: a Notification variable its + notification type ("Sensor status (Smoke Alarm)"), anything else its CC ("Remaining duration (Color Switch)").""" + plain = [name(value, state) for value, state in pairs] + shared = {n for n, count in Counter(plain).items() if count > 1} + return [f"{n} ({_context(value)})" if n in shared else n for n, (value, _) in zip(plain, pairs, strict=True)] + + +def _notification_variable(value: Value) -> tuple[str, str] | None: + """A Notification CC state's (notification type, variable): zwave-js's property and property key.""" + kind, variable = value.property_, value.property_key + if value.command_class != CommandClass.NOTIFICATION or not isinstance(kind, str) or not isinstance(variable, str): + return None + return kind, variable + + +def _context(value: Value) -> str: + variable = _notification_variable(value) + return _sentence(variable[0]) if variable else value.command_class_name + + def _is_level(value: Value) -> bool: return value.command_class in LEVEL_COMMAND_CLASSES and value.property_ in LEVEL_PROPERTIES def _is_duration(value: Value) -> bool: - # zwave-js-server-python's ValueType has no "duration": it is told by the metadata type string or the property - return value.metadata.type == "duration" or value.property_ == "duration" + # zwave-js-server-python's ValueType has no "duration": it is told by the metadata type string (not by the + # property's name: an Alarm Sensor's "duration" is a number of seconds) + return value.metadata.type == "duration" + + +def _is_button(value: Value) -> bool: + """A write-only action ("Reset", "Identify", "Restore previous value"): its only state is `true`, the trigger. + Start/stop pairs (`true`/`false`, like "Up"/"Down" dimming) stay booleans.""" + metadata = value.metadata + return ( + metadata.type == ValueType.BOOLEAN + and bool(metadata.writeable) + and not metadata.readable + and set(metadata.states or {}) <= {"true"} + ) def data_type(value: Value) -> ParameterDataType: metadata = value.metadata - if metadata.type == ValueType.BOOLEAN: + kind = metadata.type + if kind not in KNOWN_VALUE_TYPES: + log.warning("[MAP] %s has the unknown value type %r: hidden as data", value.value_id, kind) + return ParameterDataType.data + if _is_button(value): + return ParameterDataType.none + if kind == ValueType.BOOLEAN: return ParameterDataType.bool - if metadata.type == ValueType.STRING or metadata.type == "color" or _is_duration(value): + if kind in ("string", "string[]", "color", "timeout") or _is_duration(value): # a list of strings: as text return ParameterDataType.string - if metadata.type == ValueType.NUMBER: + if kind in ("number[]", "boolean[]", "buffer"): + return ParameterDataType.data + if kind == ValueType.NUMBER: + if _is_level(value) or (value.command_class, value.property_) in NUMERIC_VALUES: + return ParameterDataType.integer if metadata.states and not metadata.allow_manual_entry: return ParameterDataType.enum return ( @@ -105,24 +242,42 @@ def data_type(value: Value) -> ParameterDataType: def role(value: Value) -> ParameterRole: - if value.command_class in EVENT_COMMAND_CLASSES or value.metadata.stateful is False: + if value.metadata.stateful is False or (value.command_class in EVENT_COMMAND_CLASSES and not _is_setting(value)): return ParameterRole.event return ParameterRole.control if value.metadata.writeable else ParameterRole.sensor +def _is_setting(value: Value) -> bool: + variable = _notification_variable(value) + if variable and variable[1] in NOTIFICATION_SETTING_VARIABLES: # a diagnostic, like "Maintenance status" + return True + properties = SETTING_VALUES.get(value.command_class, frozenset()) + return properties is None or value.property_ in properties + + +def _is_user(value: Value) -> bool: + properties = USER_VALUES.get(value.command_class, frozenset()) + return properties is None or value.property_ in properties + + def visibility(value: Value, node: Node) -> ParameterVisibility: command_class = value.command_class if ( command_class in SYSTEM_COMMAND_CLASSES + or (command_class, value.property_) in SYSTEM_VALUES or value.metadata.secret or _is_duration(value) or data_type(value) == ParameterDataType.data or (command_class == CommandClass.BASIC and _has_other_application_values(node)) ): return ParameterVisibility.system - if command_class in DIAGNOSTIC_COMMAND_CLASSES: + if _is_setting(value): return ParameterVisibility.setting - return ParameterVisibility.user + if _is_user(value): + return ParameterVisibility.user + if command_class in REVIEWED_COMMAND_CLASSES: + return ParameterVisibility.setting + return ParameterVisibility.system # a CC nobody reviewed: hidden ("when in doubt, hide it"), still usable def _has_other_application_values(node: Node) -> bool: @@ -134,9 +289,13 @@ def _has_other_application_values(node: Node) -> bool: def unit(value: Value) -> ParameterUnit: if _is_level(value): return ParameterUnit.percentage + if (value.command_class, value.property_) in VALUE_UNITS: + return VALUE_UNITS[value.command_class, value.property_] raw = value.metadata.unit or "" if raw in CONVERTED_UNITS: return CONVERTED_UNITS[raw][0] + if raw and raw not in UNIT_MAP and raw not in PLAIN_UNITS: + log.warning("[MAP] %s has the unknown unit %r: reported as plain", value.value_id, raw) return UNIT_MAP.get(raw, ParameterUnit.plain) @@ -155,7 +314,11 @@ def valid_values(value: Value) -> dict[int | float | str, str] | None: if data_type(value) != ParameterDataType.enum or not value.metadata.states: return None result: dict[int | float | str, str] = {} - for key, label in value.metadata.states.items(): + for key, raw in value.metadata.states.items(): + label = _readable(raw) + if value.command_class == CommandClass.CENTRAL_SCENE: # "Key pressed", "Key held down": a key's anyway + label = label.removeprefix("Key ") + label = label[:1].upper() + label[1:] # "idle", "off" as the other labels try: result[int(key)] = label except ValueError: @@ -167,6 +330,10 @@ def to_hub(value: Value, raw: Any) -> Any: """A value as the device reports it → as MajorDom expresses it.""" if _is_duration(value): return str(Duration(raw)) if raw is not None else "unknown" + if value.metadata.type == "timeout" and raw is not None: + return str(raw) + if value.metadata.type == "string[]" and isinstance(raw, list): + return ", ".join(str(item) for item in raw) if raw is None or isinstance(raw, bool) or not isinstance(raw, int | float): return raw if _is_level(value): @@ -174,12 +341,14 @@ def to_hub(value: Value, raw: Any) -> Any: converted = CONVERTED_UNITS.get(value.metadata.unit or "") if converted: _, factor, offset = converted - return round(raw * factor + offset, 2) + return round(raw * factor + offset, 6) return raw def to_device(value: Value, hub: Any) -> Any: """A value as MajorDom commands it → as the device takes it.""" + if _is_button(value): + return True # a button parameter (`none`) is commanded without a value if hub is None or isinstance(hub, bool) or not isinstance(hub, int | float): return hub if _is_level(value): @@ -187,7 +356,7 @@ def to_device(value: Value, hub: Any) -> Any: converted = CONVERTED_UNITS.get(value.metadata.unit or "") if converted: _, factor, offset = converted - return round((hub - offset) / factor, 2) + return round((hub - offset) / factor, 6) return hub @@ -198,3 +367,141 @@ def main_value(node: Node) -> tuple[Value, Any] | None: if value.command_class == command_class and value.property_ == spec.property_name: return value, spec.default_value return None + + +# Notifications ------------------------------------------------------------------------------------------------------ +# What devices send as one-off events rather than values (zwave-js "notification" events). They have no value id, so +# their parameters get one of the same shape, `---`; zwave-js says which keypad and +# Notification CC events a device supports (node.get_supported_notification_events), so those are exact enums. + +LEVEL_CHANGES: dict[int | float | str, str] = {0: "Stopped", 1: "Started up", 2: "Started down"} +BATTERY_REPLACEMENT: dict[int | float | str, str] = {0: "Not needed", 1: "Soon", 2: "Now"} +POWERLEVEL_TEST: dict[int | float | str, str] = {0: "Failed", 1: "Success", 2: "In progress"} + + +@dataclass(frozen=True) +class EventParameter: + value_id: str + name: str + data_type: ParameterDataType + visibility: ParameterVisibility + unit: ParameterUnit = ParameterUnit.plain + valid_values: dict[int | float | str, str] | None = None + + +def _labels(supported: dict[str, str]) -> dict[int | float | str, str]: + return {int(key): label for key, label in supported.items()} + + +def event_parameters(node: Node, supported: list[dict[str, Any]]) -> list[EventParameter]: + """The event parameters of what a node sends as notifications; `supported`: its supported notification events.""" + + def value_id(command_class: int, endpoint: int, prop: str) -> str: + return f"{node.node_id}-{command_class}-{endpoint}-{prop}" + + def named(name: str, endpoint: int) -> str: + return f"{name} {endpoint}" if endpoint else name + + events: list[EventParameter] = [] + for capability in supported: + command_class, endpoint = capability["commandClass"], capability["endpoint"] + if command_class == CommandClass.ENTRY_CONTROL: + events += [ + EventParameter( + value_id(command_class, endpoint, "event"), + named("Keypad", endpoint), + ParameterDataType.enum, + ParameterVisibility.user, + valid_values=_labels(capability["supportedEventTypes"]), + ), + # what was typed with it: for automations (checking a code), never shown + EventParameter( + value_id(command_class, endpoint, "code"), + named("Keypad code", endpoint), + ParameterDataType.string, + ParameterVisibility.system, + ), + ] + elif command_class == CommandClass.NOTIFICATION: + for kind, notification in capability["supportedNotificationTypes"].items(): + label = _sentence(notification["label"]) + events += [ + EventParameter( + value_id(command_class, endpoint, f"event-{kind}"), + named(f"{label} event", endpoint), + ParameterDataType.enum, + ParameterVisibility.user, + valid_values=_labels(notification["supportedEvents"]), + ), + # what came with it (a user id, a code...): for automations, never shown + EventParameter( + value_id(command_class, endpoint, f"details-{kind}"), + named(f"{label} event details", endpoint), + ParameterDataType.string, + ParameterVisibility.system, + ParameterUnit.json, + ), + ] + for endpoint in node.endpoints.values(): + supports = {info.id for info in endpoint.command_classes} + if CommandClass.SWITCH_MULTILEVEL in supports: # dimming started or stopped on the device (or a remote) itself + events.append( + EventParameter( + value_id(CommandClass.SWITCH_MULTILEVEL, endpoint.index, "levelChange"), + named("Level change", endpoint.index), + ParameterDataType.enum, + ParameterVisibility.system, # for automations (a remote's dimming), not a state to show + valid_values=LEVEL_CHANGES, + ) + ) + if CommandClass.BATTERY in supports: # "battery low" + events.append( + EventParameter( + value_id(CommandClass.BATTERY, endpoint.index, "replacement"), + named("Battery replacement", endpoint.index), + ParameterDataType.enum, + ParameterVisibility.user, # "replace the battery": what people need to see + valid_values=BATTERY_REPLACEMENT, + ) + ) + if CommandClass.POWERLEVEL in supports: # a radio test's result, when one is run + events.append( + EventParameter( + value_id(CommandClass.POWERLEVEL, endpoint.index, "test"), + named("Powerlevel test", endpoint.index), + ParameterDataType.enum, + ParameterVisibility.setting, + valid_values=POWERLEVEL_TEST, + ) + ) + return events + + +def notification_events(notification: Any) -> list[tuple[str, Any]]: + """A notification → (value id of its event parameter, value) for each of its parameters it reports.""" + + def value_id(prop: str) -> str: + return f"{notification.node_id}-{notification.command_class}-{notification.endpoint_idx}-{prop}" + + if isinstance(notification, EntryControlNotification): + events: list[tuple[str, Any]] = [(value_id("event"), notification.event_type)] + if notification.event_data not in (None, ""): + data = notification.event_data + events.append((value_id("code"), data if isinstance(data, str) else json.dumps(data, sort_keys=True))) + return events + if isinstance(notification, NotificationNotification): + events = [(value_id(f"event-{notification.type_}"), notification.event)] + if notification.parameters: + events.append( + (value_id(f"details-{notification.type_}"), json.dumps(notification.parameters, sort_keys=True)) + ) + return events + if isinstance(notification, MultilevelSwitchNotification): + started = notification.event_type == MultilevelSwitchCommand.START_LEVEL_CHANGE + change = (1 if notification.direction == "up" else 2) if started else 0 + return [(value_id("levelChange"), change)] + if isinstance(notification, BatteryNotification): + return [(value_id("replacement"), int(notification.urgency))] + if isinstance(notification, PowerLevelNotification): + return [(value_id("test"), int(notification.status))] + return [] diff --git a/majordom_zwave/zwave_spec.py b/majordom_zwave/zwave_spec.py index 5423756..22a1765 100644 --- a/majordom_zwave/zwave_spec.py +++ b/majordom_zwave/zwave_spec.py @@ -76,8 +76,8 @@ ) # CCs that report one-shot occurrences (button presses, scene activations) rather than a state — modeled as -# ParameterRole.event. zwave-js marks such values `stateful: false` and sends them as "value notification". -# Notification CC is not one: zwave-js models its variables as states with an idle value. +# ParameterRole.event (except their settings, see SETTING_VALUES). zwave-js marks such values `stateful: false` and +# sends them as "value notification". Notification CC is not one: zwave-js models its variables as states. EVENT_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( { CommandClass.CENTRAL_SCENE, @@ -97,21 +97,224 @@ } ) -# Levels 0-99 (99 is fully on) are percentages in MajorDom, as with Matter, Zigbee and ESPHome: 0-98 stay, 99 is 100 -LEVEL_COMMAND_CLASSES: frozenset[CommandClass] = frozenset({CommandClass.SWITCH_MULTILEVEL, CommandClass.BASIC}) +# Levels 0-99 (99 is fully on) are percentages in MajorDom, as with Matter, Zigbee and ESPHome: 0-98 stay, 99 is 100. +# A window covering's position is one too (its 0 "closed" and 99 "open" labels are the ends of the range). +LEVEL_COMMAND_CLASSES: frozenset[CommandClass] = frozenset( + {CommandClass.SWITCH_MULTILEVEL, CommandClass.BASIC, CommandClass.WINDOW_COVERING} +) LEVEL_PROPERTIES: frozenset[str] = frozenset({"currentValue", "targetValue"}) -# zwave-js's ValueMetadata.unit is the unit string of the CC's scale (packages/core/src/registries/Scales.ts and -# Meters.ts) — map the ones MajorDom has a unit for, leave the rest `plain` rather than guessed. +# The device's configuration within a CC that is otherwise everyday use: shown as settings (None: the whole CC) +SETTING_VALUES: dict[CommandClass, frozenset[str] | None] = { + CommandClass.DOOR_LOCK: frozenset( + { + "operationType", + "lockTimeoutConfiguration", + "autoRelockTime", + "holdAndReleaseTime", + "twistAssist", + "blockToBlock", + "insideHandlesCanOpenDoorConfiguration", + "outsideHandlesCanOpenDoorConfiguration", + } + ), + CommandClass.CENTRAL_SCENE: frozenset({"slowRefresh"}), # "send held down notifications" + CommandClass.BARRIER_OPERATOR: frozenset({"signalingState"}), # audible / visual warning while moving + CommandClass.SOUND_SWITCH: frozenset({"defaultToneId", "defaultVolume"}), + CommandClass.NOTIFICATION: frozenset({"alarmType", "alarmLevel"}), # legacy (V1) vendor alarm codes + CommandClass.USER_CODE: None, # user slots and keypad mode (the codes themselves are secrets: system) + CommandClass.ENTRY_CONTROL: None, # a keypad's key cache (its key presses arrive as notifications, not values) +} + +# Values that describe another value rather than a state of the device, or protocol wiring: hidden +SYSTEM_VALUES: frozenset[tuple[int, int | str]] = frozenset( + { + (CommandClass.HUMIDITY_CONTROL_SETPOINT, "setpointScale"), # the setpoint's unit + (CommandClass.THERMOSTAT_SETPOINT, "setpointScale"), + (CommandClass.WAKE_UP, "controllerNodeId"), # which node gets the wake-ups: the controller + } +) + +# Notification CC variables (`-`) that are diagnostics, not states people look at: settings +NOTIFICATION_SETTING_VARIABLES: frozenset[str] = frozenset( + { + "Maintenance status", + "Periodic inspection status", + "Dust in device status", + "Test status", + "Hardware status", + "Software status", + "Battery maintenance status", + "Device configuration status", + "Barrier performing initialization process status", + "Barrier UL disabling status", + "Barrier vacation mode status", + } +) + +# Notification CC variables several notification types have: named with their type ("Sensor status (Water Alarm)") +NOTIFICATION_SHARED_VARIABLES: frozenset[str] = frozenset( + { + "Sensor status", + "Alarm status", + "Cover status", + "Maintenance status", + "Periodic inspection status", + "Dust in device status", + "Test status", + } +) + +# The everyday controls and live readings: `user` (None: every value of the CC). Following the docs' "when in doubt, +# hide it", every other value of a REVIEWED_COMMAND_CLASSES CC is a `setting`, and a CC nobody reviewed is `system` +# (still usable by automations and the API). +USER_VALUES: dict[CommandClass, frozenset[str] | None] = { + CommandClass.BASIC: frozenset({"targetValue"}), # when it is the device's only control + CommandClass.SWITCH_BINARY: frozenset({"targetValue"}), + CommandClass.SWITCH_MULTILEVEL: frozenset({"targetValue"}), + CommandClass.SWITCH_COLOR: frozenset({"hexColor"}), # one colour control; the channels are settings + CommandClass.SENSOR_BINARY: None, + CommandClass.SENSOR_MULTILEVEL: None, + CommandClass.METER: frozenset({"value"}), # the readings; their resets are settings + CommandClass.NOTIFICATION: None, # its states (but see NOTIFICATION_SETTING_VARIABLES) + CommandClass.SENSOR_ALARM: frozenset({"state"}), + CommandClass.DOOR_LOCK: frozenset({"targetMode", "doorStatus", "latchStatus", "boltStatus"}), + CommandClass.LOCK: frozenset({"locked"}), + CommandClass.BARRIER_OPERATOR: frozenset({"targetState", "position"}), + CommandClass.WINDOW_COVERING: frozenset({"targetValue"}), + CommandClass.THERMOSTAT_MODE: frozenset({"mode"}), + CommandClass.THERMOSTAT_SETPOINT: frozenset({"setpoint"}), + CommandClass.THERMOSTAT_OPERATING_STATE: frozenset({"state"}), + CommandClass.THERMOSTAT_FAN_MODE: frozenset({"mode"}), + CommandClass.THERMOSTAT_FAN_STATE: frozenset({"state"}), + CommandClass.HUMIDITY_CONTROL_SETPOINT: frozenset({"setpoint"}), + CommandClass.HUMIDITY_CONTROL_MODE: frozenset({"mode"}), + CommandClass.HUMIDITY_CONTROL_OPERATING_STATE: frozenset({"state"}), + CommandClass.BATTERY: frozenset({"level"}), # a live reading (the docs' example), despite the CC's diagnostics + CommandClass.CENTRAL_SCENE: frozenset({"scene"}), + CommandClass.SCENE_ACTIVATION: frozenset({"sceneId"}), + CommandClass.SOUND_SWITCH: frozenset({"toneId"}), + CommandClass.ENERGY_PRODUCTION: frozenset({"value"}), +} + +# CCs whose values were reviewed one by one (the catalogue hosts each): what is not `user` is a `setting` +REVIEWED_COMMAND_CLASSES: frozenset[int] = ( + frozenset(USER_VALUES) + | DIAGNOSTIC_COMMAND_CLASSES + | { + CommandClass.SCHEDULE_ENTRY_LOCK, + CommandClass.USER_CODE, + CommandClass.ENTRY_CONTROL, + CommandClass.THERMOSTAT_SETBACK, + CommandClass.HUMIDITY_CONTROL_SETPOINT, + 0x83, # User Credential (newer than zwave-js-server-python's CommandClass) + } +) + +# Names for values whose zwave-js label reads as protocol, not as a control, by (CC, property[, property key]); +# {key}: the value's key (a colour channel, a covering) +VALUE_NAMES: dict[tuple[int, int | str] | tuple[int, int | str, int | str | None], str] = { + (CommandClass.BASIC, "targetValue"): "Level", + (CommandClass.BASIC, "restorePrevious"): "Restore last level", + (CommandClass.SWITCH_BINARY, "targetValue"): "Power", + (CommandClass.SWITCH_MULTILEVEL, "targetValue"): "Level", + (CommandClass.SWITCH_MULTILEVEL, "Up"): "Level up", + (CommandClass.SWITCH_MULTILEVEL, "Down"): "Level down", + (CommandClass.SWITCH_MULTILEVEL, "restorePrevious"): "Restore last level", + (CommandClass.SWITCH_COLOR, "hexColor"): "Color", + (CommandClass.SWITCH_COLOR, "targetColor"): "{key}", + (CommandClass.WINDOW_COVERING, "targetValue"): "{key}", + (CommandClass.DOOR_LOCK, "targetMode"): "Lock", + (CommandClass.DOOR_LOCK, "doorStatus"): "Door", + (CommandClass.DOOR_LOCK, "latchStatus"): "Latch", + (CommandClass.DOOR_LOCK, "boltStatus"): "Bolt", + (CommandClass.DOOR_LOCK, "lockTimeout"): "Time until relock", + (CommandClass.DOOR_LOCK, "operationType"): "Operation type", + (CommandClass.DOOR_LOCK, "lockTimeoutConfiguration"): "Timed mode duration", + (CommandClass.DOOR_LOCK, "autoRelockTime"): "Auto-relock time", + (CommandClass.DOOR_LOCK, "holdAndReleaseTime"): "Hold and release time", + (CommandClass.DOOR_LOCK, "twistAssist"): "Twist assist", + (CommandClass.DOOR_LOCK, "blockToBlock"): "Block to block", + (CommandClass.DOOR_LOCK, "insideHandlesCanOpenDoorConfiguration"): "Inside handles that open the door", + (CommandClass.DOOR_LOCK, "outsideHandlesCanOpenDoorConfiguration"): "Outside handles that open the door", + (CommandClass.DOOR_LOCK, "insideHandlesCanOpenDoor"): "Inside handles open the door", + (CommandClass.DOOR_LOCK, "outsideHandlesCanOpenDoor"): "Outside handles open the door", + (CommandClass.BARRIER_OPERATOR, "targetState"): "Door", + (CommandClass.BARRIER_OPERATOR, "position"): "Position", + (CommandClass.VERSION, "firmwareVersions"): "Firmware version", + (CommandClass.VERSION, "libraryType"): "Z-Wave library type", + (CommandClass.WAKE_UP, "wakeUpInterval"): "Wake-up interval", + (CommandClass.WAKE_UP, "controllerNodeId"): "Wake-up destination", + (CommandClass.CENTRAL_SCENE, "slowRefresh"): "Slow refresh while held", + (CommandClass.SCENE_ACTIVATION, "sceneId"): "Scene", + (CommandClass.SCENE_ACTIVATION, "dimmingDuration"): "Scene dimming duration", + (CommandClass.USER_CODE, "userIdStatus"): "User {key} status", + (CommandClass.USER_CODE, "userCode"): "User {key} code", + (CommandClass.SOUND_SWITCH, "toneId"): "Play tone", + (CommandClass.SOUND_SWITCH, "volume"): "Tone volume", + (CommandClass.SOUND_SWITCH, "defaultToneId"): "Default tone", + (CommandClass.PROTECTION, "local"): "Local protection", + (CommandClass.PROTECTION, "rf"): "Remote protection", + (CommandClass.PROTECTION, "exclusiveControlNodeId"): "Exclusive control", + (CommandClass.PROTECTION, "timeout"): "Remote protection timeout", + (CommandClass.THERMOSTAT_FAN_MODE, "mode"): "Fan mode", + (CommandClass.THERMOSTAT_FAN_MODE, "off"): "Fan off", + (CommandClass.THERMOSTAT_FAN_STATE, "state"): "Fan state", + (CommandClass.HUMIDITY_CONTROL_MODE, "mode"): "Humidity mode", + (CommandClass.HUMIDITY_CONTROL_OPERATING_STATE, "state"): "Humidity operating state", + (CommandClass.ENERGY_PRODUCTION, "value", 1): "Total production", + (CommandClass.ENERGY_PRODUCTION, "value", 2): "Production today", + (CommandClass.ENERGY_PRODUCTION, "value", 3): "Operating time", +} + +# A meter value's quantity, by the scale in its label ("Electric Consumption [kWh]" → "Electric energy") +METER_QUANTITIES: dict[str, str] = { + "kWh": "energy", + "kVAh": "apparent energy", + "W": "power", + "Pulse count": "pulses", + "V": "voltage", + "A": "current", + "Power Factor": "power factor", + "kVar": "reactive power", + "kVarh": "reactive energy", + "Cubic meters": "volume", + "Cubic feet": "volume (ft³)", + "US gallons": "volume (gal)", +} + +# Numbers whose `states` label special values of a range, not the only values: integers, not enums +NUMERIC_VALUES: frozenset[tuple[int, int | str]] = frozenset( + { + (CommandClass.SOUND_SWITCH, "volume"), # 0 is "the default volume" + (CommandClass.WAKE_UP, "wakeUpInterval"), # 0 is "disabled" + } +) + +# Units zwave-js puts only in a value's label ("Duration in seconds …"), not in its metadata +VALUE_UNITS: dict[tuple[int, int | str], ParameterUnit] = { + (CommandClass.DOOR_LOCK, "lockTimeoutConfiguration"): ParameterUnit.second, + (CommandClass.DOOR_LOCK, "lockTimeout"): ParameterUnit.second, + (CommandClass.DOOR_LOCK, "autoRelockTime"): ParameterUnit.second, + (CommandClass.DOOR_LOCK, "holdAndReleaseTime"): ParameterUnit.second, + (CommandClass.WAKE_UP, "wakeUpInterval"): ParameterUnit.second, +} + +# zwave-js's ValueMetadata.unit is the unit string of the CC's scale (its sensor, meter and named-scale registries, +# plus a few CC-specific ones). Every registry unit is mapped, converted, or deliberately plain (test_catalogue checks). UNIT_MAP: dict[str, ParameterUnit] = { "%": ParameterUnit.percentage, "s": ParameterUnit.second, + "seconds": ParameterUnit.second, # Energy Production's total time "Hz": ParameterUnit.hertz, "kg": ParameterUnit.kilogram, "°": ParameterUnit.arcdegree, + "°N": ParameterUnit.arcdegree, # a direction + "°S": ParameterUnit.arcdegree, "m": ParameterUnit.meters, "m/s": ParameterUnit.mps, "m/s²": ParameterUnit.mps2, + "m³/h": ParameterUnit.m3h, "rpm": ParameterUnit.rpm, "N": ParameterUnit.newton, "J": ParameterUnit.joule, @@ -134,8 +337,54 @@ CONVERTED_UNITS: dict[str, tuple[ParameterUnit, float, float]] = { "°F": (ParameterUnit.celsius, 5 / 9, -32 * 5 / 9), "kPa": (ParameterUnit.pascal, 1000, 0), + "inHg": (ParameterUnit.pascal, 3386.389, 0), + "mmHg": (ParameterUnit.pascal, 133.322, 0), + "psi": (ParameterUnit.pascal, 6894.757, 0), + "Wh": (ParameterUnit.kwh, 0.001, 0), + "Btu/h": (ParameterUnit.watt, 0.29307107, 0), + "mV": (ParameterUnit.volt, 0.001, 0), + "mA": (ParameterUnit.ampere, 0.001, 0), + "kHz": (ParameterUnit.hertz, 1000, 0), + "cm": (ParameterUnit.meters, 0.01, 0), + "ft": (ParameterUnit.meters, 0.3048, 0), + "lb": (ParameterUnit.kilogram, 0.45359237, 0), + "Mph": (ParameterUnit.mps, 0.44704, 0), + "cfm": (ParameterUnit.m3h, 1.699011, 0), } +# Units MajorDom has no unit for, and no unit it has to convert them to: reported as they are, unit `plain` +PLAIN_UNITS: frozenset[str] = frozenset( + { + "g/m³", # absolute humidity + "W/m²", # solar radiation + "mm/h", # rain rate + "in/h", + "l", # volume + "m³", + "gallon", + "gal", + "ft³", + "l/h", # water flow + "Ωm", # soil / water conductivity + "S/m", + "kΩ", + "dB", # sound level + "dBA", + "dBm", # signal strength + "m³/m³", # soil moisture + "aw", # water activity + "mol/m³", + "bq/m³", # radon + "pCi/l", + "pH", + "bpm", # heart rate + "mg/l", + "kVAh", # apparent energy, reactive power and energy + "kVar", + "kVarh", + } +) + @dataclass(frozen=True) class MainValueSpec: @@ -162,4 +411,10 @@ class MainValueSpec: CommandClass.BARRIER_OPERATOR: MainValueSpec("targetState", {0, 255}), # DoorLockMode UNSECURED = 0, SECURED = 255: not a cycle through every mode the lock has. CommandClass.DOOR_LOCK: MainValueSpec("targetMode", {0, 255}), + # Lock CC (older locks): a bool, so a tap toggles it. + CommandClass.LOCK: MainValueSpec("locked", None), + # A window covering's position: closed and fully open, as percentages. + CommandClass.WINDOW_COVERING: MainValueSpec("targetValue", {0, 100}), + # Basic CC, when it is the only control the device has: off and fully on, as percentages. + CommandClass.BASIC: MainValueSpec("targetValue", {0, 100}), }