feat: add safe BLE discovery and network baseline tools

This commit is contained in:
DCCONSTRUCTIONS 2026-07-15 15:26:14 +03:00
parent 4c401bc59b
commit faa442eefc
16 changed files with 666 additions and 6 deletions

View File

@ -6,7 +6,8 @@ evidence.
## Non-negotiable safety boundaries ## Non-negotiable safety boundaries
- Default to passive or read-only operations. - Default to non-mutating discovery or read-only operations. CoreBluetooth BLE
scanning on macOS is active discovery, not passive radio sniffing.
- Do not send BLE writes until the exact service, characteristic, framing, - Do not send BLE writes until the exact service, characteristic, framing,
payload semantics, rollback, and expected state transition are documented. payload semantics, rollback, and expected state transition are documented.
- BLE notification subscription may perform the standard temporary CCCD write; - BLE notification subscription may perform the standard temporary CCCD write;

View File

@ -50,6 +50,20 @@ uv run pytest
and reports external tools; it does not request Bluetooth permission, scan the and reports external tools; it does not request Bluetooth permission, scan the
LAN, touch the K1, alter Homebrew, or change capture permissions. LAN, touch the K1, alter Homebrew, or change capture permissions.
The currently implemented laboratory commands are:
```bash
uv run k1link ble scan --duration 30 --out sessions/<id>/captures/ble.json
uv run k1link ble gatt-dump --device <corebluetooth-uuid> \
--out sessions/<id>/captures/gatt.json
uv run k1link net snapshot --out sessions/<id>/captures/network.json
```
On macOS the scan is active CoreBluetooth discovery, but it does not connect to
or modify devices. `gatt-dump` connects and performs service discovery only; it
does not read characteristic values, subscribe to notifications or write
configuration. Session output is sensitive and ignored by Git.
## Documentation ## Documentation
- [Technical audit](docs/00_TECHNICAL_AUDIT.md) - [Technical audit](docs/00_TECHNICAL_AUDIT.md)
@ -65,7 +79,7 @@ provenance remains clear.
## Safety boundary ## Safety boundary
Allowed initial work is passive discovery, standard device-information reads, Allowed initial work is non-mutating discovery, standard device-information reads,
controlled notification listening, autonomous button operation, targeted controlled notification listening, autonomous button operation, targeted
capture of traffic to or from the confirmed K1 address, and offline analysis of capture of traffic to or from the confirmed K1 address, and offline analysis of
owned artifacts. owned artifacts.

View File

@ -43,7 +43,7 @@ documenting the missing ground truth.
STOP: serious fault, overheating, activation lock, or destructive/ambiguous STOP: serious fault, overheating, activation lock, or destructive/ambiguous
device state. device state.
## Stage 1 — BLE read-only toolkit ## Stage 1 — BLE non-mutating discovery and metadata toolkit
Minimal dependencies: `bleak` plus the existing CLI stack. Add them only in the Minimal dependencies: `bleak` plus the existing CLI stack. Add them only in the
repository-local environment. repository-local environment.
@ -59,6 +59,8 @@ k1link ble listen
Requirements: Requirements:
- preserve advertisement snapshots, manufacturer/service data and RSSI; - preserve advertisement snapshots, manufacturer/service data and RSSI;
- describe macOS scanning as active discovery: CoreBluetooth does not support
passive scan mode, although discovery does not change K1 configuration;
- identify by observed name + macOS UUID + advertisement fingerprint; - identify by observed name + macOS UUID + advertisement fingerprint;
- inspect standby, scanning and USB states separately; - inspect standby, scanning and USB states separately;
- start with standard Device Information/Battery services; - start with standard Device Information/Battery services;

View File

@ -49,7 +49,7 @@ Decision:
- PAUSE USB analysis if recording works but USB is unavailable. - PAUSE USB analysis if recording works but USB is unavailable.
- STOP on activation lock or device fault. - STOP on activation lock or device fault.
## Experiment C — passive BLE surface ## Experiment C — BLE discovery and metadata surface
Prerequisite: Stage 1 BLE commands implemented and reviewed. Prerequisite: Stage 1 BLE commands implemented and reviewed.
@ -131,4 +131,3 @@ GO / PAUSE / BLOCKED:
Evidence: Evidence:
Next smallest experiment: Next smallest experiment:
``` ```

View File

@ -11,6 +11,7 @@ requires-python = ">=3.12,<3.13"
license = { text = "Proprietary" } license = { text = "Proprietary" }
authors = [{ name = "NODE.DC" }] authors = [{ name = "NODE.DC" }]
dependencies = [ dependencies = [
"bleak==3.0.2",
"rich>=13.9,<15", "rich>=13.9,<15",
"typer>=0.15,<1", "typer>=0.15,<1",
] ]

39
src/k1link/artifacts.py Normal file
View File

@ -0,0 +1,39 @@
from __future__ import annotations
import json
import os
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
def utc_now_iso() -> str:
"""Return a stable UTC timestamp for manifests and capture artifacts."""
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def write_json_atomic(path: Path, payload: Any) -> None:
"""Write JSON without exposing a partially written artifact."""
path = path.expanduser()
path.parent.mkdir(parents=True, exist_ok=True)
serialized = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
temp_name: str | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
delete=False,
) as stream:
temp_name = stream.name
stream.write(serialized)
stream.flush()
os.fsync(stream.fileno())
Path(temp_name).replace(path)
finally:
if temp_name is not None:
Path(temp_name).unlink(missing_ok=True)

View File

@ -0,0 +1 @@
"""Bluetooth Low Energy discovery tools."""

104
src/k1link/ble/gatt.py Normal file
View File

@ -0,0 +1,104 @@
from __future__ import annotations
import asyncio
from importlib.metadata import version
from typing import TypedDict
from bleak import BleakClient, BleakScanner
from bleak.exc import BleakDeviceNotFoundError
from k1link.artifacts import utc_now_iso
class DescriptorRecord(TypedDict):
uuid: str
handle: int
description: str
class CharacteristicRecord(TypedDict):
uuid: str
handle: int
description: str
properties: list[str]
descriptors: list[DescriptorRecord]
class ServiceRecord(TypedDict):
uuid: str
handle: int
description: str
characteristics: list[CharacteristicRecord]
class GattDumpResult(TypedDict):
schema_version: int
started_at_utc: str
completed_at_utc: str
adapter: str
bleak_version: str
device_macos_uuid: str
device_name: str
metadata_only: bool
services: list[ServiceRecord]
async def dump_metadata(device_macos_uuid: str, timeout_seconds: float) -> GattDumpResult:
"""Connect and enumerate GATT metadata without characteristic reads or writes."""
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
started_at = utc_now_iso()
async with asyncio.timeout(timeout_seconds):
device = await BleakScanner.find_device_by_address(
device_macos_uuid,
timeout=min(20.0, timeout_seconds),
)
if device is None:
raise BleakDeviceNotFoundError(
device_macos_uuid,
"Device was not rediscovered; keep the K1 powered and nearby.",
)
async with BleakClient(device, timeout=timeout_seconds, pair=False) as client:
services: list[ServiceRecord] = []
for service in client.services:
characteristics: list[CharacteristicRecord] = []
for characteristic in service.characteristics:
descriptors: list[DescriptorRecord] = []
for descriptor in characteristic.descriptors:
descriptors.append(
{
"uuid": descriptor.uuid,
"handle": descriptor.handle,
"description": descriptor.description,
}
)
characteristics.append(
{
"uuid": characteristic.uuid,
"handle": characteristic.handle,
"description": characteristic.description,
"properties": sorted(characteristic.properties),
"descriptors": descriptors,
}
)
services.append(
{
"uuid": service.uuid,
"handle": service.handle,
"description": service.description,
"characteristics": characteristics,
}
)
return {
"schema_version": 1,
"started_at_utc": started_at,
"completed_at_utc": utc_now_iso(),
"adapter": "CoreBluetooth",
"bleak_version": version("bleak"),
"device_macos_uuid": device_macos_uuid,
"device_name": client.name,
"metadata_only": True,
"services": services,
}

81
src/k1link/ble/scanner.py Normal file
View File

@ -0,0 +1,81 @@
from __future__ import annotations
from importlib.metadata import version
from typing import TypedDict
from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from k1link.artifacts import utc_now_iso
class BleDeviceRecord(TypedDict):
macos_uuid: str
id_kind: str
name: str | None
local_name: str | None
rssi: int
tx_power: int | None
service_uuids: list[str]
manufacturer_data_hex: dict[str, str]
service_data_hex: dict[str, str]
k1_name_candidate: bool
class BleScanResult(TypedDict):
schema_version: int
started_at_utc: str
completed_at_utc: str
duration_seconds: float
adapter: str
bleak_version: str
device_count: int
devices: list[BleDeviceRecord]
def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) -> BleDeviceRecord:
name = advertisement.local_name or device.name
normalized_name = (name or "").casefold()
return {
"macos_uuid": device.address,
"id_kind": "corebluetooth_uuid",
"name": device.name,
"local_name": advertisement.local_name,
"rssi": advertisement.rssi,
"tx_power": advertisement.tx_power,
"service_uuids": sorted(advertisement.service_uuids),
"manufacturer_data_hex": {
str(company_id): data.hex()
for company_id, data in sorted(advertisement.manufacturer_data.items())
},
"service_data_hex": {
service_uuid: data.hex()
for service_uuid, data in sorted(advertisement.service_data.items())
},
"k1_name_candidate": any(marker in normalized_name for marker in ("lixel", "xgrids", "k1")),
}
async def scan(duration_seconds: float) -> BleScanResult:
if duration_seconds <= 0:
raise ValueError("duration_seconds must be positive")
started_at = utc_now_iso()
discovered = await BleakScanner.discover(timeout=duration_seconds, return_adv=True)
devices = [
advertisement_record(device, advertisement) for device, advertisement in discovered.values()
]
devices.sort(
key=lambda item: (not item["k1_name_candidate"], -item["rssi"], item["macos_uuid"])
)
return {
"schema_version": 1,
"started_at_utc": started_at,
"completed_at_utc": utc_now_iso(),
"duration_seconds": duration_seconds,
"adapter": "CoreBluetooth",
"bleak_version": version("bleak"),
"device_count": len(devices),
"devices": devices,
}

View File

@ -1,18 +1,24 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import json import json
import platform import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import TypedDict from typing import Annotated, TypedDict
import typer import typer
from bleak.exc import BleakError
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from k1link import __version__ from k1link import __version__
from k1link.artifacts import write_json_atomic
from k1link.ble.gatt import dump_metadata
from k1link.ble.scanner import scan
from k1link.net.snapshot import snapshot
app = typer.Typer( app = typer.Typer(
name="k1link", name="k1link",
@ -20,6 +26,10 @@ app = typer.Typer(
no_args_is_help=True, no_args_is_help=True,
) )
console = Console() console = Console()
ble_app = typer.Typer(help="Bluetooth LE discovery and metadata commands.", no_args_is_help=True)
net_app = typer.Typer(help="Passive local network observation commands.", no_args_is_help=True)
app.add_typer(ble_app, name="ble")
app.add_typer(net_app, name="net")
class ToolStatus(TypedDict): class ToolStatus(TypedDict):
@ -168,5 +178,86 @@ def doctor(
console.print(f"- {note}") console.print(f"- {note}")
@ble_app.command("scan")
def ble_scan(
out: Annotated[
Path,
typer.Option(help="Ignored session JSON path; parent directories are created."),
],
duration: Annotated[
float,
typer.Option(min=1.0, max=300.0, help="Scan duration in seconds."),
] = 30.0,
) -> None:
"""Discover BLE advertisements without connecting or changing device configuration."""
try:
result = asyncio.run(scan(duration))
except (BleakError, OSError, ValueError) as exc:
console.print(f"[red]BLE scan failed:[/red] {type(exc).__name__}: {exc}")
console.print("Check System Settings → Privacy & Security → Bluetooth.")
raise typer.Exit(code=2) from exc
write_json_atomic(out, result)
table = Table(title=f"BLE devices ({result['device_count']})")
table.add_column("Candidate")
table.add_column("Name")
table.add_column("RSSI")
table.add_column("macOS UUID")
for device in result["devices"]:
table.add_row(
"K1?" if device["k1_name_candidate"] else "",
device["local_name"] or device["name"] or "-",
str(device["rssi"]),
device["macos_uuid"],
)
console.print(table)
console.print(f"Saved: {out}")
@ble_app.command("gatt-dump")
def ble_gatt_dump(
device: Annotated[str, typer.Option(help="CoreBluetooth/macOS UUID from ble scan.")],
out: Annotated[
Path,
typer.Option(help="Ignored session JSON path; parent directories are created."),
],
timeout: Annotated[
float,
typer.Option(min=5.0, max=120.0, help="Connection timeout in seconds."),
] = 45.0,
) -> None:
"""Enumerate GATT metadata only: no characteristic reads, subscriptions or writes."""
console.print(
"Connecting for service discovery only; no characteristic values will be read or written."
)
try:
result = asyncio.run(dump_metadata(device, timeout))
except (BleakError, OSError, ValueError) as exc:
console.print(f"[red]GATT metadata failed:[/red] {type(exc).__name__}: {exc}")
raise typer.Exit(code=2) from exc
write_json_atomic(out, result)
characteristic_count = sum(len(service["characteristics"]) for service in result["services"])
console.print(
f"Device: {result['device_name']}; services: {len(result['services'])}; "
f"characteristics: {characteristic_count}"
)
console.print(f"Saved: {out}")
@net_app.command("snapshot")
def net_snapshot(
out: Annotated[
Path,
typer.Option(help="Ignored session JSON path; parent directories are created."),
],
) -> None:
"""Save routes, interfaces and the existing neighbor table without scanning the LAN."""
result = snapshot()
write_json_atomic(out, result)
console.print("Saved a sensitive local network snapshot; do not commit the output.")
console.print(f"Saved: {out}")
if __name__ == "__main__": if __name__ == "__main__":
app() app()

View File

@ -0,0 +1 @@
"""Passive network observation tools."""

View File

@ -0,0 +1,65 @@
from __future__ import annotations
import subprocess
from typing import TypedDict
from k1link.artifacts import utc_now_iso
class CommandRecord(TypedDict):
argv: list[str]
returncode: int | None
stdout: str
stderr: str
error: str | None
class NetworkSnapshot(TypedDict):
schema_version: int
created_at_utc: str
sensitivity: str
commands: list[CommandRecord]
def command_record(argv: list[str]) -> CommandRecord:
try:
result = subprocess.run(
argv,
check=False,
capture_output=True,
text=True,
timeout=10,
)
except (OSError, subprocess.SubprocessError) as exc:
return {
"argv": argv,
"returncode": None,
"stdout": "",
"stderr": "",
"error": f"{type(exc).__name__}: {exc}",
}
return {
"argv": argv,
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"error": None,
}
def snapshot() -> NetworkSnapshot:
"""Collect a local read-only network snapshot; no packets are transmitted intentionally."""
commands = [
["route", "-n", "get", "default"],
["networksetup", "-listallhardwareports"],
["scutil", "--nwi"],
["arp", "-an"],
]
return {
"schema_version": 1,
"created_at_utc": utc_now_iso(),
"sensitivity": (
"contains local interface, route, IP/MAC and neighbor metadata; do not commit"
),
"commands": [command_record(command) for command in commands],
}

11
tests/test_artifacts.py Normal file
View File

@ -0,0 +1,11 @@
import json
from pathlib import Path
from k1link.artifacts import write_json_atomic
def test_write_json_atomic(tmp_path: Path) -> None:
output = tmp_path / "nested" / "artifact.json"
write_json_atomic(output, {"value": "тест"})
assert json.loads(output.read_text(encoding="utf-8")) == {"value": "тест"}
assert not list(output.parent.glob("*.tmp"))

23
tests/test_ble_scanner.py Normal file
View File

@ -0,0 +1,23 @@
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from k1link.ble.scanner import advertisement_record
def test_advertisement_record_marks_k1_candidate() -> None:
device = BLEDevice("TEST-UUID", "Unknown", details=None)
advertisement = AdvertisementData(
local_name="Lixel-K1-1234",
manufacturer_data={123: b"\x01\x02"},
service_data={"service": b"\xaa"},
service_uuids=["B", "a"],
tx_power=-4,
rssi=-52,
platform_data=(),
)
record = advertisement_record(device, advertisement)
assert record["k1_name_candidate"] is True
assert record["id_kind"] == "corebluetooth_uuid"
assert record["manufacturer_data_hex"] == {"123": "0102"}
assert record["service_data_hex"] == {"service": "aa"}
assert record["service_uuids"] == ["B", "a"]

View File

@ -0,0 +1,14 @@
from k1link.net.snapshot import command_record, snapshot
def test_command_record_captures_success() -> None:
result = command_record(["/usr/bin/true"])
assert result["returncode"] == 0
assert result["error"] is None
def test_snapshot_is_explicitly_sensitive() -> None:
result = snapshot()
assert result["schema_version"] == 1
assert "do not commit" in result["sensitivity"]
assert [record["argv"] for record in result["commands"]][-1] == ["arp", "-an"]

213
uv.lock
View File

@ -11,6 +11,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
] ]
[[package]]
name = "bleak"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dbus-fast", marker = "sys_platform == 'linux'" },
{ name = "pyobjc-core", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" },
{ 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'" },
{ name = "winrt-windows-devices-bluetooth-genericattributeprofile", marker = "sys_platform == 'win32'" },
{ name = "winrt-windows-devices-enumeration", marker = "sys_platform == 'win32'" },
{ name = "winrt-windows-devices-radios", marker = "sys_platform == 'win32'" },
{ name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" },
{ name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" },
{ name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/df/05a3f80ca8e3f7f5b0dba68a9e618147c909ccdba1468f07487dc8d72a9d/bleak-3.0.2.tar.gz", hash = "sha256:c2229cb8238d5876b4bd05c74bf7a1aea1f88da39d2e51ac9dfd5cc319d5265f", size = 125293, upload-time = "2026-05-02T23:01:04.066Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/54/05aceb9cd80073805b3ed8522e3196e8cb22f70e741873fa51406c31f4e7/bleak-3.0.2-py3-none-any.whl", hash = "sha256:39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", size = 146490, upload-time = "2026-05-02T23:01:02.592Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -20,6 +44,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "dbus-fast"
version = "5.0.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/db/b621610e50b1bc46ff63534d75239553c1bf33256de6096b58214fd9808a/dbus_fast-5.0.22.tar.gz", hash = "sha256:34dc67d7d21a12399828dd13e63b352750580beea54ea7c729e708f2d2905fef", size = 83224, upload-time = "2026-06-05T18:47:59.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/06/233b0bc13919474f70320bf389cb81ce02811956d6cf86c45e84679b63c3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f0bcad7f71d2304a68a5b0bc0d24c3fcc14710a2ffcf5f2a27521e3aece71ca", size = 799464, upload-time = "2026-06-05T18:56:02.617Z" },
{ url = "https://files.pythonhosted.org/packages/68/e9/77bc23a6f5aebfb8f2c34489795e8517aed7eca31738438e1a4c4a4891d3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", size = 852687, upload-time = "2026-06-05T18:56:04.433Z" },
{ url = "https://files.pythonhosted.org/packages/ae/04/56769d0936d1273d1801ef574ec426ccb3f61f4b0a7a0eeb9eb2b8ccafa5/dbus_fast-5.0.22-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98de6d2c200d8182e1fd0bdde3206fa556b8fa14ebb752a044cd8daa87b4658c", size = 833814, upload-time = "2026-06-05T18:56:05.87Z" },
{ url = "https://files.pythonhosted.org/packages/06/ca/964f0d39a3be03b12a98f39519d34ad95b74360c7e3adf4cd4907dc25fd6/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b013437b66dc22b8d9aca5e0b0d46bf1980208a143409469fe482d9684a2a717", size = 806891, upload-time = "2026-06-05T18:56:07.623Z" },
{ url = "https://files.pythonhosted.org/packages/f4/25/57fe6ab509ad9da2e190498fa9c37f868e38ac521d940a9edb9ba6b6c657/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:855f15b7f7805171da2b82de1c317d01cfbb9fb8ac61fcc1e8dec54d8c69fab7", size = 830867, upload-time = "2026-06-05T18:56:09.473Z" },
{ url = "https://files.pythonhosted.org/packages/be/2b/da036e9f4aeb776833139575fe0774544aa6cd13ba997eea7fbc4ab99852/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d1c42963235cfc015a2d2b8c5fe42b65387493b4ad4ce0ec122601c805e6742", size = 860651, upload-time = "2026-06-05T18:56:10.952Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -107,6 +145,7 @@ name = "ndc-xgrids-k1-connector"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "bleak" },
{ name = "rich" }, { name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
@ -120,6 +159,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "bleak", specifier = "==3.0.2" },
{ name = "rich", specifier = ">=13.9,<15" }, { name = "rich", specifier = ">=13.9,<15" },
{ name = "typer", specifier = ">=0.15,<1" }, { name = "typer", specifier = ">=0.15,<1" },
] ]
@ -167,6 +207,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
] ]
[[package]]
name = "pyobjc-core"
version = "12.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383, upload-time = "2026-06-19T16:19:39.357Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347, upload-time = "2026-06-19T16:04:49.341Z" },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751, upload-time = "2026-06-19T16:20:05.159Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113, upload-time = "2026-06-19T16:07:38.9Z" },
]
[[package]]
name = "pyobjc-framework-corebluetooth"
version = "12.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/91/c76f3c5e8e80c7047e43c4c05b3e6fda9a7cefad5aae85487007674c966c/pyobjc_framework_corebluetooth-12.2.1.tar.gz", hash = "sha256:7dbb285295097205bebbcb11f55161e5faa02111108fb7b17536176e31971eb0", size = 37568, upload-time = "2026-06-19T16:20:12.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/c4/7938016860850e28c001dc9b7c653352c43f7aebfffc6d3c5fd087281f22/pyobjc_framework_corebluetooth-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2f8d2ed65c0e98ba044b6a7fc2f9a290a5c579a28d33a57c642f8617ccbcca0f", size = 13217, upload-time = "2026-06-19T16:08:27.802Z" },
]
[[package]]
name = "pyobjc-framework-libdispatch"
version = "12.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/3f/561653aff3f19873457c95c053f0298da517be89fdfc0ec35115ed5b7030/pyobjc_framework_libdispatch-12.2.1.tar.gz", hash = "sha256:0d24eda41c6c258135077f60d410e704bc7b5a67adcb2ca463918896c7363795", size = 40336, upload-time = "2026-06-19T16:20:56.371Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/8f/42cfa987c07a2b5ce8c236a42b0fb388b8807dac72c25e004cd4905ea9a3/pyobjc_framework_libdispatch-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8f41b5021ff70bc51220a79b41ebd1eacb55fe3ceb67448594f30e491a2c42a5", size = 15656, upload-time = "2026-06-19T16:12:50.361Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "8.4.2"
@ -253,3 +340,129 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
] ]
[[package]]
name = "winrt-runtime"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
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/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" },
]
[[package]]
name = "winrt-windows-devices-bluetooth"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-devices-bluetooth-advertisement"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-devices-bluetooth-genericattributeprofile"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-devices-enumeration"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-devices-radios"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-foundation"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-foundation-collections"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]
[[package]]
name = "winrt-windows-storage-streams"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "winrt-runtime" },
]
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/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" },
]