From 5b499429e39de2863e60390e3ce68119c3298f32 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:30:03 -0400 Subject: [PATCH 1/3] feat(transport): add WiFi/LAN (TCP + TLS-PSK) transport Add an IP transport alongside BLE so displays can be driven over WiFi/LAN. - transport/ip.py: TCP + TLS-PSK connection, framing per the wire protocol - transport/base.py, connection.py: transport abstraction and selection - discovery_ip.py: zeroconf-based IP discovery (new optional `wifi` extra) - device.py/cli.py: host/port/tls/psk addressing and transport selection - DATA-ack timeout handling on the compressed path; TLS-PSK derived from the device master key Requires Python >= 3.13 (TLS-PSK needs ssl.set_psk_client_callback). CI matrix updated to 3.13 (floor) + 3.14 (non-blocking). --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 6 +- AGENTS.md | 2 +- pyproject.toml | 16 +- src/opendisplay/__init__.py | 12 + src/opendisplay/cli.py | 102 ++++- src/opendisplay/device.py | 152 +++++-- src/opendisplay/discovery_ip.py | 127 ++++++ src/opendisplay/exceptions.py | 26 +- src/opendisplay/protocol/__init__.py | 12 + src/opendisplay/protocol/commands.py | 27 +- src/opendisplay/transport/__init__.py | 10 +- src/opendisplay/transport/base.py | 75 ++++ src/opendisplay/transport/connection.py | 14 + src/opendisplay/transport/ip.py | 210 ++++++++++ tests/conftest.py | 75 ++++ .../unit/test_device_command_serialization.py | 4 +- tests/unit/test_device_partial.py | 2 + tests/unit/test_device_upload_flow.py | 22 +- tests/unit/test_pipe_write.py | 4 +- tests/unit/test_transport_ip.py | 193 +++++++++ tests/unit/test_transport_selection.py | 165 ++++++++ uv.lock | 376 +++--------------- 24 files changed, 1244 insertions(+), 392 deletions(-) create mode 100644 src/opendisplay/discovery_ip.py create mode 100644 src/opendisplay/transport/base.py create mode 100644 src/opendisplay/transport/ip.py create mode 100644 tests/unit/test_transport_ip.py create mode 100644 tests/unit/test_transport_selection.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b231c34..cdfdc69 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ jobs: uses: astral-sh/setup-uv@v8.1.0 - name: Set up Python - run: uv python install 3.11 + run: uv python install 3.13 - name: Install dependencies run: uv sync --all-extras --group dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09a1ffe..73b682f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: "3.13" - name: Install uv uses: astral-sh/setup-uv@v8.1.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4ea89c..d78021b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13", "3.14"] + python-version: ["3.13", "3.14"] steps: - uses: actions/checkout@v6 @@ -39,11 +39,11 @@ jobs: run: uv run pytest tests/ -v --tb=short - name: Generate coverage report - if: matrix.python-version == '3.11' + if: matrix.python-version == '3.13' run: uv run pytest tests/ --cov=src/opendisplay --cov-report=xml --cov-report=term - name: Upload coverage to Codecov - if: matrix.python-version == '3.11' + if: matrix.python-version == '3.13' uses: codecov/codecov-action@v6 with: files: ./coverage.xml diff --git a/AGENTS.md b/AGENTS.md index 26e3069..5e21ae1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands -Always use `uv`, never `pip`. Requires Python ≥ 3.11. +Always use `uv`, never `pip`. Requires Python ≥ 3.13. ```bash uv sync --all-extras # install deps (pytest lives in the `test` extra, not the dev group) diff --git a/pyproject.toml b/pyproject.toml index ef3f002..f332d19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "7.14.0" description = "Python library for OpenDisplay BLE e-paper displays" readme = "README.md" license = "MIT" -requires-python = ">=3.11" +requires-python = ">=3.13" authors = [ {name = "g4bri3lDev", email = "admin@g4bri3l.de"} ] @@ -19,10 +19,7 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Hardware", "Typing :: Typed", @@ -42,6 +39,9 @@ dependencies = [ cli = [ "rich>=13.0.0", ] +wifi = [ + "zeroconf>=0.132.0", +] nrf-ota = [ "nrf-ota>=0.4.0", ] @@ -69,13 +69,13 @@ include = ["src/opendisplay/py.typed"] [tool.ruff] line-length = 120 -target-version = "py311" +target-version = "py313" [tool.ruff.lint] select = ["E", "F", "W", "I"] [tool.mypy] -python_version = "3.11" +python_version = "3.13" strict = true warn_return_any = true warn_unused_configs = true @@ -95,8 +95,8 @@ argument-rgx = "^[a-zA-Z_][a-zA-Z0-9_]*$" attr-rgx = "^[a-zA-Z_][a-zA-Z0-9_]*$" [tool.pylint.design] -max-args = 16 -max-positional-arguments = 16 +max-args = 20 # OpenDisplayDevice adds transport/host/port/tls/psk addressing params +max-positional-arguments = 20 max-locals = 28 # _build_info_tree renders a complex tree with many conditional vars max-attributes = 22 # config dataclasses have many fields by nature max-branches = 25 diff --git a/src/opendisplay/__init__.py b/src/opendisplay/__init__.py index 257f437..0f9268e 100644 --- a/src/opendisplay/__init__.py +++ b/src/opendisplay/__init__.py @@ -8,6 +8,7 @@ from .battery import voltage_to_percent from .device import OpenDisplayDevice, prepare_image from .discovery import discover_devices, discover_devices_with_adv +from .discovery_ip import IpDeviceInfo, discover_ip_devices from .exceptions import ( AuthenticationError, AuthenticationFailedError, @@ -20,7 +21,9 @@ InvalidResponseError, NfcNotSupportedError, NfcWriteError, + OpenDisplayConnectionError, OpenDisplayError, + OpenDisplayTimeoutError, OTAError, OTANotSupportedError, ProtocolError, @@ -87,6 +90,7 @@ from .ota import find_nrf_dfu_device, perform_nrf_dfu, perform_silabs_ota from .partial import PartialState from .protocol import MANUFACTURER_ID, SERVICE_UUID +from .transport import BleTransport, TcpTransport, Transport __version__ = "0.1.0" @@ -95,10 +99,18 @@ "OpenDisplayDevice", "discover_devices", "discover_devices_with_adv", + "discover_ip_devices", + "IpDeviceInfo", "prepare_image", "PartialState", + # Transports + "Transport", + "TcpTransport", + "BleTransport", # Exceptions "OpenDisplayError", + "OpenDisplayConnectionError", + "OpenDisplayTimeoutError", "AuthenticationError", "AuthenticationFailedError", "AuthenticationRequiredError", diff --git a/src/opendisplay/cli.py b/src/opendisplay/cli.py index a5d135b..28b12e9 100644 --- a/src/opendisplay/cli.py +++ b/src/opendisplay/cli.py @@ -110,13 +110,29 @@ def _parse_compression_value(flag: str, value: str) -> float | str: return f -def _device_kwargs(device: str, key: bytes | None, timeout: float) -> dict[str, Any]: +def _device_kwargs( + device: str | None, + key: bytes | None, + timeout: float, + host: str | None = None, + port: int | None = None, + tls: bool = False, +) -> dict[str, Any]: """Build OpenDisplayDevice constructor kwargs from CLI args. - Detects MAC addresses (contains ':') and macOS UUIDs (36-char with 4 dashes) - vs. human-readable device names. + With ``--host`` the device is addressed over TCP/LAN (WiFi). Otherwise a + ``--device`` MAC address (contains ':') / macOS UUID (36-char, 4 dashes) or a + human-readable device name is used over BLE. """ kwargs: dict[str, Any] = {"timeout": timeout, "encryption_key": key} + if host is not None: + kwargs["host"] = host + if port is not None: + kwargs["port"] = port + kwargs["tls"] = tls + return kwargs + if not device: + _error("Provide --device (BLE) or --host (WiFi/LAN)") if ":" in device or (len(device) == 36 and device.count("-") == 4): kwargs["mac_address"] = device else: @@ -125,15 +141,28 @@ def _device_kwargs(device: str, key: bytes | None, timeout: float) -> dict[str, def _add_device_options(parser: argparse.ArgumentParser) -> None: - """Add shared --device, --key, --timeout options to a subcommand parser.""" - parser.add_argument("--device", required=True, metavar="ADDR", help="Device MAC address or name") + """Add shared device-addressing (--device / --host) + --key/--timeout options.""" + parser.add_argument("--device", metavar="ADDR", help="Device MAC address or name (BLE)") + parser.add_argument("--host", default=None, metavar="IP", help="Device IP/hostname (WiFi/LAN TCP transport)") + parser.add_argument( + "--port", + type=int, + default=None, + metavar="PORT", + help="TCP port for --host (default: 2446 plaintext / 2447 TLS)", + ) + parser.add_argument( + "--tls", + action="store_true", + help="Use TLS-PSK for the --host connection", + ) parser.add_argument("--key", default=None, metavar="HEX", help="Encryption key as 32 hex characters") parser.add_argument( "--timeout", type=float, default=10.0, metavar="SECS", - help="BLE timeout in seconds (default: 10.0)", + help="Connection timeout in seconds (default: 10.0)", ) @@ -171,7 +200,7 @@ def _spinner() -> Progress: def _add_scan_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: - p = subparsers.add_parser("scan", help="Scan for nearby OpenDisplay BLE devices") + p = subparsers.add_parser("scan", help="Scan for nearby OpenDisplay devices (BLE, or --lan for WiFi/mDNS)") p.add_argument( "--timeout", type=float, @@ -179,12 +208,57 @@ def _add_scan_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentPar metavar="SECS", help="Scan duration in seconds (default: 10.0)", ) + p.add_argument("--lan", action="store_true", help="Discover WiFi devices via mDNS instead of BLE") p.add_argument("--json", dest="output_json", action="store_true", help="Output results as JSON") p.set_defaults(func=_cmd_scan) def _cmd_scan(args: argparse.Namespace) -> None: - _run(_scan(args.timeout, args.output_json)) + if args.lan: + _run(_scan_lan(args.timeout, args.output_json)) + else: + _run(_scan(args.timeout, args.output_json)) + + +async def _scan_lan(timeout: float, output_json: bool) -> None: + from .discovery_ip import discover_ip_devices + + with _spinner() as progress: + progress.add_task(f"Browsing mDNS for {timeout:.0f}s...", total=None) + try: + devices = await discover_ip_devices(scan_seconds=timeout) + except RuntimeError as exc: + _error(str(exc)) + + if output_json: + rows = [ + { + "name": info.name, + "host": info.host, + "port": info.port, + "mac": info.mac, + "tls": info.tls, + "fw": info.fw, + "cm": info.cm, + } + for _, info in sorted(devices.items()) + ] + _stdout.print_json(json.dumps({"devices": rows})) + return + + if not devices: + _console.print("No WiFi OpenDisplay devices found.") + return + + table = Table(show_header=True) + table.add_column("Name") + table.add_column("Host") + table.add_column("Port") + table.add_column("MAC") + table.add_column("TLS") + for _, info in sorted(devices.items()): + table.add_row(info.name, info.host, str(info.port), info.mac or "—", "yes" if info.tls else "no") + _console.print(table) async def _scan(timeout: float, output_json: bool) -> None: @@ -399,7 +473,7 @@ def _add_info_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentPar def _cmd_info(args: argparse.Namespace) -> None: key = _parse_hex_key(args.key) - _run(_info(_device_kwargs(args.device, key, args.timeout), args.output_json)) + _run(_info(_device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls), args.output_json)) async def _info(device_kwargs: dict[str, Any], output_json: bool) -> None: @@ -561,7 +635,7 @@ def _cmd_upload(args: argparse.Namespace) -> None: gamut = _parse_compression_value("--gamut", args.gamut) _run( _upload( - _device_kwargs(args.device, key, args.timeout), + _device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls), args.image, _REFRESH_CHOICES[args.refresh_mode], _DITHER_CHOICES[args.dither_mode], @@ -683,7 +757,7 @@ def _add_reboot_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentP def _cmd_reboot(args: argparse.Namespace) -> None: key = _parse_hex_key(args.key) - _run(_reboot(_device_kwargs(args.device, key, args.timeout))) + _run(_reboot(_device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls))) async def _reboot(device_kwargs: dict[str, Any]) -> None: @@ -714,7 +788,7 @@ def _add_sleep_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentPa def _cmd_sleep(args: argparse.Namespace) -> None: key = _parse_hex_key(args.key) - _run(_sleep(_device_kwargs(args.device, key, args.timeout))) + _run(_sleep(_device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls))) async def _sleep(device_kwargs: dict[str, Any]) -> None: @@ -758,7 +832,7 @@ def _add_export_config_parser(subparsers: argparse._SubParsersAction[argparse.Ar def _cmd_export_config(args: argparse.Namespace) -> None: key = _parse_hex_key(args.key) output = args.output or _default_export_path(args.device) - _run(_export_config(_device_kwargs(args.device, key, args.timeout), output)) + _run(_export_config(_device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls), output)) async def _export_config(device_kwargs: dict[str, Any], output_path: str) -> None: @@ -785,7 +859,7 @@ def _add_write_config_parser(subparsers: argparse._SubParsersAction[argparse.Arg def _cmd_write_config(args: argparse.Namespace) -> None: key = _parse_hex_key(args.key) - _run(_write_config(_device_kwargs(args.device, key, args.timeout), args.input)) + _run(_write_config(_device_kwargs(args.device, key, args.timeout, args.host, args.port, args.tls), args.input)) async def _write_config(device_kwargs: dict[str, Any], input_path: str) -> None: diff --git a/src/opendisplay/device.py b/src/opendisplay/device.py index d55c2c1..9bcd477 100644 --- a/src/opendisplay/device.py +++ b/src/opendisplay/device.py @@ -17,6 +17,7 @@ from PIL import Image from .crypto import ( + aes_cmac, compute_challenge_response, compute_server_proof, decrypt_response, @@ -77,6 +78,7 @@ NFC_CHUNK_SIZE, NFC_INLINE_MAX, NFC_WRITE_MAX_TOTAL, + OD_LAN_TCP_PORT, PIPE_FLAG_PARTIAL, PIPE_FRAME_OVERHEAD, PIPE_MAX_RETX_FRACTION, @@ -137,7 +139,7 @@ unpack_command_code, validate_nfc_response, ) -from .transport import BLEConnection +from .transport import BLEConnection, TcpTransport, Transport if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -446,7 +448,13 @@ class OpenDisplayDevice: # pylint: disable=too-many-instance-attributes TIMEOUT_CONFIG_CHUNK = 2.0 # Subsequent config read chunks (interrogate) TIMEOUT_ACK = 5.0 # Command acknowledgments TIMEOUT_NFC_WRITE = 15.0 # NFC EEPROM commit (inline write / chunk end): slow I2C work - TIMEOUT_UNCOMPRESSED_DATA_ACK = 90.0 # Uncompressed DATA: bbepWriteData() blocks SPI on Spectra/ACeP (~60s max) + # DIRECT_WRITE DATA (0x0071), both protocols. The ACK follows the firmware's + # bbepWriteData() call, which blocks SPI for up to ~60s on Spectra/ACeP. The + # compressed path does that same blocking write plus an inflate — one 4KB frame + # expands to ~90KB of panel data — so it needs the budget at least as much as + # the uncompressed one. It previously fell through to TIMEOUT_ACK (5s), which + # made a healthy-but-slow transfer look dead mid-stream. + TIMEOUT_DIRECT_WRITE_DATA_ACK = 90.0 TIMEOUT_UNCOMPRESSED_END_ACK = 90.0 # Uncompressed END: some firmware variants refresh before replying (~60s max) TIMEOUT_COMPRESSED_END_ACK = 90.0 # Compressed END: decompression + full SPI write to IC (~60s on Spectra/ACeP) TIMEOUT_REFRESH = 90.0 # Display refresh (firmware spec: up to 60s) @@ -473,6 +481,11 @@ def __init__( mac_address: str | None = None, device_name: str | None = None, ble_device: BLEDevice | None = None, + transport: Transport | None = None, + host: str | None = None, + port: int | None = None, + tls: bool = False, + psk: bytes | None = None, config: GlobalConfig | None = None, capabilities: DeviceCapabilities | None = None, timeout: float = 10.0, @@ -486,10 +499,21 @@ def __init__( ): """Initialize OpenDisplay device. + Exactly one addressing mode must be given: an explicit ``transport``, a + TCP/LAN ``host``, or a BLE address (``mac_address`` or ``device_name``). + Args: mac_address: Device MAC address (mutually exclusive with device_name) device_name: Device name to resolve via BLE scan (mutually exclusive with mac_address) ble_device: Optional BLEDevice from HA bluetooth integration + transport: Explicit pre-built transport (BLE or TCP). Takes precedence + over host/mac addressing. + host: Device IP/hostname for a TCP/LAN connection (WiFi transport). + port: TCP port (default OD_LAN_TCP_PORT / 2446). TLS uses the derived + port; pass it explicitly here when connecting over TLS. + tls: Wrap the TCP connection in TLS-PSK (host mode only). Learned from + the mDNS ``tls`` TXT flag. + psk: Pre-shared key for the TLS-PSK handshake (host mode, tls=True). config: Optional full TLV config (skips interrogation) capabilities: Optional minimal device info (skips interrogation) timeout: BLE operation timeout in seconds (default: 10) @@ -505,17 +529,30 @@ def __init__( transfer entirely — legacy stop-and-wait only, no 0x0080 probe. Raises: - ValueError: If neither or both mac_address and device_name provided + ValueError: If not exactly one addressing mode is provided """ - # Validation: exactly one of mac_address or device_name must be provided + # Validation: exactly one addressing mode (explicit transport, TCP host, + # or a BLE address). These groups are mutually exclusive. + ble_addressed = bool(mac_address or device_name) + modes_given = sum((transport is not None, host is not None, ble_addressed)) + if modes_given == 0: + raise ValueError("Must provide an addressing mode: transport=, host=, mac_address=, or device_name=") + if modes_given > 1: + raise ValueError( + "Provide exactly one addressing mode: transport=, host=, or a BLE address " + "(mac_address=/device_name=) — not a combination" + ) if mac_address and device_name: raise ValueError("Provide either mac_address or device_name, not both") - if not mac_address and not device_name: - raise ValueError("Must provide either mac_address or device_name") # Store for resolution in __aenter__ self._mac_address_param = mac_address self._device_name = device_name + self._transport_param = transport + self._host = host + self._port = port if port is not None else OD_LAN_TCP_PORT + self._tls = tls + self._psk = psk self._discovery_timeout = discovery_timeout self._ble_device = ble_device self._timeout = timeout @@ -525,7 +562,7 @@ def __init__( # Will be set after resolution self.mac_address = mac_address or "" # Resolved in __aenter__ - self._connection: BLEConnection | None = None # Created after MAC resolution + self._connection: Transport | None = None # Created after MAC resolution self._config = config self._capabilities = capabilities @@ -556,7 +593,8 @@ def __init__( async def __aenter__(self) -> OpenDisplayDevice: """Connect and optionally interrogate device.""" - # Resolve device name to MAC address if needed + # Resolve device name to MAC address if needed (BLE addressing only; + # explicit-transport and host modes skip this block). if self._device_name: _LOGGER.debug("Resolving device name '%s' to MAC address", self._device_name) @@ -581,18 +619,44 @@ async def __aenter__(self) -> OpenDisplayDevice: # MAC was provided directly — validated non-empty in __init__ self.mac_address = self._mac_address_param or "" - # Create connection with resolved MAC - self._connection = BLEConnection( - self.mac_address, - self._ble_device, - self._timeout, - max_attempts=self._max_attempts, - use_services_cache=self._use_services_cache, - disconnected_callback=self._on_ble_disconnect, - ) + # Transport selection (mutually exclusive, validated in __init__): + # explicit transport= > host=/port= (TCP) > BLE address (default). + if self._transport_param is not None: + self._connection = self._transport_param + elif self._host is not None: + # The TLS-PSK key is NOT the master key: firmware deriveTlsPsk() uses + # AES-CMAC(master_key, "opendisplay-tls-psk") so the TLS channel never + # reuses the key that protects the app-layer AES-CCM session. Derive it + # here when the caller passed only encryption_key — handing OpenSSL an + # empty PSK fails the handshake locally as PSK_IDENTITY_NOT_FOUND. + psk = self._psk + if psk is None and self._tls and self._encryption_key: + psk = aes_cmac(self._encryption_key, b"opendisplay-tls-psk") + self._connection = TcpTransport( + self._host, + self._port, + timeout=self._timeout, + tls=self._tls, + psk=psk, + ) + else: + self._connection = BLEConnection( + self.mac_address, + self._ble_device, + self._timeout, + max_attempts=self._max_attempts, + use_services_cache=self._use_services_cache, + disconnected_callback=self._on_disconnect, + ) await self._conn.connect() + # Over TCP the device gates AES-CCM decrypt on origin: TLS frames are + # already-secure, plaintext frames are never decrypted. Running the + # app-layer challenge/response would double-encrypt (and hit the 154 B + # encrypted-chunk cap), so authentication is BLE-only. + is_tcp = isinstance(self._connection, TcpTransport) + # The link is now up with notifications registered. If any step below # raises, __aexit__ will NOT run (Python only calls it when __aenter__ # returns) — so we must tear the link down here or leak the connection @@ -601,8 +665,8 @@ async def __aenter__(self) -> OpenDisplayDevice: # CancelledError from an outer asyncio.timeout() (e.g. the HA config-flow # connection probe). try: - # Authenticate before any other commands if key provided - if self._encryption_key is not None: + # Authenticate before any other commands if key provided (BLE only). + if self._encryption_key is not None and not is_tcp: await self.authenticate(self._encryption_key) # Auto-interrogate if no config or capabilities provided @@ -634,8 +698,8 @@ async def __aexit__( self._clear_session() @property - def _conn(self) -> BLEConnection: - """Return active BLE connection, raising RuntimeError if not connected.""" + def _conn(self) -> Transport: + """Return the active transport, raising RuntimeError if not connected.""" if self._connection is None: raise RuntimeError("Device not connected") return self._connection @@ -670,8 +734,8 @@ def _clear_session(self) -> None: self._nonce_counter = 0 self._auth_time = None - def _on_ble_disconnect(self) -> None: - """Handle an unexpected BLE drop: forget the (now-dead) session and pipe state.""" + def _on_disconnect(self) -> None: + """Handle an unexpected link drop: forget the (now-dead) session and pipe state.""" _LOGGER.debug("Link to %s dropped; clearing session state", self.mac_address) self._clear_session() # All pipe negotiation/capability state is per-connection (Part 1 §1.1). @@ -1849,6 +1913,21 @@ def _update_partial_state( state.width, state.height = processed_image.size state.bytes_per_pixel = 1 + def _direct_write_chunk_size(self) -> int: + """Data bytes per 0x71 DIRECT_WRITE chunk for the current transport. + + Encrypted BLE session -> ENCRYPTED_CHUNK_SIZE (154); plain BLE -> + CHUNK_SIZE (230); a large-frame LAN transport -> ``max_frame - 2`` (the + 2-byte opcode header), e.g. 4092 for a 4094-byte payload / 4096-byte + wire frame. + """ + if self._session_key is not None: + return ENCRYPTED_CHUNK_SIZE + max_frame = self._conn.max_frame + if max_frame > DEFAULT_MAX_FRAME: + return max_frame - 2 + return CHUNK_SIZE + async def _send_partial_chunks( self, remaining: bytes, @@ -1862,14 +1941,14 @@ async def _send_partial_chunks( before the refresh started (firmware aborts the partial on such a NACK, so a subsequent full upload is safe). """ - chunk_size = ENCRYPTED_CHUNK_SIZE if self._session_key is not None else CHUNK_SIZE + chunk_size = self._direct_write_chunk_size() total_stream_bytes = len(stream_bytes) bytes_sent = total_stream_bytes - len(remaining) offset = 0 while offset < len(remaining): chunk = remaining[offset : offset + chunk_size] # Write Without Response; the per-chunk ACK read below keeps flow control. - await self._write(build_direct_write_data_command(chunk), response=False) + await self._write(build_direct_write_data_command(chunk, max_data_len=chunk_size), response=False) ack = await self._read(self.TIMEOUT_ACK) nack = parse_nack(ack) if nack is not None: @@ -2123,6 +2202,9 @@ async def _execute_upload( bool(display_cfg and display_cfg.supports_pipe_write) and self._max_queue_size > 1 and not (self._pipe_probed and not self._pipe_supported) + # PIPE_WRITE (0x0080-0x82) is forbidden on LAN (SECTION 9 rule 3): a + # large-frame transport streams via plain DIRECT_WRITE instead. + and self._conn.max_frame <= DEFAULT_MAX_FRAME ) if pipe_eligible: total_size = len(image_data) @@ -2190,10 +2272,12 @@ async def _execute_upload( auto_completed = False if use_compression: if remaining_compressed: - auto_completed = await self._send_data_chunks(remaining_compressed, progress_callback) + auto_completed = await self._send_data_chunks( + remaining_compressed, progress_callback, chunk_timeout=self.TIMEOUT_DIRECT_WRITE_DATA_ACK + ) else: auto_completed = await self._send_data_chunks( - image_data, progress_callback, chunk_timeout=self.TIMEOUT_UNCOMPRESSED_DATA_ACK + image_data, progress_callback, chunk_timeout=self.TIMEOUT_DIRECT_WRITE_DATA_ACK ) # 4. Send END (unless device auto-triggered refresh), then wait for 0x73 @@ -2241,19 +2325,23 @@ async def _send_data_chunks( Raises: ProtocolError: If device responds with an unexpected code BLETimeoutError: If no response within chunk_timeout + + ``chunk_timeout`` defaults to the DATA-ACK budget rather than the general + TIMEOUT_ACK: every 0x0071 ACK waits on a blocking panel write, so the 5s + command-ack budget is never right here. """ - timeout = chunk_timeout if chunk_timeout is not None else self.TIMEOUT_ACK + timeout = chunk_timeout if chunk_timeout is not None else self.TIMEOUT_DIRECT_WRITE_DATA_ACK bytes_sent = 0 chunks_sent = 0 + chunk_size = self._direct_write_chunk_size() while bytes_sent < len(image_data): - chunk_size = ENCRYPTED_CHUNK_SIZE if self._session_key is not None else CHUNK_SIZE chunk_data = image_data[bytes_sent : bytes_sent + chunk_size] # Send the data chunk without waiting for the ATT write confirmation # (Write Without Response). Flow control is preserved by the per-chunk # application ACK read below, so only one write is ever in flight. - await self._write(build_direct_write_data_command(chunk_data), response=False) + await self._write(build_direct_write_data_command(chunk_data, max_data_len=chunk_size), response=False) bytes_sent += len(chunk_data) chunks_sent += 1 @@ -2305,7 +2393,7 @@ async def _negotiate_pipe( Returns: PipeParams (effective, post-min-rule) on success, else None. """ - req_frame = DEFAULT_MAX_FRAME # HA GATT ceiling; also our client_max_frame + req_frame = self._conn.max_frame # transport frame ceiling; our client_max_frame (BLE=244) # The 0x0080 is the single pre-stream write; _write re-authenticates here # (once, never again mid-stream). await self._write( @@ -2377,7 +2465,7 @@ async def _negotiate_pipe_partial( _PipePartialEtagMismatch: NACK 0x05 — caller skips 0x76, goes full. _PipePartialRejected: NACK 0x06/0x07 — caller skips 0x76, goes full. """ - req_frame = DEFAULT_MAX_FRAME # HA GATT ceiling; also our client_max_frame + req_frame = self._conn.max_frame # transport frame ceiling; our client_max_frame (BLE=244) partial = PipePartialRequest(old_etag=old_etag, x=region.rx, y=region.ry, w=region.rw, h=region.rh) # The 0x0080 is the single pre-stream write; _write re-authenticates here # (once, never again mid-stream). diff --git a/src/opendisplay/discovery_ip.py b/src/opendisplay/discovery_ip.py new file mode 100644 index 0000000..2bcf070 --- /dev/null +++ b/src/opendisplay/discovery_ip.py @@ -0,0 +1,127 @@ +"""mDNS / DNS-SD discovery of WiFi (LAN) OpenDisplay devices. + +Browses the ``_opendisplay._tcp.local.`` service (protocol 2.2 SECTION 9) and +reads the device TXT record (``mac`` REQUIRED, ``tls`` REQUIRED, plus optional +``fw`` / ``cm`` / ``msd`` / ``id`` / ``pv``). The ``mac`` value is passed through +**raw** (lowercase-colon from the TXT); Home Assistant uppercases it to match the +BlueZ form it stores. + +Requires the optional ``zeroconf`` dependency (``pip install py-opendisplay[wifi]``). +The import is guarded so the core BLE package works without it installed. +""" + +from __future__ import annotations + +import asyncio +import logging +from dataclasses import dataclass + +from .protocol import OD_LAN_MDNS_SERVICE + +_LOGGER = logging.getLogger(__name__) + +# DNS-SD fully-qualified service type (the browse form is OD_LAN_MDNS_SERVICE). +_SERVICE_TYPE = f"{OD_LAN_MDNS_SERVICE}.local." + +_MISSING_ZEROCONF_MSG = ( + "WiFi/LAN discovery requires the 'zeroconf' package. Install it with: pip install py-opendisplay[wifi]" +) + + +@dataclass(frozen=True) +class IpDeviceInfo: + """A WiFi-reachable OpenDisplay device discovered via mDNS.""" + + name: str + host: str + port: int + mac: str | None # raw lowercase-colon from the TXT record; None if absent + tls: bool + msd: str | None = None + fw: str | None = None + cm: str | None = None + + +def _txt_str(properties: dict[bytes, bytes | None], key: str) -> str | None: + """Decode a TXT value to str, or None if absent/undecodable.""" + raw = properties.get(key.encode()) + if raw is None: + return None + try: + return raw.decode("utf-8") + except UnicodeDecodeError: + return None + + +def _txt_bool(properties: dict[bytes, bytes | None], key: str) -> bool: + """Interpret a TXT flag as boolean (``1`` / ``true`` / ``yes`` → True).""" + value = _txt_str(properties, key) + return value is not None and value.strip().lower() in ("1", "true", "yes") + + +async def discover_ip_devices(scan_seconds: float = 3.0) -> dict[str, IpDeviceInfo]: + """Discover WiFi OpenDisplay devices advertised via mDNS. + + Args: + scan_seconds: How long to browse for services before resolving them. + + Returns: + Mapping of friendly device name -> :class:`IpDeviceInfo`. + + Raises: + RuntimeError: If the optional ``zeroconf`` dependency is not installed. + """ + try: + from zeroconf import ServiceStateChange + from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf + except ImportError as err: # pragma: no cover - exercised only without extra + raise RuntimeError(_MISSING_ZEROCONF_MSG) from err + + found_names: set[str] = set() + + def _on_change( + _zeroconf: object, + _service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: + if state_change is ServiceStateChange.Added: + found_names.add(name) + + result: dict[str, IpDeviceInfo] = {} + aiozc = AsyncZeroconf() + try: + browser = AsyncServiceBrowser(aiozc.zeroconf, _SERVICE_TYPE, handlers=[_on_change]) + await asyncio.sleep(scan_seconds) + await browser.async_cancel() + + for service_name in found_names: + info = AsyncServiceInfo(_SERVICE_TYPE, service_name) + if not await info.async_request(aiozc.zeroconf, timeout=3000): + _LOGGER.debug("mDNS service %s did not resolve", service_name) + continue + addresses = info.parsed_scoped_addresses() + if not addresses or info.port is None: + _LOGGER.debug("mDNS service %s has no address/port", service_name) + continue + properties: dict[bytes, bytes | None] = info.properties + friendly = service_name.removesuffix("." + _SERVICE_TYPE) + if friendly == service_name: # unexpected suffix; fall back to first label + friendly = service_name.split(".", 1)[0] + device = IpDeviceInfo( + name=friendly, + host=addresses[0], + port=info.port, + mac=_txt_str(properties, "mac"), + tls=_txt_bool(properties, "tls"), + msd=_txt_str(properties, "msd"), + fw=_txt_str(properties, "fw"), + cm=_txt_str(properties, "cm"), + ) + result[friendly] = device + _LOGGER.debug("Discovered LAN device %s at %s:%d (tls=%s)", friendly, device.host, device.port, device.tls) + finally: + await aiozc.async_close() + + _LOGGER.info("LAN discovery complete: found %d device(s)", len(result)) + return result diff --git a/src/opendisplay/exceptions.py b/src/opendisplay/exceptions.py index 4ea9d65..4b7782c 100644 --- a/src/opendisplay/exceptions.py +++ b/src/opendisplay/exceptions.py @@ -9,13 +9,35 @@ class OpenDisplayError(Exception): pass -class BLEConnectionError(OpenDisplayError): +class OpenDisplayConnectionError(OpenDisplayError): + """Transport-neutral connection failure. + + Superclass for connection errors on any transport (BLE or TCP/LAN). Callers + that don't care which transport failed should catch this; BLE-specific code + can still catch the ``BLEConnectionError`` subclass. Named to avoid shadowing + the builtin ``ConnectionError``. + """ + + pass + + +class OpenDisplayTimeoutError(OpenDisplayError): + """Transport-neutral operation timeout. + + Superclass for read/connect timeouts on any transport. Named to avoid + shadowing the builtin ``TimeoutError``. + """ + + pass + + +class BLEConnectionError(OpenDisplayConnectionError): """BLE connection failed.""" pass -class BLETimeoutError(OpenDisplayError): +class BLETimeoutError(OpenDisplayTimeoutError): """Operation timed out.""" pass diff --git a/src/opendisplay/protocol/__init__.py b/src/opendisplay/protocol/__init__.py index f8a34d9..ba9aed8 100644 --- a/src/opendisplay/protocol/__init__.py +++ b/src/opendisplay/protocol/__init__.py @@ -16,6 +16,12 @@ NFC_SUB_WRITE_INLINE, NFC_SUB_WRITE_START, NFC_WRITE_MAX_TOTAL, + OD_LAN_MAX_FRAME, + OD_LAN_MAX_PAYLOAD, + OD_LAN_MDNS_SERVICE, + OD_LAN_READ_TIMEOUT_S, + OD_LAN_TCP_PORT, + OD_LAN_TLS_PORT, PIPE_FLAG_COMPRESSED, PIPE_FLAG_PARTIAL, PIPE_FRAME_OVERHEAD, @@ -111,6 +117,12 @@ "NFC_INLINE_MAX", "NFC_CHUNK_SIZE", "NFC_WRITE_MAX_TOTAL", + "OD_LAN_TCP_PORT", + "OD_LAN_TLS_PORT", + "OD_LAN_MAX_FRAME", + "OD_LAN_MAX_PAYLOAD", + "OD_LAN_MDNS_SERVICE", + "OD_LAN_READ_TIMEOUT_S", "build_nfc_write_inline_command", "build_nfc_write_start_command", "build_nfc_write_data_command", diff --git a/src/opendisplay/protocol/commands.py b/src/opendisplay/protocol/commands.py index bf4fc5c..572b9dc 100644 --- a/src/opendisplay/protocol/commands.py +++ b/src/opendisplay/protocol/commands.py @@ -53,6 +53,19 @@ class CommandCode(IntEnum): MANUFACTURER_ID = 0x2446 # 9286 decimal RESPONSE_HIGH_BIT_FLAG = 0x8000 # High bit set in response codes indicates ACK +# Network transport (LAN) constants — mirror opendisplay_protocol.h SECTION 9 +# (protocol 2.2). The plaintext port is the configured WifiConfig.server_port +# (this default when 0); the TLS-PSK port is derived as server_port + 1 +# (== OD_LAN_TCP_PORT + 1). Frames are [len:2 LE][payload]; the complete WIRE +# frame (prefix + payload) is capped at OD_LAN_MAX_FRAME, so valid payload length +# is 1..OD_LAN_MAX_PAYLOAD (0 invalid; > max MUST be rejected + connection dropped). +OD_LAN_TCP_PORT = 2446 # DEFAULT plaintext port +OD_LAN_TLS_PORT = 2447 # DEFAULT TLS-PSK port (== OD_LAN_TCP_PORT + 1) +OD_LAN_MAX_FRAME = 4096 # max WIRE frame: [len:2 LE] prefix + payload, inclusive +OD_LAN_MAX_PAYLOAD = OD_LAN_MAX_FRAME - 2 # max payload bytes (4094) after the prefix +OD_LAN_MDNS_SERVICE = "_opendisplay._tcp" # DNS-SD service; FQDN "_opendisplay._tcp.local." +OD_LAN_READ_TIMEOUT_S = 30 # idle timeout: server drops a client after this many idle seconds + # Chunking constants CHUNK_SIZE = 230 # Maximum data bytes per chunk (unencrypted) ENCRYPTED_CHUNK_SIZE = 154 # Maximum data bytes per chunk when session is active @@ -259,22 +272,26 @@ def build_direct_write_partial_start( return cmd + fixed + initial, remaining -def build_direct_write_data_command(chunk_data: bytes) -> bytes: +def build_direct_write_data_command(chunk_data: bytes, max_data_len: int = CHUNK_SIZE) -> bytes: """Build command to send image data chunk. Args: - chunk_data: Image data chunk (max CHUNK_SIZE bytes) + chunk_data: Image data chunk (max ``max_data_len`` bytes) + max_data_len: Maximum allowed chunk length. Defaults to ``CHUNK_SIZE`` + (230) for BLE. The LAN transport passes a larger cap (up to + ``OD_LAN_MAX_PAYLOAD - 2`` = 4092, keeping the wire frame within + ``OD_LAN_MAX_FRAME`` = 4096) so large TCP frames pass. Returns: Command bytes: 0x0071 + chunk_data Format: - [cmd:2][data:230] + [cmd:2][data:N] - cmd: 0x0071 (big-endian) - data: Image data chunk """ - if len(chunk_data) > CHUNK_SIZE: - raise ValueError(f"Chunk size {len(chunk_data)} exceeds maximum {CHUNK_SIZE}") + if len(chunk_data) > max_data_len: + raise ValueError(f"Chunk size {len(chunk_data)} exceeds maximum {max_data_len}") cmd = CommandCode.DIRECT_WRITE_DATA.to_bytes(2, byteorder="big") return cmd + chunk_data diff --git a/src/opendisplay/transport/__init__.py b/src/opendisplay/transport/__init__.py index 7f2780d..1ab10cb 100644 --- a/src/opendisplay/transport/__init__.py +++ b/src/opendisplay/transport/__init__.py @@ -1,7 +1,15 @@ -"""BLE transport layer.""" +"""Transport layer: BLE and TCP/LAN command/response links.""" +from .base import Transport from .connection import BLEConnection +from .ip import TcpTransport + +# Transport-neutral alias; BLEConnection stays importable under its own name. +BleTransport = BLEConnection __all__ = [ + "Transport", "BLEConnection", + "BleTransport", + "TcpTransport", ] diff --git a/src/opendisplay/transport/base.py b/src/opendisplay/transport/base.py new file mode 100644 index 0000000..db83023 --- /dev/null +++ b/src/opendisplay/transport/base.py @@ -0,0 +1,75 @@ +"""Transport protocol shared by BLE and TCP/LAN connections. + +Defines the structural interface (:class:`Transport`) that +:class:`~opendisplay.transport.connection.BLEConnection` and +:class:`~opendisplay.transport.ip.TcpTransport` both satisfy, so +:class:`~opendisplay.device.OpenDisplayDevice` can drive either without +knowing which link is underneath. Method names deliberately match the +existing ``BLEConnection`` API (``write_command`` / ``read_response`` / +``drain_notifications``) so call sites stay unchanged. +""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class Transport(Protocol): + """Structural interface for a command/response link to a device. + + A transport frames a stop-and-wait command stream: write one command, + read one response frame. It carries no protocol semantics (encryption, + upload chunking, etc.) — those live in ``OpenDisplayDevice``. + """ + + #: Maximum application payload the link can carry in a single frame. BLE is + #: capped at the HA GATT write ceiling (244); LAN allows up to 4094 + #: (a 4096-byte wire frame including the 2-byte length prefix). + max_frame: int + + #: Human-readable device name, if known (BLE advertised name); None on LAN. + device_name: str | None + + @property + def supports_write_without_response(self) -> bool: + """Whether a fire-and-forget (unacknowledged) write is available. + + True for BLE write-without-response; False for TCP (every write is + reliable). Declared read-only so a property-backed implementation + (BLEConnection) and a plain attribute (TcpTransport) both satisfy it. + """ + + @property + def is_connected(self) -> bool: + """Whether the link is currently up.""" + + async def connect(self) -> None: + """Establish the link. Raises on failure (transport-neutral errors).""" + + async def disconnect(self) -> None: + """Tear the link down. Best-effort; must not raise on a dead link.""" + + async def write_command(self, data: bytes, response: bool = True, drain_stale: bool = True) -> None: + """Send one command frame. + + Args: + data: Command bytes to send. + response: BLE only — request an acknowledged write. Accepted and + ignored by transports where every write is already reliable. + drain_stale: BLE only — discard queued stale frames first. Accepted + and ignored where there is no notification queue. + """ + + async def read_response(self, timeout: float = 5.0) -> bytes: + """Read exactly one response frame, or raise on timeout.""" + + def drain_notifications(self) -> int: + """Discard any buffered frames; return how many were dropped.""" + + async def clear_cache(self) -> bool: + """Clear any per-device GATT cache. + + Returns True if a cache was cleared, False if the transport has no + cache (e.g. TCP, which always returns False). + """ diff --git a/src/opendisplay/transport/connection.py b/src/opendisplay/transport/connection.py index 0e84394..ee95a11 100644 --- a/src/opendisplay/transport/connection.py +++ b/src/opendisplay/transport/connection.py @@ -33,8 +33,13 @@ class BLEConnection: - Service caching for faster reconnections - Context manager for automatic cleanup - Notification queue for response handling + + Satisfies the :class:`~opendisplay.transport.base.Transport` interface. """ + #: Application payload ceiling per frame — the HA native GATT write ceiling. + max_frame: int = 244 + def __init__( self, mac_address: str, @@ -437,3 +442,12 @@ async def read_response(self, timeout: float = 5.0) -> bytes: def is_connected(self) -> bool: """Check if currently connected to device.""" return self._client is not None and self._client.is_connected + + @property + def supports_write_without_response(self) -> bool: + """Whether the command characteristic advertises Write Without Response. + + Set during notification setup; used to safely opt 0x71 data chunks into + unacknowledged writes and fall back otherwise. + """ + return self._write_no_response_supported diff --git a/src/opendisplay/transport/ip.py b/src/opendisplay/transport/ip.py new file mode 100644 index 0000000..91da93e --- /dev/null +++ b/src/opendisplay/transport/ip.py @@ -0,0 +1,210 @@ +"""TCP/LAN transport for OpenDisplay devices (WiFi). + +Implements the SECTION 9 (protocol 2.2) LAN wire format: a length-prefixed +stream of ``[len:2 LE][payload]`` frames over a single TCP connection, plaintext +on ``OD_LAN_TCP_PORT`` (configured ``server_port``) or TLS-PSK on the derived +``server_port + 1``. One client at a time; request+response over the same pipe. + +Structurally satisfies :class:`~opendisplay.transport.base.Transport`. Unlike +BLE there is no notification queue and no GATT cache, so ``drain_notifications`` +is a no-op returning 0 and ``clear_cache`` a no-op returning False. Over TLS the +application-layer AES-CCM MUST NOT run (the device gates decrypt on origin), so +the caller (``OpenDisplayDevice``) skips authentication on this transport. +""" + +from __future__ import annotations + +import asyncio +import logging +import ssl +from contextlib import suppress + +from ..exceptions import OpenDisplayConnectionError, OpenDisplayTimeoutError +from ..protocol import OD_LAN_MAX_PAYLOAD + +_LOGGER = logging.getLogger(__name__) + +# PSK identity presented to the device during the TLS-PSK handshake. Matches the +# firmware's expected identity; the shared key itself is the ``psk`` argument. +_PSK_IDENTITY = "opendisplay" + +# Length prefix is a 2-byte little-endian unsigned integer. +_LEN_PREFIX = 2 + + +class TcpTransport: + """Length-prefixed TCP (optionally TLS-PSK) command/response link. + + Args: + host: Device IP address or hostname. + port: TCP port (plaintext ``server_port`` or the derived TLS port). + timeout: Connect / per-frame read timeout in seconds (default 10). + tls: When True, wrap the socket in TLS-PSK. Learn this from the mDNS + ``tls`` TXT flag — port numbers are configurable and unreliable. + psk: Pre-shared key bytes for the TLS-PSK handshake. Required when + ``tls=True``; ignored otherwise. + """ + + #: LAN frame payload ceiling — OD_LAN_MAX_PAYLOAD (4094), keeping the wire + #: frame (2-byte len prefix + payload) within OD_LAN_MAX_FRAME (4096). + max_frame: int = OD_LAN_MAX_PAYLOAD + #: TCP writes are always reliable; there is no write-without-response. + supports_write_without_response: bool = False + + def __init__( + self, + host: str, + port: int, + *, + timeout: float = 10.0, + tls: bool = False, + psk: bytes | None = None, + ) -> None: + self.host = host + self.port = port + self.timeout = timeout + self.tls = tls + self._psk = psk + # LAN has no advertised name; kept for Transport structural conformance. + self.device_name: str | None = None + + self._reader: asyncio.StreamReader | None = None + self._writer: asyncio.StreamWriter | None = None + + # ── connection lifecycle ──────────────────────────────────────────────── + + def _build_ssl_context(self) -> ssl.SSLContext: + """Build an ECDHE-PSK client context. + + PSK is the sole authentication (no certificates), so hostname/cert + verification is disabled. The device serves a TLS-1.2-style ECDHE-PSK + ciphersuite (mbedTLS), so the version is pinned to 1.2 where + ``set_psk_client_callback`` + PSK ciphers negotiate cleanly. + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.maximum_version = ssl.TLSVersion.TLSv1_2 + # Restrict to PSK ciphersuites (includes ECDHE-PSK) so the handshake + # never expects a certificate. + with suppress(ssl.SSLError): + context.set_ciphers("PSK") + psk = self._psk or b"" + + def _psk_client_callback(_hint: str | None) -> tuple[str, bytes]: + return (_PSK_IDENTITY, psk) + + context.set_psk_client_callback(_psk_client_callback) + return context + + async def connect(self) -> None: + """Open the TCP (and optionally TLS) connection. + + Raises: + OpenDisplayConnectionError: on connect / TLS handshake failure. + OpenDisplayTimeoutError: if the connect exceeds ``timeout``. + """ + if self.is_connected: + return + + ssl_context = self._build_ssl_context() if self.tls else None + try: + self._reader, self._writer = await asyncio.wait_for( + asyncio.open_connection(self.host, self.port, ssl=ssl_context), + timeout=self.timeout, + ) + except asyncio.TimeoutError as err: + raise OpenDisplayTimeoutError( + f"Connection to {self.host}:{self.port} timed out after {self.timeout}s" + ) from err + except (OSError, ssl.SSLError) as err: + raise OpenDisplayConnectionError(f"Failed to connect to {self.host}:{self.port}: {err}") from err + _LOGGER.debug("Connected to %s:%d (tls=%s)", self.host, self.port, self.tls) + + async def disconnect(self) -> None: + """Close the connection. Best-effort; never raises.""" + writer = self._writer + self._reader = None + self._writer = None + if writer is None: + return + try: + writer.close() + # A half-open/aborted TLS or TCP link raises OSError on close; the + # link is going away regardless, so swallow it. + with suppress(OSError, ssl.SSLError, asyncio.TimeoutError): + await asyncio.wait_for(writer.wait_closed(), timeout=self.timeout) + except Exception as err: # noqa: BLE001 - best-effort teardown + _LOGGER.debug("Error during TCP disconnect: %s", err) + + @property + def is_connected(self) -> bool: + """Whether the link is up (writer present and not closing).""" + return self._writer is not None and not self._writer.is_closing() + + # ── framing ───────────────────────────────────────────────────────────── + + async def write_command( # pylint: disable=unused-argument + self, data: bytes, response: bool = True, drain_stale: bool = True + ) -> None: + """Send one ``[len:2 LE][payload]`` frame. + + ``response`` and ``drain_stale`` are accepted for Transport conformance + and ignored: every TCP write is reliable and there is no stale queue. + """ + if self._writer is None: + raise OpenDisplayConnectionError("Not connected") + if not 0 < len(data) <= self.max_frame: + raise ValueError(f"Frame payload length {len(data)} out of range (1..{self.max_frame})") + frame = len(data).to_bytes(_LEN_PREFIX, "little") + data + try: + self._writer.write(frame) + await self._writer.drain() + except (OSError, ssl.SSLError) as err: + raise OpenDisplayConnectionError(f"Write failed: {err}") from err + + async def read_response(self, timeout: float = 5.0) -> bytes: + """Read exactly one length-prefixed frame. + + Header and body get independent timeouts: once a length prefix arrives + the peer is committed to sending that many bytes, so the body read gets + a fresh deadline. A truncated frame (peer closed mid-body) surfaces as a + connection error rather than a timeout. + + Raises: + OpenDisplayTimeoutError: no frame within ``timeout``. + OpenDisplayConnectionError: link closed, or a protocol violation + (zero-length or oversize frame — the connection is dropped). + """ + if self._reader is None: + raise OpenDisplayConnectionError("Not connected") + reader = self._reader + try: + header = await asyncio.wait_for(reader.readexactly(_LEN_PREFIX), timeout=timeout) + except asyncio.TimeoutError as err: + raise OpenDisplayTimeoutError(f"No response within {timeout}s") from err + except (asyncio.IncompleteReadError, OSError, ssl.SSLError) as err: + raise OpenDisplayConnectionError(f"Connection closed while reading frame header: {err}") from err + + length = int.from_bytes(header, "little") + if length == 0 or length > OD_LAN_MAX_PAYLOAD: + # Protocol error: drop the connection (SECTION 9 rule). + await self.disconnect() + raise OpenDisplayConnectionError(f"Invalid LAN frame length {length} (expected 1..{OD_LAN_MAX_PAYLOAD})") + + try: + payload = await asyncio.wait_for(reader.readexactly(length), timeout=timeout) + except asyncio.TimeoutError as err: + raise OpenDisplayTimeoutError(f"Frame body ({length} bytes) not received within {timeout}s") from err + except (asyncio.IncompleteReadError, OSError, ssl.SSLError) as err: + raise OpenDisplayConnectionError(f"Connection closed mid-frame ({length} bytes expected): {err}") from err + return payload + + def drain_notifications(self) -> int: + """No notification queue on TCP; nothing to drain.""" + return 0 + + async def clear_cache(self) -> bool: + """TCP has no GATT cache.""" + return False diff --git a/tests/conftest.py b/tests/conftest.py index 18123a6..c7c342e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,8 @@ """Pytest configuration and fixtures.""" +from __future__ import annotations + +from collections import deque from pathlib import Path import pytest @@ -9,6 +12,78 @@ FIXTURES_DIR = Path(__file__).parent / "fixtures/real_protocol_data" +class FakeTransport: + """In-memory Transport implementation for driving device flows without bleak. + + Replays a scripted list of response frames (bytes, or an Exception + class/instance to raise) and records every written command. Structurally + satisfies ``opendisplay.transport.Transport``, so it can back PIPE/crypto/ + upload suites without a real BLE or TCP link. + """ + + def __init__( + self, + responses: list | None = None, + *, + max_frame: int = 244, + supports_write_without_response: bool = True, + device_name: str | None = "FakeDevice", + ) -> None: + self.max_frame = max_frame + self.supports_write_without_response = supports_write_without_response + self.device_name = device_name + self.written: list[bytes] = [] + self.write_responses: list[bool] = [] + self.drain_flags: list[bool] = [] + self.timeouts: list[float] = [] + self._responses: deque = deque(responses or []) + self._connected = False + self.drained = 0 + + async def connect(self) -> None: + self._connected = True + + async def disconnect(self) -> None: + self._connected = False + + @property + def is_connected(self) -> bool: + return self._connected + + async def write_command(self, data: bytes, response: bool = True, drain_stale: bool = True) -> None: + self.written.append(data) + self.write_responses.append(response) + self.drain_flags.append(drain_stale) + + async def read_response(self, timeout: float = 5.0) -> bytes: + self.timeouts.append(timeout) + if not self._responses: + raise RuntimeError("FakeTransport: no responses left") + item = self._responses.popleft() + if isinstance(item, type) and issubclass(item, BaseException): + raise item("scripted") + if isinstance(item, BaseException): + raise item + return item + + def drain_notifications(self) -> int: + self.drained += 1 # record the call; scripted responses are never dropped + return 0 + + async def clear_cache(self) -> bool: + return False + + +@pytest.fixture +def fake_transport(): + """Factory fixture returning a configured :class:`FakeTransport`.""" + + def _make(responses: list | None = None, **kwargs: object) -> FakeTransport: + return FakeTransport(responses, **kwargs) + + return _make + + @pytest.fixture def small_test_image(): """Create a small RGB test image for encoding tests.""" diff --git a/tests/unit/test_device_command_serialization.py b/tests/unit/test_device_command_serialization.py index 089e27f..0fdd3b9 100644 --- a/tests/unit/test_device_command_serialization.py +++ b/tests/unit/test_device_command_serialization.py @@ -64,12 +64,12 @@ def test_clear_session_resets_all_session_state() -> None: assert device._auth_time is None -def test_on_ble_disconnect_clears_session() -> None: +def test_on_disconnect_clears_session() -> None: device = _make_device() device._session_key = b"k" * 16 device._nonce_counter = 9 - device._on_ble_disconnect() + device._on_disconnect() assert device._session_key is None assert device._nonce_counter == 0 diff --git a/tests/unit/test_device_partial.py b/tests/unit/test_device_partial.py index 98fdc63..5852c35 100644 --- a/tests/unit/test_device_partial.py +++ b/tests/unit/test_device_partial.py @@ -142,6 +142,7 @@ async def capture_write(data: bytes) -> None: async def read_response(timeout: float) -> bytes: return responses.pop(0) + device._connection = ScriptedConn([]) # supplies max_frame for chunk sizing monkeypatch.setattr(device, "_write", capture_write) monkeypatch.setattr(device, "_read", read_response) @@ -299,6 +300,7 @@ async def read_response(timeout: float) -> bytes: async def fail_full_upload(*args, **kwargs) -> None: raise AssertionError("partial request unexpectedly fell back to full upload") + device._connection = ScriptedConn([]) # supplies max_frame for chunk sizing monkeypatch.setattr(device, "_write", capture_write) monkeypatch.setattr(device, "_read", read_response) monkeypatch.setattr(device, "_execute_upload", fail_full_upload) diff --git a/tests/unit/test_device_upload_flow.py b/tests/unit/test_device_upload_flow.py index 74337ee..7a1a210 100644 --- a/tests/unit/test_device_upload_flow.py +++ b/tests/unit/test_device_upload_flow.py @@ -44,6 +44,8 @@ class _FakeConnection: """Replays scripted responses; records written commands and read timeouts.""" + max_frame: int = 244 # Transport structural conformance (BLE GATT ceiling) + def __init__(self, responses: list[bytes]) -> None: self.written: list[bytes] = [] self.write_responses: list[bool] = [] @@ -190,7 +192,7 @@ async def test_uncompressed_data_chunks_use_90s_timeout() -> None: device._connection = fake await device._execute_upload(image_data, RefreshMode.FULL, use_compression=False) assert fake.timeouts[0] == device.TIMEOUT_FIRST_CHUNK - assert fake.timeouts[1] == device.TIMEOUT_UNCOMPRESSED_DATA_ACK + assert fake.timeouts[1] == device.TIMEOUT_DIRECT_WRITE_DATA_ACK assert fake.timeouts[2] == device.TIMEOUT_UNCOMPRESSED_END_ACK assert fake.timeouts[3] == device.TIMEOUT_REFRESH @@ -250,8 +252,14 @@ async def test_compressed_end_ack_uses_90s_timeout() -> None: @pytest.mark.asyncio -async def test_compressed_data_chunk_acks_use_5s_timeout() -> None: - """Compressed DATA chunk ACKs use TIMEOUT_ACK (5s) — data is buffered, not written to SPI.""" +async def test_compressed_data_chunk_acks_use_same_timeout_as_uncompressed() -> None: + """Compressed DATA chunk ACKs get the full 90s DATA budget, same as uncompressed. + + Regression: they used to fall through to TIMEOUT_ACK (5s) on the theory that a + compressed chunk is only buffered. It is not — firmware inflates it and streams + the result to the panel inside the ACK, so one 4KB frame can mean ~90KB of + blocking SPI. The 5s budget aborted healthy transfers mid-stream. + """ image_data = b"\x00" * 500 # 300B > 194B (MAX_START_PAYLOAD - 6) → START gets 194B, remaining 106B sent as DATA chunk compressed = b"\xff" * 300 @@ -265,8 +273,8 @@ async def test_compressed_data_chunk_acks_use_5s_timeout() -> None: compressed_data=compressed, uncompressed_size=len(image_data), ) - # Timeouts: [FIRST_CHUNK, ACK (data), COMPRESSED_END_ACK, REFRESH] - assert fake.timeouts[1] == device.TIMEOUT_ACK + # Timeouts: [FIRST_CHUNK, DIRECT_WRITE_DATA_ACK, COMPRESSED_END_ACK, REFRESH] + assert fake.timeouts[1] == device.TIMEOUT_DIRECT_WRITE_DATA_ACK assert fake.timeouts[2] == device.TIMEOUT_COMPRESSED_END_ACK @@ -381,8 +389,8 @@ async def test_after_fallback_data_chunks_use_uncompressed_timeout() -> None: compressed_data=compressed, uncompressed_size=len(image_data), ) - # timeouts: [FIRST_CHUNK(err), FIRST_CHUNK(retry), UNCOMPRESSED_DATA_ACK, ACK, REFRESH] - assert device.TIMEOUT_UNCOMPRESSED_DATA_ACK in fake.timeouts + # timeouts: [FIRST_CHUNK(err), FIRST_CHUNK(retry), DIRECT_WRITE_DATA_ACK, ACK, REFRESH] + assert device.TIMEOUT_DIRECT_WRITE_DATA_ACK in fake.timeouts # ─── _dispatch_upload: ZIPXL zlib window and size semantics ────────────────── diff --git a/tests/unit/test_pipe_write.py b/tests/unit/test_pipe_write.py index d9b313b..b5ffc7e 100644 --- a/tests/unit/test_pipe_write.py +++ b/tests/unit/test_pipe_write.py @@ -107,6 +107,8 @@ class ScriptedConn: count at each read (for window-invariant assertions). """ + max_frame: int = 244 # Transport structural conformance (BLE GATT ceiling) + def __init__(self, responses: list) -> None: self.written: list[bytes] = [] self.write_responses: list[bool] = [] @@ -691,7 +693,7 @@ async def test_disconnect_resets_pipe_cache() -> None: dev._pipe_probed = True dev._pipe_supported = False dev._pipe_params = PipeParams(8, 4, 244, True, False) - dev._on_ble_disconnect() + dev._on_disconnect() assert dev._pipe_probed is False assert dev._pipe_supported is False assert dev._pipe_params is None diff --git a/tests/unit/test_transport_ip.py b/tests/unit/test_transport_ip.py new file mode 100644 index 0000000..b743656 --- /dev/null +++ b/tests/unit/test_transport_ip.py @@ -0,0 +1,193 @@ +"""Tests for the TCP/LAN transport framer and lifecycle (TcpTransport). + +The [len:2 LE][payload] framer is exercised two ways: by injecting an +``asyncio.StreamReader`` (precise control over partial / coalesced / truncated +byte streams) and by a real loopback asyncio server (end-to-end open_connection ++ write + read). +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from opendisplay.exceptions import OpenDisplayConnectionError, OpenDisplayTimeoutError +from opendisplay.protocol import OD_LAN_MAX_PAYLOAD +from opendisplay.transport.ip import TcpTransport + + +class _FakeWriter: + """Minimal StreamWriter stand-in recording written bytes.""" + + def __init__(self) -> None: + self.buffer = bytearray() + self.closed = False + + def write(self, data: bytes) -> None: + self.buffer.extend(data) + + async def drain(self) -> None: + return None + + def close(self) -> None: + self.closed = True + + async def wait_closed(self) -> None: + return None + + def is_closing(self) -> bool: + return self.closed + + +def _framed(payload: bytes) -> bytes: + return len(payload).to_bytes(2, "little") + payload + + +def _wire(reader: asyncio.StreamReader) -> TcpTransport: + t = TcpTransport("127.0.0.1", 2446) + t._reader = reader + t._writer = _FakeWriter() # type: ignore[assignment] + return t + + +# ── write framing ──────────────────────────────────────────────────────────── + + +async def test_write_command_prepends_little_endian_length() -> None: + t = _wire(asyncio.StreamReader()) + await t.write_command(b"\x00\x40hello") + assert bytes(t._writer.buffer) == _framed(b"\x00\x40hello") # type: ignore[union-attr] + + +async def test_write_command_rejects_empty_and_oversize() -> None: + t = _wire(asyncio.StreamReader()) + with pytest.raises(ValueError): + await t.write_command(b"") + with pytest.raises(ValueError): + await t.write_command(b"\x00" * (OD_LAN_MAX_PAYLOAD + 1)) + + +# ── read framing ───────────────────────────────────────────────────────────── + + +async def test_read_single_frame() -> None: + reader = asyncio.StreamReader() + reader.feed_data(_framed(b"\x00\x71")) + t = _wire(reader) + assert await t.read_response(timeout=1.0) == b"\x00\x71" + + +async def test_read_coalesced_frames() -> None: + reader = asyncio.StreamReader() + reader.feed_data(_framed(b"AAA") + _framed(b"BBBB")) + t = _wire(reader) + assert await t.read_response(timeout=1.0) == b"AAA" + assert await t.read_response(timeout=1.0) == b"BBBB" + + +async def test_read_partial_frame_reassembles() -> None: + reader = asyncio.StreamReader() + t = _wire(reader) + payload = b"partial-body" + frame = _framed(payload) + + async def dribble() -> None: + for byte in frame: + reader.feed_data(bytes([byte])) + await asyncio.sleep(0) + + feeder = asyncio.create_task(dribble()) + assert await t.read_response(timeout=1.0) == payload + await feeder + + +async def test_read_large_max_payload_frame() -> None: + reader = asyncio.StreamReader() + payload = (bytes(range(256)) * 16)[:OD_LAN_MAX_PAYLOAD] # exactly 4094 bytes + assert len(payload) == OD_LAN_MAX_PAYLOAD + reader.feed_data(_framed(payload)) + t = _wire(reader) + assert await t.read_response(timeout=1.0) == payload + + +async def test_zero_length_frame_is_protocol_error_and_disconnects() -> None: + reader = asyncio.StreamReader() + reader.feed_data((0).to_bytes(2, "little")) + t = _wire(reader) + with pytest.raises(OpenDisplayConnectionError): + await t.read_response(timeout=1.0) + assert not t.is_connected # connection dropped on protocol violation + + +async def test_oversize_frame_is_protocol_error() -> None: + reader = asyncio.StreamReader() + reader.feed_data((OD_LAN_MAX_PAYLOAD + 1).to_bytes(2, "little")) + t = _wire(reader) + with pytest.raises(OpenDisplayConnectionError): + await t.read_response(timeout=1.0) + + +async def test_read_timeout() -> None: + reader = asyncio.StreamReader() # never fed + t = _wire(reader) + with pytest.raises(OpenDisplayTimeoutError): + await t.read_response(timeout=0.02) + + +async def test_truncated_body_raises_connection_error() -> None: + reader = asyncio.StreamReader() + reader.feed_data((10).to_bytes(2, "little") + b"abc") # promises 10, sends 3 + reader.feed_eof() + t = _wire(reader) + with pytest.raises(OpenDisplayConnectionError): + await t.read_response(timeout=1.0) + + +async def test_read_when_not_connected_raises() -> None: + t = TcpTransport("127.0.0.1", 2446) + with pytest.raises(OpenDisplayConnectionError): + await t.read_response(timeout=0.1) + + +# ── end-to-end loopback ────────────────────────────────────────────────────── + + +async def test_roundtrip_against_real_server() -> None: + received: list[bytes] = [] + + async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + header = await reader.readexactly(2) + length = int.from_bytes(header, "little") + body = await reader.readexactly(length) + received.append(body) + # Echo a distinct framed response. + writer.write(_framed(b"\x00\x71")) + await writer.drain() + + server = await asyncio.start_server(handle, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + async with server: + t = TcpTransport("127.0.0.1", port, timeout=2.0) + await t.connect() + assert t.is_connected + await t.write_command(b"\x00\x40ping") + assert await t.read_response(timeout=2.0) == b"\x00\x71" + await t.disconnect() + assert not t.is_connected + + assert received == [b"\x00\x40ping"] + + +async def test_connect_refused_raises_connection_error() -> None: + # Port 1 is privileged and closed; connect must fail fast with a neutral error. + t = TcpTransport("127.0.0.1", 1, timeout=1.0) + with pytest.raises(OpenDisplayConnectionError): + await t.connect() + + +def test_class_attributes() -> None: + t = TcpTransport("h", 2446) + assert t.max_frame == OD_LAN_MAX_PAYLOAD + assert t.supports_write_without_response is False + assert t.device_name is None diff --git a/tests/unit/test_transport_selection.py b/tests/unit/test_transport_selection.py new file mode 100644 index 0000000..62db6f4 --- /dev/null +++ b/tests/unit/test_transport_selection.py @@ -0,0 +1,165 @@ +"""Transport selection + TCP auth-gating in OpenDisplayDevice. + +Covers the __init__ mutual-exclusion rules and the __aenter__ selection order +(explicit transport > host > BLE), plus the critical no-double-encrypt gate: +over TCP the app-layer AUTHENTICATE (0x0050) must never run. +""" + +from __future__ import annotations + +import pytest +from epaper_dithering import ColorScheme + +import opendisplay.device as device_mod +from opendisplay import OpenDisplayDevice +from opendisplay.models.capabilities import DeviceCapabilities +from opendisplay.transport.ip import TcpTransport + + +class _SpyTcp(TcpTransport): + """Real TcpTransport subclass with an in-memory link (so isinstance holds).""" + + def __init__(self, host: str, port: int, *, timeout: float = 10.0, tls: bool = False, psk: bytes | None = None): + super().__init__(host, port, timeout=timeout, tls=tls, psk=psk) + self.connected = False + self.written: list[bytes] = [] + + async def connect(self) -> None: + self.connected = True + + async def disconnect(self) -> None: + self.connected = False + + @property + def is_connected(self) -> bool: + return self.connected + + async def write_command(self, data: bytes, response: bool = True, drain_stale: bool = True) -> None: + self.written.append(data) + + +class _SpyBle: + """Minimal BLE-connection stand-in (records construction + writes).""" + + max_frame = 244 + supports_write_without_response = False + device_name = "ble" + + def __init__(self, *args: object, **kwargs: object) -> None: + self.args = args + self.kwargs = kwargs + self.connected = False + self.written: list[bytes] = [] + + async def connect(self) -> None: + self.connected = True + + async def disconnect(self) -> None: + self.connected = False + + @property + def is_connected(self) -> bool: + return self.connected + + async def write_command(self, data: bytes, response: bool = True, drain_stale: bool = True) -> None: + self.written.append(data) + + +def _caps() -> DeviceCapabilities: + return DeviceCapabilities(width=8, height=8, color_scheme=ColorScheme.MONO) + + +# ── __init__ mutual exclusion ──────────────────────────────────────────────── + + +def test_requires_an_addressing_mode() -> None: + with pytest.raises(ValueError): + OpenDisplayDevice() + + +@pytest.mark.parametrize( + "kwargs", + [ + {"mac_address": "AA:BB:CC:DD:EE:FF", "host": "1.2.3.4"}, + {"mac_address": "AA:BB:CC:DD:EE:FF", "transport": object()}, + {"host": "1.2.3.4", "transport": object()}, + {"device_name": "tag", "host": "1.2.3.4"}, + ], +) +def test_combined_addressing_modes_rejected(kwargs: dict) -> None: + with pytest.raises(ValueError): + OpenDisplayDevice(**kwargs) + + +def test_mac_and_name_still_mutually_exclusive() -> None: + with pytest.raises(ValueError): + OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF", device_name="tag") + + +# ── __aenter__ selection order ─────────────────────────────────────────────── + + +async def test_host_builds_tcp_transport(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(device_mod, "TcpTransport", _SpyTcp) + dev = OpenDisplayDevice(host="10.0.0.5", port=2447, tls=True, psk=b"k", capabilities=_caps()) + async with dev as connected: + conn = connected._conn + assert isinstance(conn, _SpyTcp) + assert conn.host == "10.0.0.5" + assert conn.port == 2447 + assert conn.tls is True + + +async def test_mac_builds_ble_connection(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(device_mod, "BLEConnection", _SpyBle) + dev = OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF", capabilities=_caps()) + async with dev as connected: + assert isinstance(connected._conn, _SpyBle) + + +async def test_explicit_transport_wins(fake_transport) -> None: + fake = fake_transport() + dev = OpenDisplayDevice(transport=fake, capabilities=_caps()) + async with dev as connected: + assert connected._conn is fake + assert fake.is_connected + + +def test_default_port_is_2446() -> None: + dev = OpenDisplayDevice(host="1.2.3.4") + assert dev._port == 2446 + + +# ── no-double-encrypt gate ─────────────────────────────────────────────────── + + +async def test_tcp_with_key_does_not_authenticate(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(device_mod, "TcpTransport", _SpyTcp) + calls: list[bytes] = [] + + async def _spy_auth(self: OpenDisplayDevice, key: bytes) -> None: + calls.append(key) + + monkeypatch.setattr(OpenDisplayDevice, "authenticate", _spy_auth) + dev = OpenDisplayDevice(host="10.0.0.5", encryption_key=b"0123456789abcdef", capabilities=_caps()) + async with dev as connected: + conn = connected._conn + assert isinstance(conn, _SpyTcp) + # The app-layer challenge/response must not run over TCP (TLS gates it). + assert calls == [] + assert all(w[:2] != b"\x00\x50" for w in conn.written) + assert connected._session_key is None + + +async def test_ble_with_key_authenticates(monkeypatch: pytest.MonkeyPatch) -> None: + """Positive control: the BLE path DOES invoke authenticate for the same key.""" + monkeypatch.setattr(device_mod, "BLEConnection", _SpyBle) + calls: list[bytes] = [] + + async def _spy_auth(self: OpenDisplayDevice, key: bytes) -> None: + calls.append(key) + + monkeypatch.setattr(OpenDisplayDevice, "authenticate", _spy_auth) + dev = OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF", encryption_key=b"0123456789abcdef", capabilities=_caps()) + async with dev: + assert calls == [b"0123456789abcdef"] diff --git a/uv.lock b/uv.lock index c5bc379..be81634 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", - "python_full_version < '3.12'", + "python_full_version < '3.15'", ] [[package]] @@ -72,7 +71,6 @@ dependencies = [ { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, { name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" }, { name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "winrt-runtime", marker = "sys_platform == 'win32'" }, { name = "winrt-windows-devices-bluetooth", marker = "sys_platform == 'win32'" }, { name = "winrt-windows-devices-bluetooth-advertisement", marker = "sys_platform == 'win32'" }, @@ -127,31 +125,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, @@ -203,36 +176,6 @@ version = "7.13.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, @@ -296,11 +239,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cryptography" version = "48.0.0" @@ -352,12 +290,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, ] [[package]] @@ -366,14 +298,6 @@ version = "4.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5f/cd/402b0e524bdf37d8b1d22b1d926c538bf1d2eedf115ea1d401c6c08a7d81/dbus_fast-4.0.4.tar.gz", hash = "sha256:43137f0b73a7adbf7d5c0e9eb9d8d34df9e6e0aeafade2166e641c52dfe0a853", size = 75260, upload-time = "2026-04-02T04:41:16.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/fc/e57373d034fbca2d51a4d2a3e13c64e008f2728ae3b650dc2c2f009f2c7e/dbus_fast-4.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf5109bbc98c8427f3cc22030de55e57a7288550bc0701a493905bbad3b3dc93", size = 829986, upload-time = "2026-04-02T04:49:53.356Z" }, - { url = "https://files.pythonhosted.org/packages/be/98/c23f8ef850d3c67c7d9e36d5c2f71a6654baad2a5079c5e7b2d7d085c692/dbus_fast-4.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:baca8ffed5a5b18cce9c1f17820cc699512bc97ac0743b8b88c61159aa0c961a", size = 875581, upload-time = "2026-04-02T04:49:55.112Z" }, - { url = "https://files.pythonhosted.org/packages/ad/65/3a605f3d6e7f97b9744795c02d496805057cd2b5414619870a68a21bfc57/dbus_fast-4.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb570c6c8dac1ee2ca02621c0d410ec78d1a96535deee8a34e3028f475be7675", size = 835865, upload-time = "2026-04-02T04:49:56.624Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/05d503fe790a5aeae09ed32a85f25b0d4fa93a10befac3b603d6247a4e90/dbus_fast-4.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a2ee216e0ddd7b4db397bb31ff71741f6c7fc9618d9ca201e0096d9523b4f33", size = 881676, upload-time = "2026-04-02T04:49:58.36Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/86b4b7057d11af1549135234612e40401c69f339551a1c0cd011439a6b8b/dbus_fast-4.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00a3db08cf1d98bbedfb73467b774fd49adcf83935b8a92f6c552ce78d93491f", size = 792151, upload-time = "2026-04-02T04:50:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/95/e5/056a5845b19202336caed7d3b18896c75ec059b68c16f4ded71749c9e0a2/dbus_fast-4.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74183b4ef4cc79a9cd1a46e006c19d00ce9abc7d99d36e6adcd094f414446d38", size = 844081, upload-time = "2026-04-02T04:50:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/25/03/aa1ef750da5f4cfd15eac9892c05ff95b6380cdc44d5bb63b8379c63e81f/dbus_fast-4.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:003f042b1299ebac900d6ee30e8ab2a29988937f56792894da7b5d39a26b1f5f", size = 799573, upload-time = "2026-04-02T04:50:05.58Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/2338ca2a51eca2392eddd1c1854a5127a8deb705a65a1fc66dc49d1a5557/dbus_fast-4.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87be8a41330481c3d3a5e30af10ec713e4f08fe0d6c36f5a401eaef7b5526417", size = 852067, upload-time = "2026-04-02T04:50:07.348Z" }, { url = "https://files.pythonhosted.org/packages/e9/3e/d8b733cca2dc03746496c2ea799125414cd875830a8cb384f1395ab6993d/dbus_fast-4.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fe60ce3a97e265a3ad117b7f40fc8c08357781b1a2ed3e853f29f9e35d24af7", size = 790985, upload-time = "2026-04-02T04:50:10.742Z" }, { url = "https://files.pythonhosted.org/packages/aa/85/ec643d891f4bb38347172ca99e4fbeaa55b9a5c3a744bac42c3074893482/dbus_fast-4.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982235dabb7e7187df4c2c299d95a6232aebc95c8c906496f630265a10bfdf95", size = 840552, upload-time = "2026-04-02T04:50:12.43Z" }, { url = "https://files.pythonhosted.org/packages/83/d5/50fb58f11387ab165f4ad010db9f5434aa386b8308467b5d968a10d09864/dbus_fast-4.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33c0168e0e65ed2d0fa91682773cc893472e3d71a5e085ee082e6484c2d29f4a", size = 797179, upload-time = "2026-04-02T04:50:14.435Z" }, @@ -407,20 +331,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/80/3d/6ee82654ff6ee9f3e3ac58d5159ef84d7f295c4556a82386a46dd4822b7a/epaper_dithering-6.0.0.tar.gz", hash = "sha256:9b45236e6bacf7d9126e797069f3ae0ed749ce8b59e4a28e5f3cbe62216286d5", size = 84940, upload-time = "2026-07-23T10:47:03.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/42/85ad0cc125b4b50ee5e2fffc2b0fe6d43d9dd289329a54a3451d2c6a252c/epaper_dithering-6.0.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7d30200e0196542a0bcaae83ed9e8b1182c93187afd04772312eb873144b500e", size = 647149, upload-time = "2026-07-23T10:45:58.157Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c3/23d61e4e099c08474c0bd59fafee8a560225c79b68df2fd06423bf8a43e3/epaper_dithering-6.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13420f9950bce215e7f4cc0071350a7e41599bf2cdab5b49f9dad1f86de5fe23", size = 377088, upload-time = "2026-07-23T10:45:59.972Z" }, - { url = "https://files.pythonhosted.org/packages/34/af/33ed937318ef82c1662669765ccca7a19270678451cfd223d74ea3704d33/epaper_dithering-6.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277aceb8af8ab2cb01195a936c8b70d4fa832a0e2523fd5b742fdba8362f3003", size = 376228, upload-time = "2026-07-23T10:46:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e7/2ba6b2a416de2dabbe0ca5925f2f04871850eb09d8f0d189dce99371f6bc/epaper_dithering-6.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aebfed36f046907312ea588c7696f351177e9619d3fb602e325a6d4dc344679", size = 379249, upload-time = "2026-07-23T10:46:03.288Z" }, - { url = "https://files.pythonhosted.org/packages/75/15/6a6bfe1b5a0ecb53109c61e6d5b3e44ddb3df5b285278fe0721e7fe111f9/epaper_dithering-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b32a597739bbab944c4894f8e2d6b997fae16203f9b433c780c1f2e41893f0", size = 553778, upload-time = "2026-07-23T10:46:05.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/66/5011f33faaa3acdc31570e5d55d31ccc44494f08c5047220c629ab021b4c/epaper_dithering-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:05fa525cee4cdbd3c821f6032cfad6f11a53a937af94d090a080ed7151e57fda", size = 585985, upload-time = "2026-07-23T10:46:07.262Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4e/f9e6630fab6f07255dc64badfc0677839a5794eb848a24b468e7f399ac20/epaper_dithering-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:148a3f840f9ce24c50e07b045f3a24494b0b3a494ba2b1147b980a3fb41c6a6b", size = 218414, upload-time = "2026-07-23T10:46:08.997Z" }, - { url = "https://files.pythonhosted.org/packages/82/8a/d24fa24f747150783a05d4ad493527961afe06df8c491cbefade35b7f275/epaper_dithering-6.0.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd9603ada7163ccd239e136098b977887844d24a4ff3903ccdb55fb77598d9c9", size = 641894, upload-time = "2026-07-23T10:46:10.531Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b6/90a85f3fe08bfced676a811cd9cdcd9e92293210b7f37b296be574671f92/epaper_dithering-6.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f19328ab4359c0b57794503cb5d9978bb79ec6462cf584d369face6c476d618", size = 374469, upload-time = "2026-07-23T10:46:12.107Z" }, - { url = "https://files.pythonhosted.org/packages/47/af/5288e5bcd0e5e3f2808b3deb816239498d1c3ead2dc54deee6de8e09c171/epaper_dithering-6.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00764f2969a4ab05c57407d070650547f2bd7e6dadc973751666e106505c75a6", size = 373808, upload-time = "2026-07-23T10:46:13.483Z" }, - { url = "https://files.pythonhosted.org/packages/07/d4/9fe87e66a1ca0d82eed94d31c1f7fc5e90d8e4678b5e4251a2e3c418b191/epaper_dithering-6.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c64c39c7a08bfcc9d14ebc637a83143b8818f6675f4f5e2bf610eed5efd9519", size = 376891, upload-time = "2026-07-23T10:46:14.825Z" }, - { url = "https://files.pythonhosted.org/packages/d1/54/9c322244fd01b665eae5fa39d7a1196a52a8d0d221cfa692952117d9054d/epaper_dithering-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3c2465d3df96257bc7767e881ea6044edeab3b688461ba080d758036d068b6c", size = 550063, upload-time = "2026-07-23T10:46:16.595Z" }, - { url = "https://files.pythonhosted.org/packages/16/27/11096ffa0c405370869f134c807122678c3fcce4c32e661da9c0b420c0b7/epaper_dithering-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f9fca4b328e0dcfabbaf6c2c7342ae497d0a62ca64069ebf1cd16438d49609d", size = 582675, upload-time = "2026-07-23T10:46:18.261Z" }, - { url = "https://files.pythonhosted.org/packages/13/8d/474a864d1c1b1a8716e8d5f9e9daeb1e4011ef173142d95ec07f95f8fc87/epaper_dithering-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a29ac57a94c66d1f5a31e6881fe8a80954ca9a7af6a1c2abf4bd69849a113df", size = 214872, upload-time = "2026-07-23T10:46:19.64Z" }, { url = "https://files.pythonhosted.org/packages/d4/46/99b714313249f21ea6d331666dd51de8b5e79a07dea5bf7e2c1ca823c022/epaper_dithering-6.0.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:21a7a27e4ea6359bc4b260b315bdf6ee742ca4204e272281ec7289da0de95c37", size = 640769, upload-time = "2026-07-23T10:46:21.227Z" }, { url = "https://files.pythonhosted.org/packages/f1/7f/7fbffe71387a4b97bd5d3568ac511f5a34631436896447efba69c7a69a9a/epaper_dithering-6.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6c2733c049099348b58b55ec0f3bb4e2549e317b9e0a982709fe50eb34d30ae", size = 373258, upload-time = "2026-07-23T10:46:22.697Z" }, { url = "https://files.pythonhosted.org/packages/c6/37/7190fe2ad9885a1946ddf67381d5cc77104afb96c1e5d433c1e1f09c86b1/epaper_dithering-6.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbd69ff3945512205efb4fc5363ab5b20f52d3864bcef320c9f0186aa7a061cc", size = 373099, upload-time = "2026-07-23T10:46:24.185Z" }, @@ -442,11 +352,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/e0/9d32f3b7434ef385532b5571ad7b73ae2ec0b0f69bbf0ae1ad1a499fa836/epaper_dithering-6.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7bcd2859d5c60d39f44bdecb5e5c2e6671dd508e732e46fcda474798db9e05ec", size = 580613, upload-time = "2026-07-23T10:46:50.746Z" }, { url = "https://files.pythonhosted.org/packages/a4/11/466a127dddff8c7b34c6ceabef33a5a498937b7db69a73826e56348d23c4/epaper_dithering-6.0.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a906e496a09609ae7c5a8d172e32af228dbbd0ba5d9986d0fdbed6edc97b1ff", size = 376307, upload-time = "2026-07-23T10:46:52.388Z" }, { url = "https://files.pythonhosted.org/packages/27/96/6d45b4379f6a9e8329034e82376855d3ea6385fcdfbf28959ed98ca8c132/epaper_dithering-6.0.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05d99ec811a2209f3038dc45d5cfd2b942f0f521ccbce1938e7772e88ac17bcd", size = 375960, upload-time = "2026-07-23T10:46:54.152Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f2/2164d566d0f53568a6e796d51e5a95cc5ac14aacf053d69a763bf85384f5/epaper_dithering-6.0.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45fb1469e063e31750fb0b1210fd79e012ddd326e890b3a78003107d6f4f3b6e", size = 377717, upload-time = "2026-07-23T10:46:55.951Z" }, - { url = "https://files.pythonhosted.org/packages/fd/83/e459ccf156ff90dbc200262e2d96a49d3d1065bae5182b32bf60b015ddb3/epaper_dithering-6.0.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6206094fa2c756844ba46fb35796164b9bfc499741adf458151beebedccbe69", size = 377852, upload-time = "2026-07-23T10:46:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/27/97/c249b88ff53016390a7b09a90c90776e64f0b3ed1cbe08a31093d298a67b/epaper_dithering-6.0.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aab9e5a1e1d4e034b87f20523060260ad09c586ef8da8ec6f1a282f231727e15", size = 380164, upload-time = "2026-07-23T10:46:59.207Z" }, - { url = "https://files.pythonhosted.org/packages/05/d9/e012aafbb92edf677405d595ec53106e0f59a0abd38558614f85917e9503/epaper_dithering-6.0.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1f1f12409a61f6080a6b97f3d95999b4076ae418469670bf7f37f96c1a3b60da", size = 554648, upload-time = "2026-07-23T10:47:00.656Z" }, - { url = "https://files.pythonhosted.org/packages/46/4e/ebebb62356a9494f8e7d6eed508f19e6afae2781240e102760a12334d349/epaper_dithering-6.0.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:31c1a1a3679e9b9b4c07a2a19ed0e6a2bd02e94dcdb39f959077ea77b1b1b475", size = 587507, upload-time = "2026-07-23T10:47:02.301Z" }, ] [[package]] @@ -470,6 +375,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/89/0f50dd0d92e8a7dffc24f69ab910ff81db89b2f082ba42682bd57695e4d2/hypothesis-6.152.4-py3-none-any.whl", hash = "sha256:e730fd93c7578182efadc7f90b3c5437ee4d55edf738930eb5043c81ac1d97e8", size = 532145, upload-time = "2026-04-27T20:18:35.043Z" }, ] +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -494,32 +408,6 @@ version = "0.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/a3/1472717d2325adacc8d335ba2e4078015c09d75b599f3cf48e967b3d306e/librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72", size = 76045, upload-time = "2026-05-05T16:29:18.731Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/bfe32355d4b369aef3d7aa442df663bb5558c2ffa2de286cb2956346bc24/librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c", size = 79466, upload-time = "2026-05-05T16:29:20.052Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f1/83f8a2c715ba2cac9b7387a5a5cea25f717f7184320cfe48b36bed9c58e9/librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950", size = 242283, upload-time = "2026-05-05T16:29:21.596Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/c3a4ce94857f0004a542f86662806383611858f522722db58efaec0a1472/librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0", size = 230735, upload-time = "2026-05-05T16:29:23.335Z" }, - { url = "https://files.pythonhosted.org/packages/d1/41/e962bb26c7728eb7b3a69e490d0c800fd9968a6970e390c1f18ddb56093d/librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275", size = 256606, upload-time = "2026-05-05T16:29:24.91Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/4e46a707b1ecc993fd691071623b9beab89703a63bd21cc7807e06c28209/librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9", size = 249739, upload-time = "2026-05-05T16:29:26.648Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f5/dc5b7eb294656ad23d4ff4cf8514208d54fe1026b909d726a0dc026689c9/librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f", size = 261414, upload-time = "2026-05-05T16:29:28.702Z" }, - { url = "https://files.pythonhosted.org/packages/58/e4/990ed8d12c7f114ac8f8ccd47f7d9bd9704ef61acfcb1df4a05047da7710/librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7", size = 256614, upload-time = "2026-05-05T16:29:30.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/eb/52d2726c7fb22818507dc3cc166c8f36dd4a4b68a7be67f12006ac8777c1/librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de", size = 255144, upload-time = "2026-05-05T16:29:32.106Z" }, - { url = "https://files.pythonhosted.org/packages/bc/df/bd5591a78f7531fce4b6eb9962aadc6adc9560a01570442a884b6e554abe/librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa", size = 279121, upload-time = "2026-05-05T16:29:33.688Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/7c2b838dfc89a1762dd156d8b0c39848a7a2845d725a50be5a6e021fb8ba/librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090", size = 62593, upload-time = "2026-05-05T16:29:35.152Z" }, - { url = "https://files.pythonhosted.org/packages/91/19/22ff572981049a9d436a083dbea1572d0f5dc068b7353637d2dd9977c8f1/librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083", size = 70914, upload-time = "2026-05-05T16:29:36.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/1697cc64f4a5c7e9bce55e99c6d234a346beaedaefcd1e2ca90dd285f98c/librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681", size = 61176, upload-time = "2026-05-05T16:29:37.62Z" }, - { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, - { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, - { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, - { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, - { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, @@ -604,20 +492,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cf/dc/7e6d49f04fca40b9dd5c752a51a432ffe67fb45200702bc9eee0cb4bbb26/mypy-2.0.0.tar.gz", hash = "sha256:1a9e3900ac5c40f1fe813506c7739da6e6f0eab2729067ebd94bfb0bbba53532", size = 3869036, upload-time = "2026-05-06T19:26:43.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/1e/268b81393b81d64683f670680215553e70ae92c55805915b3440080e05e4/mypy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17b7222e9fdfd352e61fb3131da117e55cc465f701ff232f1bd97a02bbad91f", size = 14580849, upload-time = "2026-05-06T19:23:06.567Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/d159a8002d9e5c44e59ece9d641a26956c89be5b6827f819d9a9dc678c65/mypy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0a61adea1a5ffc2d47a4dc4bb180d8103f477fc2a90a1cdcbb168c2cc6caff", size = 13444955, upload-time = "2026-05-06T19:25:11.982Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5d/3b28d5a2799591da0ee5490418e94497eaf5d701e42d8b001b5e17a9b3d6/mypy-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8578f857b519993d065e5805290b71467ebfae772407a5f57e823755e4fdb850", size = 13873124, upload-time = "2026-05-06T19:20:39.684Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/f40f723955617b814d5ddc1154d8938b77aaf6926c2dbf72846e8943a0b7/mypy-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33f668a37a650df60f7b825c1ac61e6baadd4ac3c89519e929badde58d28edf5", size = 14748822, upload-time = "2026-05-06T19:25:30.972Z" }, - { url = "https://files.pythonhosted.org/packages/d6/16/eded971224a483e422a141ffd580c00e1b919df8e529f06d03a4a987878c/mypy-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29ea6da86c8c5e9addd48fa6e624f467341b3814f54ded871b28980468686dea", size = 14992675, upload-time = "2026-05-06T19:23:34.511Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6a/1cbd7290f00b4dbaa4c4502e53ac05645ea635e4d1e3dcd42687c2fc39cd/mypy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:904baa0124ebbccf0c7ba94f722cf9186ee30478f5e5b11432ffc8929248ee55", size = 10983628, upload-time = "2026-05-06T19:26:39.48Z" }, - { url = "https://files.pythonhosted.org/packages/83/3f/8caa9bcc2636cd512642050747466b695fa2540d7040544fd7ddb721d671/mypy-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:440165501295e523bf1e5d3e411b62b367b901c65610938e75f0e56ba0462461", size = 9906041, upload-time = "2026-05-06T19:24:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4b/f6cd12ef1eb63be1c342da3e8ca811d2280276177f6de4ef20cb2366d79b/mypy-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:660790551c988e69d8bf7a35c8b4149edeb22f4a339165702be843532e9dcdb5", size = 14756610, upload-time = "2026-05-06T19:26:19.221Z" }, - { url = "https://files.pythonhosted.org/packages/32/73/67d09ca28bee21feaca264b2a680cf2d300bcc2071136ad064928324c843/mypy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a15bf92cd8781f8e72f69ffa7e30d1f434402d065ee1ecd5223ef2ef100f914", size = 13554270, upload-time = "2026-05-06T19:26:08.977Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/44718b5c6b1b5a27440ff2effe6a1be0fa2a190c0f4e2e21a83728416f95/mypy-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ff370b43d7def05bbcd2f5267f0bcda72dd6a552ef2ea9375b02d6fe06da270", size = 13924663, upload-time = "2026-05-06T19:21:24.932Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2b/bbb9cc5773f946846a7c340097e59bcf84095437dda0d56bb4f6cf1f6541/mypy-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37bd246590a018e5a11703b7b09c39d47ede3df5ba3fa863c5b8590b465beb01", size = 14946862, upload-time = "2026-05-06T19:24:23.023Z" }, - { url = "https://files.pythonhosted.org/packages/43/25/e9318566f443a5130b4ff0ad3367ee6c4c4c49ff083fe5214a7318c18282/mypy-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cce87e92214fac8bf8feb8a680d0c1b6fb748d50e9b57fbb13e4b1d83a3ed19b", size = 15175090, upload-time = "2026-05-06T19:26:28.794Z" }, - { url = "https://files.pythonhosted.org/packages/67/65/2ec28c834f21e164c33bc296a7db538ad50c74f83e517c7a0be95ff6de86/mypy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e19e9cb69b66a4141009d24898259914fa2b71d026de0b46edf9fafdbf4fd46e", size = 11052899, upload-time = "2026-05-06T19:25:39.084Z" }, - { url = "https://files.pythonhosted.org/packages/9e/72/d1ec625cfc9bd101c07a6834ef1f94e820296f8fdbad2eb03f50e0983f8c/mypy-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b021614cb08d44785b025982163ec3c39c94bff766ead071fa9e82b4ef6f62cd", size = 9972935, upload-time = "2026-05-06T19:23:24.204Z" }, { url = "https://files.pythonhosted.org/packages/e5/c6/996a1e535e5d0d597c3b1460fc962733091f885f312e749350eb2ac10965/mypy-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ef5f581b61240d1cc629b12f8df6565ed6ffac0d82ed745eef7833222ab50b9", size = 14737259, upload-time = "2026-05-06T19:20:23.081Z" }, { url = "https://files.pythonhosted.org/packages/94/c5/0f9460e26b77f434bd53f47d1ce32a3cd4580c92a5331fa5dfc059f9421a/mypy-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20e3470a165dbc249bdfbe8d1c5172727ef22688cffc279f8c3aa264ab9d4d9a", size = 13538377, upload-time = "2026-05-06T19:21:08.804Z" }, { url = "https://files.pythonhosted.org/packages/b2/3e/8ea2f8dd1e5c9c279fb3c28193bdb850adf4d3d8172880abad829eced609/mypy-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224ba142eee8b4d65d4db657cb1fc22abec30b135ded6ab297302ba1f62e505d", size = 13914264, upload-time = "2026-05-06T19:24:12.875Z" }, @@ -669,28 +543,6 @@ version = "2.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, @@ -733,13 +585,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] @@ -766,28 +611,6 @@ version = "12.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, @@ -838,13 +661,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -919,6 +735,9 @@ test = [ { name = "pytest-cov" }, { name = "pytest-xdist" }, ] +wifi = [ + { name = "zeroconf" }, +] [package.dev-dependencies] dev = [ @@ -944,8 +763,9 @@ requires-dist = [ { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=3.8.0" }, { name = "rich", marker = "extra == 'cli'", specifier = ">=13.0.0" }, { name = "silabs-ble-ota", marker = "extra == 'silabs-ota'", specifier = ">=0.1.0" }, + { name = "zeroconf", marker = "extra == 'wifi'", specifier = ">=0.132.0" }, ] -provides-extras = ["cli", "nrf-ota", "silabs-ota", "test"] +provides-extras = ["cli", "wifi", "nrf-ota", "silabs-ota", "test"] [package.metadata.requires-dev] dev = [ @@ -997,8 +817,6 @@ version = "12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, @@ -1014,8 +832,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, @@ -1032,8 +848,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/7a/26ae106beb97e9c4745065edb3ce3c2bdd91d81f5b52b8224f82ce9d5fb9/pyobjc_framework_corebluetooth-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:37e6456c8a076bd5a2bdd781d0324edd5e7397ef9ac9234a97433b522efb13cf", size = 13189, upload-time = "2025-11-14T09:44:06.229Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, { url = "https://files.pythonhosted.org/packages/e0/6c/831139ebf6a811aed36abfdfad846bc380dcdf4e6fb751a310ce719ddcfd/pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc", size = 13229, upload-time = "2025-11-14T09:44:10.463Z" }, { url = "https://files.pythonhosted.org/packages/09/3c/3a6fe259a9e0745aa4612dee86b61b4fd7041c44b62642814e146b654463/pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32", size = 13409, upload-time = "2025-11-14T09:44:12.438Z" }, { url = "https://files.pythonhosted.org/packages/2f/41/90640a4db62f0bf0611cf8a161129c798242116e2a6a44995668b017b106/pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa", size = 13222, upload-time = "2025-11-14T09:44:14.345Z" }, @@ -1050,8 +864,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/75/c4aeab6ce7268373d4ceabbc5c406c4bbf557038649784384910932985f8/pyobjc_framework_libdispatch-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:954cc2d817b71383bd267cc5cd27d83536c5f879539122353ca59f1c945ac706", size = 20463, upload-time = "2025-11-14T09:52:55.703Z" }, - { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, { url = "https://files.pythonhosted.org/packages/38/3a/d85a74606c89b6b293782adfb18711026ff79159db20fc543740f2ac0bc7/pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0", size = 15668, upload-time = "2025-11-14T09:53:01.354Z" }, { url = "https://files.pythonhosted.org/packages/cc/40/49b1c1702114ee972678597393320d7b33f477e9d24f2a62f93d77f23dfb/pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332", size = 15938, upload-time = "2025-11-14T09:53:03.461Z" }, { url = "https://files.pythonhosted.org/packages/59/d8/7d60a70fc1a546c6cb482fe0595cb4bd1368d75c48d49e76d0bc6c0a2d0f/pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5", size = 15693, upload-time = "2025-11-14T09:53:05.531Z" }, @@ -1080,7 +892,6 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ @@ -1092,7 +903,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -1174,60 +985,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - [[package]] name = "tomlkit" version = "0.14.0" @@ -1273,12 +1030,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/8d/d7ae0e07cd85c7768de76e8578261854f2af72bd3a8a527bb675e8ae0eda/winrt_runtime-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9e9b64f1ba631cc4b9fe60b8ff16fef3f32c7ce2fcc84735a63129ff8b15c022", size = 210798, upload-time = "2025-06-06T06:44:04.775Z" }, - { url = "https://files.pythonhosted.org/packages/ac/66/d05f6e6c0517654734e7f87fa1f0fbc965add9f27cc36b524d96331ab3d8/winrt_runtime-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0a9046ae416808420a358c51705af8ae100acd40bc578be57ddfdd51cbb0f9c", size = 242032, upload-time = "2025-06-06T06:44:06.103Z" }, - { url = "https://files.pythonhosted.org/packages/39/a5/760c8396110f6d3e4c417752da1a2bf3b89e0998329c2f10afc717ef6291/winrt_runtime-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:e94f3cb40ea2d723c44c82c16d715c03c6b3bd977d135b49535fdd5415fd9130", size = 415659, upload-time = "2025-06-06T06:44:07.007Z" }, - { url = "https://files.pythonhosted.org/packages/d3/54/3dd06f2341fab6abb06588a16b30e0b213b0125be7b79dafc3bdba3b334a/winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1", size = 210090, upload-time = "2025-06-06T06:44:08.151Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a1/1d7248d5c62ccbea5f3e0da64ca4529ce99c639c3be2485b6ed709f5c740/winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576", size = 241391, upload-time = "2025-06-06T06:44:09.442Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ae/6a205d8dafc79f7c242be7f940b1e0c1971fd64ab3079bda4b514aa3d714/winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195", size = 415242, upload-time = "2025-06-06T06:44:10.407Z" }, { url = "https://files.pythonhosted.org/packages/79/d4/1a555d8bdcb8b920f8e896232c82901cc0cda6d3e4f92842199ae7dff70a/winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1", size = 210022, upload-time = "2025-06-06T06:44:11.767Z" }, { url = "https://files.pythonhosted.org/packages/aa/24/2b6e536ca7745d788dfd17a2ec376fa03a8c7116dc638bb39b035635484f/winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d", size = 241349, upload-time = "2025-06-06T06:44:12.661Z" }, { url = "https://files.pythonhosted.org/packages/d4/7f/6d72973279e2929b2a71ed94198ad4a5d63ee2936e91a11860bf7b431410/winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159", size = 415126, upload-time = "2025-06-06T06:44:13.702Z" }, @@ -1296,12 +1047,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/cf/671bf29337323cc08f9969cb32312f217d2927d29dbf2964f0dbb378cb90/winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win32.whl", hash = "sha256:f4082a00b834c1e34b961e0612f3e581356bdb38c5798bd6842f88ec02e5152b", size = 105535, upload-time = "2025-06-06T07:00:08.146Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d5/5761a8b6dcc56957018970dd443059c8ee8a79de7b07f0b4d143f8e7dc15/winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:44277a3f2cc5ac32ce9b4b2d96c5c5f601d394ac5f02cc71bcd551f738660e2d", size = 114612, upload-time = "2025-06-06T07:00:08.984Z" }, - { url = "https://files.pythonhosted.org/packages/24/0b/7819bb102286752d3572a75d03e6a8000ffe3c6cb7aee3eb136dca383fe2/winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:0803a417403a7d225316b9b0c4fe3f8446579d6a22f2f729a2c21f4befc74a80", size = 105017, upload-time = "2025-06-06T07:00:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/54/ff/c4a3de909a875b46fad5e9f4fd412bba48571405bfa802b878954abf128c/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c", size = 105752, upload-time = "2025-06-06T07:00:10.684Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/bfee1f0c8d188c561c5b946ab21f6a0037e60dea110e80b1d6a1d529639f/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca", size = 113356, upload-time = "2025-06-06T07:00:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1b/d9da9c29d36cabadef4e19c3e9ba6d2692f6a28224c81fcff757132ea0da/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148", size = 104724, upload-time = "2025-06-06T07:00:12.406Z" }, { url = "https://files.pythonhosted.org/packages/d4/cc/797516c5c0f8d7f5b680862e0ed7c1087c58aec0bcf57a417fa90f7eb983/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4", size = 105757, upload-time = "2025-06-06T07:00:13.269Z" }, { url = "https://files.pythonhosted.org/packages/05/6d/f60588846a065e69a2ec5e67c5f85eb45cb7edef2ee8974cd52fa8504de6/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3", size = 113363, upload-time = "2025-06-06T07:00:14.135Z" }, { url = "https://files.pythonhosted.org/packages/2c/13/2d3c4762018b26a9f66879676ea15d7551cdbf339c8e8e0c56ea05ea31ef/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2", size = 104722, upload-time = "2025-06-06T07:00:14.999Z" }, @@ -1319,12 +1064,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/5e/c628719e877a89f00cac7ce53f9666acbc5ed6f074130729d5d6768b63ff/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win32.whl", hash = "sha256:fe17c2cf63284646622e8b2742b064bf7970bbf53cfab02062136c67fa6b06c9", size = 89614, upload-time = "2025-06-06T07:00:20.952Z" }, - { url = "https://files.pythonhosted.org/packages/ac/1a/d172d6f1c2fae53535e7f23835025cf39e3002749a0304f18a38e8ed490d/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:78e99dd48b4d89b71b7778c5085fdba64e754dd3ebc54fd09c200fe5222c6e09", size = 95783, upload-time = "2025-06-06T07:00:21.764Z" }, - { url = "https://files.pythonhosted.org/packages/67/c1/568dfdaea62ca3b13bb70162cb292e5cd0be5bbb98b738961ddcc2edd374/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6d5d2295474deab444fc4311580c725a2ca8a814b0f3344d0779828891d75401", size = 89253, upload-time = "2025-06-06T07:00:22.603Z" }, - { url = "https://files.pythonhosted.org/packages/c9/15/ad05c28e049208c97011728e2debdb45439175f75efe357b6faa4c9ba099/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4", size = 90033, upload-time = "2025-06-06T07:00:23.421Z" }, - { url = "https://files.pythonhosted.org/packages/26/48/074779081841f6eba4987930c4e7adcec38a5985b7dffd9fecc41f39a89c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89", size = 95824, upload-time = "2025-06-06T07:00:24.238Z" }, - { url = "https://files.pythonhosted.org/packages/aa/25/e01966033a02b2d0718710bb47ef4f6b9b5a619ca2c857e06eb5c8e3ed13/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff", size = 89311, upload-time = "2025-06-06T07:00:25.029Z" }, { url = "https://files.pythonhosted.org/packages/34/01/8fc8e57605ea08dd0723c035ed0c2d0435dace2bc80a66d33aecfea49a56/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90", size = 90037, upload-time = "2025-06-06T07:00:25.818Z" }, { url = "https://files.pythonhosted.org/packages/86/83/503cf815d84c5ba8c8bc61480f32e55579ebf76630163405f7df39aa297b/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943", size = 95822, upload-time = "2025-06-06T07:00:26.666Z" }, { url = "https://files.pythonhosted.org/packages/32/13/052be8b6642e6f509b30c194312b37bfee8b6b60ac3bd5ca2968c3ea5b80/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d", size = 89326, upload-time = "2025-06-06T07:00:27.477Z" }, @@ -1342,12 +1081,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/349a5d958be8c0570f0a49bbb746088bcfaa81555accb57503ba01185359/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win32.whl", hash = "sha256:832cf65d035a11e6dbfef4fd66abdcc46be7e911ec96e2e72e98e12d8d5b9d3c", size = 182312, upload-time = "2025-06-06T07:00:49.974Z" }, - { url = "https://files.pythonhosted.org/packages/90/db/929ab0085ec89e46bd3a58c74b451dd770c3285dfa0cbd4f4aa4730da004/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8179638a6c721b0bbf04ba251ef98d5e02d9a17f0cce377398e42c4fbb441415", size = 187768, upload-time = "2025-06-06T07:00:50.853Z" }, - { url = "https://files.pythonhosted.org/packages/a3/53/f316e2224c384178204430439f04f9b72017fe8237e341a9aebb20da8191/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:70b7edfca3190b89ae38bf60972b11978311b6d933d3142ae45560c955dbf5c7", size = 184189, upload-time = "2025-06-06T07:00:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a1/75ac783a5faee9b455fef2f53b7fef97b21ed60d52401b44c690202141e4/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557", size = 183326, upload-time = "2025-06-06T07:00:52.662Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d9/a9dcc15322d2f5c7dfd491bd7ab121e36437caf78ebfa92bc0dd0546e2ca/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d", size = 187810, upload-time = "2025-06-06T07:00:53.594Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fc/47d00af076f558267097af3050910beda6bf8a21ceaa5830bbd26fcaf85e/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984", size = 184516, upload-time = "2025-06-06T07:00:55.24Z" }, { url = "https://files.pythonhosted.org/packages/ec/93/30b45ce473d1a604908221a1fa035fe8d5e4bb9008e820ae671a21dab94c/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0", size = 183342, upload-time = "2025-06-06T07:00:56.16Z" }, { url = "https://files.pythonhosted.org/packages/5b/3b/eb9d99b82a36002d7885206d00ea34f4a23db69c16c94816434ded728fa3/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30", size = 187844, upload-time = "2025-06-06T07:00:57.134Z" }, { url = "https://files.pythonhosted.org/packages/84/9b/ebbbe9be9a3e640dcfc5f166eb48f2f9d8ce42553f83aa9f4c5dcd9eb5f5/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383", size = 184540, upload-time = "2025-06-06T07:00:58.081Z" }, @@ -1365,12 +1098,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/92/ca1fd311d96fce15fba25543a2ae3cb829744a8af548a11d74233d0e4f64/winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9f29465a6c6b0456e4330d4ad09eccdd53a17e1e97695c2e57db0d4666cc0011", size = 129898, upload-time = "2025-06-06T07:01:59.687Z" }, - { url = "https://files.pythonhosted.org/packages/03/fd/5bd5da5d7997725ba3f1995c16aa1c3362937f8ff68ad4cadfd3415eebcb/winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2a725d04b4cb43aa0e2af035f73a60d16a6c0ff165fcb6b763383e4e33a975fd", size = 142361, upload-time = "2025-06-06T07:02:00.546Z" }, - { url = "https://files.pythonhosted.org/packages/df/be/d423b63e740600e0617ddb85fba3ef99e7bbff02299fe46323bfe624a382/winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6365ef5978d4add26678827286034acf474b6b133aa4054e76567d12194e6817", size = 135808, upload-time = "2025-06-06T07:02:01.4Z" }, - { url = "https://files.pythonhosted.org/packages/31/3e/81642208ecd6c6c936f35a39a433c54e3f68e09d316546b8f953581ae334/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3", size = 130249, upload-time = "2025-06-06T07:02:02.237Z" }, - { url = "https://files.pythonhosted.org/packages/00/f4/a9ede5f3f0d86abfc7590726cf711133d97419b49ced372fca532e4f0696/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc", size = 141512, upload-time = "2025-06-06T07:02:03.424Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/4fad07c03124bdc3acd64f80f3bd3cc4417ea641e07bb16a9503afd3e554/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca", size = 135383, upload-time = "2025-06-06T07:02:04.312Z" }, { url = "https://files.pythonhosted.org/packages/ff/7d/ebd712ab8ccd599c593796fbcd606abe22b5a8e20db134aa87987d67ac0e/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9", size = 130276, upload-time = "2025-06-06T07:02:05.178Z" }, { url = "https://files.pythonhosted.org/packages/70/de/f30daaaa0e6f4edb6bd7ddb3e058bd453c9ad90c032a4545c4d4639338aa/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015", size = 141536, upload-time = "2025-06-06T07:02:06.067Z" }, { url = "https://files.pythonhosted.org/packages/75/4b/9a6aafdc74a085c550641a325be463bf4b811f6f605766c9cd4f4b5c19d2/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4", size = 135362, upload-time = "2025-06-06T07:02:06.997Z" }, @@ -1388,12 +1115,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/a0/4a8b51da15de218cec04bcc1cd85b4b93bcfd8ebe50a5f0a7eee28836dc6/winrt_windows_devices_radios-3.2.1-cp311-cp311-win32.whl", hash = "sha256:7c02790472414b6cda00d24a8cd23bca18e4b7474ddad4f9264f4484b891807e", size = 38505, upload-time = "2025-06-06T07:07:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/de/49/ba69e3180585dbc6f3336a09fef7cba4558a6a1e7d500500f62c1478418e/winrt_windows_devices_radios-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:f87745486d313ba1e7562ca97f25ad436ec01ad4b3b9ea349fb6b6f25cb41104", size = 40157, upload-time = "2025-06-06T07:07:59.948Z" }, - { url = "https://files.pythonhosted.org/packages/9c/92/64817f71a20ecf842da36dc3848f42614217688137a69c93fda8a6103155/winrt_windows_devices_radios-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6cee6f946ff3a3571850d1ca745edaee7c331d06ca321873e650779654effc4a", size = 36976, upload-time = "2025-06-06T07:08:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/a5/e0/4731a3c412318b2c5e74a8803a32e2fb9afc2c98368c6b61a422eb359e7e/winrt_windows_devices_radios-3.2.1-cp312-cp312-win32.whl", hash = "sha256:c3e683ce682338a5a5ed465f735e223ba7a22f16d0bbea2d070962bc7657edbb", size = 38606, upload-time = "2025-06-06T07:08:01.477Z" }, - { url = "https://files.pythonhosted.org/packages/37/8e/91464854dfc9e0be9ce8dcbe2bd6a67c19b68ab91584fc5de0f4f13e78f8/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a116e552a3f38607b9be558fb2e7de9b4450d1f9080069944d74d80cdda1873e", size = 40172, upload-time = "2025-06-06T07:08:02.214Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0d/1bd62f606b6c4dfa936fccc4712be5506a40fc5d1b7177c3d3cbcaf30972/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4c28822f9251c9d547324f596b5c2581f050254ded05e5b786c650a3502744c1", size = 36989, upload-time = "2025-06-06T07:08:03.295Z" }, { url = "https://files.pythonhosted.org/packages/d1/94/c22a14fd424632f3f3c0b25672218db9e8f4ae9e1355e0b148f2fe6015b5/winrt_windows_devices_radios-3.2.1-cp313-cp313-win32.whl", hash = "sha256:ae4a0065927fcd2d10215223f8a46be6fb89bad71cb4edd25dae3d01c137b3a8", size = 38613, upload-time = "2025-06-06T07:08:04.077Z" }, { url = "https://files.pythonhosted.org/packages/39/c1/24cec0cc228642554b48d436a7617d7162fb952919c55fc26e2d99c310bd/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:bf1a975f46a2aa271ffea1340be0c7e64985050d07433e701343dddc22a72290", size = 40180, upload-time = "2025-06-06T07:08:04.849Z" }, { url = "https://files.pythonhosted.org/packages/ca/d3/776453af26e78c0d0c0e1bfa89f86fd81322872f31a3e5dafb344dd47bf2/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:10b298ed154c5824cea2de174afce1694ed2aabfb58826de814074027ffef96f", size = 36989, upload-time = "2025-06-06T07:08:05.576Z" }, @@ -1411,12 +1132,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/36/09b9757f7cbf269e67008ea2ad188a44f974c94c9b49ebf0b52d1a8c4069/winrt_windows_foundation-3.2.1-cp311-cp311-win32.whl", hash = "sha256:d1b5970241ccd61428f7330d099be75f4f52f25e510d82c84dbbdaadd625e437", size = 111944, upload-time = "2025-06-06T07:10:58.496Z" }, - { url = "https://files.pythonhosted.org/packages/05/a5/216d66df6bdcee58eb3877fabc1544337e23f850bf9f93838db7f5698371/winrt_windows_foundation-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:f3762be2f6e0f2aedf83a0742fd727290b397ffe3463d963d29211e4ebb53a7e", size = 118465, upload-time = "2025-06-06T07:10:59.678Z" }, - { url = "https://files.pythonhosted.org/packages/be/ca/48ca8b5bc5be5c7a5516c9e1d9a21861b4217e1b4ee57923aab6f13fa411/winrt_windows_foundation-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:806c77818217b3476e6c617293b3d5b0ff8a9901549dc3417586f6799938d671", size = 109609, upload-time = "2025-06-06T07:11:00.54Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/495e304ddedd5ff2f196efbde906265cb75ade4d79e2937837f72ef654a0/winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b", size = 112169, upload-time = "2025-06-06T07:11:01.438Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5e/b5059e4ece095351c496c9499783130c302d25e353c18031d5231b1b3b3c/winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9", size = 118668, upload-time = "2025-06-06T07:11:02.475Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/acbcb3ef07b1b67e2de4afab9176a5282cfd775afd073efe6828dfc65ace/winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37", size = 109671, upload-time = "2025-06-06T07:11:03.538Z" }, { url = "https://files.pythonhosted.org/packages/7b/71/5e87131e4aecc8546c76b9e190bfe4e1292d028bda3f9dd03b005d19c76c/winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46", size = 112184, upload-time = "2025-06-06T07:11:04.459Z" }, { url = "https://files.pythonhosted.org/packages/ba/7f/8d5108461351d4f6017f550af8874e90c14007f9122fa2eab9f9e0e9b4e1/winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479", size = 118672, upload-time = "2025-06-06T07:11:05.55Z" }, { url = "https://files.pythonhosted.org/packages/44/f5/2edf70922a3d03500dab17121b90d368979bd30016f6dbca0d043f0c71f1/winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4", size = 109673, upload-time = "2025-06-06T07:11:06.398Z" }, @@ -1434,12 +1149,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/b3/7e4a75c62e86bedf9458b7ec8dfed74cff3236e0b4b2288f95967d5cc4d2/winrt_windows_foundation_collections-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9b272d9936e7db4840881c5dcf921eb26789ae4ef23fb6ec15e13e19a16254e7", size = 59693, upload-time = "2025-06-06T07:11:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/32/58/049db1d95fdfc0c8451dc6db17442ed4e6b2aba361c425c0bb8dc8c98c4a/winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c646a5d442dd6540ade50890081ca118b41f073356e19032d0a5d7d0d38fbc89", size = 70828, upload-time = "2025-06-06T07:11:14.54Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6b/a04974f5555c86452e54c19d063d9fd45f0fe9f2a6858e7fe12c639043fb/winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:2c4630027c93cdd518b0cf4cc726b8fbdbc3388e36d02aa1de190a0fc18ca523", size = 59051, upload-time = "2025-06-06T07:11:15.379Z" }, - { url = "https://files.pythonhosted.org/packages/1d/0b/7802349391466d3f7e8f62f588f36a1a0b6560abfcdbdaa426fe21d322b4/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6", size = 60060, upload-time = "2025-06-06T07:11:16.173Z" }, - { url = "https://files.pythonhosted.org/packages/37/94/5b888713e472746635a382e523513ab1b8200af55c5b56bc70e1e4369115/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b", size = 69058, upload-time = "2025-06-06T07:11:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/829273622c9b37c67b97f187b92be318404f7d33db045e31d72b7d50f54c/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84", size = 58793, upload-time = "2025-06-06T07:11:17.837Z" }, { url = "https://files.pythonhosted.org/packages/a6/cd/99ef050d80bea2922fa1ded93e5c250732634095d8bd3595dd808083e5ca/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9", size = 60063, upload-time = "2025-06-06T07:11:18.65Z" }, { url = "https://files.pythonhosted.org/packages/94/93/4f75fd6a4c96f1e9bee198c5dc9a9b57e87a9c38117e1b5e423401886353/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10", size = 69057, upload-time = "2025-06-06T07:11:19.446Z" }, { url = "https://files.pythonhosted.org/packages/40/76/de47ccc390017ec5575e7e7fd9f659ee3747c52049cdb2969b1b538ce947/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2", size = 58792, upload-time = "2025-06-06T07:11:20.24Z" }, @@ -1457,12 +1166,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/60/a9e0dc03434aa29e6b5c83067e988cd5934adf830cd9f87cbbc06569ca32/winrt_windows_storage_streams-3.2.1-cp311-cp311-win32.whl", hash = "sha256:7dace2f9e364422255d0e2f335f741bfe7abb1f4d4f6003622b2450b87c91e69", size = 127509, upload-time = "2025-06-06T14:01:58.971Z" }, - { url = "https://files.pythonhosted.org/packages/23/98/6c9c21b5e75ff5927a130da9eaf5ab628dfa1f93b64c181f0193706cbd6c/winrt_windows_storage_streams-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:b02fa251a7eef6081eca1a5f64ecf349cfd1ac0ac0c5a5a30be52897d060bed5", size = 132491, upload-time = "2025-06-06T14:01:59.788Z" }, - { url = "https://files.pythonhosted.org/packages/38/ca/d0a02045d445cbf1029d65f01b487fdded5b333c0367a8bae0565b3def00/winrt_windows_storage_streams-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:efdf250140340a75647e8e8ad002782d91308e9fdd1e19470a5b9cc969ae4780", size = 128577, upload-time = "2025-06-06T14:02:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/87/e7/7d3f2a4a442f264e05cab2bdf20ed1b95cb3f753bd1b0f277f2b49fb8335/winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78", size = 127787, upload-time = "2025-06-06T14:02:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2f/cc36f475f8af293f40e2c2a5d6c2e75a189c2c2d4d01ecb3551578518c79/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc", size = 131849, upload-time = "2025-06-06T14:02:03.09Z" }, - { url = "https://files.pythonhosted.org/packages/94/84/896fb734f7456910ec412f3f3adfdc3f0dc3134864a496d5b120592f3bfd/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307", size = 128144, upload-time = "2025-06-06T14:02:03.946Z" }, { url = "https://files.pythonhosted.org/packages/d9/d2/24d9f59bdc05e741261d5bec3bcea9a848d57714126a263df840e2b515a8/winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163", size = 127774, upload-time = "2025-06-06T14:02:04.752Z" }, { url = "https://files.pythonhosted.org/packages/15/59/601724453b885265c7779d5f8025b043a68447cbc64ceb9149d674d5b724/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915", size = 131827, upload-time = "2025-06-06T14:02:05.601Z" }, { url = "https://files.pythonhosted.org/packages/fb/c2/a419675a6087c9ea496968c9b7805ef234afa585b7483e2269608a12b044/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d", size = 128180, upload-time = "2025-06-06T14:02:06.759Z" }, @@ -1470,3 +1173,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/3d/aae50b1d0e37b5a61055759aedd42c6c99d7c17ab8c3e568ab33c0288938/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5bf41d725369b9986e6d64bad7079372b95c329897d684f955d7028c7f27a0", size = 135566, upload-time = "2025-09-20T07:17:17.69Z" }, { url = "https://files.pythonhosted.org/packages/bb/c3/6d3ce7a58e6c828e0795c9db8790d0593dd7fdf296e513c999150deb98d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:293e09825559d0929bbe5de01e1e115f7a6283d8996ab55652e5af365f032987", size = 134393, upload-time = "2025-09-20T07:17:18.802Z" }, ] + +[[package]] +name = "zeroconf" +version = "0.150.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ifaddr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/ea/34bb185645ecaa18d34e5883bffea71aa9bffbbb994634884e8b2f3ad0c4/zeroconf-0.150.0.tar.gz", hash = "sha256:a5fe7feab1de6ef5e541e0a3d07e534fd91629b813fc27281593584100f63164", size = 213634, upload-time = "2026-06-22T17:52:37.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/61/635e7e6405855afa85a424cc0549f74ba077690287776ad45a11d78b7cdc/zeroconf-0.150.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f79d9ce9cc40e14d2f7b8e4af9dcc893b7f7793be91d14a61a77ccb1ab5c90b", size = 1670236, upload-time = "2026-06-22T18:24:28.471Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/83b5825eb1d7fd4d32b7acbff5f3becca11c544403fc7f05835d9b3aa3f3/zeroconf-0.150.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f9ba6980ad92820f56a02a5035e21338c116145db44749462fc46f8f50028bb", size = 2080139, upload-time = "2026-06-22T18:24:30.158Z" }, + { url = "https://files.pythonhosted.org/packages/bf/68/8e764531d395acaf69f382e252a5e02b9841e417e30c53019442c9e9daee/zeroconf-0.150.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1a67dfae0d99b4f29f34721c7bbb2f3529507c64089addec9a565dc791524688", size = 1881686, upload-time = "2026-06-22T18:24:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/2d/60a04c3cf7e243e47d60152394a5e29a4839ea8fd1c260868ff7af08240d/zeroconf-0.150.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:489a51320d20e7f07b2f447cd515f3645d17ceced0212f7249a83ce6d3b748e0", size = 2179819, upload-time = "2026-06-22T18:24:34.051Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/1bb519d2843f383a625ad82e76d0dd8c185eab77b6d11f08e68fd3e04a09/zeroconf-0.150.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc160b8ba6a2420d665fb677c37fd2ab006141344bbf92abea72328bfd38bd3b", size = 2115584, upload-time = "2026-06-22T18:24:35.777Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ce/4cd36bfc74f7cf41cfef83333771cfa69ebab95f357ce605fc049b727f1d/zeroconf-0.150.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33843e0317bff2e03e56e246c3f25aab72ddb0a704f05bd792a4e78bdb5db801", size = 2111253, upload-time = "2026-06-22T18:24:37.911Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/ad3179888582ad22fd8c7c640e66a834669510deb24b1dedd9bfe6d6b0c5/zeroconf-0.150.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:024b00e64aae022ac6bdd7fe5afdabae570937c10d1242438ca04a4c62dfcd17", size = 1975576, upload-time = "2026-06-22T18:24:39.622Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/44d5dc609037a2d4b1ca53ff44d22c2cec4a5b2cff81c7b75e99136679a0/zeroconf-0.150.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2ee2297e383ac21193d84982e714029eafec470926315ab78b27a024858e4e5d", size = 2124978, upload-time = "2026-06-22T18:24:41.315Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/d4ff809d86280e756599fd712624376731aef6928be15f98e8834c3f45d2/zeroconf-0.150.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a03d493c3e9c4b4a5aaaf0408da7c025ed014f3c3df639cdd2c6cb518c42b4b", size = 2202468, upload-time = "2026-06-22T18:24:44.154Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/d0ca371828acebc7cab64d8460832b9bca7e9cbecefebd130bd8f3068f68/zeroconf-0.150.0-cp313-cp313-win32.whl", hash = "sha256:dc054a6b7772ec97fe215e8f8f35f43509cc9f1bf909791fe003acbaa78fd9b2", size = 1274140, upload-time = "2026-06-22T18:24:46.456Z" }, + { url = "https://files.pythonhosted.org/packages/d8/43/46bcbf3260c4726242042b69ebab13878bf14c167530bde56b6df267b55b/zeroconf-0.150.0-cp313-cp313-win_amd64.whl", hash = "sha256:122c6bec1f462c0feaa7f8fe4a4bc605f5c0d8f8e82e2ebd5aeae48a24da4564", size = 1511540, upload-time = "2026-06-22T18:24:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/18/08/22ccf6d3ba5f29d343dc8f9d45e2d8adbfc80b780c55a601fa7811d30d6f/zeroconf-0.150.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcfba6f9d344ade1f60aa8b5621a2fa8f65fbe740edc40b05627516bc55eec8f", size = 1686768, upload-time = "2026-06-22T18:24:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/de/4a/f4df1ee10bf74cff25c8825d3490b8b71f57d7ec559c84b76bae42ee3444/zeroconf-0.150.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31832790ed1bcf829b91d4697c6497fade20dbe1094c35d10d5af456cd0e8360", size = 2105247, upload-time = "2026-06-22T18:24:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d6/8af2192c3ffd8d67c36a369cdbf1ab388b672ef1a94e5baf73c8f003f08f/zeroconf-0.150.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ed611401d1e39f7d27e9cec8ef0b06fffad06955cc3c54714075b037d7b6a8b", size = 1859144, upload-time = "2026-06-22T18:24:54.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/09ae5f45ad6bde1decf246152c5a3c5183829ab3966d90cec2b06e9dc468/zeroconf-0.150.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0aac92f26cfe8075d6e210e5edb8194cc0b1d16eecdcdb04f31056a14cbc6466", size = 2191079, upload-time = "2026-06-22T18:24:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/60/78/3d46201c05a22b3eae1e48e82540590c8689a7646183f9f2c28b70e6edfa/zeroconf-0.150.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2fdaffee6f71432c64b609ade44e48763484b6084c7954eafeeff0cf4dbc4aef", size = 2125023, upload-time = "2026-06-22T18:24:58.322Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d9/65bf08d612551306227cc0843d0d468678d88dc1b101fdc2861c6938c40b/zeroconf-0.150.0-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:2bce10cb490119ad051f3caf74088317fec9f5e9acbe36e3690abe8d3de2ac31", size = 2187807, upload-time = "2026-06-22T17:52:35.597Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a6/fa1f712409f438d6a5b8f61c6592747f54894ae770841f8a2d659d79c638/zeroconf-0.150.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:048533286135c467a3bb90a268a9ca3ee3b98c3809d0106359e4d4c0e375e8ad", size = 2136974, upload-time = "2026-06-22T18:25:00.098Z" }, + { url = "https://files.pythonhosted.org/packages/35/a4/e8075345b6a61cbc8c7d75ef4d6bb58d0055dbb75887c6f3789f64214b60/zeroconf-0.150.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12f14afb42c08b8365d638ba2a01e5a73641dc8d5433fcdf8751d636210db053", size = 1944626, upload-time = "2026-06-22T18:25:02.376Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/417e588bb208d9298e081f00aa41134becf6d05738169f67f68f9a0faa0f/zeroconf-0.150.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cf19d67827af63c54175ef76a62a004734c0d18ac67ff383a5df36c26049707e", size = 2133245, upload-time = "2026-06-22T18:25:04.358Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/6de53f203e36275f2ea97566c36af7ad28e0def6a84da639ad4eef84910d/zeroconf-0.150.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49ae4f9109b2b7aa04fd75677ce33d552bb3db6f9e757acc6e2f6a547cbe26b2", size = 2213535, upload-time = "2026-06-22T18:25:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/e3509c21b146048759ea63af392972d5d3b39a218db58415ae84fbf3cfce/zeroconf-0.150.0-cp314-cp314-win32.whl", hash = "sha256:afad9aae3a10c4e54a0b55925c29b9d82d1e807f996e5e106a2b2fade6a9e3cc", size = 1306214, upload-time = "2026-06-22T18:25:08.291Z" }, + { url = "https://files.pythonhosted.org/packages/eb/97/d0d1221906963596d59f7a76086d22392a530f5851278fbe093db46809bf/zeroconf-0.150.0-cp314-cp314-win_amd64.whl", hash = "sha256:cd09bf1242af33769c88e1f0236ce52f6c74144a741c848fe38a79e68df80414", size = 1549661, upload-time = "2026-06-22T18:25:11.245Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/631f56f09dbe1aad76a3dd38bb92d4e5cce50a6630064590926d578d21da/zeroconf-0.150.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fed39ccd107ee219ad805ee4c38dca253effc1c96541b4954dee3c70dca13a71", size = 3362888, upload-time = "2026-06-22T18:25:13.066Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/53d1111468a69f395272531cb292c09e7b03ecf6186476d2c4a6dbe78198/zeroconf-0.150.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55d0c37be291364268688fe5dcd33bd5156f669289bb9b71a23b041ca53c9017", size = 4023043, upload-time = "2026-06-22T18:25:15.023Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d7/4ce3504120afa854fb55799b4f0e36c27b8d3def3d41f2d1731179bd7c19/zeroconf-0.150.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b97234eda197d4243d859b64004d175de76353dbca17527d11705f841fb02977", size = 3560681, upload-time = "2026-06-22T18:25:17.158Z" }, + { url = "https://files.pythonhosted.org/packages/82/60/5804e6362e4e3dae64923423b46c0fcc291a341d1e8ed50fec7af1cd73e7/zeroconf-0.150.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a31f0ec4bc3f125ae541fb96259a081f3f83d201f1fca59929155b90185df4", size = 4178405, upload-time = "2026-06-22T18:25:19.735Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2c/502e92caa81a7932fca22f78778e622ddca318f8d2243eddcf24d24ec0e7/zeroconf-0.150.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ee162b43e455043e22753cdc1fae0f2990d3fe3767f2e88695fb92273d7229b", size = 4044586, upload-time = "2026-06-22T18:25:21.812Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/a37dd7fc0b22e0c2e13b279611ae8060fb046d3712a7d33313c8b7b556a9/zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:178b6141e34a777593a16b8bf986b2d7ae806e47849a3ad517597f63bcaade47", size = 4089064, upload-time = "2026-06-22T18:25:24.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/7f/c2e7aeb8feb1a6ef3474d66774fad1c7b6040ac1bc48b188110d3c9d17a2/zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:410e30a80db9b73bfdee3c54dfe306ebb140e053dfe3b28cefd54756a19a73fe", size = 3717837, upload-time = "2026-06-22T18:25:26.305Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ee/b6b5b5f4f51296b7c686f57642003369478fe60a33a917f4421a6e20fc85/zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c539950bba739bc3980dd0f01187637e6237ada8bd70d64f93d8eb5eb2837a7b", size = 4072641, upload-time = "2026-06-22T18:25:28.356Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/3c292c091b636c3a0af5c69e1d1ffe83261737d34075efc889fe25380b32/zeroconf-0.150.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ccc9e1772ce0030f77e6a49542e9b7ca4e8ae9ce1bef84a3a983be68ddf81c", size = 4225357, upload-time = "2026-06-22T18:25:30.468Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5b/54758b003fe687757f9d8313a8ceaa7560e1c392ac109890782283ba1840/zeroconf-0.150.0-cp314-cp314t-win32.whl", hash = "sha256:576e72a9e9140d96ae110ceb0935f06fa41eeea8b2be53e442a6a00c84035194", size = 2599274, upload-time = "2026-06-22T18:25:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/ff/be/9eb576ae5eeede25cd1e59f0be1ce6b2fcf7aa5a4ab4d6d05de2d30efc4a/zeroconf-0.150.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bdae152e80b848117691ab59aa8b16bf15323c0e9f3ddc38c6345af95a700a84", size = 3106484, upload-time = "2026-06-22T18:25:34.89Z" }, +] From 2a4ed752def057b101233a70919d35fd3dbdf293 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:53:42 -0400 Subject: [PATCH 2/3] test: cover IP transport TLS-PSK and mDNS discovery paths Patch coverage on #147 was 59.58% against a 79.05% target, failing codecov/patch. Brings transport/ip.py and discovery_ip.py to 100%. - test_transport_ip.py: _build_ssl_context() verified against a real loopback TLS-PSK server (handshake, framed round-trip, wrong-PSK rejection), asserting the "opendisplay" identity reaches the wire; skipped on OpenSSL builds without PSK ciphersuites. Adds the connect/disconnect/write/read error branches. - test_discovery_ip.py (new): zeroconf stubbed at the lazily-imported module attributes. Covers resolution skips, the name-suffix fallback, teardown on error, and the TXT decode helpers. cli.py's new lines stay uncovered: it is 0% repo-wide on main and needs CLI test scaffolding that does not exist yet. --- tests/unit/test_discovery_ip.py | 196 ++++++++++++++++++++++++++++++ tests/unit/test_transport_ip.py | 206 +++++++++++++++++++++++++++++++- 2 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_discovery_ip.py diff --git a/tests/unit/test_discovery_ip.py b/tests/unit/test_discovery_ip.py new file mode 100644 index 0000000..e4a2cce --- /dev/null +++ b/tests/unit/test_discovery_ip.py @@ -0,0 +1,196 @@ +"""Tests for mDNS/DNS-SD discovery of WiFi devices (discover_ip_devices). + +zeroconf is stubbed at the module attributes the function imports lazily, so +no real multicast traffic happens. The TXT decode helpers are tested directly +since they define how firmware-supplied records map onto IpDeviceInfo. +""" + +from __future__ import annotations + +from typing import Any + +import pytest +import zeroconf +import zeroconf.asyncio +from zeroconf import ServiceStateChange + +from opendisplay.discovery_ip import IpDeviceInfo, _txt_bool, _txt_str, discover_ip_devices + +SERVICE_TYPE = "_opendisplay._tcp.local." + + +def _txt(**kwargs: str) -> dict[bytes, bytes | None]: + return {key.encode(): value.encode() for key, value in kwargs.items()} + + +class _FakeServiceInfo: + """Stands in for AsyncServiceInfo; behavior comes from _REGISTRY.""" + + def __init__(self, _service_type: str, name: str) -> None: + self.name = name + spec = _REGISTRY[name] + self._resolves: bool = spec.get("resolves", True) + self._addresses: list[str] = spec.get("addresses", ["192.168.1.50"]) + self.port: int | None = spec.get("port", 2446) + self.properties: dict[bytes, bytes | None] = spec.get("properties", {}) + + async def async_request(self, _zc: object, timeout: int = 3000) -> bool: + return self._resolves + + def parsed_scoped_addresses(self) -> list[str]: + return self._addresses + + +class _FakeBrowser: + """Fires Added for every registered name as soon as browsing starts.""" + + instances: list[_FakeBrowser] = [] + + def __init__(self, _zc: object, _service_type: str, handlers: list[Any]) -> None: + self.cancelled = False + _FakeBrowser.instances.append(self) + for name in _REGISTRY: + for handler in handlers: + handler(_zc, _service_type, name, ServiceStateChange.Added) + # A concurrent Removed for the same name must not register it. + handler(_zc, _service_type, name, ServiceStateChange.Removed) + + async def async_cancel(self) -> None: + self.cancelled = True + + +class _FakeAsyncZeroconf: + instances: list[_FakeAsyncZeroconf] = [] + + def __init__(self) -> None: + self.zeroconf = object() + self.closed = False + _FakeAsyncZeroconf.instances.append(self) + + async def async_close(self) -> None: + self.closed = True + + +_REGISTRY: dict[str, dict[str, Any]] = {} + + +@pytest.fixture(autouse=True) +def _stub_zeroconf(monkeypatch: pytest.MonkeyPatch) -> None: + _REGISTRY.clear() + _FakeAsyncZeroconf.instances.clear() + _FakeBrowser.instances.clear() + monkeypatch.setattr(zeroconf.asyncio, "AsyncZeroconf", _FakeAsyncZeroconf) + monkeypatch.setattr(zeroconf.asyncio, "AsyncServiceBrowser", _FakeBrowser) + monkeypatch.setattr(zeroconf.asyncio, "AsyncServiceInfo", _FakeServiceInfo) + + +# ── TXT decoding helpers ───────────────────────────────────────────────────── + + +def test_txt_str_decodes_present_key() -> None: + assert _txt_str(_txt(mac="aa:bb:cc:dd:ee:ff"), "mac") == "aa:bb:cc:dd:ee:ff" + + +def test_txt_str_missing_key_is_none() -> None: + assert _txt_str({}, "mac") is None + assert _txt_str({b"mac": None}, "mac") is None + + +def test_txt_str_undecodable_value_is_none() -> None: + assert _txt_str({b"fw": b"\xff\xfe"}, "fw") is None + + +@pytest.mark.parametrize("raw", ["1", "true", "TRUE", "yes", " Yes "]) +def test_txt_bool_truthy_forms(raw: str) -> None: + assert _txt_bool({b"tls": raw.encode()}, "tls") is True + + +@pytest.mark.parametrize("raw", ["0", "false", "no", "", "maybe"]) +def test_txt_bool_falsy_forms(raw: str) -> None: + assert _txt_bool({b"tls": raw.encode()}, "tls") is False + + +def test_txt_bool_missing_key_is_false() -> None: + assert _txt_bool({}, "tls") is False + + +# ── discovery ──────────────────────────────────────────────────────────────── + + +async def test_discovers_device_with_full_txt_record() -> None: + _REGISTRY[f"kitchen.{SERVICE_TYPE}"] = { + "addresses": ["192.168.1.50"], + "port": 2446, + "properties": _txt(mac="aa:bb:cc:dd:ee:ff", tls="1", msd="4.2", fw="2.2.0", cm="1"), + } + found = await discover_ip_devices(scan_seconds=0) + assert found == { + "kitchen": IpDeviceInfo( + name="kitchen", + host="192.168.1.50", + port=2446, + mac="aa:bb:cc:dd:ee:ff", # passed through raw (lowercase-colon) + tls=True, + msd="4.2", + fw="2.2.0", + cm="1", + ) + } + + +async def test_optional_txt_fields_default_to_none() -> None: + _REGISTRY[f"bare.{SERVICE_TYPE}"] = {"properties": _txt(mac="11:22:33:44:55:66", tls="0")} + device = (await discover_ip_devices(scan_seconds=0))["bare"] + assert (device.tls, device.msd, device.fw, device.cm) == (False, None, None, None) + + +async def test_unresolvable_service_is_skipped() -> None: + _REGISTRY[f"ghost.{SERVICE_TYPE}"] = {"resolves": False} + assert await discover_ip_devices(scan_seconds=0) == {} + + +async def test_service_without_address_is_skipped() -> None: + _REGISTRY[f"noaddr.{SERVICE_TYPE}"] = {"addresses": []} + assert await discover_ip_devices(scan_seconds=0) == {} + + +async def test_service_without_port_is_skipped() -> None: + _REGISTRY[f"noport.{SERVICE_TYPE}"] = {"port": None} + assert await discover_ip_devices(scan_seconds=0) == {} + + +async def test_unexpected_suffix_falls_back_to_first_label() -> None: + _REGISTRY["odd-name._other._tcp.local."] = {"properties": _txt(mac="aa:bb:cc:dd:ee:ff")} + found = await discover_ip_devices(scan_seconds=0) + assert list(found) == ["odd-name"] + + +async def test_mixed_results_keeps_only_usable_devices() -> None: + _REGISTRY[f"good.{SERVICE_TYPE}"] = {"properties": _txt(mac="aa:bb:cc:dd:ee:ff", tls="1")} + _REGISTRY[f"ghost.{SERVICE_TYPE}"] = {"resolves": False} + _REGISTRY[f"noaddr.{SERVICE_TYPE}"] = {"addresses": []} + found = await discover_ip_devices(scan_seconds=0) + assert list(found) == ["good"] + + +async def test_no_services_returns_empty_mapping() -> None: + assert await discover_ip_devices(scan_seconds=0) == {} + + +async def test_zeroconf_is_closed_even_when_resolution_raises(monkeypatch: pytest.MonkeyPatch) -> None: + class _ExplodingInfo(_FakeServiceInfo): + async def async_request(self, _zc: object, timeout: int = 3000) -> bool: + raise RuntimeError("mDNS stack died") + + monkeypatch.setattr(zeroconf.asyncio, "AsyncServiceInfo", _ExplodingInfo) + _REGISTRY[f"kitchen.{SERVICE_TYPE}"] = {} + with pytest.raises(RuntimeError): + await discover_ip_devices(scan_seconds=0) + assert _FakeAsyncZeroconf.instances[-1].closed # no leaked socket + + +async def test_browser_and_zeroconf_are_torn_down() -> None: + _REGISTRY[f"kitchen.{SERVICE_TYPE}"] = {} + await discover_ip_devices(scan_seconds=0) + assert _FakeBrowser.instances[-1].cancelled + assert _FakeAsyncZeroconf.instances[-1].closed diff --git a/tests/unit/test_transport_ip.py b/tests/unit/test_transport_ip.py index b743656..b276d1a 100644 --- a/tests/unit/test_transport_ip.py +++ b/tests/unit/test_transport_ip.py @@ -9,12 +9,26 @@ from __future__ import annotations import asyncio +import ssl import pytest from opendisplay.exceptions import OpenDisplayConnectionError, OpenDisplayTimeoutError from opendisplay.protocol import OD_LAN_MAX_PAYLOAD -from opendisplay.transport.ip import TcpTransport +from opendisplay.transport.ip import _PSK_IDENTITY, TcpTransport + + +def _openssl_has_psk() -> bool: + """Whether this OpenSSL build offers PSK ciphersuites (not all do).""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + try: + context.set_ciphers("PSK") + except ssl.SSLError: + return False + return any("PSK" in cipher["name"] for cipher in context.get_ciphers()) + + +requires_psk = pytest.mark.skipif(not _openssl_has_psk(), reason="OpenSSL build lacks PSK ciphersuites") class _FakeWriter: @@ -186,6 +200,196 @@ async def test_connect_refused_raises_connection_error() -> None: await t.connect() +async def test_connect_when_already_connected_is_a_noop() -> None: + t = _wire(asyncio.StreamReader()) + writer = t._writer + await t.connect() # must not dial out or replace the live link + assert t._writer is writer + + +async def test_connect_timeout_raises_timeout_error(monkeypatch: pytest.MonkeyPatch) -> None: + async def never_connects(*_args: object, **_kwargs: object) -> None: + await asyncio.sleep(10) + + monkeypatch.setattr(asyncio, "open_connection", never_connects) + t = TcpTransport("127.0.0.1", 2446, timeout=0.02) + with pytest.raises(OpenDisplayTimeoutError): + await t.connect() + + +# ── TLS-PSK ────────────────────────────────────────────────────────────────── + + +def test_ssl_context_is_psk_only_tls12() -> None: + context = TcpTransport("h", 2447, tls=True, psk=b"k")._build_ssl_context() + assert context.check_hostname is False + assert context.verify_mode is ssl.CERT_NONE + assert context.minimum_version is ssl.TLSVersion.TLSv1_2 + assert context.maximum_version is ssl.TLSVersion.TLSv1_2 + + +@requires_psk +def test_ssl_context_offers_only_psk_ciphersuites() -> None: + # set_ciphers() cannot filter TLS 1.3 suites, but the 1.2 version pin makes + # them unreachable, so every negotiable (TLS 1.2) suite must be PSK — the + # handshake can then never ask for a certificate. + context = TcpTransport("h", 2447, tls=True, psk=b"k")._build_ssl_context() + negotiable = [c["name"] for c in context.get_ciphers() if c["protocol"] != "TLSv1.3"] + assert negotiable, "no TLS 1.2 ciphersuites enabled" + assert all("PSK" in name for name in negotiable) + + +def test_psk_callback_returns_identity_and_key(monkeypatch: pytest.MonkeyPatch) -> None: + captured: list[object] = [] + monkeypatch.setattr( + ssl.SSLContext, + "set_psk_client_callback", + lambda _self, callback: captured.append(callback), + ) + TcpTransport("h", 2447, tls=True, psk=b"secret")._build_ssl_context() + assert captured + assert captured[0](None) == (_PSK_IDENTITY, b"secret") # type: ignore[operator] + + +def test_psk_callback_without_key_yields_empty_bytes(monkeypatch: pytest.MonkeyPatch) -> None: + # tls=True with psk=None must not blow up building the context; the empty + # key simply fails the handshake at the peer. + captured: list[object] = [] + monkeypatch.setattr( + ssl.SSLContext, + "set_psk_client_callback", + lambda _self, callback: captured.append(callback), + ) + TcpTransport("h", 2447, tls=True)._build_ssl_context() + assert captured[0](None) == (_PSK_IDENTITY, b"") # type: ignore[operator] + + +async def _psk_server(psk: bytes, identities: list[str]) -> asyncio.Server: + """Loopback TLS-PSK server that echoes one framed response.""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.maximum_version = ssl.TLSVersion.TLSv1_2 + context.set_ciphers("PSK") + + def _server_callback(identity: str | None) -> bytes: + identities.append(identity or "") + return psk + + context.set_psk_server_callback(_server_callback, _PSK_IDENTITY) + + async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + header = await reader.readexactly(2) + await reader.readexactly(int.from_bytes(header, "little")) + writer.write(_framed(b"\x00\x71")) + await writer.drain() + + return await asyncio.start_server(handle, "127.0.0.1", 0, ssl=context) + + +@requires_psk +async def test_tls_psk_roundtrip_against_real_server() -> None: + identities: list[str] = [] + server = await _psk_server(b"sh4red-k3y", identities) + port = server.sockets[0].getsockname()[1] + async with server: + t = TcpTransport("127.0.0.1", port, timeout=5.0, tls=True, psk=b"sh4red-k3y") + await t.connect() + assert t.is_connected + await t.write_command(b"\x00\x40ping") + assert await t.read_response(timeout=5.0) == b"\x00\x71" + await t.disconnect() + assert identities == [_PSK_IDENTITY] # firmware-expected identity on the wire + + +@requires_psk +async def test_tls_psk_mismatch_raises_connection_error() -> None: + identities: list[str] = [] + server = await _psk_server(b"the-right-key", identities) + port = server.sockets[0].getsockname()[1] + async with server: + t = TcpTransport("127.0.0.1", port, timeout=5.0, tls=True, psk=b"the-wrong-key") + with pytest.raises(OpenDisplayConnectionError): + await t.connect() + assert not t.is_connected + + +# ── disconnect / teardown ──────────────────────────────────────────────────── + + +async def test_disconnect_without_a_connection_is_a_noop() -> None: + t = TcpTransport("127.0.0.1", 2446) + await t.disconnect() # must not raise + assert not t.is_connected + + +async def test_disconnect_swallows_close_errors() -> None: + class _AngryWriter(_FakeWriter): + def close(self) -> None: + raise OSError("half-open link") + + t = TcpTransport("127.0.0.1", 2446) + t._writer = _AngryWriter() # type: ignore[assignment] + await t.disconnect() # best-effort: never raises + assert not t.is_connected + + +async def test_disconnect_swallows_wait_closed_timeout() -> None: + class _StuckWriter(_FakeWriter): + async def wait_closed(self) -> None: + await asyncio.sleep(10) + + t = TcpTransport("127.0.0.1", 2446, timeout=0.02) + t._writer = _StuckWriter() # type: ignore[assignment] + await t.disconnect() + assert not t.is_connected + + +# ── write / read error paths ───────────────────────────────────────────────── + + +async def test_write_when_not_connected_raises() -> None: + t = TcpTransport("127.0.0.1", 2446) + with pytest.raises(OpenDisplayConnectionError): + await t.write_command(b"\x00\x40") + + +async def test_write_failure_raises_connection_error() -> None: + class _BrokenWriter(_FakeWriter): + async def drain(self) -> None: + raise ConnectionResetError("peer went away") + + t = _wire(asyncio.StreamReader()) + t._writer = _BrokenWriter() # type: ignore[assignment] + with pytest.raises(OpenDisplayConnectionError): + await t.write_command(b"\x00\x40") + + +async def test_eof_before_header_raises_connection_error() -> None: + reader = asyncio.StreamReader() + reader.feed_data(b"\x01") # one byte of a 2-byte prefix, then EOF + reader.feed_eof() + t = _wire(reader) + with pytest.raises(OpenDisplayConnectionError): + await t.read_response(timeout=1.0) + + +async def test_body_timeout_raises_timeout_error() -> None: + reader = asyncio.StreamReader() + reader.feed_data((10).to_bytes(2, "little")) # header only; body never arrives + t = _wire(reader) + with pytest.raises(OpenDisplayTimeoutError): + await t.read_response(timeout=0.02) + + +# ── Transport conformance no-ops ───────────────────────────────────────────── + + +async def test_notification_and_cache_hooks_are_noops() -> None: + t = TcpTransport("h", 2446) + assert t.drain_notifications() == 0 + assert await t.clear_cache() is False + + def test_class_attributes() -> None: t = TcpTransport("h", 2446) assert t.max_frame == OD_LAN_MAX_PAYLOAD From d5a7ed63393556a133f0a7ee32d97c08342c4daf Mon Sep 17 00:00:00 2001 From: gabriel Date: Thu, 30 Jul 2026 19:39:55 +0200 Subject: [PATCH 3/3] fix(discovery): bind zeroconf handler by the keyword names it dispatches with `_on_change` declared its first two parameters as `_zeroconf` / `_service_type`, but zeroconf's Signal.fire() invokes handlers with KEYWORD arguments (zeroconf=, service_type=, name=, state_change=). Every service-state-change event therefore raised TypeError: _on_change() got an unexpected keyword argument 'zeroconf' which the browser swallows, so `scan --lan` reported "No WiFi OpenDisplay devices found" while the device was on the network advertising itself. Reproduced 100% of the time with zeroconf 0.150.0. The underscore prefix reads as the usual unused-argument convention, but these names are part of the callback contract; only positional-only parameters are safe to rename. _FakeBrowser called the handler positionally, so the fake and the callback shared the same wrong assumption and agreed with each other all the way to green. Dispatch by keyword in the fake so it matches real zeroconf: reverting the discovery_ip.py half now fails 9 tests instead of 0. Verified on hardware (reTerminal E1003, ESP32-S3): `scan --lan` finds the device first try. --- src/opendisplay/discovery_ip.py | 7 +++++-- tests/unit/test_discovery_ip.py | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/opendisplay/discovery_ip.py b/src/opendisplay/discovery_ip.py index 2bcf070..df900bb 100644 --- a/src/opendisplay/discovery_ip.py +++ b/src/opendisplay/discovery_ip.py @@ -80,11 +80,14 @@ async def discover_ip_devices(scan_seconds: float = 3.0) -> dict[str, IpDeviceIn found_names: set[str] = set() def _on_change( - _zeroconf: object, - _service_type: str, + zeroconf: object, # noqa: ARG001 # pylint: disable=unused-argument + service_type: str, # noqa: ARG001 # pylint: disable=unused-argument name: str, state_change: ServiceStateChange, ) -> None: + # zeroconf's Signal.fire() dispatches to handlers with KEYWORD arguments + # (zeroconf=, service_type=, name=, state_change=), so these parameter + # names are part of the contract and must not be underscore-prefixed. if state_change is ServiceStateChange.Added: found_names.add(name) diff --git a/tests/unit/test_discovery_ip.py b/tests/unit/test_discovery_ip.py index e4a2cce..3f68726 100644 --- a/tests/unit/test_discovery_ip.py +++ b/tests/unit/test_discovery_ip.py @@ -51,9 +51,22 @@ def __init__(self, _zc: object, _service_type: str, handlers: list[Any]) -> None _FakeBrowser.instances.append(self) for name in _REGISTRY: for handler in handlers: - handler(_zc, _service_type, name, ServiceStateChange.Added) + # Real zeroconf dispatches via Signal.fire() using KEYWORD + # arguments; calling positionally here would let a handler with + # mismatched parameter names pass the suite and fail on hardware. + handler( + zeroconf=_zc, + service_type=_service_type, + name=name, + state_change=ServiceStateChange.Added, + ) # A concurrent Removed for the same name must not register it. - handler(_zc, _service_type, name, ServiceStateChange.Removed) + handler( + zeroconf=_zc, + service_type=_service_type, + name=name, + state_change=ServiceStateChange.Removed, + ) async def async_cancel(self) -> None: self.cancelled = True