From 913550046bd8c96eaf16a2edad978d09c60c529e Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:30:19 -0700 Subject: [PATCH 01/36] Fix PV nameplate capacity sensor unit to watts Update sensor to use UnitOfPower.WATT to match the actual firmware value. Bump version to 2.0.3 and require span-panel-api>=2.2.5. --- .github/workflows/ci.yml | 2 +- custom_components/span_panel/manifest.json | 4 ++-- custom_components/span_panel/sensor_definitions.py | 4 ++-- docs/dev/mqtt-sensor-topic.md | 8 ++++---- tests/test_promoted_sensors.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 400d2504..ab7db1d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.4"/' pyproject.toml + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.5"/' pyproject.toml sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml # Regenerate lock file with the modified dependencies poetry lock diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 05dcdea7..28c0aad9 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -12,9 +12,9 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api>=2.2.4" + "span-panel-api>=2.2.5" ], - "version": "2.0.2", + "version": "2.0.3", "zeroconf": [ { "type": "_span._tcp.local." diff --git a/custom_components/span_panel/sensor_definitions.py b/custom_components/span_panel/sensor_definitions.py index 19611cb7..1eb3262d 100644 --- a/custom_components/span_panel/sensor_definitions.py +++ b/custom_components/span_panel/sensor_definitions.py @@ -443,9 +443,9 @@ class SpanPVMetadataSensorEntityDescription( key="pv_nameplate_capacity", translation_key="pv_nameplate_capacity", device_class=SensorDeviceClass.POWER, - native_unit_of_measurement=UnitOfPower.KILO_WATT, + native_unit_of_measurement=UnitOfPower.WATT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda s: s.pv.nameplate_capacity_kw, + value_fn=lambda s: s.pv.nameplate_capacity_w, ), ) diff --git a/docs/dev/mqtt-sensor-topic.md b/docs/dev/mqtt-sensor-topic.md index 8767628e..2960a047 100644 --- a/docs/dev/mqtt-sensor-topic.md +++ b/docs/dev/mqtt-sensor-topic.md @@ -149,7 +149,7 @@ These are exposed as attributes on the corresponding power sensors: | ----------------------- | ---------------------------- | --------------------------------------------- | | `vendor_name` | `s.pv.vendor_name` | PV inverter vendor (e.g., "Enphase", "Other") | | `product_name` | `s.pv.product_name` | PV inverter product (e.g., "IQ8+") | -| `nameplate_capacity_kw` | `s.pv.nameplate_capacity_kw` | Rated inverter capacity in kW | +| `nameplate_capacity_w` | `s.pv.nameplate_capacity_w` | Rated inverter capacity in W | **Battery Power sensor** (`batteryPowerW`) attributes: @@ -161,7 +161,7 @@ These are exposed as attributes on the corresponding power sensors: **Library models:** -- `SpanPVSnapshot` (new): `vendor_name`, `product_name`, `nameplate_capacity_kw` — populated from first PV metadata node +- `SpanPVSnapshot` (new): `vendor_name`, `product_name`, `nameplate_capacity_w` — populated from first PV metadata node - `SpanBatterySnapshot` (extended): `vendor_name`, `product_name`, `nameplate_capacity_kwh` — parsed from BESS metadata node ### 1E. Enriched circuit sensor attributes @@ -274,7 +274,7 @@ power_flow_pv = _parse_float(self._get_prop(pf_node, "pv")) if pf_node else None class SpanPVSnapshot: vendor_name: str | None = None product_name: str | None = None - nameplate_capacity_kw: float | None = None + nameplate_capacity_w: float | None = None ``` **`models.py` — extend `SpanBatterySnapshot`:** @@ -486,6 +486,6 @@ to work with the new sensors. 7. Circuit power sensor attributes include: breaker_rating, device_type, always_on, relay_state, shed_priority, is_sheddable 8. Panel power sensor (Current Power) attributes include: l1_voltage, l2_voltage, l1_amperage, l2_amperage, main_breaker_rating, grid_islandable 8b. Panel power sensor (Feed Through Power) attributes include: l1_amperage, l2_amperage -9. PV Power sensor attributes include: vendor_name, product_name, nameplate_capacity_kw +9. PV Power sensor attributes include: vendor_name, product_name, nameplate_capacity_w 10. Battery Power sensor attributes include: vendor_name, product_name, nameplate_capacity_kwh 11. Software version sensor attributes include: panel_size, wifi_ssid diff --git a/tests/test_promoted_sensors.py b/tests/test_promoted_sensors.py index fe2d6c7d..92cd9c1d 100644 --- a/tests/test_promoted_sensors.py +++ b/tests/test_promoted_sensors.py @@ -311,10 +311,10 @@ def test_pv_product_value_function(self): def test_pv_nameplate_capacity_value_function(self): snapshot = SpanPanelSnapshotFactory.create( - pv=SpanPVSnapshot(nameplate_capacity_kw=7.6) + pv=SpanPVSnapshot(nameplate_capacity_w=7600.0) ) desc = next(d for d in PV_METADATA_SENSORS if d.key == "pv_nameplate_capacity") - assert desc.value_fn(snapshot) == 7.6 + assert desc.value_fn(snapshot) == 7600.0 assert desc.device_class == SensorDeviceClass.POWER def test_pv_none_metadata(self): From 498c4f773647e6bd0f513353b782066e506cb7e5 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:18:34 -0700 Subject: [PATCH 02/36] Add schema validation cross-check (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare span-panel-api field metadata against sensor definitions at startup to detect unit mismatches and unmapped fields. All output is DEBUG-level log-only — no sensor behavior changes. - schema_expectations.py: SENSOR_FIELD_MAP (sensor key → field path) - schema_validation.py: unit cross-check, unmapped field reporting, collect_sensor_definitions() - coordinator.py: one-shot _run_schema_validation() after first refresh - docs/dev/schema_driven_changes.md: phased plan documentation - tests/test_schema_validation.py: 14 tests covering mapping, cross-check, unmapped detection, and no-op behavior --- CHANGELOG.md | 8 +- custom_components/span_panel/coordinator.py | 33 ++ .../span_panel/schema_expectations.py | 101 ++++++ .../span_panel/schema_validation.py | 174 ++++++++++ docs/dev/mqtt-sensor-topic.md | 10 +- docs/dev/schema_driven_changes.md | 171 ++++++++++ tests/test_schema_validation.py | 310 ++++++++++++++++++ 7 files changed, 798 insertions(+), 9 deletions(-) create mode 100644 custom_components/span_panel/schema_expectations.py create mode 100644 custom_components/span_panel/schema_validation.py create mode 100644 docs/dev/schema_driven_changes.md create mode 100644 tests/test_schema_validation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e2e0a2..f41487f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,15 @@ All notable changes to this project will be documented in this file. ### Fixed - **Panel size always available** — `panel_size` is now sourced from the Homie schema by the underlying `span-panel-api` Previously some users could see fewer - unmapped sensors when trailing breaker positions were empty. Topology service reflects panel size. + unmapped sensors when trailing breaker positions were empty. Topology service reflects panel size. - **Battery power sign inverted** — Battery power sensor now uses the correct sign convention. Previously, charging was reported as positive and discharging as negative, which caused HA energy cards to show the battery discharging when it was actually charging. The panel reports power from its own perspective; the sensor now negates the value to match HA conventions (positive = discharging), consistent with how PV power is already handled. (#184) - **Idle circuits showing -0W** — Power sensors that negate values (PV circuits, battery, PV power) could produce IEEE 754 negative zero (`-0.0`) when the circuit was idle, causing HA to display `-0W` instead of `0W`. All negation sites now normalize zero to positive. (#185) -- **Net energy inconsistent with dip-compensated consumed/produced** — When energy dip compensation was enabled, consumed and produced sensors applied an - offset but net energy computed from raw snapshot values, causing a visible mismatch. Net energy now reads dip offsets from its sibling sensors so the - displayed value always equals compensated consumed minus compensated produced. +- **Net energy inconsistent with dip-compensated consumed/produced** — When energy dip compensation was enabled, consumed and produced sensors applied an offset + but net energy computed from raw snapshot values, causing a visible mismatch. Net energy now reads dip offsets from its sibling sensors so the displayed value + always equals compensated consumed minus compensated produced. ## [2.0.1] - 3/2026 diff --git a/custom_components/span_panel/coordinator.py b/custom_components/span_panel/coordinator.py index 96669582..82a7259c 100644 --- a/custom_components/span_panel/coordinator.py +++ b/custom_components/span_panel/coordinator.py @@ -34,6 +34,7 @@ from .const import DOMAIN from .helpers import build_circuit_unique_id from .options import ENERGY_REPORTING_GRACE_PERIOD +from .schema_validation import collect_sensor_definitions, validate_field_metadata class SpanCircuitEnergySensorProtocol(Protocol): @@ -100,6 +101,9 @@ def __init__( # and trigger a reload so the factory creates the appropriate sensors. self._known_capabilities: frozenset[str] | None = None + # Schema validation — run once after first successful refresh + self._schema_validated = False + # Energy dip compensation — sensors append events here during updates; # drained and surfaced as a persistent notification after each cycle. self._pending_dip_events: list[tuple[str, float, float]] = [] @@ -258,6 +262,30 @@ def _is_simulation_offline(self) -> bool: return True + # --- Schema validation --- + + def _run_schema_validation(self) -> None: + """Run schema field metadata validation once at startup. + + Compares the library's schema-derived field metadata against the + integration's sensor definitions to detect unit mismatches. Also + reports fields the integration doesn't map to any sensor. + """ + field_metadata: dict[str, dict[str, object]] | None = None + if isinstance(self._client, SpanMqttClient): + raw = self._client.field_metadata + if raw is not None: + field_metadata = { + k: {"unit": v.unit, "datatype": v.datatype} for k, v in raw.items() + } + + if field_metadata is None: + _LOGGER.debug("Schema validation skipped — no field metadata available") + return + + sensor_defs = collect_sensor_definitions() + validate_field_metadata(field_metadata, sensor_defs=sensor_defs) + # --- Hardware capability detection --- @staticmethod @@ -308,6 +336,11 @@ async def _run_post_update_tasks(self, snapshot: SpanPanelSnapshot) -> None: ensures reload requests and pending migrations are processed regardless of transport mode. """ + # One-shot schema validation after first successful refresh + if not self._schema_validated: + self._schema_validated = True + self._run_schema_validation() + # Fire persistent notification for any energy dips detected this cycle await self._fire_dip_notification() diff --git a/custom_components/span_panel/schema_expectations.py b/custom_components/span_panel/schema_expectations.py new file mode 100644 index 00000000..ae2ab87b --- /dev/null +++ b/custom_components/span_panel/schema_expectations.py @@ -0,0 +1,101 @@ +"""Sensor-to-snapshot-field mapping for schema validation. + +Maps integration sensor definition keys to snapshot field paths. This is the +integration's declaration of which snapshot fields it reads, expressed in +transport-agnostic terms. + +The integration does NOT know about Homie, MQTT, node types, or property IDs. +The ``span-panel-api`` library owns that knowledge and exposes field-level +metadata keyed by snapshot field paths. This module bridges from sensor +definitions (HA side) to field paths (library side). + +Field path convention: ``{snapshot_type}.{field_name}`` + - ``panel`` — SpanPanelSnapshot fields + - ``circuit`` — SpanCircuitSnapshot fields + - ``battery`` — SpanBatterySnapshot fields + - ``pv`` — SpanPVSnapshot fields + - ``evse`` — SpanEvseSnapshot fields + +Derived sensors (net energy, dsm_state, current_run_config) that compute +values from multiple fields have no single source field and are excluded. +""" + +from __future__ import annotations + +# --------------------------------------------------------------------------- +# Sensor definition key → snapshot field path +# +# Every sensor the integration creates that reads a single snapshot field +# should appear here. The sensor definition provides the HA unit; the +# library's field metadata provides the schema-declared unit. The validation +# module compares them. +# +# Entries are grouped by snapshot type for readability. +# --------------------------------------------------------------------------- + +SENSOR_FIELD_MAP: dict[str, str] = { + # --- Panel power sensors ------------------------------------------------- + "instantGridPowerW": "panel.instant_grid_power_w", + "feedthroughPowerW": "panel.feedthrough_power_w", + "batteryPowerW": "panel.power_flow_battery", + "pvPowerW": "panel.power_flow_pv", + "sitePowerW": "panel.power_flow_site", + # --- Panel energy sensors ------------------------------------------------ + "mainMeterEnergyProducedWh": "panel.main_meter_energy_produced_wh", + "mainMeterEnergyConsumedWh": "panel.main_meter_energy_consumed_wh", + "feedthroughEnergyProducedWh": "panel.feedthrough_energy_produced_wh", + "feedthroughEnergyConsumedWh": "panel.feedthrough_energy_consumed_wh", + # --- Panel diagnostic sensors -------------------------------------------- + "l1_voltage": "panel.l1_voltage", + "l2_voltage": "panel.l2_voltage", + "upstream_l1_current": "panel.upstream_l1_current_a", + "upstream_l2_current": "panel.upstream_l2_current_a", + "downstream_l1_current": "panel.downstream_l1_current_a", + "downstream_l2_current": "panel.downstream_l2_current_a", + "main_breaker_rating": "panel.main_breaker_rating_a", + # --- Panel status sensors (enum/string — no unit, but tracked) ----------- + "main_relay_state": "panel.main_relay_state", + "grid_forming_entity": "panel.dominant_power_source", + "vendor_cloud": "panel.vendor_cloud", + "software_version": "panel.firmware_version", + # --- Circuit sensors ----------------------------------------------------- + "circuit_power": "circuit.instant_power_w", + "circuit_energy_produced": "circuit.produced_energy_wh", + "circuit_energy_consumed": "circuit.consumed_energy_wh", + "circuit_current": "circuit.current_a", + "circuit_breaker_rating": "circuit.breaker_rating_a", + # --- Unmapped circuit sensors (same fields, different sensor keys) -------- + "instantPowerW": "circuit.instant_power_w", + "producedEnergyWh": "circuit.produced_energy_wh", + "consumedEnergyWh": "circuit.consumed_energy_wh", + # --- Battery sensors ----------------------------------------------------- + "storage_battery_percentage": "battery.soe_percentage", + "nameplate_capacity": "battery.nameplate_capacity_kwh", + "soe_kwh": "battery.soe_kwh", + # --- BESS metadata sensors ----------------------------------------------- + "vendor": "battery.vendor_name", + "model": "battery.product_name", + "serial_number": "battery.serial_number", + "firmware_version": "battery.software_version", + # --- PV metadata sensors ------------------------------------------------- + "pv_vendor": "pv.vendor_name", + "pv_product": "pv.product_name", + "pv_nameplate_capacity": "pv.nameplate_capacity_w", + # --- EVSE sensors -------------------------------------------------------- + "evse_status": "evse.status", + "evse_advertised_current": "evse.advertised_current_a", + "evse_lock_state": "evse.lock_state", +} + +# Derived sensors excluded from the map (computed from multiple fields): +# dsm_state — multi-signal heuristic +# dsm_grid_state — deprecated alias for dsm_state +# current_run_config — tri-state derivation +# mainMeterNetEnergyWh — consumed_wh - produced_wh +# feedthroughNetEnergyWh — consumed_wh - produced_wh +# circuit_energy_net — consumed_wh - produced_wh (or inverse for PV) + + +def all_referenced_field_paths() -> frozenset[str]: + """Return the set of all snapshot field paths referenced by any sensor.""" + return frozenset(SENSOR_FIELD_MAP.values()) diff --git a/custom_components/span_panel/schema_validation.py b/custom_components/span_panel/schema_validation.py new file mode 100644 index 00000000..8f50e249 --- /dev/null +++ b/custom_components/span_panel/schema_validation.py @@ -0,0 +1,174 @@ +"""Schema validation — cross-check field metadata against sensor definitions. + +Compares the ``span-panel-api`` library's field metadata (schema-derived units +and datatypes keyed by snapshot field paths) against the integration's sensor +definitions. All Homie/MQTT knowledge stays in the library; this module only +sees snapshot field paths and HA sensor metadata. + +Schema drift detection (diffing schema versions between firmware updates) is +the library's responsibility. The integration only consumes the result. + +All output is log-only. No entity creation or sensor behavior changes. + +Phase 1 of the schema-driven changes plan (see docs/Dev/schema_driven_changes.md). + +Usage: + Called from the coordinator after the first successful data refresh. + Requires ``span-panel-api`` to expose field metadata via the client protocol. + Until that library change lands, ``validate_field_metadata()`` is a safe no-op. +""" + +from __future__ import annotations + +import logging + +from homeassistant.components.sensor import SensorEntityDescription + +from .schema_expectations import SENSOR_FIELD_MAP, all_referenced_field_paths +from .sensor_definitions import ( + BATTERY_POWER_SENSOR, + BATTERY_SENSOR, + BESS_METADATA_SENSORS, + CIRCUIT_BREAKER_RATING_SENSOR, + CIRCUIT_CURRENT_SENSOR, + CIRCUIT_SENSORS, + DOWNSTREAM_L1_CURRENT_SENSOR, + DOWNSTREAM_L2_CURRENT_SENSOR, + EVSE_SENSORS, + L1_VOLTAGE_SENSOR, + L2_VOLTAGE_SENSOR, + MAIN_BREAKER_RATING_SENSOR, + PANEL_DATA_STATUS_SENSORS, + PANEL_ENERGY_SENSORS, + PANEL_POWER_SENSORS, + PV_METADATA_SENSORS, + PV_POWER_SENSOR, + SITE_POWER_SENSOR, + STATUS_SENSORS, + UNMAPPED_SENSORS, + UPSTREAM_L1_CURRENT_SENSOR, + UPSTREAM_L2_CURRENT_SENSOR, +) + +_LOGGER = logging.getLogger(__name__) + + +def _cross_check_units( + field_metadata: dict[str, dict[str, object]], + sensor_defs: dict[str, SensorEntityDescription], +) -> None: + """Compare library-reported units against sensor definition units. + + For each sensor in SENSOR_FIELD_MAP that has a ``native_unit_of_measurement``, + look up the corresponding field path in the library's metadata and compare + the declared unit. + """ + for sensor_key, field_path in SENSOR_FIELD_MAP.items(): + sensor_def = sensor_defs.get(sensor_key) + if sensor_def is None: + continue + + ha_unit = sensor_def.native_unit_of_measurement + if ha_unit is None: + # Sensor has no unit (enum, string) — nothing to cross-check + continue + + field_info = field_metadata.get(field_path) + if field_info is None: + _LOGGER.debug( + "Schema cross-check: sensor '%s' reads field '%s' but " + "library reports no metadata for it", + sensor_key, + field_path, + ) + continue + + schema_unit = field_info.get("unit") + if schema_unit is None: + _LOGGER.debug( + "Schema cross-check: field '%s' (sensor '%s') has no unit " + "in library metadata, integration expects '%s'", + field_path, + sensor_key, + ha_unit, + ) + elif str(schema_unit) != str(ha_unit): + _LOGGER.debug( + "Schema cross-check: field '%s' (sensor '%s') unit is '%s' " + "in library metadata, integration expects '%s'", + field_path, + sensor_key, + schema_unit, + ha_unit, + ) + + +def _report_unmapped_fields( + field_metadata: dict[str, dict[str, object]], +) -> None: + """Log fields in library metadata that no sensor definition references.""" + referenced = all_referenced_field_paths() + for field_path in sorted(set(field_metadata) - referenced): + _LOGGER.debug( + "Schema: field '%s' in library metadata is not mapped to any sensor", + field_path, + ) + + +def validate_field_metadata( + field_metadata: dict[str, dict[str, object]] | None, + sensor_defs: dict[str, SensorEntityDescription] | None = None, +) -> None: + """Run integration-side schema validation checks. + + Args: + field_metadata: The library's field metadata, keyed by snapshot field + path (e.g. ``"panel.instant_grid_power_w"``). Each value is a dict + with at least ``"unit"`` and ``"datatype"`` keys. None if the + library does not yet expose metadata. + sensor_defs: Dict of sensor_key → SensorEntityDescription for unit + cross-checking. None skips the cross-check. + + """ + if field_metadata is None: + _LOGGER.debug("Schema validation skipped — library does not expose field metadata") + return + + if sensor_defs is not None: + _cross_check_units(field_metadata, sensor_defs) + + _report_unmapped_fields(field_metadata) + + +def collect_sensor_definitions() -> dict[str, SensorEntityDescription]: + """Collect all sensor definitions into a dict keyed by sensor key. + + Only includes sensors that appear in SENSOR_FIELD_MAP (i.e. sensors + that read a single snapshot field and are eligible for cross-checking). + """ + all_defs: list[SensorEntityDescription] = [ + *PANEL_DATA_STATUS_SENSORS, + *STATUS_SENSORS, + *UNMAPPED_SENSORS, + BATTERY_SENSOR, + L1_VOLTAGE_SENSOR, + L2_VOLTAGE_SENSOR, + UPSTREAM_L1_CURRENT_SENSOR, + UPSTREAM_L2_CURRENT_SENSOR, + DOWNSTREAM_L1_CURRENT_SENSOR, + DOWNSTREAM_L2_CURRENT_SENSOR, + MAIN_BREAKER_RATING_SENSOR, + CIRCUIT_CURRENT_SENSOR, + CIRCUIT_BREAKER_RATING_SENSOR, + *BESS_METADATA_SENSORS, + *PV_METADATA_SENSORS, + *PANEL_POWER_SENSORS, + BATTERY_POWER_SENSOR, + PV_POWER_SENSOR, + SITE_POWER_SENSOR, + *PANEL_ENERGY_SENSORS, + *CIRCUIT_SENSORS, + *EVSE_SENSORS, + ] + mapped_keys = set(SENSOR_FIELD_MAP.keys()) + return {d.key: d for d in all_defs if d.key in mapped_keys} diff --git a/docs/dev/mqtt-sensor-topic.md b/docs/dev/mqtt-sensor-topic.md index 2960a047..90eb2f3d 100644 --- a/docs/dev/mqtt-sensor-topic.md +++ b/docs/dev/mqtt-sensor-topic.md @@ -145,11 +145,11 @@ These are exposed as attributes on the corresponding power sensors: **PV Power sensor** (`pvPowerW`) attributes: -| Attribute | Value source | Notes | -| ----------------------- | ---------------------------- | --------------------------------------------- | -| `vendor_name` | `s.pv.vendor_name` | PV inverter vendor (e.g., "Enphase", "Other") | -| `product_name` | `s.pv.product_name` | PV inverter product (e.g., "IQ8+") | -| `nameplate_capacity_w` | `s.pv.nameplate_capacity_w` | Rated inverter capacity in W | +| Attribute | Value source | Notes | +| ---------------------- | --------------------------- | --------------------------------------------- | +| `vendor_name` | `s.pv.vendor_name` | PV inverter vendor (e.g., "Enphase", "Other") | +| `product_name` | `s.pv.product_name` | PV inverter product (e.g., "IQ8+") | +| `nameplate_capacity_w` | `s.pv.nameplate_capacity_w` | Rated inverter capacity in W | **Battery Power sensor** (`batteryPowerW`) attributes: diff --git a/docs/dev/schema_driven_changes.md b/docs/dev/schema_driven_changes.md new file mode 100644 index 00000000..15d38fa1 --- /dev/null +++ b/docs/dev/schema_driven_changes.md @@ -0,0 +1,171 @@ +# Schema-Driven Sensor Discovery + +This document describes a phased approach to reducing the manual coupling between the SPAN Homie MQTT schema, the `span-panel-api` library, and the HA +integration's sensor definitions. The goal is not full auto-generation but a practical reduction in the maintenance surface when SPAN firmware adds, corrects, +or extends properties. + +## Motivation + +Today the system has three layers of hardcoded knowledge about SPAN panel properties: + +1. **Homie schema** (`GET /api/v2/homie/schema`) -- declares every node type, property, datatype, unit, format, and settable flag. Self-describing and + firmware-versioned. +2. **span-panel-api** -- hand-coded `HomieDeviceConsumer._build_snapshot()` (653 lines) maps MQTT properties to frozen dataclass fields. Sign conventions, + cross-references, and derived state are embedded here. +3. **span integration** -- 47+ `SensorEntityDescription` instances in `sensor_definitions.py`, each with a `value_fn` lambda reaching into a specific snapshot + field plus HA metadata (`device_class`, `state_class`, `native_unit_of_measurement`). + +Adding a new sensor requires changes to all three layers. Correcting a unit requires changes to layers 2 and 3. The schema itself evolves with firmware +releases. + +### Why Not Go Fully Schema-Driven + +The Homie schema is self-describing but not self-correct. The 202609 changelog documented unit declaration errors (`kW` declared when values were actually `W`) +for `active-power` and PV `nameplate-capacity`. The integration's hardcoded knowledge of the correct units protected users from displaying values 1000x off. A +schema-driven integration would have propagated the error. + +Additional blockers: + +- **No schema versioning** -- the schema is tied to firmware releases (`rYYYYWW`), with no mechanism to request a specific version or negotiate compatibility. +- **Irreducible semantic layer** -- sign conventions, derived state machines (`dsm_state`, `current_run_config`), cross-references (EVSE `feed` to circuit), + unmapped tab synthesis, and energy dip compensation are domain logic not representable in the Homie schema. +- **HA-specific metadata** -- `device_class`, `state_class`, `entity_category`, `suggested_display_precision` have no Homie equivalent. +- **User stability** -- HA users build automations and dashboards against stable entity IDs and sensor behaviors. Schema-driven changes that silently alter a + sensor's unit or meaning would break installations. + +The phased approach below progressively surfaces schema metadata for validation and diagnostic purposes first, then optionally for entity discovery, without +ever trusting the schema blindly for units or semantics. + +## Phase 1: Schema Metadata Exposure (Validation and Diagnostics) — COMPLETE + +**Status**: Implemented and tested across both repositories. + +### Architectural Boundary + +The integration knows nothing about Homie, MQTT, node types, or property IDs. All transport knowledge lives in `span-panel-api`. The integration sees only: + +- **Snapshot field paths** -- `"panel.instant_grid_power_w"`, `"circuit.current_a"`, etc. +- **Field metadata** -- unit and datatype per field, exposed by the library in transport-agnostic terms. +- **Sensor definitions** -- the integration's own HA metadata for each sensor. + +### span-panel-api Implementation + +| Module | Purpose | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `models.py` | `FieldMetadata(unit, datatype)` frozen dataclass | +| `mqtt/field_metadata.py` | `_PROPERTY_FIELD_MAP` (Homie property → field path), `build_field_metadata()`, `log_schema_drift()` | +| `mqtt/client.py` | Retains schema across connections, builds/caches field metadata during `connect()`, detects schema hash changes and diffs properties | +| `protocol.py` | `field_metadata` property added to `SpanPanelClientProtocol` | + +**Data flow at connect time:** + +1. `SpanMqttClient.connect()` fetches the Homie schema +2. If the schema hash changed since last connection, `log_schema_drift()` diffs the old and new schemas at the property level (new/removed node types, + new/removed properties, unit/datatype/format changes) -- all logged internally, never exposed to the integration +3. `build_field_metadata(schema.types)` iterates `_PROPERTY_FIELD_MAP`, looks up each Homie property in the live schema for its declared unit and datatype, and + produces `dict[str, FieldMetadata]` keyed by snapshot field path +4. The result is cached on the client as the `field_metadata` property + +**Field path convention:** `{snapshot_type}.{field_name}` where snapshot_type is one of `panel`, `circuit`, `battery`, `pv`, `evse`. The library defines this +convention in `_PROPERTY_FIELD_MAP` (~55 entries covering all properties that `_build_snapshot()` reads). + +### span Integration Implementation + +| Module | Purpose | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `schema_expectations.py` | `SENSOR_FIELD_MAP` -- sensor definition key → snapshot field path (the ONLY manually-maintained data) | +| `schema_validation.py` | `validate_field_metadata()` -- unit cross-check, unmapped field reporting. `collect_sensor_definitions()` -- builds the sensor defs dict | +| `coordinator.py` | `_run_schema_validation()` -- one-shot call after first successful refresh | + +**Data flow at first refresh:** + +1. `SpanPanelCoordinator._run_post_update_tasks()` fires `_run_schema_validation()` once +2. Reads `client.field_metadata` and converts `FieldMetadata` objects to plain dicts +3. `collect_sensor_definitions()` gathers all sensor descriptors into a dict keyed by sensor key +4. `validate_field_metadata()` walks `SENSOR_FIELD_MAP`, for each entry: + - Looks up the field path in the library's metadata to get the schema-declared unit + - Looks up the sensor key in the sensor definitions to get the HA `native_unit_of_measurement` + - Compares them and logs mismatches +5. Reports fields in the library's metadata that no sensor references + +**Validation checks:** + +| Check | Severity | Example | +| ---------------- | -------- | ------------------------------------------------------------ | +| Unit mismatch | DEBUG | Field metadata says `kW`, sensor definition has `W` | +| Missing metadata | DEBUG | Sensor reads a field the library has no metadata for | +| Missing unit | DEBUG | Field metadata has no unit but sensor definition expects `V` | +| Unmapped field | DEBUG | Library metadata contains a field no sensor references | + +All output is DEBUG-level only -- invisible to users at default HA log levels. A maintainer enables it by setting +`logger: custom_components.span_panel.schema_validation: debug` in their HA configuration. No entity creation or sensor behavior changes. + +### Tests + +**span-panel-api** (`tests/test_field_metadata.py`, 15 tests): field metadata building for all snapshot types, unit/datatype correctness, enum and boolean +handling, empty schema, field path convention, generic lugs fallback. + +**span integration** (`tests/test_schema_validation.py`, 13 tests): mapping structure validation, sensor keys match definitions, field paths match snapshot +attributes, unit cross-check (match, mismatch, missing), unmapped field detection, no-op when metadata unavailable. + +### What This Achieves + +- Zero risk to users -- no sensor behavior changes, log-only output. +- Clean architectural boundary -- integration never sees Homie/MQTT details. +- Early warning when schema-derived field metadata disagrees with sensor definitions (e.g. the kW/W error). +- Foundation for Phase 2 -- the field metadata and mapping are reusable. + +## Phase 2: Override-Table Entity Creation (Future) + +**Prerequisite**: Phase 1 complete. Schema metadata proven stable across multiple firmware releases. Schema unit corrections resolved (no outstanding known +errors). + +Replace the 47+ hardcoded `SensorEntityDescription` instances with: + +1. **A declarative override table** mapping snapshot field paths to HA metadata (`device_class`, `state_class`, sign convention, entity category). The library's + field metadata provides the base unit and datatype; the override table adds HA-specific semantics. +2. **A generic entity factory** that iterates the library's field metadata, applies overrides where present, and creates entities with sensible defaults + otherwise. +3. **An "unknown field" entity** -- generic sensor, unit from library metadata, no `device_class`, diagnostic category. Surfaces new library fields without + integration code changes. + +The override table inverts the maintenance model: everything works generically, and the maintainer only writes overrides for HA-specific semantics (device +class, sign convention, etc.). The integration never references Homie node types or property IDs -- it operates entirely in terms of snapshot field paths and +the library's field metadata. + +### Trigger Criteria for Phase 2 + +Do not proceed to Phase 2 until: + +- [ ] The Homie schema has had at least two firmware releases with no unit corrections +- [ ] Phase 1 validation logging has run in production and confirmed schema accuracy +- [ ] SPAN introduces schema versioning or a backwards-compatibility guarantee +- [ ] The rate of new properties is high enough that manual sensor additions are a meaningful maintenance burden + +## Phase 3: Build-Time Dataclass Generation (Future) + +**Prerequisite**: Phase 2 complete. Schema stable. Property additions are frequent. + +Auto-generate `span-panel-api` snapshot dataclasses from the schema at build time: + +1. A codegen script reads the schema from a reference panel (or saved fixture). +2. Outputs `models_generated.py` with typed frozen dataclasses matching the schema. +3. Manual `models.py` inherits from generated classes and adds derived fields (`dsm_state`, `current_run_config`, etc.). +4. `mypy` and IDE autocomplete continue working against concrete types. + +This means `pip install span-panel-api==X.Y.Z` picks up new fields without integration code changes. The integration's Phase 2 override table handles the +HA-side mapping. + +### Trigger Criteria for Phase 3 + +Do not proceed to Phase 3 until: + +- [ ] Phase 2 override-table model is proven and the generic entity factory is stable +- [ ] SPAN releases firmware updates with new properties frequently enough to justify the codegen infrastructure +- [ ] The manual snapshot dataclass maintenance cost exceeds the codegen maintenance cost + +## Cross-References + +- [Architecture](architecture.md) -- system overview and data flow +- [Dynamic Enum Options](dynamic_enum_options.md) -- runtime enum handling and schema trust limitations (directly relevant to Phase 1 validation) +- [SPAN API Client Docs](https://github.com/spanio/SPAN-API-Client-Docs) -- upstream Homie schema documentation and changelog diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py new file mode 100644 index 00000000..3b50bb57 --- /dev/null +++ b/tests/test_schema_validation.py @@ -0,0 +1,310 @@ +"""Tests for schema validation and sensor-to-field mapping.""" + +from __future__ import annotations + +import logging +from types import TracebackType +from unittest.mock import MagicMock + +from custom_components.span_panel.schema_expectations import ( + SENSOR_FIELD_MAP, + all_referenced_field_paths, +) +from custom_components.span_panel.schema_validation import validate_field_metadata + +# --------------------------------------------------------------------------- +# Test utility — capture log records at DEBUG level +# --------------------------------------------------------------------------- + + +class CaptureHandler(logging.Handler): + """Minimal log capture handler for test assertions.""" + + def __init__(self, logger: logging.Logger, level: int = logging.DEBUG) -> None: + """Attach to a logger at the given level and collect records.""" + super().__init__(level) + self._logger = logger + self._prev_level = logger.level + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + """Store the log record for later assertion.""" + self.records.append(record) + + def __enter__(self) -> CaptureHandler: + """Attach handler and lower logger level for capture.""" + self._prev_level = self._logger.level + self._logger.setLevel(self.level) + self._logger.addHandler(self) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Detach handler and restore logger level.""" + self._logger.removeHandler(self) + self._logger.setLevel(self._prev_level) + + +# --------------------------------------------------------------------------- +# Sensor field mapping tests +# --------------------------------------------------------------------------- + + +class TestSensorFieldMap: + """Tests for the sensor-to-snapshot-field mapping.""" + + def test_no_empty_keys_or_paths(self) -> None: + """Every entry must have non-empty sensor key and field path.""" + for sensor_key, field_path in SENSOR_FIELD_MAP.items(): + assert sensor_key, "Empty sensor key in SENSOR_FIELD_MAP" + assert field_path, f"Empty field path for sensor key '{sensor_key}'" + + def test_field_paths_follow_convention(self) -> None: + """All field paths must be {snapshot_type}.{field_name}.""" + valid_prefixes = {"panel", "circuit", "battery", "pv", "evse"} + for sensor_key, field_path in SENSOR_FIELD_MAP.items(): + parts = field_path.split(".", 1) + assert len(parts) == 2, ( + f"Field path '{field_path}' for sensor '{sensor_key}' " + f"does not follow 'type.field' convention" + ) + assert parts[0] in valid_prefixes, ( + f"Field path '{field_path}' for sensor '{sensor_key}' " + f"has unknown prefix '{parts[0]}'" + ) + + def test_sensor_keys_exist_in_definitions(self) -> None: + """Every sensor key should match a real sensor definition.""" + from custom_components.span_panel.sensor_definitions import ( + BATTERY_POWER_SENSOR, + BATTERY_SENSOR, + BESS_METADATA_SENSORS, + CIRCUIT_BREAKER_RATING_SENSOR, + CIRCUIT_CURRENT_SENSOR, + CIRCUIT_SENSORS, + DOWNSTREAM_L1_CURRENT_SENSOR, + DOWNSTREAM_L2_CURRENT_SENSOR, + EVSE_SENSORS, + L1_VOLTAGE_SENSOR, + L2_VOLTAGE_SENSOR, + MAIN_BREAKER_RATING_SENSOR, + PANEL_DATA_STATUS_SENSORS, + PANEL_ENERGY_SENSORS, + PANEL_POWER_SENSORS, + PV_METADATA_SENSORS, + PV_POWER_SENSOR, + SITE_POWER_SENSOR, + STATUS_SENSORS, + UNMAPPED_SENSORS, + UPSTREAM_L1_CURRENT_SENSOR, + UPSTREAM_L2_CURRENT_SENSOR, + ) + + all_defs = [ + *PANEL_DATA_STATUS_SENSORS, + *STATUS_SENSORS, + *UNMAPPED_SENSORS, + BATTERY_SENSOR, + L1_VOLTAGE_SENSOR, + L2_VOLTAGE_SENSOR, + UPSTREAM_L1_CURRENT_SENSOR, + UPSTREAM_L2_CURRENT_SENSOR, + DOWNSTREAM_L1_CURRENT_SENSOR, + DOWNSTREAM_L2_CURRENT_SENSOR, + MAIN_BREAKER_RATING_SENSOR, + CIRCUIT_CURRENT_SENSOR, + CIRCUIT_BREAKER_RATING_SENSOR, + *BESS_METADATA_SENSORS, + *PV_METADATA_SENSORS, + *PANEL_POWER_SENSORS, + BATTERY_POWER_SENSOR, + PV_POWER_SENSOR, + SITE_POWER_SENSOR, + *PANEL_ENERGY_SENSORS, + *CIRCUIT_SENSORS, + *EVSE_SENSORS, + ] + known_keys = {d.key for d in all_defs} + + for sensor_key in SENSOR_FIELD_MAP: + assert sensor_key in known_keys, ( + f"Sensor key '{sensor_key}' in SENSOR_FIELD_MAP not found in sensor definitions" + ) + + def test_field_paths_match_snapshot_attrs(self) -> None: + """Field names should match actual snapshot dataclass attributes.""" + from span_panel_api import ( + SpanBatterySnapshot, + SpanCircuitSnapshot, + SpanEvseSnapshot, + SpanPanelSnapshot, + SpanPVSnapshot, + ) + + snapshot_classes = { + "panel": SpanPanelSnapshot, + "circuit": SpanCircuitSnapshot, + "battery": SpanBatterySnapshot, + "pv": SpanPVSnapshot, + "evse": SpanEvseSnapshot, + } + + for sensor_key, field_path in SENSOR_FIELD_MAP.items(): + prefix, field_name = field_path.split(".", 1) + cls = snapshot_classes[prefix] + assert hasattr(cls, field_name) or field_name in { + f.name for f in cls.__dataclass_fields__.values() + }, ( + f"Field '{field_name}' from path '{field_path}' " + f"(sensor '{sensor_key}') not found on {cls.__name__}" + ) + + def test_all_referenced_field_paths(self) -> None: + """all_referenced_field_paths should return all unique values.""" + paths = all_referenced_field_paths() + assert paths == frozenset(SENSOR_FIELD_MAP.values()) + + +# --------------------------------------------------------------------------- +# Unit cross-check tests +# --------------------------------------------------------------------------- + + +def _make_sensor_def(key: str, unit: str | None) -> MagicMock: + """Create a minimal mock SensorEntityDescription with key and unit.""" + mock = MagicMock(spec=["key", "native_unit_of_measurement"]) + mock.key = key + mock.native_unit_of_measurement = unit + return mock + + +class TestUnitCrossCheck: + """Tests for field metadata unit vs sensor definition unit cross-checking.""" + + def test_matching_units_no_cross_check_message(self) -> None: + """Matching units should produce no cross-check log messages.""" + metadata = {"panel.instant_grid_power_w": {"unit": "W", "datatype": "float"}} + sensor_defs = {"instantGridPowerW": _make_sensor_def("instantGridPowerW", "W")} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + cross_check_msgs = [ + r for r in handler.records if "cross-check" in r.getMessage().lower() + ] + assert len(cross_check_msgs) == 0 + + def test_mismatched_units_logs_debug(self) -> None: + """Unit mismatch should produce a debug message naming both units.""" + metadata = {"panel.instant_grid_power_w": {"unit": "kW", "datatype": "float"}} + sensor_defs = {"instantGridPowerW": _make_sensor_def("instantGridPowerW", "W")} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + msgs = [r for r in handler.records if "cross-check" in r.getMessage().lower()] + assert len(msgs) == 1 + assert "'kW'" in msgs[0].getMessage() + assert "'W'" in msgs[0].getMessage() + + def test_missing_metadata_logs_debug(self) -> None: + """Sensor reading a field with no metadata should log debug.""" + metadata: dict[str, dict[str, object]] = {} + sensor_defs = {"l1_voltage": _make_sensor_def("l1_voltage", "V")} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + msgs = [r for r in handler.records if "no metadata" in r.getMessage()] + assert len(msgs) == 1 + + def test_missing_schema_unit_logs_debug(self) -> None: + """Field with no unit in metadata but unit in sensor def should log debug.""" + metadata = {"panel.l1_voltage": {"datatype": "float"}} + sensor_defs = {"l1_voltage": _make_sensor_def("l1_voltage", "V")} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + msgs = [r for r in handler.records if "no unit" in r.getMessage()] + assert len(msgs) == 1 + + def test_sensor_without_unit_skipped(self) -> None: + """Sensor with no native_unit_of_measurement should be skipped.""" + metadata = {"panel.main_relay_state": {"datatype": "enum"}} + sensor_defs = {"main_relay_state": _make_sensor_def("main_relay_state", None)} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + cross_check_msgs = [ + r for r in handler.records if "cross-check" in r.getMessage().lower() + ] + assert len(cross_check_msgs) == 0 + + def test_all_output_is_debug_level(self) -> None: + """All schema validation output should be DEBUG — never visible to users.""" + metadata = { + "panel.instant_grid_power_w": {"unit": "kW", "datatype": "float"}, + "panel.l1_voltage": {"datatype": "float"}, + "panel.new_fancy_field": {"unit": "W", "datatype": "float"}, + } + sensor_defs = { + "instantGridPowerW": _make_sensor_def("instantGridPowerW", "W"), + "l1_voltage": _make_sensor_def("l1_voltage", "V"), + } + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata, sensor_defs=sensor_defs) + above_debug = [r for r in handler.records if r.levelno > logging.DEBUG] + assert len(above_debug) == 0, ( + f"Expected all DEBUG, got: {[(r.levelname, r.getMessage()) for r in above_debug]}" + ) + + +# --------------------------------------------------------------------------- +# Unmapped field detection tests +# --------------------------------------------------------------------------- + + +class TestUnmappedFields: + """Tests for detecting fields the integration doesn't consume.""" + + def test_unmapped_field_logs_debug(self) -> None: + """Field not in SENSOR_FIELD_MAP values should log at DEBUG.""" + metadata = {"panel.new_fancy_field": {"unit": "W", "datatype": "float"}} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata) + msgs = [ + r + for r in handler.records + if r.levelno == logging.DEBUG and "new_fancy_field" in r.getMessage() + ] + assert len(msgs) == 1 + + def test_mapped_field_not_reported(self) -> None: + """Field that IS in SENSOR_FIELD_MAP should not be reported as unmapped.""" + metadata = {"panel.instant_grid_power_w": {"unit": "W", "datatype": "float"}} + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(metadata) + unmapped_msgs = [r for r in handler.records if "not mapped" in r.getMessage()] + assert len(unmapped_msgs) == 0 + + +# --------------------------------------------------------------------------- +# No-op when metadata unavailable +# --------------------------------------------------------------------------- + + +class TestNoOp: + """Tests for graceful handling when library doesn't expose metadata.""" + + def test_none_metadata_is_noop(self) -> None: + """None metadata should produce no output above DEBUG.""" + logger = logging.getLogger("custom_components.span_panel.schema_validation") + with CaptureHandler(logger) as handler: + validate_field_metadata(None) + assert any("skipped" in r.getMessage() for r in handler.records) + above_debug = [r for r in handler.records if r.levelno > logging.DEBUG] + assert len(above_debug) == 0 From e19a25151d680578df2b551ae776bf3cd768dc7b Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:05:17 -0700 Subject: [PATCH 03/36] Externalize simulator process --- .github/workflows/ci.yml | 2 +- custom_components/span_panel/__init__.py | 154 ++---- custom_components/span_panel/binary_sensor.py | 15 +- custom_components/span_panel/button.py | 2 +- custom_components/span_panel/config_flow.py | 482 +----------------- .../span_panel/config_flow_utils/__init__.py | 21 - .../span_panel/config_flow_utils/options.py | 39 -- .../config_flow_utils/simulation.py | 113 ---- .../config_flow_utils/validation.py | 67 +-- custom_components/span_panel/const.py | 11 +- custom_components/span_panel/coordinator.py | 62 +-- custom_components/span_panel/entity.py | 5 +- custom_components/span_panel/helpers.py | 38 -- custom_components/span_panel/manifest.json | 2 +- custom_components/span_panel/select.py | 2 +- custom_components/span_panel/sensor.py | 15 +- custom_components/span_panel/sensor_evse.py | 8 +- .../migration_test_config.yaml | 236 --------- .../simple_test_config.yaml | 73 --- .../simulation_config_32_circuit.yaml | 444 ---------------- ...lation_config_40_circuit_with_battery.yaml | 325 ------------ .../simulation_config_8_tab_workshop.yaml | 170 ------ .../span_panel/simulation_factory.py | 138 ----- .../span_panel/simulation_utils.py | 74 +-- custom_components/span_panel/strings.json | 49 +- custom_components/span_panel/switch.py | 4 +- .../span_panel/translations/en.json | 50 +- .../span_panel/translations/es.json | 50 +- .../span_panel/translations/fr.json | 50 +- .../span_panel/translations/ja.json | 48 +- .../span_panel/translations/pt.json | 50 +- custom_components/span_panel/util.py | 16 +- tests/conftest.py | 13 +- tests/docs/simulation_test_documentation.md | 188 ------- tests/providers/integration_data_provider.py | 229 --------- tests/test_dps_and_bess.py | 6 +- .../span_panel_simulation_factory.py | 426 ---------------- tests/test_v2_config_flow.py | 54 +- 38 files changed, 162 insertions(+), 3569 deletions(-) delete mode 100644 custom_components/span_panel/config_flow_utils/simulation.py delete mode 100644 custom_components/span_panel/simulation_configs/migration_test_config.yaml delete mode 100644 custom_components/span_panel/simulation_configs/simple_test_config.yaml delete mode 100644 custom_components/span_panel/simulation_configs/simulation_config_32_circuit.yaml delete mode 100644 custom_components/span_panel/simulation_configs/simulation_config_40_circuit_with_battery.yaml delete mode 100644 custom_components/span_panel/simulation_configs/simulation_config_8_tab_workshop.yaml delete mode 100644 custom_components/span_panel/simulation_factory.py delete mode 100644 tests/docs/simulation_test_documentation.md delete mode 100644 tests/providers/integration_data_provider.py delete mode 100644 tests/test_factories/span_panel_simulation_factory.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab7db1d8..199a85f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.5"/' pyproject.toml + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.3.0"/' pyproject.toml sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml # Regenerate lock file with the modified dependencies poetry lock diff --git a/custom_components/span_panel/__init__.py b/custom_components/span_panel/__init__.py index 027b90d2..930476c9 100644 --- a/custom_components/span_panel/__init__.py +++ b/custom_components/span_panel/__init__.py @@ -4,10 +4,7 @@ import asyncio from dataclasses import dataclass -from datetime import datetime import logging -import os -from pathlib import Path from homeassistant.components.persistent_notification import async_create as pn_create from homeassistant.config_entries import ConfigEntry @@ -19,9 +16,7 @@ ConfigEntryNotReady, ) from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import slugify from span_panel_api import ( - DynamicSimulationEngine, SpanMqttClient, SpanPanelSnapshot, detect_api_version, @@ -37,14 +32,11 @@ from . import config_flow # noqa: F401 # type: ignore[misc] from .const import ( CONF_API_VERSION, - CONF_DEVICE_NAME, CONF_EBUS_BROKER_HOST, CONF_EBUS_BROKER_PASSWORD, CONF_EBUS_BROKER_PORT, CONF_EBUS_BROKER_USERNAME, - CONF_SIMULATION_CONFIG, - CONF_SIMULATION_OFFLINE_MINUTES, - CONF_SIMULATION_START_TIME, + CONF_HTTP_PORT, DEFAULT_SNAPSHOT_INTERVAL, DOMAIN, ) @@ -74,8 +66,8 @@ class SpanPanelRuntimeData: _LOGGER = logging.getLogger(__name__) -# Config entry version — bumped to 5 for v2 sensor alignment (remove wwanLink binary) -CURRENT_CONFIG_VERSION = 5 +# Config entry version — bumped to 6 for simulation removal +CURRENT_CONFIG_VERSION = 6 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -237,6 +229,31 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) _LOGGER.debug("Migrated config entry %s to version 5", config_entry.entry_id) + # --- v5 → v6: reject simulation entries --- + if config_entry.version < 6: + if config_entry.data.get(CONF_API_VERSION) == "simulation" or config_entry.data.get( + "simulation_mode", False + ): + pn_create( + hass, + "This SPAN Panel config entry was a **built-in simulator** which " + "has been removed in this version. Please remove this entry and " + "use the standalone SPAN simulator instead.", + title="SPAN Panel: Simulation Entry Removed", + notification_id=f"span_simulation_removed_{config_entry.entry_id}", + ) + _LOGGER.warning( + "Config entry %s is a simulation entry — rejecting migration", + config_entry.entry_id, + ) + return False + + hass.config_entries.async_update_entry( + config_entry, + version=6, + ) + _LOGGER.debug("Migrated config entry %s to version 6", config_entry.entry_id) + return True @@ -290,6 +307,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> mqtts_port=int(config[CONF_EBUS_BROKER_PORT]), ) + panel_http_port = int(config.get(CONF_HTTP_PORT, 80)) + snapshot_interval = entry.options.get( SNAPSHOT_UPDATE_INTERVAL, DEFAULT_SNAPSHOT_INTERVAL ) @@ -298,6 +317,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> serial_number, broker_config, snapshot_interval=snapshot_interval, + panel_http_port=panel_http_port, ) try: await client.connect() @@ -312,44 +332,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> await coordinator.async_config_entry_first_refresh() await coordinator.async_setup_streaming() - # --- Simulation entries --- - elif api_version == "simulation": - selected_config = config.get(CONF_SIMULATION_CONFIG, "simulation_config_32_circuit") - current_dir = os.path.dirname(__file__) - config_path = Path(current_dir) / "simulation_configs" / f"{selected_config}.yaml" - - serial_number = entry.unique_id or f"SPAN-SIM-{entry.entry_id[:8]}" - - engine = DynamicSimulationEngine( - serial_number=serial_number, - config_path=config_path, - ) - await engine.initialize_async() - - # Apply simulation start time override if configured - simulation_start_time_str = config.get(CONF_SIMULATION_START_TIME) or entry.options.get( - CONF_SIMULATION_START_TIME - ) - if simulation_start_time_str: - try: - datetime.fromisoformat(simulation_start_time_str) - engine.override_simulation_start_time(simulation_start_time_str) - _LOGGER.debug("Using simulation start time: %s", simulation_start_time_str) - except (ValueError, TypeError) as e: - _LOGGER.warning( - "Invalid simulation start time '%s': %s", - simulation_start_time_str, - e, - ) - - coordinator = SpanPanelCoordinator(hass, engine, entry) - await coordinator.async_config_entry_first_refresh() - - # Apply simulation offline mode if configured - simulation_offline_minutes = entry.options.get(CONF_SIMULATION_OFFLINE_MINUTES, 0) - if simulation_offline_minutes > 0: - coordinator.set_simulation_offline_mode(simulation_offline_minutes) - else: raise ConfigEntryError(f"Unknown api_version: {api_version}") @@ -362,10 +344,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> snapshot: SpanPanelSnapshot = coordinator.data serial_number = snapshot.serial_number - is_simulator = api_version == "simulation" - - # Create smart default name - base_name = "SPAN Simulator" if is_simulator else "SPAN Panel" + base_name = "SPAN Panel" # Check existing config entries to avoid conflicts existing_entries = hass.config_entries.async_entries(DOMAIN) @@ -424,10 +403,7 @@ async def async_remove_config_entry_device( snapshot = coordinator.data # Identify the main panel device identifier - serial_number = snapshot.serial_number - is_simulator = config_entry.data.get(CONF_API_VERSION) == "simulation" - device_name = config_entry.data.get(CONF_DEVICE_NAME, config_entry.title) - panel_identifier = slugify(device_name) if is_simulator and device_name else serial_number + panel_identifier = snapshot.serial_number # Prevent removal of the main panel device for identifier in device_entry.identifiers: @@ -445,24 +421,8 @@ async def update_listener(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> N if hass.state is not CoreState.running: return - coordinator = entry.runtime_data.coordinator - - # Update simulation offline mode if this is a simulation entry - api_version = entry.data.get(CONF_API_VERSION) - if api_version == "simulation": - simulation_offline_minutes = entry.options.get(CONF_SIMULATION_OFFLINE_MINUTES, 0) - _LOGGER.info( - "Update listener: processing simulation_offline_minutes = %s", - simulation_offline_minutes, - ) - coordinator.set_simulation_offline_mode(simulation_offline_minutes) - - if hass.state is not CoreState.running: - return - - if _requires_full_reload(entry): - await hass.config_entries.async_reload(entry.entry_id) - _LOGGER.debug("Successfully reloaded SPAN Panel integration") + await hass.config_entries.async_reload(entry.entry_id) + _LOGGER.debug("Successfully reloaded SPAN Panel integration") except asyncio.CancelledError: raise @@ -470,20 +430,6 @@ async def update_listener(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> N _LOGGER.error("Failed to reload SPAN Panel integration: %s", e, exc_info=True) -def _requires_full_reload(entry: ConfigEntry) -> bool: - """Determine if a full integration reload is required. - - Simulation-only option changes (offline minutes, start time) are applied - in-place via the coordinator and do not require a reload. - """ - has_simulation_flag = entry.options.get("_simulation_only_change", False) - if has_simulation_flag: - _LOGGER.debug("Simulation-only change detected - no reload needed") - return False - - return True - - async def ensure_device_registered( hass: HomeAssistant, entry: SpanPanelConfigEntry, @@ -493,40 +439,18 @@ async def ensure_device_registered( """Register or reconcile the HA Device before creating sensors. Ensures the device exists in the device registry with proper naming and - identifiers. For simulators, moves existing entities to the correct device - if the identifier changed due to a name change. + identifiers. """ device_registry = dr.async_get(hass) serial_number = snapshot.serial_number - is_simulator = entry.data.get(CONF_API_VERSION) == "simulation" host = entry.data.get(CONF_HOST) - desired_identifier = slugify(device_name) if is_simulator and device_name else serial_number - existing_device = device_registry.async_get_device(identifiers={(DOMAIN, desired_identifier)}) + existing_device = device_registry.async_get_device(identifiers={(DOMAIN, serial_number)}) if existing_device: if existing_device.name == serial_number: device_registry.async_update_device(existing_device.id, name=device_name) - target_device = existing_device else: - device_info = snapshot_to_device_info(snapshot, device_name, is_simulator, host) - device = device_registry.async_get_or_create(config_entry_id=entry.entry_id, **device_info) - target_device = device - - # For simulators: move entities to the target device if their current device differs - try: - if is_simulator: - entity_registry = er.async_get(hass) - entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - for ent in entries: - if ent.device_id != target_device.id: - _LOGGER.debug( - "Moving entity %s from device %s to %s", - ent.entity_id, - ent.device_id, - target_device.id, - ) - entity_registry.async_update_entity(ent.entity_id, device_id=target_device.id) - except (KeyError, ValueError, AttributeError) as err: - _LOGGER.warning("Failed to reassign entities to target device: %s", err) + device_info = snapshot_to_device_info(snapshot, device_name, host=host) + device_registry.async_get_or_create(config_entry_id=entry.entry_id, **device_info) diff --git a/custom_components/span_panel/binary_sensor.py b/custom_components/span_panel/binary_sensor.py index cef353fb..dda0c853 100644 --- a/custom_components/span_panel/binary_sensor.py +++ b/custom_components/span_panel/binary_sensor.py @@ -15,12 +15,10 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify from span_panel_api import SpanEvseSnapshot, SpanPanelSnapshot from . import SpanPanelConfigEntry from .const import ( - CONF_API_VERSION, CONF_DEVICE_NAME, PANEL_STATUS, SYSTEM_DOOR_STATE, @@ -301,17 +299,13 @@ def __init__( self._value_fn = description.value_fn # Build EVSE sub-device info - is_simulator = data_coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" panel_name = ( data_coordinator.config_entry.data.get( CONF_DEVICE_NAME, data_coordinator.config_entry.title ) or "Span Panel" ) - if is_simulator: - panel_identifier = slugify(panel_name) - else: - panel_identifier = snapshot.serial_number + panel_identifier = snapshot.serial_number evse = snapshot.evse.get(evse_id, _EMPTY_EVSE) use_circuit_numbers = data_coordinator.config_entry.options.get(USE_CIRCUIT_NUMBERS, False) @@ -365,17 +359,12 @@ async def async_setup_entry( # Add BESS connected sensor on the BESS sub-device when battery is commissioned if has_bess(snapshot): - is_simulator = coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" panel_name = ( coordinator.config_entry.data.get(CONF_DEVICE_NAME, coordinator.config_entry.title) or "Span Panel" ) - if is_simulator: - panel_identifier = slugify(panel_name) - else: - panel_identifier = snapshot.serial_number - bess_info = bess_device_info(panel_identifier, snapshot.battery, panel_name) + bess_info = bess_device_info(snapshot.serial_number, snapshot.battery, panel_name) entities.append( SpanPanelBinarySensor( coordinator, BESS_CONNECTED_SENSOR, device_info_override=bess_info diff --git a/custom_components/span_panel/button.py b/custom_components/span_panel/button.py index 08e3c0da..604081b2 100644 --- a/custom_components/span_panel/button.py +++ b/custom_components/span_panel/button.py @@ -66,7 +66,7 @@ async def async_press(self) -> None: """Publish the GFE override to the panel.""" client = self.coordinator.client if not hasattr(client, "set_dominant_power_source"): - _LOGGER.warning("GFE override not available in simulation mode") + _LOGGER.warning("Client does not support GFE override") return try: diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index 72247ee6..19721e59 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -5,10 +5,7 @@ from collections.abc import Mapping import enum import logging -from pathlib import Path -import shutil -from time import time -from typing import Any, cast +from typing import Any from homeassistant import config_entries from homeassistant.config_entries import ( @@ -23,18 +20,14 @@ from homeassistant.util.network import is_ipv4_address from span_panel_api import V2AuthResponse, detect_api_version from span_panel_api.exceptions import SpanPanelAuthError, SpanPanelConnectionError -from span_panel_api.simulation import DynamicSimulationEngine, SimulationConfig import voluptuous as vol -import yaml from .config_flow_utils import ( build_general_options_schema, - get_available_simulation_configs, get_general_options_defaults, process_general_options_input, validate_auth_token, validate_host, - validate_simulation_time, validate_v2_passphrase, validate_v2_proximity, ) @@ -45,10 +38,8 @@ CONF_EBUS_BROKER_PORT, CONF_EBUS_BROKER_USERNAME, CONF_HOP_PASSPHRASE, + CONF_HTTP_PORT, CONF_PANEL_SERIAL, - CONF_SIMULATION_CONFIG, - CONF_SIMULATION_OFFLINE_MINUTES, - CONF_SIMULATION_START_TIME, DOMAIN, ENABLE_ENERGY_DIP_COMPENSATION, ENTITY_NAMING_PATTERN, @@ -56,7 +47,6 @@ USE_DEVICE_PREFIX, EntityNamingPattern, ) -from .helpers import generate_unique_simulator_serial_number from .options import ( ENERGY_DISPLAY_PRECISION, ENERGY_REPORTING_GRACE_PERIOD, @@ -68,12 +58,6 @@ _LOGGER = logging.getLogger(__name__) -# Simulation config import/export option keys -SIM_FILE_KEY = "simulation_config_file" -SIM_EXPORT_PATH = "simulation_export_path" -SIM_IMPORT_PATH = "simulation_import_path" - - class ConfigFlowError(Exception): """Custom exception for config flow internal errors.""" @@ -83,7 +67,7 @@ def get_user_data_schema(default_host: str = "") -> vol.Schema: return vol.Schema( { vol.Optional(CONF_HOST, default=default_host): str, - vol.Optional("simulator_mode", default=False): bool, + vol.Optional(CONF_HTTP_PORT, default=80): int, vol.Optional(POWER_DISPLAY_PRECISION, default=0): int, vol.Optional(ENERGY_DISPLAY_PRECISION, default=2): int, vol.Optional(ENABLE_ENERGY_DIP_COMPENSATION, default=True): bool, @@ -145,6 +129,7 @@ def __init__(self) -> None: self._v2_broker_password: str | None = None self._v2_passphrase: str | None = None self._v2_panel_serial: str | None = None + self._http_port: int = 80 # Energy dip compensation default for fresh installs self._enable_dip_compensation: bool = True @@ -155,7 +140,7 @@ async def setup_flow(self, trigger_type: TriggerFlowType, host: str) -> None: _LOGGER.error("Flow setup attempted when already set up") raise ConfigFlowError("Flow is already set up") - result = await detect_api_version(host) + result = await detect_api_version(host, port=self._http_port) self.api_version = result.api_version self.trigger_flow_type = trigger_type @@ -212,7 +197,13 @@ async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo) -> Conf if is_v2_service: # v2 panels discovered via eBus / secure-mqtt service types - detection = await detect_api_version(discovery_info.host) + # Read optional httpPort from mDNS TXT records (non-standard port) + props = discovery_info.properties or {} + http_port_str = props.get("httpPort", props.get("httpport", "")) + http_port = int(http_port_str) if http_port_str else 80 + self._http_port = http_port + + detection = await detect_api_version(discovery_info.host, port=http_port) if detection.api_version != "v2" or detection.status_info is None: # The v2 endpoint did not respond — this IP is not a valid # v2 panel (e.g., an internal link address we didn't filter). @@ -257,12 +248,8 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con user_input, ) - # Check if simulator mode is enabled - if user_input.get("simulator_mode", False): - return await self._handle_simulator_setup(user_input) - - # For non-simulator mode, host is required host: str = user_input.get(CONF_HOST, "").strip() + self._http_port = int(user_input.get(CONF_HTTP_PORT, 80)) if not host: return self.async_show_form( step_id="user", @@ -271,7 +258,7 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con ) # Validate host before setting up flow - if not await validate_host(self.hass, host): + if not await validate_host(self.hass, host, port=self._http_port): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, @@ -279,7 +266,7 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con ) # Detect v2 API before setting up the v1 flow - detection = await detect_api_version(host) + detection = await detect_api_version(host, port=self._http_port) self.api_version = detection.api_version if self.api_version == "v2": @@ -306,151 +293,13 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con return await self.async_step_choose_auth_type() - async def _handle_simulator_setup(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Handle simulator mode setup.""" - # Precision settings already stored in async_step_user - - # Check if this is the initial simulator selection or the config selection - if CONF_SIMULATION_CONFIG not in user_input: - # Show simulator configuration selection - return await self.async_step_simulator_config() - - # Get the simulation config and host - simulation_config = user_input[CONF_SIMULATION_CONFIG] - host = user_input.get(CONF_HOST, "").strip() - simulation_start_time = user_input.get(CONF_SIMULATION_START_TIME, "").strip() - - # Generate unique simulator serial number first - simulator_serial = generate_unique_simulator_serial_number(self.hass) - - # Use the generated simulator serial number as the host - # This ensures the span panel API uses the correct serial number - host = simulator_serial - - # Create entry for simulator mode - base_name = "Span Simulator" - device_name = self.get_unique_device_name(base_name) - - # Prepare config data - config_data: dict[str, Any] = { - CONF_HOST: host, # This is now the simulator serial number (sim-nnn) - CONF_ACCESS_TOKEN: "simulator_token", - CONF_API_VERSION: "simulation", - "simulation_mode": True, - CONF_SIMULATION_CONFIG: simulation_config, - "device_name": device_name, - "simulator_serial_number": simulator_serial, - } - - # Add simulation start time if provided - if simulation_start_time: - try: - validated_time = validate_simulation_time(simulation_start_time) - config_data[CONF_SIMULATION_START_TIME] = validated_time - except ValueError as e: - return self.async_show_form( - step_id="simulator_config", - data_schema=self.add_suggested_values_to_schema( - vol.Schema( - { - vol.Required( - CONF_SIMULATION_CONFIG, default="simulation_config_32_circuit" - ): vol.In(get_available_simulation_configs()), - vol.Optional(CONF_HOST, default=""): str, - vol.Optional(CONF_SIMULATION_START_TIME, default=""): str, - } - ), - user_input, - ), - errors={"base": str(e)}, - ) - - _LOGGER.debug( - "SIMULATOR_CONFIG_DEBUG: Creating simulator entry with precision - power: %s, energy: %s", - self.power_display_precision, - self.energy_display_precision, - ) - # Determine simulator naming flags based on selection (default Friendly Names) - selected_pattern = user_input.get( - ENTITY_NAMING_PATTERN, EntityNamingPattern.FRIENDLY_NAMES.value - ) - sim_use_device_prefix = True - sim_use_circuit_numbers = selected_pattern == EntityNamingPattern.CIRCUIT_NUMBERS.value - - return self.async_create_entry( - title=device_name, - data=config_data, - options={ - USE_DEVICE_PREFIX: sim_use_device_prefix, - USE_CIRCUIT_NUMBERS: sim_use_circuit_numbers, - POWER_DISPLAY_PRECISION: self.power_display_precision, - ENERGY_DISPLAY_PRECISION: self.energy_display_precision, - ENABLE_ENERGY_DIP_COMPENSATION: self._enable_dip_compensation, - }, - ) - - async def async_step_simulator_config( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle simulator configuration selection.""" - if user_input is None: - # Discover files dynamically and build dropdown options - available_configs = get_available_simulation_configs() - options_list = [ - {"value": key, "label": label} for key, label in available_configs.items() - ] - - # Choose a sensible default - default_key = ( - "simulation_config_32_circuit" - if "simulation_config_32_circuit" in available_configs - else next(iter(available_configs.keys())) - ) - - # Create schema with forced dropdown for simulation configuration - schema = vol.Schema( - { - vol.Required(CONF_SIMULATION_CONFIG, default=default_key): selector( - { - "select": { - "options": options_list, - "mode": "dropdown", - } - } - ), - vol.Optional(CONF_HOST, default=""): str, - vol.Optional(CONF_SIMULATION_START_TIME, default=""): str, - vol.Required( - ENTITY_NAMING_PATTERN, default=EntityNamingPattern.FRIENDLY_NAMES.value - ): vol.In( - { - EntityNamingPattern.FRIENDLY_NAMES.value: "Circuit Friendly Names", - EntityNamingPattern.CIRCUIT_NUMBERS.value: "Tab Based Names", - } - ), - } - ) - - return self.async_show_form( - step_id="simulator_config", - data_schema=schema, - description_placeholders={ - "config_count": str(len(available_configs)), - }, - ) - - # Continue with simulator setup using the selected config - # Ensure simulator_mode is set since it's not in the form data - user_input_with_sim_mode = dict(user_input) - user_input_with_sim_mode["simulator_mode"] = True - return await self._handle_simulator_setup(user_input_with_sim_mode) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by re-auth.""" host = entry_data[CONF_HOST] + self._http_port = int(entry_data.get(CONF_HTTP_PORT, 80)) # Detect current API version of the panel - detection = await detect_api_version(host) + detection = await detect_api_version(host, port=self._http_port) self.api_version = detection.api_version if self.api_version == "v2": @@ -535,7 +384,7 @@ async def async_step_auth_proximity( return self.async_abort(reason="host_not_set") try: - result = await validate_v2_proximity(self.host) + result = await validate_v2_proximity(self.host, port=self._http_port) except SpanPanelAuthError: return self.async_show_form( step_id="auth_proximity", @@ -617,7 +466,7 @@ async def async_step_auth_passphrase( return self.async_abort(reason="host_not_set") try: - result = await validate_v2_passphrase(self.host, passphrase) + result = await validate_v2_passphrase(self.host, passphrase, port=self._http_port) except SpanPanelAuthError: return self.async_show_form( step_id="auth_passphrase", @@ -720,6 +569,8 @@ def create_new_entry( entry_data[CONF_EBUS_BROKER_PASSWORD] = self._v2_broker_password entry_data[CONF_HOP_PASSPHRASE] = self._v2_passphrase entry_data[CONF_PANEL_SERIAL] = self._v2_panel_serial + if self._http_port != 80: + entry_data[CONF_HTTP_PORT] = self._http_port return self.async_create_entry( title=device_name, @@ -749,6 +600,8 @@ def _update_v2_entry(self, entry_id: str) -> ConfigFlowResult: updated_data[CONF_EBUS_BROKER_PASSWORD] = self._v2_broker_password updated_data[CONF_HOP_PASSPHRASE] = self._v2_passphrase updated_data[CONF_PANEL_SERIAL] = self._v2_panel_serial + if self._http_port != 80: + updated_data[CONF_HTTP_PORT] = self._http_port self.hass.config_entries.async_update_entry(entry, data=updated_data) self.hass.async_create_task(self.hass.config_entries.async_reload(entry_id)) @@ -847,8 +700,9 @@ async def async_step_reconfigure( ) # Validate the host is reachable and is a v2 panel + http_port = int(reconfigure_entry.data.get(CONF_HTTP_PORT, 80)) try: - detection = await detect_api_version(host) + detection = await detect_api_version(host, port=http_port) except (SpanPanelConnectionError, Exception): return self.async_show_form( step_id="reconfigure", @@ -900,22 +754,14 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Con if user_input is None: menu_options = { "general_options": "General Options", + "clone_panel_to_simulation": "Clone Panel To Simulation", } - # Add simulation options if this is a simulation mode integration - if self.config_entry.data.get("simulation_mode", False): - menu_options["simulation_start_time"] = "Simulation Start Time" - menu_options["simulation_offline_minutes"] = "Simulation Offline Minutes" - else: - # Live panel: offer cloning into a simulation config - menu_options["clone_panel_to_simulation"] = "Clone Panel To Simulation" - return self.async_show_menu( step_id="init", menu_options=menu_options, ) - # This shouldn't be reached since we're showing a menu return self.async_abort(reason="unknown") async def async_step_general_options( @@ -942,236 +788,10 @@ async def async_step_general_options( errors=errors, ) - async def async_step_simulation_start_time( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Edit simulation start time settings.""" - if user_input is not None: - simulation_start_time = user_input.get(CONF_SIMULATION_START_TIME, "").strip() - - _LOGGER.info("Edit simulation start time - start_time: %s", simulation_start_time) - - if simulation_start_time: - try: - simulation_start_time = validate_simulation_time(simulation_start_time) - user_input[CONF_SIMULATION_START_TIME] = simulation_start_time - except ValueError as e: - return self.async_show_form( - step_id="simulation_start_time", - data_schema=self.add_suggested_values_to_schema( - self._get_simulation_start_time_schema(), - self._get_simulation_start_time_defaults(), - ), - errors={"base": str(e)}, - ) - - # Merge with existing options to preserve other settings - merged_options = dict(self.config_entry.options) - merged_options.update(user_input) - - # Clean up any simulation-only change flag since this will trigger a reload - merged_options.pop("_simulation_only_change", None) - - _LOGGER.info("Saving simulation start time: %s", user_input) - _LOGGER.info("Merged options: %s", merged_options) - - return self.async_create_entry(title="", data=merged_options) - - return self.async_show_form( - step_id="simulation_start_time", - data_schema=self.add_suggested_values_to_schema( - self._get_simulation_start_time_schema(), - self._get_simulation_start_time_defaults(), - ), - ) - - async def async_step_simulation_offline_minutes( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Edit simulation offline minutes settings.""" - if user_input is not None: - offline_minutes = user_input.get(CONF_SIMULATION_OFFLINE_MINUTES, 0) - - _LOGGER.info("Edit simulation offline minutes - offline_minutes: %s", offline_minutes) - - # Merge with existing options to preserve other settings - merged_options = dict(self.config_entry.options) - merged_options.update(user_input) - - # Add a flag to indicate this is a simulation-only change - merged_options["_simulation_only_change"] = True - - # Add a timestamp to force change detection even when offline_minutes value is the same - # This ensures the update listener is called to restart the offline timer - merged_options["_simulation_timestamp"] = int(time()) - - return self.async_create_entry(title="", data=merged_options) - - return self.async_show_form( - step_id="simulation_offline_minutes", - data_schema=self.add_suggested_values_to_schema( - self._get_simulation_offline_minutes_schema(), - self._get_simulation_offline_minutes_defaults(), - ), - ) - - async def async_step_simulation_export( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle simulation config export.""" - errors: dict[str, str] = {} - - if user_input is not None: - config_key = user_input.get(SIM_FILE_KEY, "") - export_path_raw = str(user_input.get(SIM_EXPORT_PATH, "")).strip() - - if not config_key: - errors[SIM_FILE_KEY] = "Please select a simulation config to export" - elif not export_path_raw: - errors[SIM_EXPORT_PATH] = "Export path is required" - else: - try: - current_file = Path(__file__) - config_dir = current_file.parent / "simulation_configs" - src_yaml = config_dir / f"{config_key}.yaml" - - export_path = Path(export_path_raw) - await self.hass.async_add_executor_job( - lambda: export_path.parent.mkdir(parents=True, exist_ok=True) - ) - if not await self.hass.async_add_executor_job(src_yaml.exists): - raise FileNotFoundError(f"Source simulation file not found: {src_yaml}") - await self.hass.async_add_executor_job(shutil.copyfile, src_yaml, export_path) - _LOGGER.info("Exported simulation config '%s' to %s", config_key, export_path) - - # Build friendly name for confirmation - friendly = get_available_simulation_configs().get(config_key, config_key) - return self.async_create_entry( - title="", - data={}, - description=f"Exported '{friendly}' to {export_path}", - ) - - except Exception as e: - _LOGGER.error("Simulation config export error: %s", e) - errors["base"] = f"Export failed: {e}" - - # Show export form - available_configs = get_available_simulation_configs() - options_list = [{"value": k, "label": v} for k, v in available_configs.items()] - current_config_key = self.config_entry.data.get( - CONF_SIMULATION_CONFIG, "simulation_config_32_circuit" - ) - default_export = f"/tmp/{current_config_key}.yaml" # nosec - - export_schema = vol.Schema( - { - vol.Required(SIM_FILE_KEY, default=current_config_key): selector( - { - "select": { - "options": options_list, - "mode": "dropdown", - } - } - ), - vol.Required(SIM_EXPORT_PATH, default=default_export): str, - } - ) - - return self.async_show_form( - step_id="simulation_export", - data_schema=export_schema, - errors=errors, - ) - - async def async_step_simulation_import( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle simulation config import.""" - errors: dict[str, str] = {} - - if user_input is not None: - import_path_raw = str(user_input.get(SIM_IMPORT_PATH, "")).strip() - - if not import_path_raw: - errors[SIM_IMPORT_PATH] = "Import path is required" - else: - try: - import_path = Path(import_path_raw) - if not await self.hass.async_add_executor_job(import_path.exists): - raise FileNotFoundError(f"Import file not found: {import_path}") - - # Load and validate YAML using span-panel-api's validator - def load_yaml_file() -> dict[str, Any]: - with import_path.open("r", encoding="utf-8") as f: - result = yaml.safe_load(f) - if result is None: - return {} - if isinstance(result, dict): - return result - return {} - - loaded_yaml = await self.hass.async_add_executor_job(load_yaml_file) - # Use DynamicSimulationEngine internal validation - config: SimulationConfig = cast(SimulationConfig, loaded_yaml) - engine = DynamicSimulationEngine(config_data=config) - await engine.initialize_async() - - # Copy to simulation_configs directory - current_file = Path(__file__) - config_dir = current_file.parent / "simulation_configs" - dest_name = ( - import_path.name if import_path.suffix else f"{import_path.name}.yaml" - ) - dest_yaml = config_dir / dest_name - await self.hass.async_add_executor_job( - lambda: dest_yaml.parent.mkdir(parents=True, exist_ok=True) - ) - await self.hass.async_add_executor_job(shutil.copyfile, import_path, dest_yaml) - _LOGGER.info("Imported and validated simulation config to %s", dest_yaml) - - # Update config entry to point to the imported simulation config - try: - new_data = dict(self.config_entry.data) - new_data[CONF_SIMULATION_CONFIG] = dest_yaml.stem - self.hass.config_entries.async_update_entry( - self.config_entry, data=new_data - ) - _LOGGER.debug("Set CONF_SIMULATION_CONFIG to %s", dest_yaml.stem) - except Exception as update_err: - _LOGGER.warning( - "Failed to set CONF_SIMULATION_CONFIG to %s: %s", - dest_yaml.stem, - update_err, - ) - - return self.async_create_entry( - title="", - data={}, - description=f"Imported '{dest_yaml.stem}' into simulation configurations", - ) - - except Exception as e: - _LOGGER.error("Simulation config import error: %s", e) - errors["base"] = f"Import failed: {e}" - - # Show import form - import_schema = vol.Schema( - { - vol.Required(SIM_IMPORT_PATH, default=""): str, - } - ) - - return self.async_show_form( - step_id="simulation_import", - data_schema=import_schema, - errors=errors, - ) - async def async_step_clone_panel_to_simulation( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Clone the live panel into a simulation YAML stored in simulation_configs.""" + """Clone the live panel into a simulation YAML for the standalone simulator.""" result = await clone_panel_to_simulation(self.hass, self.config_entry, user_input) # If result is a ConfigFlowResult, return it directly @@ -1195,7 +815,7 @@ async def async_step_clone_panel_to_simulation( return self.async_create_entry( title="Simulation Created", data={}, - description=f"Cloned panel to {dest_path.name} in simulation_configs", + description=f"Cloned panel to {dest_path.name}", ) # Compute device name for form display @@ -1218,52 +838,6 @@ async def async_step_clone_panel_to_simulation( errors=errors, ) - async def async_step_manage_simulation_configs( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Menu to import or export simulation configs.""" - if user_input is None: - return self.async_show_menu( - step_id="manage_simulation_configs", - menu_options={ - "simulation_import": "Import Simulation Config", - "simulation_export": "Export Simulation Config", - }, - ) - return self.async_abort(reason="unknown") - - def _get_simulation_start_time_schema(self) -> vol.Schema: - """Get the simulation start time schema.""" - return vol.Schema( - { - vol.Optional(CONF_SIMULATION_START_TIME): str, - } - ) - - def _get_simulation_start_time_defaults(self) -> dict[str, Any]: - """Get the simulation start time defaults.""" - return { - CONF_SIMULATION_START_TIME: self.config_entry.options.get( - CONF_SIMULATION_START_TIME, "" - ), - } - - def _get_simulation_offline_minutes_schema(self) -> vol.Schema: - """Get the simulation offline minutes schema.""" - return vol.Schema( - { - vol.Optional(CONF_SIMULATION_OFFLINE_MINUTES): int, - } - ) - - def _get_simulation_offline_minutes_defaults(self) -> dict[str, Any]: - """Get the simulation offline minutes defaults.""" - return { - CONF_SIMULATION_OFFLINE_MINUTES: self.config_entry.options.get( - CONF_SIMULATION_OFFLINE_MINUTES, 0 - ), - } - # Register the config flow handler config_entries.HANDLERS.register(DOMAIN)(SpanPanelConfigFlow) diff --git a/custom_components/span_panel/config_flow_utils/__init__.py b/custom_components/span_panel/config_flow_utils/__init__.py index 6976b421..e5e9e2c2 100644 --- a/custom_components/span_panel/config_flow_utils/__init__.py +++ b/custom_components/span_panel/config_flow_utils/__init__.py @@ -4,23 +4,12 @@ build_general_options_schema, get_current_naming_pattern, get_general_options_defaults, - get_simulation_offline_minutes_defaults, - get_simulation_offline_minutes_schema, - get_simulation_start_time_defaults, - get_simulation_start_time_schema, process_general_options_input, ) -from .simulation import ( - extract_serial_from_config, - get_available_simulation_configs, - get_simulation_config_path, - validate_yaml_config, -) from .validation import ( validate_auth_token, validate_host, validate_ipv4_address, - validate_simulation_time, validate_v2_passphrase, validate_v2_proximity, ) @@ -30,21 +19,11 @@ "validate_auth_token", "validate_host", "validate_ipv4_address", - "validate_simulation_time", "validate_v2_passphrase", "validate_v2_proximity", - # Simulation - "extract_serial_from_config", - "get_available_simulation_configs", - "get_simulation_config_path", - "validate_yaml_config", # Options "build_general_options_schema", "get_current_naming_pattern", "get_general_options_defaults", - "get_simulation_offline_minutes_defaults", - "get_simulation_offline_minutes_schema", - "get_simulation_start_time_defaults", - "get_simulation_start_time_schema", "process_general_options_input", ] diff --git a/custom_components/span_panel/config_flow_utils/options.py b/custom_components/span_panel/config_flow_utils/options.py index f2a16bb9..7e0739a1 100644 --- a/custom_components/span_panel/config_flow_utils/options.py +++ b/custom_components/span_panel/config_flow_utils/options.py @@ -8,8 +8,6 @@ import voluptuous as vol from custom_components.span_panel.const import ( - CONF_SIMULATION_OFFLINE_MINUTES, - CONF_SIMULATION_START_TIME, DEFAULT_SNAPSHOT_INTERVAL, ENABLE_CIRCUIT_NET_ENERGY_SENSORS, ENABLE_ENERGY_DIP_COMPENSATION, @@ -118,9 +116,6 @@ def process_general_options_input( filtered_input[USE_DEVICE_PREFIX] = use_prefix filtered_input[USE_CIRCUIT_NUMBERS] = use_circuit_numbers - # Clean up any simulation-only change flag since this will trigger a reload - filtered_input.pop("_simulation_only_change", None) - return filtered_input, errors @@ -135,37 +130,3 @@ def get_current_naming_pattern(config_entry: ConfigEntry) -> str: return EntityNamingPattern.FRIENDLY_NAMES.value else: return EntityNamingPattern.LEGACY_NAMES.value - - -def get_simulation_start_time_schema() -> vol.Schema: - """Get the simulation start time schema.""" - return vol.Schema( - { - vol.Optional(CONF_SIMULATION_START_TIME): str, - } - ) - - -def get_simulation_start_time_defaults(config_entry: ConfigEntry) -> dict[str, Any]: - """Get the simulation start time defaults.""" - return { - CONF_SIMULATION_START_TIME: config_entry.options.get(CONF_SIMULATION_START_TIME, ""), - } - - -def get_simulation_offline_minutes_schema() -> vol.Schema: - """Get the simulation offline minutes schema.""" - return vol.Schema( - { - vol.Optional(CONF_SIMULATION_OFFLINE_MINUTES): int, - } - ) - - -def get_simulation_offline_minutes_defaults(config_entry: ConfigEntry) -> dict[str, Any]: - """Get the simulation offline minutes defaults.""" - return { - CONF_SIMULATION_OFFLINE_MINUTES: config_entry.options.get( - CONF_SIMULATION_OFFLINE_MINUTES, 0 - ), - } diff --git a/custom_components/span_panel/config_flow_utils/simulation.py b/custom_components/span_panel/config_flow_utils/simulation.py deleted file mode 100644 index 46c4a60e..00000000 --- a/custom_components/span_panel/config_flow_utils/simulation.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Simulation utilities for Span Panel config flow.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import yaml - - -def get_available_simulation_configs() -> dict[str, str]: - """Get available simulation configuration files. - - Returns: - Dictionary mapping config keys to display names - - """ - configs = {} - - # Get the integration's simulation_configs directory - current_file = Path(__file__) - config_dir = current_file.parent.parent / "simulation_configs" - - if config_dir.exists(): - for yaml_file in config_dir.glob("*.yaml"): - config_key = yaml_file.stem - - # Create user-friendly display names from filename - display_name = config_key.replace("simulation_config_", "").replace("_", " ").title() - - configs[config_key] = display_name - - # If no configs found, provide a default - if not configs: - configs["simulation_config_32_circuit"] = "32-Circuit Residential Panel (Default)" - - return configs - - -def extract_serial_from_config(config_path: Path) -> str: - """Extract serial number from simulation config file. - - Args: - config_path: Path to the simulation config YAML file - - Returns: - Serial number from the config, or default if not found - - """ - try: - if config_path.exists(): - with config_path.open("r", encoding="utf-8") as f: - config_data = yaml.safe_load(f) - if isinstance(config_data, dict): - # Try to extract serial from various possible locations - if "serial_number" in config_data: - return str(config_data["serial_number"]) - if "panel" in config_data and isinstance(config_data["panel"], dict): - if "serial_number" in config_data["panel"]: - return str(config_data["panel"]["serial_number"]) - if "status" in config_data and isinstance(config_data["status"], dict): - if "serial_number" in config_data["status"]: - return str(config_data["status"]["serial_number"]) - except (FileNotFoundError, yaml.YAMLError, KeyError, ValueError): - pass - - # Fallback to a default - return "span-sim-001" - - -def get_simulation_config_path(config_key: str) -> Path: - """Get the path to a simulation config file. - - Args: - config_key: The config key (filename without extension) - - Returns: - Path to the simulation config file - - """ - current_file = Path(__file__) - config_dir = current_file.parent.parent / "simulation_configs" - return config_dir / f"{config_key}.yaml" - - -def validate_yaml_config(yaml_path: Path) -> dict[str, Any]: - """Validate and load a YAML configuration file. - - Args: - yaml_path: Path to the YAML file - - Returns: - Loaded YAML data as dictionary - - Raises: - FileNotFoundError: If file doesn't exist - yaml.YAMLError: If YAML is invalid - ValueError: If YAML doesn't contain expected structure - - """ - if not yaml_path.exists(): - raise FileNotFoundError(f"Configuration file not found: {yaml_path}") - - with yaml_path.open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) - - if data is None: - return {} - - if not isinstance(data, dict): - raise TypeError("Configuration file must contain a YAML dictionary") - - return data diff --git a/custom_components/span_panel/config_flow_utils/validation.py b/custom_components/span_panel/config_flow_utils/validation.py index 72362599..97acc7a0 100644 --- a/custom_components/span_panel/config_flow_utils/validation.py +++ b/custom_components/span_panel/config_flow_utils/validation.py @@ -2,18 +2,12 @@ from __future__ import annotations -from datetime import datetime import logging from homeassistant.core import HomeAssistant from homeassistant.util.network import is_ipv4_address from span_panel_api import V2AuthResponse, detect_api_version, register_v2 -from custom_components.span_panel.const import ( - ISO_DATETIME_FORMAT, - TIME_ONLY_FORMATS, -) - _LOGGER = logging.getLogger(__name__) @@ -21,10 +15,11 @@ async def validate_host( hass: HomeAssistant, host: str, access_token: str | None = None, + port: int = 80, ) -> bool: """Validate the host connection by probing the panel's status endpoint.""" try: - result = await detect_api_version(host) + result = await detect_api_version(host, port=port) return result.api_version in ("v1", "v2") except Exception: return False @@ -47,57 +42,7 @@ def validate_ipv4_address(host: str) -> bool: return is_ipv4_address(host) -def validate_simulation_time(time_input: str) -> str: - """Validate and convert simulation time input. - - Supports: - - Time-only formats: "17:30", "5:30" (24-hour and 12-hour) - - Full ISO datetime: "2024-06-15T17:30:00" - - Returns: - ISO datetime string with current date if time-only, or original if full datetime - - Raises: - ValueError: If the time format is invalid - - """ - if not time_input.strip(): - return "" - - time_input = time_input.strip() - - # Check if it's a full ISO datetime first - try: - datetime.fromisoformat(time_input) - return time_input # Valid ISO datetime, return as-is - except ValueError: - pass # Not a full datetime, try time-only formats - - # Try time-only formats (HH:MM or H:MM) - try: - if ":" in time_input: - parts = time_input.split(":") - if len(parts) == 2: - hour = int(parts[0]) - minute = int(parts[1]) - - # Validate hour and minute ranges - if 0 <= hour <= 23 and 0 <= minute <= 59: - # Convert to current date with the specified time - now = datetime.now() - time_only = now.replace(hour=hour, minute=minute, second=0, microsecond=0) - return time_only.isoformat() - - raise ValueError( - f"Invalid time format. Use {', '.join(TIME_ONLY_FORMATS)} or {ISO_DATETIME_FORMAT}" - ) - except (ValueError, IndexError) as e: - raise ValueError( - f"Invalid time format. Use {', '.join(TIME_ONLY_FORMATS)} or {ISO_DATETIME_FORMAT}" - ) from e - - -async def validate_v2_passphrase(host: str, passphrase: str) -> V2AuthResponse: +async def validate_v2_passphrase(host: str, passphrase: str, port: int = 80) -> V2AuthResponse: """Validate a v2 panel passphrase and return MQTT credentials. Raises: @@ -106,10 +51,10 @@ async def validate_v2_passphrase(host: str, passphrase: str) -> V2AuthResponse: SpanPanelTimeoutError: on request timeout. """ - return await register_v2(host, "Home Assistant", passphrase) + return await register_v2(host, "Home Assistant", passphrase, port=port) -async def validate_v2_proximity(host: str) -> V2AuthResponse: +async def validate_v2_proximity(host: str, port: int = 80) -> V2AuthResponse: """Validate v2 panel proximity (door bypass) and return MQTT credentials. Calls register_v2 without a passphrase, which triggers door-bypass @@ -122,4 +67,4 @@ async def validate_v2_proximity(host: str) -> V2AuthResponse: SpanPanelTimeoutError: on request timeout. """ - return await register_v2(host, "Home Assistant") + return await register_v2(host, "Home Assistant", port=port) diff --git a/custom_components/span_panel/const.py b/custom_components/span_panel/const.py index 1ffe63ba..49b3f6f9 100644 --- a/custom_components/span_panel/const.py +++ b/custom_components/span_panel/const.py @@ -4,7 +4,6 @@ from typing import Final DOMAIN: Final = "span_panel" -COORDINATOR = "coordinator" CONF_SERIAL_NUMBER = "serial_number" CONF_USE_SSL = "use_ssl" @@ -17,17 +16,9 @@ CONF_EBUS_BROKER_PASSWORD = "ebus_broker_password" CONF_EBUS_BROKER_PORT = "ebus_broker_mqtts_port" CONF_HOP_PASSPHRASE = "hop_passphrase" +CONF_HTTP_PORT = "http_port" CONF_PANEL_SERIAL = "panel_serial" -# Simulation configuration -CONF_SIMULATION_CONFIG = "simulation_config" -CONF_SIMULATION_START_TIME = "simulation_start_time" -CONF_SIMULATION_OFFLINE_MINUTES = "simulation_offline_minutes" - -# Time format constants for simulation -TIME_ONLY_FORMATS = ["HH:MM", "H:MM"] # 24-hour and 12-hour formats -ISO_DATETIME_FORMAT = "YYYY-MM-DDTHH:MM:SS" # Full ISO datetime format - # Binary sensor / status field keys (used in entity definitions) SYSTEM_DOOR_STATE = "doorState" SYSTEM_DOOR_STATE_CLOSED = "CLOSED" diff --git a/custom_components/span_panel/coordinator.py b/custom_components/span_panel/coordinator.py index 82a7259c..5cd5259c 100644 --- a/custom_components/span_panel/coordinator.py +++ b/custom_components/span_panel/coordinator.py @@ -19,7 +19,6 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from span_panel_api import ( - DynamicSimulationEngine, SpanMqttClient, SpanPanelSnapshot, ) @@ -65,9 +64,6 @@ def filter(self, record: logging.LogRecord) -> bool: # Fallback poll interval for MQTT streaming mode (push is the primary update path) _STREAMING_FALLBACK_INTERVAL = timedelta(seconds=60) -# Poll interval for simulation mode (no streaming, coordinator polls get_snapshot) -_SIMULATION_POLL_INTERVAL = timedelta(seconds=5) - class SpanPanelCoordinator(DataUpdateCoordinator[SpanPanelSnapshot]): """Coordinator for managing Span Panel data updates and entity migrations.""" @@ -75,7 +71,7 @@ class SpanPanelCoordinator(DataUpdateCoordinator[SpanPanelSnapshot]): def __init__( self, hass: HomeAssistant, - client: SpanMqttClient | DynamicSimulationEngine, + client: SpanMqttClient, config_entry: ConfigEntry, ) -> None: """Initialize the coordinator.""" @@ -93,10 +89,6 @@ def __init__( # Streaming state self._unregister_streaming: Callable[[], None] | None = None - # Simulation offline mode - self._simulation_offline_minutes: int = 0 - self._offline_start_time: float | None = None - # Hardware capability tracking — detect when BESS/PV are commissioned # and trigger a reload so the factory creates the appropriate sensors. self._known_capabilities: frozenset[str] | None = None @@ -112,12 +104,7 @@ def __init__( # here so net energy sensors can read their dip offsets directly. self._circuit_energy_sensors: dict[tuple[str, str], SpanCircuitEnergySensorProtocol] = {} - # MQTT streaming: push is the primary update path; poll is a safety net. - # Simulation: poll is the only update path; use the snapshot interval. - if isinstance(client, SpanMqttClient): - update_interval = _STREAMING_FALLBACK_INTERVAL - else: - update_interval = _SIMULATION_POLL_INTERVAL + update_interval = _STREAMING_FALLBACK_INTERVAL _LOGGER.info( "Span Panel coordinator: poll interval %s seconds", @@ -136,7 +123,7 @@ def __init__( self.config_entry = config_entry @property - def client(self) -> SpanMqttClient | DynamicSimulationEngine: + def client(self) -> SpanMqttClient: """Return the underlying panel client for entity control.""" return self._client @@ -206,10 +193,7 @@ async def _fire_dip_notification(self) -> None: # --- Streaming --- async def async_setup_streaming(self) -> None: - """Set up push streaming if the client supports it.""" - if not isinstance(self._client, SpanMqttClient): - return - + """Set up push streaming.""" self._unregister_streaming = self._client.register_snapshot_callback(self._on_snapshot_push) await self._client.start_streaming() _LOGGER.info("MQTT push streaming started") @@ -227,41 +211,11 @@ async def async_shutdown(self) -> None: self._unregister_streaming() self._unregister_streaming = None - if isinstance(self._client, SpanMqttClient): - await self._client.stop_streaming() - await self._client.close() + await self._client.stop_streaming() + await self._client.close() _LOGGER.info("Coordinator shutdown complete") - # --- Simulation offline mode --- - - def set_simulation_offline_mode(self, minutes: int) -> None: - """Configure simulation offline mode duration. - - When minutes > 0, the coordinator will raise SpanPanelConnectionError - during data updates for the specified duration, triggering the energy - grace period path in entity base classes. - """ - self._simulation_offline_minutes = minutes - if minutes > 0: - self._offline_start_time = _epoch_time() - else: - self._offline_start_time = None - - def _is_simulation_offline(self) -> bool: - """Check if the simulation is currently in offline mode.""" - if self._simulation_offline_minutes <= 0 or self._offline_start_time is None: - return False - - elapsed = _epoch_time() - self._offline_start_time - if elapsed >= self._simulation_offline_minutes * 60: - # Window expired — resume normal operation - self._simulation_offline_minutes = 0 - self._offline_start_time = None - return False - - return True - # --- Schema validation --- def _run_schema_validation(self) -> None: @@ -365,10 +319,6 @@ async def _async_update_data(self) -> SpanPanelSnapshot: cycle_start = _epoch_time() self._last_tick_epoch = cycle_start - # Simulation offline mode: raise before fetching to trigger grace period - if self._is_simulation_offline(): - raise SpanPanelConnectionError("Panel is offline in simulation mode") - fetch_start = _epoch_time() snapshot = await self._client.get_snapshot() fetch_duration = _epoch_time() - fetch_start diff --git a/custom_components/span_panel/entity.py b/custom_components/span_panel/entity.py index ebd1d207..afc2590f 100644 --- a/custom_components/span_panel/entity.py +++ b/custom_components/span_panel/entity.py @@ -7,7 +7,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from span_panel_api import SpanPanelSnapshot -from .const import CONF_API_VERSION, CONF_DEVICE_NAME +from .const import CONF_DEVICE_NAME from .coordinator import SpanPanelCoordinator from .util import snapshot_to_device_info @@ -26,6 +26,5 @@ def _build_device_info( device_name = coordinator.config_entry.data.get( CONF_DEVICE_NAME, coordinator.config_entry.title ) - is_simulator = coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" host = coordinator.config_entry.data.get(CONF_HOST) - return snapshot_to_device_info(snapshot, device_name, is_simulator, host) + return snapshot_to_device_info(snapshot, device_name, host=host) diff --git a/custom_components/span_panel/helpers.py b/custom_components/span_panel/helpers.py index 94e29234..d372af59 100644 --- a/custom_components/span_panel/helpers.py +++ b/custom_components/span_panel/helpers.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any from homeassistant.components.persistent_notification import async_create -from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify @@ -637,43 +636,6 @@ def construct_sensor_set_id(device_identifier: str) -> str: return f"{device_identifier}_sensors" -def generate_unique_simulator_serial_number(hass: HomeAssistant) -> str: - """Generate a unique simulator serial number in the format sim-nnn. - - Args: - hass: Home Assistant instance - - Returns: - Unique serial number in format sim-nnn (e.g., sim-001, sim-002, etc.) - - """ - - # Get all existing span panel config entries - existing_entries = hass.config_entries.async_entries(DOMAIN) - - # Find existing simulator serial numbers - existing_serials = set() - for entry in existing_entries: - if entry.data.get("simulation_mode", False): - # Check both simulator_serial_number and CONF_HOST fields - # simulator_serial_number is the newer field - serial = entry.data.get("simulator_serial_number") - if serial and serial.startswith("sim-"): - existing_serials.add(serial) - - # CONF_HOST may contain the serial for existing simulator configurations - host_serial = entry.data.get(CONF_HOST) - if host_serial and host_serial.startswith("sim-"): - existing_serials.add(host_serial) - - # Find the next available number - counter = 1 - while f"sim-{counter:03d}" in existing_serials: - counter += 1 - - return f"sim-{counter:03d}" - - def _get_device_identifier_for_unique_ids( coordinator: SpanPanelCoordinator, snapshot: SpanPanelSnapshot, diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 28c0aad9..70171b74 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -12,7 +12,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api>=2.2.5" + "span-panel-api>=2.3.0" ], "version": "2.0.3", "zeroconf": [ diff --git a/custom_components/span_panel/select.py b/custom_components/span_panel/select.py index bbe146c7..8b561a38 100644 --- a/custom_components/span_panel/select.py +++ b/custom_components/span_panel/select.py @@ -167,7 +167,7 @@ async def async_select_option(self, option: str) -> None: _LOGGER.debug("Selecting option: %s", option) client = self.coordinator.client if not hasattr(client, "set_circuit_priority"): - _LOGGER.warning("Circuit priority control not available in simulation mode") + _LOGGER.warning("Client does not support priority control") return priority = CircuitPriority(option) diff --git a/custom_components/span_panel/sensor.py b/custom_components/span_panel/sensor.py index e8f8a3c1..aa9cfd71 100644 --- a/custom_components/span_panel/sensor.py +++ b/custom_components/span_panel/sensor.py @@ -10,12 +10,10 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify from span_panel_api import SpanPanelSnapshot from . import SpanPanelConfigEntry from .const import ( - CONF_API_VERSION, CONF_DEVICE_NAME, ENABLE_CIRCUIT_NET_ENERGY_SENSORS, ENABLE_PANEL_NET_ENERGY_SENSORS, @@ -176,15 +174,11 @@ def _build_evse_device_info_map( if not snapshot.evse: return {} - is_simulator = coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" panel_name = ( coordinator.config_entry.data.get(CONF_DEVICE_NAME, coordinator.config_entry.title) or "Span Panel" ) - if is_simulator: - panel_identifier = slugify(panel_name) - else: - panel_identifier = snapshot.serial_number + panel_identifier = snapshot.serial_number use_circuit_numbers = coordinator.config_entry.options.get(USE_CIRCUIT_NUMBERS, False) @@ -293,17 +287,12 @@ def _build_bess_device_info( coordinator: SpanPanelCoordinator, snapshot: SpanPanelSnapshot ) -> DeviceInfo: """Build BESS sub-device info, resolving the panel identifier.""" - is_simulator = coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" panel_name = ( coordinator.config_entry.data.get(CONF_DEVICE_NAME, coordinator.config_entry.title) or "Span Panel" ) - if is_simulator: - panel_identifier = slugify(panel_name) - else: - panel_identifier = snapshot.serial_number - return bess_device_info(panel_identifier, snapshot.battery, panel_name) + return bess_device_info(snapshot.serial_number, snapshot.battery, panel_name) def create_battery_sensors( diff --git a/custom_components/span_panel/sensor_evse.py b/custom_components/span_panel/sensor_evse.py index 513f3cb3..e7d89a78 100644 --- a/custom_components/span_panel/sensor_evse.py +++ b/custom_components/span_panel/sensor_evse.py @@ -4,11 +4,9 @@ import logging -from homeassistant.util import slugify from span_panel_api import SpanEvseSnapshot, SpanPanelSnapshot from .const import ( - CONF_API_VERSION, CONF_DEVICE_NAME, USE_CIRCUIT_NUMBERS, ) @@ -42,17 +40,13 @@ def __init__( super().__init__(data_coordinator, description, snapshot) # Override device_info to point to EVSE sub-device instead of panel - is_simulator = data_coordinator.config_entry.data.get(CONF_API_VERSION) == "simulation" panel_name = ( data_coordinator.config_entry.data.get( CONF_DEVICE_NAME, data_coordinator.config_entry.title ) or "Span Panel" ) - if is_simulator: - panel_identifier = slugify(panel_name) - else: - panel_identifier = snapshot.serial_number + panel_identifier = snapshot.serial_number evse = snapshot.evse.get(evse_id, _EMPTY_EVSE) use_circuit_numbers = data_coordinator.config_entry.options.get(USE_CIRCUIT_NUMBERS, False) diff --git a/custom_components/span_panel/simulation_configs/migration_test_config.yaml b/custom_components/span_panel/simulation_configs/migration_test_config.yaml deleted file mode 100644 index a002c35e..00000000 --- a/custom_components/span_panel/simulation_configs/migration_test_config.yaml +++ /dev/null @@ -1,236 +0,0 @@ -# Migration test simulation configuration using actual registry circuit IDs -# This matches the real circuit IDs from the 1.0.10 entity registry - -panel_config: - serial_number: "nj-2316-005k6" - total_tabs: 32 - main_size: 200 # Main breaker size in Amps - -# Circuit templates define reusable behavior patterns -circuit_templates: - # Lighting circuits - lighting: - energy_profile: - mode: "consumer" - power_range: [0.0, 500.0] - typical_power: 300.0 - power_variation: 0.05 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # Outlet circuits - outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 1800.0] - typical_power: 150.0 - power_variation: 0.4 - relay_behavior: "controllable" - priority: "NEVER" - - # Kitchen specific outlets (higher capacity) - kitchen_outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 2400.0] - typical_power: 300.0 - power_variation: 0.5 - relay_behavior: "controllable" - priority: "NEVER" - - # Major appliances - major_appliance: - energy_profile: - mode: "consumer" - power_range: [0.0, 2500.0] - typical_power: 800.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # Large appliances (240V) - large_appliance_240v: - energy_profile: - mode: "consumer" - power_range: [0.0, 4000.0] - typical_power: 2000.0 - power_variation: 0.15 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # HVAC/AC systems - hvac: - energy_profile: - mode: "consumer" - power_range: [500.0, 4000.0] - typical_power: 2800.0 - power_variation: 0.25 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # EV chargers - ev_charger: - energy_profile: - mode: "consumer" - power_range: [0.0, 11500.0] - typical_power: 7200.0 - power_variation: 0.05 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # Special equipment - special_equipment: - energy_profile: - mode: "consumer" - power_range: [0.0, 1200.0] - typical_power: 400.0 - power_variation: 0.2 - relay_behavior: "controllable" - priority: "OFF_GRID" - -circuits: - # Using actual circuit IDs from the registry - - id: "0dad2f16cd514812ae1807b0457d473e" - name: "Lights Dining Room" - template: "lighting" - tabs: [1] - - - id: "11a47a0f69d54e12b7200f730c2ffda1" - name: "Lights-Outlets Bedroom" - template: "outlets" - tabs: [2] - - - id: "12ce227695cd44338864b0ef2ec4168b" - name: "Dining Room Wine Fridge" - template: "major_appliance" - tabs: [3] - overrides: - typical_power: 150.0 - - - id: "1ad73b0eb44e4022bb6270c76baed0c1" - name: "Dryer" - template: "large_appliance_240v" - tabs: [4, 6] # 240V appliance - overrides: - typical_power: 3000.0 - - - id: "31b36cde0fc642b39eec515267707a6f" - name: "Outlets / Kitchen" - template: "kitchen_outlets" - tabs: [5] - - - id: "3a847fe6eb374ec3bf92cee1ffaf2eda" - name: "Microwave & Oven" - template: "large_appliance_240v" - tabs: [7, 9] # 240V appliance - overrides: - typical_power: 2500.0 - - - id: "497ae7ebb2844772bf926f2f094c81bc" - name: "Dishwasher" - template: "major_appliance" - tabs: [8] - overrides: - typical_power: 1200.0 - - - id: "617059df47bb49bd8545a36a6b6b23d2" - name: "Spa" - template: "special_equipment" - tabs: [10, 12] # 240V equipment - overrides: - typical_power: 1500.0 - - - id: "6ad65cf2ad6443448e33973f26f7df3e" - name: "Kitchen Disposal" - template: "major_appliance" - tabs: [11] - overrides: - typical_power: 800.0 - - - id: "795e8eddb4f448af9625130332a41df8" - name: "Fountain" - template: "special_equipment" - tabs: [13] - overrides: - typical_power: 200.0 - - - id: "82a0888dc072416598d99f8b74ee3d8d" - name: "Internet Living room" - template: "outlets" - tabs: [14] - overrides: - typical_power: 100.0 - - - id: "8a2ffda9dbd24bada9a01b880e910612" - name: "Large Garage EV" - template: "ev_charger" - tabs: [15, 17] # 240V EV charger - overrides: - typical_power: 7200.0 - - - id: "914943d4798c462cadf5e5144989dd5b" - name: "Other Outlets" - template: "outlets" - tabs: [16] - - - id: "926441605113492189f9aa13dac356cd" - name: "Master bedroom" - template: "outlets" - tabs: [18] - - - id: "941d6a8b41ab4c57a6b8be14b5981fe6" - name: "Air Conditioner" - template: "hvac" - tabs: [19, 21] # 240V AC unit - overrides: - typical_power: 3500.0 - - - id: "988af7bb1fc04aac8bb3b45c660d920a" - name: "Garage Outlets" - template: "outlets" - tabs: [20] - - - id: "9941b417bfa046ef908d97c998c54c50" - name: "Refrigerator" - template: "major_appliance" - tabs: [22] - overrides: - typical_power: 150.0 - - - id: "cbe98d7307ca42658983b9f673211e14" - name: "Furnace" - template: "hvac" - tabs: [23, 25] # 240V furnace - overrides: - typical_power: 2800.0 - - - id: "d88c0c0e7c584472b2ec7e4d3c53c3b8" - name: "Small Garage EV" - template: "ev_charger" - tabs: [24, 26] # 240V EV charger - overrides: - typical_power: 3600.0 - - - id: "e3cb78d38dae41cdb50faee7bfa4ab80" - name: "Washing Machine Area" - template: "major_appliance" - tabs: [27] - overrides: - typical_power: 1000.0 - - - id: "ece352c3dc5c417e80757b716f24ba90" - name: "Kitchen and Master Bath" - template: "outlets" - tabs: [28] - - - id: "f19d909abcc4421f9ec630f82458c7ac" - name: "Lights Kitchen Master bathroom" - template: "lighting" - tabs: [29] - -# Global simulation parameters -simulation_params: - update_interval: 5 - time_acceleration: 1.0 - noise_factor: 0.02 - enable_realistic_behaviors: true diff --git a/custom_components/span_panel/simulation_configs/simple_test_config.yaml b/custom_components/span_panel/simulation_configs/simple_test_config.yaml deleted file mode 100644 index 96b8e302..00000000 --- a/custom_components/span_panel/simulation_configs/simple_test_config.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Simple test configuration with minimal circuits for testing -# Demonstrates basic YAML simulation functionality - -panel_config: - serial_number: "SPAN-TEST-001" - total_tabs: 8 - main_size: 100 - -circuit_templates: - lighting: - energy_profile: - mode: "consumer" - power_range: [5.0, 50.0] - typical_power: 25.0 - power_variation: 0.1 - relay_behavior: "controllable" - priority: "NEVER" - - outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 1800.0] - typical_power: 150.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "NEVER" - - hvac: - energy_profile: - mode: "consumer" - power_range: [0.0, 3000.0] - typical_power: 2000.0 - power_variation: 0.15 - relay_behavior: "controllable" - priority: "OFF_GRID" - - solar: - energy_profile: - mode: "producer" - power_range: [-5000.0, 0.0] - typical_power: -2500.0 - power_variation: 0.2 - relay_behavior: "non_controllable" - priority: "NEVER" - -circuits: - - id: "living_room_lights" - name: "Living Room Lights" - template: "lighting" - tabs: [1, 3] # L1 + L2 = Valid 240V - - - id: "kitchen_outlets" - name: "Kitchen Outlets" - template: "outlets" - tabs: [2, 4] # L1 + L2 = Valid 240V - - - id: "main_hvac" - name: "Main HVAC" - template: "hvac" - tabs: [5, 7] # L1 + L2 = Valid 240V - - - id: "solar_inverter" - name: "Solar Inverter" - template: "solar" - tabs: [6, 8] # L1 + L2 = Valid 240V - -unmapped_tabs: [] - -simulation_params: - update_interval: 5 - time_acceleration: 1.0 - noise_factor: 0.02 - enable_realistic_behaviors: true diff --git a/custom_components/span_panel/simulation_configs/simulation_config_32_circuit.yaml b/custom_components/span_panel/simulation_configs/simulation_config_32_circuit.yaml deleted file mode 100644 index 837e9a3b..00000000 --- a/custom_components/span_panel/simulation_configs/simulation_config_32_circuit.yaml +++ /dev/null @@ -1,444 +0,0 @@ -# Example YAML simulation configuration for a 32-circuit residential panel -# This demonstrates the new flexible simulation system - -panel_config: - serial_number: "SPAN-32-SIM-001" - total_tabs: 32 - main_size: 200 # Main breaker size in Amps - -# Circuit templates define reusable behavior patterns -circuit_templates: - # Always-on base load - always_on: - energy_profile: - mode: "consumer" # Consumes power only - power_range: [40.0, 100.0] - typical_power: 60.0 - power_variation: 0.1 - relay_behavior: "controllable" - priority: "NEVER" - - # Exterior lighting with time-based patterns - lighting: - energy_profile: - mode: "consumer" - power_range: [0.0, 500.0] - typical_power: 300.0 - power_variation: 0.05 - relay_behavior: "controllable" - priority: "OFF_GRID" - time_of_day_profile: - enabled: true - peak_hours: [18, 19, 20, 21, 22] # Evening hours - - # HVAC with cycling behavior - hvac: - energy_profile: - mode: "consumer" - power_range: [0.0, 3500.0] - typical_power: 2800.0 - power_variation: 0.1 - relay_behavior: "controllable" - priority: "NEVER" - cycling_pattern: - on_duration: 1200 # 20 minutes - off_duration: 2400 # 40 minutes - - # Large appliances (dishwasher, laundry, etc.) - large_appliance: - energy_profile: - mode: "consumer" - power_range: [0.0, 2500.0] - typical_power: 1800.0 - power_variation: 0.15 - relay_behavior: "controllable" - priority: "OFF_GRID" - - # Refrigerator with compressor cycling - refrigerator: - energy_profile: - mode: "consumer" - power_range: [50.0, 200.0] - typical_power: 120.0 - power_variation: 0.2 - relay_behavior: "non_controllable" - priority: "NEVER" - cycling_pattern: - on_duration: 600 # 10 minutes - off_duration: 1800 # 30 minutes - - # EV charger (SPAN Drive) with smart grid response - ev_charger: - energy_profile: - mode: "consumer" - power_range: [0.0, 11500.0] - typical_power: 7200.0 # Level 2 charging (240V @ 30A) - power_variation: 0.05 - relay_behavior: "controllable" - priority: "OFF_GRID" - device_type: "evse" # Generates EVSE (SPAN Drive) snapshot - smart_behavior: - responds_to_grid: true - max_power_reduction: 0.6 # Can reduce to 40% during grid stress - - # Pool equipment - pool_equipment: - energy_profile: - mode: "consumer" - power_range: [0.0, 1200.0] - typical_power: 800.0 - power_variation: 0.1 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - on_duration: 7200 # 2 hours on - off_duration: 14400 # 4 hours off - - # Solar production system - generic producer - solar_production: - energy_profile: - mode: "producer" # Produces power (negative values) - power_range: [-4000.0, 0.0] # 4kW peak production - typical_power: -2500.0 # Average production - power_variation: 0.3 - efficiency: 0.85 # 85% efficiency - relay_behavior: "non_controllable" - priority: "NEVER" - time_of_day_profile: - enabled: true - peak_hours: [11, 12, 13, 14, 15] # Peak production 11 AM - 3 PM - peak_multiplier: 1.0 - off_peak_multiplier: 0.0 # No production at night - hourly_multipliers: - 6: 0.1 # Dawn - 10% production - 7: 0.2 # Early morning - 8: 0.4 # Morning - 9: 0.6 # Mid morning - 10: 0.8 # Late morning - 11: 1.0 # Peak hours - 12: 1.0 - 13: 1.0 - 14: 1.0 - 15: 1.0 # End peak - 16: 0.8 # Afternoon - 17: 0.6 # Late afternoon - 18: 0.4 # Evening - 19: 0.2 # Dusk - 20: 0.0 # Night starts - - # Backup generator - another producer type - backup_generator: - energy_profile: - mode: "producer" - power_range: [-8000.0, 0.0] # 8kW backup generator - typical_power: -6000.0 - power_variation: 0.05 # Generators are very stable - efficiency: 0.92 # 92% fuel efficiency - relay_behavior: "controllable" - priority: "NEVER" - - # Battery storage - bidirectional - battery_storage: - energy_profile: - mode: "bidirectional" # Can charge or discharge - power_range: [-5000.0, 5000.0] # ±5kW battery - typical_power: 0.0 # Neutral when idle - power_variation: 0.02 # Very stable - efficiency: 0.95 # 95% round-trip efficiency - relay_behavior: "controllable" - priority: "NEVER" - battery_behavior: - enabled: true - charge_power: 3000.0 - discharge_power: -3000.0 - idle_power: 0.0 - - # Wind turbine - variable producer - wind_production: - energy_profile: - mode: "producer" - power_range: [-2000.0, 0.0] # 2kW small wind turbine - typical_power: -800.0 - power_variation: 0.5 # Very variable based on wind - efficiency: 0.75 # 75% efficiency - relay_behavior: "non_controllable" - priority: "NEVER" - - # General outlet circuits - outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 1800.0] - typical_power: 150.0 - power_variation: 0.4 # Very variable loads - relay_behavior: "controllable" - priority: "NEVER" - - # Kitchen specific outlets (higher capacity) - kitchen_outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 2400.0] - typical_power: 300.0 - power_variation: 0.5 # Very variable - appliances - relay_behavior: "controllable" - priority: "NEVER" - - # Heat pump with seasonal efficiency - heat_pump: - energy_profile: - mode: "consumer" - power_range: [500.0, 4000.0] - typical_power: 2800.0 - power_variation: 0.25 # Efficiency varies with temperature - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - on_duration: 900 # 15 minutes on - off_duration: 1800 # 30 minutes off - - # Major appliances (dishwasher, laundry, etc.) - major_appliance: - energy_profile: - mode: "consumer" - power_range: [0.0, 2500.0] - typical_power: 800.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "OFF_GRID" - -circuits: - # Lighting circuits (tabs 1-6) - - id: "master_bedroom_lights" - name: "Master Bedroom Lights" - template: "lighting" - tabs: [1] - overrides: - typical_power: 35.0 # Slightly higher for master - - - id: "living_room_lights" - name: "Living Room Lights" - template: "lighting" - tabs: [2] - overrides: - typical_power: 45.0 # Higher for main living area - - - id: "kitchen_lights" - name: "Kitchen Lights" - template: "lighting" - tabs: [3] - overrides: - typical_power: 40.0 - - - id: "bedroom_lights" - name: "Bedroom Lights" - template: "lighting" - tabs: [4] - - - id: "bathroom_lights" - name: "Bathroom Lights" - template: "lighting" - tabs: [5] - overrides: - typical_power: 30.0 - - - id: "exterior_lights" - name: "Exterior Lights" - template: "lighting" - tabs: [6] - overrides: - typical_power: 60.0 - time_of_day_profile: - enabled: true - peak_hours: [18, 19, 20, 21, 22, 23, 0, 1, 2, 3, 4, 5, 6] # Nighttime - - # Outlet circuits (tabs 7-14) - - id: "master_bedroom_outlets" - name: "Master Bedroom Outlets" - template: "outlets" - tabs: [7] - - - id: "living_room_outlets" - name: "Living Room Outlets" - template: "outlets" - tabs: [8] - overrides: - typical_power: 200.0 # TV, entertainment system - - - id: "kitchen_outlets_1" - name: "Kitchen Outlets 1" - template: "kitchen_outlets" - tabs: [9] # 120V circuit - - - id: "kitchen_outlets_2" - name: "Kitchen Outlets 2" - template: "kitchen_outlets" - tabs: [12] # 120V circuit - - - id: "office_outlets" - name: "Office Outlets" - template: "outlets" - tabs: [11] - overrides: - typical_power: 300.0 # Computers, monitors - - - id: "garage_outlets" - name: "Garage Outlets" - template: "outlets" - tabs: [10] - - - id: "laundry_outlets" - name: "Laundry Room Outlets" - template: "outlets" - tabs: [13] - - - id: "guest_room_outlets" - name: "Guest Room Outlets" - template: "outlets" - tabs: [14] - - # Major appliances (tabs 15-22) - - id: "refrigerator" - name: "Refrigerator" - template: "always_on" - tabs: [15] - overrides: - typical_power: 150.0 - - - id: "dishwasher" - name: "Dishwasher" - template: "major_appliance" - tabs: [16] - overrides: - typical_power: 1200.0 - - - id: "washing_machine" - name: "Washing Machine" - template: "major_appliance" - tabs: [17] - overrides: - typical_power: 1000.0 - - - id: "dryer" - name: "Electric Dryer" - template: "major_appliance" - tabs: [18, 20] # 240V appliance - overrides: - typical_power: 3000.0 - power_range: [0.0, 4000.0] - - - id: "oven" - name: "Electric Oven" - template: "major_appliance" - tabs: [19, 21] # 240V appliance - overrides: - typical_power: 2500.0 - power_range: [0.0, 3500.0] - - - id: "microwave" - name: "Microwave" - template: "major_appliance" - tabs: [22] - overrides: - typical_power: 1000.0 - - # HVAC systems (tabs 23-26) - - id: "main_hvac" - name: "Main HVAC Unit" - template: "hvac" - tabs: [23, 25] # 240V system - - - id: "heat_pump_backup" - name: "Heat Pump Backup" - template: "heat_pump" - tabs: [24, 26] # 240V system - - # EV charging (tabs 27-29) - - id: "ev_charger_garage" - name: "Garage EV Charger" - template: "ev_charger" - tabs: [27, 29] # 240V Level 2 charger - -# Configuration for unmapped tabs with specific behaviors -# Tabs 30 and 32: Solar production tabs for integration testing -# These tabs remain unmapped (no circuits) but have synchronized behavior -unmapped_tab_templates: - "30": - energy_profile: - mode: "producer" - power_range: [-2000.0, 0.0] # 2kW production capacity per phase - typical_power: -1500.0 # 1.5kW average per phase - power_variation: 0.2 - efficiency: 0.85 - relay_behavior: "non_controllable" - priority: "NEVER" - time_of_day_profile: - enabled: true - peak_hours: [11, 12, 13, 14, 15] # Peak production 11 AM - 3 PM - peak_multiplier: 1.0 - off_peak_multiplier: 0.0 # No production at night - hourly_multipliers: - 6: 0.1 # Dawn - 10% production - 7: 0.2 # Early morning - 8: 0.4 # Morning - 9: 0.6 # Mid morning - 10: 0.8 # Late morning - 11: 1.0 # Peak hours - 12: 1.0 - 13: 1.0 - 14: 1.0 - 15: 1.0 # End peak - 16: 0.8 # Afternoon - 17: 0.6 # Late afternoon - 18: 0.4 # Evening - 19: 0.2 # Dusk - 20: 0.0 # Night starts - - "32": - energy_profile: - mode: "producer" - power_range: [-2000.0, 0.0] # 2kW production capacity per phase - typical_power: -1500.0 # 1.5kW average per phase - power_variation: 0.2 - efficiency: 0.85 - relay_behavior: "non_controllable" - priority: "NEVER" - time_of_day_profile: - enabled: true - peak_hours: [11, 12, 13, 14, 15] # Peak production 11 AM - 3 PM - peak_multiplier: 1.0 - off_peak_multiplier: 0.0 # No production at night - hourly_multipliers: - 6: 0.1 # Dawn - 10% production - 7: 0.2 # Early morning - 8: 0.4 # Morning - 9: 0.6 # Mid morning - 10: 0.8 # Late morning - 11: 1.0 # Peak hours - 12: 1.0 - 13: 1.0 - 14: 1.0 - 15: 1.0 # End peak - 16: 0.8 # Afternoon - 17: 0.6 # Late afternoon - 18: 0.4 # Evening - 19: 0.2 # Dusk - 20: 0.0 # Night starts - -# Tab synchronization for coordinated behavior (e.g., 240V loads, multi-phase production) -tab_synchronizations: - - tabs: [30, 32] - behavior: "240v_split_phase" # Two phases of same 240V system - power_split: "equal" # Equal power on both phases - energy_sync: true # Synchronized energy accumulation - template: "solar_production" # Generic production template - -# Unmapped tabs that should have behavior but remain unmapped (no circuits created) -unmapped_tabs: [30, 32] - -# Global simulation parameters -simulation_params: - update_interval: 5 # Update every 5 seconds - time_acceleration: 1.0 # Real-time simulation - noise_factor: 0.02 # ±2% random noise on all values - enable_realistic_behaviors: true diff --git a/custom_components/span_panel/simulation_configs/simulation_config_40_circuit_with_battery.yaml b/custom_components/span_panel/simulation_configs/simulation_config_40_circuit_with_battery.yaml deleted file mode 100644 index 5d930bf8..00000000 --- a/custom_components/span_panel/simulation_configs/simulation_config_40_circuit_with_battery.yaml +++ /dev/null @@ -1,325 +0,0 @@ -# 40-tab panel configuration with battery storage and unmapped tabs -# This config intentionally leaves some tabs unmapped to test edge cases - -panel_config: - serial_number: "SPAN-40-BATTERY-001" - total_tabs: 40 - main_size: 400 # Larger main breaker for 40-tab panel - -circuit_templates: - # Standard templates - lighting: - energy_profile: - mode: "consumer" - power_range: [5.0, 50.0] - typical_power: 25.0 - power_variation: 0.15 - relay_behavior: "controllable" - priority: "NEVER" - - outlets: - energy_profile: - mode: "consumer" - power_range: [0.0, 1800.0] - typical_power: 150.0 - power_variation: 0.4 - relay_behavior: "controllable" - priority: "NEVER" - - # EV charger (SPAN Drive) with smart grid response - ev_charger: - energy_profile: - mode: "consumer" - power_range: [0.0, 9600.0] - typical_power: 7200.0 # Level 2 charging (240V @ 30A) - power_variation: 0.05 - relay_behavior: "controllable" - priority: "OFF_GRID" - device_type: "evse" # Generates EVSE (SPAN Drive) snapshot - smart_behavior: - responds_to_grid: true - max_power_reduction: 0.6 # Can reduce to 40% during grid stress - - hvac: - energy_profile: - mode: "consumer" - power_range: [0.0, 3000.0] - typical_power: 1500.0 - power_variation: 0.2 - relay_behavior: "controllable" - priority: "NEVER" - cycling_pattern: - on_duration: 900 # 15 minutes - off_duration: 1800 # 30 minutes - - solar: - energy_profile: - mode: "producer" - power_range: [-8000.0, 0.0] - typical_power: -4000.0 - power_variation: 0.4 - relay_behavior: "non_controllable" - priority: "NEVER" - - battery: - energy_profile: - mode: "bidirectional" - power_range: [-5000.0, 5000.0] # Can charge or discharge - typical_power: 0.0 # Neutral when idle - power_variation: 0.1 - relay_behavior: "non_controllable" - priority: "NEVER" - battery_behavior: - enabled: true - charge_efficiency: 0.95 # 95% efficient charging - discharge_efficiency: 0.95 # 95% efficient discharging - charge_hours: [9, 10, 11, 12, 13, 14, 15, 16] # Solar hours - discharge_hours: [17, 18, 19, 20, 21] # Peak demand hours - max_charge_power: -3000.0 # Max charging power (negative) - max_discharge_power: 2500.0 # Max discharge power (positive) - idle_hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 22, 23] # Low activity hours - idle_power_range: [-100.0, 100.0] # Random power during idle hours - # Solar intensity profile for charging (hour: intensity_factor) - solar_intensity_profile: - 9: 0.2 - 10: 0.4 - 11: 0.7 - 12: 1.0 # Peak solar - 13: 1.0 - 14: 0.8 - 15: 0.6 - 16: 0.3 - # Demand factor profile for discharging (hour: demand_factor) - demand_factor_profile: - 17: 0.6 # Early evening - 18: 0.8 # Peak start - 19: 1.0 # Peak demand - 20: 0.9 # High demand - 21: 0.7 # Demand decreasing - -circuits: - # 38 circuits for a 40-tab panel (tab 39 unmapped) - - # Main lighting circuits (tabs 1-8) - - id: "kitchen_lights" - name: "Kitchen Lights" - template: "lighting" - tabs: [1] - - - id: "living_room_lights" - name: "Living Room Lights" - template: "lighting" - tabs: [2] - - - id: "bedroom_lights" - name: "Bedroom Lights" - template: "lighting" - tabs: [3] - - - id: "bathroom_lights" - name: "Bathroom Lights" - template: "lighting" - tabs: [4] - - - id: "outdoor_lights" - name: "Outdoor Lights" - template: "lighting" - tabs: [5] - - - id: "garage_lights" - name: "Garage Lights" - template: "lighting" - tabs: [6] - - - id: "basement_lights" - name: "Basement Lights" - template: "lighting" - tabs: [7] - - - id: "office_lights" - name: "Office Lights" - template: "lighting" - tabs: [8] - - # Outlet circuits (tabs 9-16) - - id: "kitchen_outlets" - name: "Kitchen Outlets" - template: "outlets" - tabs: [9] - - - id: "living_room_outlets" - name: "Living Room Outlets" - template: "outlets" - tabs: [10] - - - id: "bedroom_outlets" - name: "Bedroom Outlets" - template: "outlets" - tabs: [11] - - - id: "bathroom_outlets" - name: "Bathroom Outlets" - template: "outlets" - tabs: [12] - - - id: "garage_outlets" - name: "Garage Outlets" - template: "outlets" - tabs: [13] - - - id: "basement_outlets" - name: "Basement Outlets" - template: "outlets" - tabs: [14] - - - id: "office_outlets" - name: "Office Outlets" - template: "outlets" - tabs: [15] - - - id: "outdoor_outlets" - name: "Outdoor Outlets" - template: "outlets" - tabs: [16] - - # HVAC systems (240V, using opposing tabs) - - id: "main_hvac" - name: "Main HVAC System" - template: "hvac" - tabs: [17, 19] - - - id: "secondary_hvac" - name: "Secondary HVAC System" - template: "hvac" - tabs: [18, 20] - - # Major appliances (240V) - - id: "electric_range" - name: "Electric Range" - template: "outlets" - tabs: [21, 23] - overrides: - power_range: [0.0, 8000.0] - typical_power: 2000.0 - - - id: "electric_dryer" - name: "Electric Dryer" - template: "outlets" - tabs: [22, 24] - overrides: - power_range: [0.0, 5000.0] - typical_power: 1500.0 - - - id: "water_heater" - name: "Water Heater" - template: "outlets" - tabs: [25, 27] - overrides: - power_range: [0.0, 4500.0] - typical_power: 2000.0 - - - id: "ev_charger_garage" - name: "Garage EV Charger" - template: "ev_charger" - tabs: [26, 28] - - - id: "ev_charger_driveway" - name: "Driveway EV Charger" - template: "ev_charger" - tabs: [38, 40] # 240V Level 2 charger - - # Solar and battery systems - - id: "solar_inverter_1" - name: "Solar Inverter 1" - template: "solar" - tabs: [29, 31] - - - id: "solar_inverter_2" - name: "Solar Inverter 2" - template: "solar" - tabs: [30, 32] - - # Battery storage on opposing phased tabs as requested - - id: "battery_system_1" - name: "Battery System 1" - template: "battery" - tabs: [33, 35] - - - id: "battery_system_2" - name: "Battery System 2" - template: "battery" - tabs: [34, 36] - - # Additional circuit (using tab 37) - - id: "pool_pump" - name: "Pool Pump" - template: "outlets" - tabs: [37] - overrides: - power_range: [0.0, 2000.0] - typical_power: 800.0 - - # Tab 39 is intentionally left unmapped to test unmapped tab creation - -# Circuit synchronizations for 240V systems -tab_synchronizations: - - tabs: [33, 35] # Battery System 1 - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "battery_sync" - - - tabs: [34, 36] # Battery System 2 - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "battery_sync" - - - tabs: [29, 31] # Solar Inverter 1 - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "solar_sync" - - - tabs: [30, 32] # Solar Inverter 2 - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "solar_sync" - - - tabs: [26, 28] # Garage EV Charger - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "ev_charger_sync" - - - tabs: [38, 40] # Driveway EV Charger - 240V split phase - behavior: "240v_split_phase" - power_split: "equal" - energy_sync: true - template: "ev_charger_sync" - -# Explicitly list unmapped tabs for testing purposes -unmapped_tabs: [39] # Tab 39 remains unmapped (38 and 40 now used by driveway EV charger) - -# Unmapped tab templates for tabs that should have behavior but remain unmapped -unmapped_tab_templates: - "39": - energy_profile: - mode: "consumer" - power_range: [0.0, 800.0] - typical_power: 150.0 - power_variation: 0.2 - relay_behavior: "non_controllable" - priority: "OFF_GRID" - -simulation_params: - enable_realistic_behaviors: true - noise_factor: 0.02 - time_acceleration: 1.0 - update_interval: 5 - # Advanced battery behavior - battery_behaviors: - charge_efficiency: 0.95 - discharge_efficiency: 0.90 - self_discharge_rate: 0.001 # 0.1% per hour diff --git a/custom_components/span_panel/simulation_configs/simulation_config_8_tab_workshop.yaml b/custom_components/span_panel/simulation_configs/simulation_config_8_tab_workshop.yaml deleted file mode 100644 index 6f477fcc..00000000 --- a/custom_components/span_panel/simulation_configs/simulation_config_8_tab_workshop.yaml +++ /dev/null @@ -1,170 +0,0 @@ -# 8-tab workshop panel configuration with 4 realistic circuits -# Tabs 5-8 left unmapped for automatic unmapped tab detection testing - -panel_config: - serial_number: "SPAN-WORKSHOP-001" - total_tabs: 8 - main_size: 200 - -circuit_templates: - # Workshop-specific templates with realistic power ranges - - workshop_lighting: - energy_profile: - mode: "consumer" - power_range: [20.0, 80.0] - typical_power: 45.0 - power_variation: 0.2 - relay_behavior: "controllable" - priority: "NEVER" - time_of_day_profile: - enabled: true - peak_hours: [7, 8, 17, 18, 19, 20] - peak_multiplier: 1.0 - off_peak_multiplier: 0.1 - - heavy_machinery: - energy_profile: - mode: "consumer" - power_range: [0.0, 3500.0] - typical_power: 2200.0 - power_variation: 0.4 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - enabled: true - on_duration_minutes: 15 - off_duration_minutes: 10 - duty_cycle: 0.6 - - power_tools: - energy_profile: - mode: "consumer" - power_range: [0.0, 1800.0] - typical_power: 400.0 - power_variation: 0.6 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - enabled: true - on_duration_minutes: 8 - off_duration_minutes: 25 - duty_cycle: 0.25 - - hvac_workshop: - energy_profile: - mode: "consumer" - power_range: [0.0, 2400.0] - typical_power: 1500.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "SOC_THRESHOLD" - time_of_day_profile: - enabled: true - peak_hours: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17] - peak_multiplier: 1.0 - off_peak_multiplier: 0.2 - cycling_pattern: - enabled: true - on_duration_minutes: 20 - off_duration_minutes: 15 - duty_cycle: 0.57 - -circuits: - - id: "workshop_led_lighting" - name: "Workshop LED Lighting" - template: "workshop_lighting" - tabs: [1] - - - id: "table_saw_planer" - name: "Table Saw & Planer" - template: "heavy_machinery" - tabs: [2] - - - id: "power_tool_outlets" - name: "Power Tool Outlets" - template: "power_tools" - tabs: [3] - - - id: "workshop_hvac" - name: "Workshop HVAC" - template: "hvac_workshop" - tabs: [4] - -# Tabs 5, 6, 7, 8 are intentionally left unmapped - -# Unmapped tab templates for tabs that should have behavior but remain unmapped -unmapped_tab_templates: - "5": - energy_profile: - mode: "consumer" - power_range: [0.0, 1000.0] - typical_power: 200.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - enabled: true - on_duration_minutes: 10 - off_duration_minutes: 20 - duty_cycle: 0.33 - - "6": - energy_profile: - mode: "consumer" - power_range: [0.0, 1500.0] - typical_power: 400.0 - power_variation: 0.4 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - enabled: true - on_duration_minutes: 15 - off_duration_minutes: 25 - duty_cycle: 0.38 - - "7": - energy_profile: - mode: "consumer" - power_range: [0.0, 800.0] - typical_power: 150.0 - power_variation: 0.2 - relay_behavior: "controllable" - priority: "OFF_GRID" - time_of_day_profile: - enabled: true - peak_hours: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17] - peak_multiplier: 1.0 - off_peak_multiplier: 0.1 - - "8": - energy_profile: - mode: "consumer" - power_range: [0.0, 1200.0] - typical_power: 300.0 - power_variation: 0.3 - relay_behavior: "controllable" - priority: "OFF_GRID" - cycling_pattern: - enabled: true - on_duration_minutes: 12 - off_duration_minutes: 18 - duty_cycle: 0.4 - -# Explicitly list unmapped tabs for testing purposes -unmapped_tabs: [5, 6, 7, 8] - -behavior_engine: - enabled: true - time_of_day_enabled: true - hvac_cycling_enabled: true - weather_simulation_enabled: true - smart_grid_enabled: true - solar: - enabled: false # Workshop typically doesn't have solar - -simulation_params: - enable_realistic_behaviors: true - noise_factor: 0.03 # Slightly more variation for workshop equipment - time_acceleration: 1.0 - update_interval: 5 diff --git a/custom_components/span_panel/simulation_factory.py b/custom_components/span_panel/simulation_factory.py deleted file mode 100644 index 97414194..00000000 --- a/custom_components/span_panel/simulation_factory.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Simulation factory for SPAN Panel integration. - -This module provides a factory pattern for handling simulation mode setup -without polluting the main integration code. It allows the integration to -treat simulation mode as if it were a real panel for YAML generation. -""" - -from __future__ import annotations - -import logging -import os -from typing import Any - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .coordinator import SpanPanelCoordinator -from .synthetic_sensors import SyntheticSensorCoordinator - -_LOGGER = logging.getLogger(__name__) - - -class SimulationModeFactory: - """Factory for handling simulation mode setup and configuration.""" - - @staticmethod - def is_simulation_mode() -> bool: - """Check if we're running in simulation mode. - - Returns: - True if SPAN_USE_REAL_SIMULATION environment variable is set - - """ - return os.environ.get("SPAN_USE_REAL_SIMULATION", "").lower() in ("1", "true", "yes") - - @staticmethod - def create_simulation_coordinator( - coordinator: SyntheticSensorCoordinator, - ) -> SimulationCoordinator: - """Create a simulation coordinator that wraps the synthetic coordinator. - - Args: - coordinator: The synthetic sensor coordinator to wrap - - Returns: - SimulationCoordinator that provides simulation-specific behavior - - """ - return SimulationCoordinator(coordinator) - - @staticmethod - def setup_simulation_logging() -> None: - """Set up enhanced logging for simulation mode.""" - if SimulationModeFactory.is_simulation_mode(): - _LOGGER.info("🔧 Simulation mode enabled - enhanced logging active") - # Set debug level for simulation-related components - logging.getLogger("custom_components.span_panel.synthetic_sensors").setLevel( - logging.DEBUG - ) - logging.getLogger("custom_components.span_panel.synthetic_panel_circuits").setLevel( - logging.DEBUG - ) - logging.getLogger("custom_components.span_panel.synthetic_named_circuits").setLevel( - logging.DEBUG - ) - - -class SimulationCoordinator: - """Coordinator that provides simulation-specific behavior for synthetic sensors. - - This class wraps the main SyntheticSensorCoordinator and provides - simulation-specific setup and logging without modifying the core logic. - """ - - def __init__(self, coordinator: SyntheticSensorCoordinator): - """Initialize the simulation coordinator. - - Args: - coordinator: The synthetic sensor coordinator to wrap - - """ - self.coordinator = coordinator - self._is_simulation = SimulationModeFactory.is_simulation_mode() - - async def setup_configuration(self, config_entry: ConfigEntry) -> Any: - """Set up configuration with simulation-specific behavior. - - This method delegates to the main coordinator but adds simulation-specific - logging and behavior. - """ - if not self._is_simulation: - # Delegate to normal coordinator behavior - return await self.coordinator._setup_live_configuration(config_entry) - - # Simulation mode: use the same logic but with enhanced logging - _LOGGER.info("🔧 Simulation mode: Generating YAML from simulated panel data") - - # For simulation mode, we use the same configuration logic as live mode - # but with enhanced logging to show it's working with simulated data - result = await self.coordinator._setup_live_configuration(config_entry) - - _LOGGER.info("🎯 Simulation mode: YAML generation completed successfully") - return result - - def get_coordinator(self) -> SyntheticSensorCoordinator: - """Get the underlying synthetic sensor coordinator.""" - return self.coordinator - - -def create_synthetic_coordinator_with_simulation_support( - hass: HomeAssistant, coordinator: SpanPanelCoordinator, device_name: str -) -> SimulationCoordinator | SyntheticSensorCoordinator: - """Create a synthetic coordinator with optional simulation support. - - This factory function creates the appropriate coordinator based on whether - simulation mode is enabled, keeping the main integration code clean. - - Args: - hass: Home Assistant instance - coordinator: SPAN panel coordinator - device_name: Device name for the coordinator - - Returns: - Either a SimulationCoordinator (if simulation mode) or SyntheticSensorCoordinator - - """ - # Set up simulation logging if needed - SimulationModeFactory.setup_simulation_logging() - - # Create the base coordinator - base_coordinator = SyntheticSensorCoordinator(hass, coordinator, device_name) - - # Wrap with simulation coordinator if in simulation mode - if SimulationModeFactory.is_simulation_mode(): - return SimulationModeFactory.create_simulation_coordinator(base_coordinator) - - # Return the base coordinator for normal operation - return base_coordinator diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py index c9fa42f3..d5c675fb 100644 --- a/custom_components/span_panel/simulation_utils.py +++ b/custom_components/span_panel/simulation_utils.py @@ -1,59 +1,22 @@ -"""Simulation utilities for SPAN Panel integration.""" +"""Clone panel utilities for SPAN Panel integration.""" from __future__ import annotations import logging from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import slugify import yaml -from .const import ( - CONF_SIMULATION_CONFIG, - COORDINATOR, - DOMAIN, -) from .simulation_generator import SimulationYamlGenerator -_LOGGER = logging.getLogger(__name__) - - -def infer_template_for(name: str, tabs: list[int]) -> str: - """Infer circuit template based on circuit name and tab configuration. +if TYPE_CHECKING: + from .coordinator import SpanPanelCoordinator - Args: - name: Circuit name to analyze - tabs: List of tab numbers for this circuit - - Returns: - Template string identifier for the circuit type - - """ - lname = str(name).lower() - if any(k in lname for k in ["light", "lights"]): - return "lighting" - if "kitchen" in lname and "outlet" in lname: - return "kitchen_outlets" - if any(k in lname for k in ["hvac", "furnace", "air conditioner", "ac", "heat pump"]): - return "hvac" - if any(k in lname for k in ["fridge", "refrigerator", "wine fridge"]): - return "refrigerator" - if any(k in lname for k in ["ev", "charger"]): - return "ev_charger" - if any(k in lname for k in ["pool", "spa", "fountain"]): - return "pool_equipment" - if any(k in lname for k in ["internet", "router", "network", "modem"]): - return "always_on" - # Heuristics: 240V multi-tab loads as major appliances - if len(tabs) >= 2: - return "major_appliance" - # Fallbacks - if "outlet" in lname: - return "outlets" - return "major_appliance" +_LOGGER = logging.getLogger(__name__) async def clone_panel_to_simulation( @@ -61,7 +24,7 @@ async def clone_panel_to_simulation( config_entry: ConfigEntry, user_input: dict[str, Any] | None = None, ) -> tuple[Path, dict[str, str]]: - """Clone the live panel into a simulation YAML stored in simulation_configs. + """Clone the live panel into a simulation YAML for the standalone simulator. Args: hass: Home Assistant instance @@ -78,13 +41,14 @@ async def clone_panel_to_simulation( device_name = config_entry.data.get("device_name", config_entry.title) safe_device = slugify(device_name) if isinstance(device_name, str) else "span_panel" - config_dir = Path(__file__).parent / "simulation_configs" + config_dir = Path(hass.config.config_dir) / "span_panel" / "exports" base_name = f"simulation_config_{safe_device}.yaml" dest_path = config_dir / base_name - # Resolve coordinator (live) - coordinator_data = hass.data.get(DOMAIN, {}).get(config_entry.entry_id, {}) - coordinator = coordinator_data.get(COORDINATOR) + # Resolve coordinator from runtime_data + coordinator: SpanPanelCoordinator | None = None + if hasattr(config_entry, "runtime_data") and config_entry.runtime_data is not None: + coordinator = config_entry.runtime_data.coordinator if coordinator is None: errors["base"] = "coordinator_unavailable" return dest_path, errors @@ -114,9 +78,6 @@ async def clone_panel_to_simulation( "" if suffix_index == 1 else f"_{suffix_index}" ) - # Use the same filename pattern (device name based, not tab count) - # The dest_path is already correctly set above - # Ensure directory exists and write file await hass.async_add_executor_job( lambda: dest_path.parent.mkdir(parents=True, exist_ok=True) @@ -129,19 +90,6 @@ def _write_yaml() -> None: await hass.async_add_executor_job(_write_yaml) _LOGGER.info("Cloned live panel to simulation YAML at %s", dest_path) - # Update config entry to point to the new simulation config - try: - new_data = dict(config_entry.data) - new_data[CONF_SIMULATION_CONFIG] = dest_path.stem - hass.config_entries.async_update_entry(config_entry, data=new_data) - _LOGGER.debug("Set CONF_SIMULATION_CONFIG to %s", dest_path.stem) - except Exception as update_err: - _LOGGER.warning( - "Failed to set CONF_SIMULATION_CONFIG to %s: %s", - dest_path.stem, - update_err, - ) - # Return success with no errors return dest_path, {} diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index f9715792..0b778b53 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -10,7 +10,7 @@ }, "error": { "cannot_connect": "Failed to connect to Span Panel", - "host_required": "Host is required for non-simulator mode", + "host_required": "Host is required", "invalid_auth": "Invalid authentication", "proximity_failed": "Proximity not proven. Please open and close the panel door 3 times and try again.", "unknown": "Unexpected error" @@ -24,19 +24,17 @@ "user": { "data": { "host": "Host", - "simulator_mode": "Simulator Mode", "power_display_precision": "Power Display Precision", "energy_display_precision": "Energy Display Precision", "enable_energy_dip_compensation": "Auto-Compensate Energy Dips" }, "data_description": { - "host": "IP address or hostname of SPAN Panel (required for real hardware, optional for simulation mode)", - "simulator_mode": "Enable simulator mode for testing (no hardware required). Host field becomes optional when simulation mode is enabled.", + "host": "IP address or hostname of SPAN Panel", "power_display_precision": "Number of decimal places for power values (0-3)", "energy_display_precision": "Number of decimal places for energy values (0-3)", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings, preventing spikes in the energy dashboard." }, - "description": "Enter the connection details for your SPAN Panel. For simulation mode, check the simulator checkbox and the host field becomes optional.", + "description": "Enter the connection details for your SPAN Panel.", "title": "Connect to the Span Panel" }, "choose_auth_type": { @@ -83,28 +81,11 @@ "data_description": { "host": "IP address or hostname of the SPAN Panel" } - }, - "simulator_config": { - "title": "Simulator Configuration", - "description": "Select a simulation configuration and optionally set a custom start time. {config_count} configurations available.", - "data": { - "simulation_config": "Simulation Configuration", - "host": "Host (Serial Number)", - "simulation_start_time": "Simulation Start Time (Optional)", - "entity_naming_pattern": "Entity Naming Pattern" - }, - "data_description": { - "simulation_config": "Choose a simulation configuration that defines circuit templates and energy profiles", - "host": "Custom serial number for the simulated panel (optional, will use config default if empty)", - "simulation_start_time": "Time format: HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS). Leave empty to use current time.", - "entity_naming_pattern": "Choose how circuit entities are named in the simulation" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "Invalid time format. Use HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS)", "directory": "Directory path is required", "base": "Export failed: {error}" }, @@ -113,9 +94,7 @@ "title": "Options Menu", "menu_options": { "general_options": "General Options", - "export_config": "Export Synthetic Sensor Config", - "simulation_start_time": "Simulation Start Time", - "simulation_offline_minutes": "Simulation Offline Minutes" + "clone_panel_to_simulation": "Clone Panel To Simulation" } }, "general_options": { @@ -136,26 +115,6 @@ "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } }, - "simulation_start_time": { - "title": "Simulation Start Time", - "description": "Configure the simulation start time. Changes to simulation time will reload the integration to apply the new time.", - "data": { - "simulation_start_time": "Simulation Start Time" - }, - "data_description": { - "simulation_start_time": "Time format: HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS). Leave empty to use current time. Integration will reload to apply changes." - } - }, - "simulation_offline_minutes": { - "title": "Simulation Offline Minutes", - "description": "Configure how long the panel should appear offline during simulation.", - "data": { - "simulation_offline_minutes": "Offline Minutes" - }, - "data_description": { - "simulation_offline_minutes": "Number of minutes the panel should appear offline during simulation. Set to 0 to disable offline simulation." - } - }, "export_config": { "title": "Export Synthetic Sensor Config", "description": "Export the current synthetic sensor configuration to a YAML file. You can specify either a directory (file will be named automatically) or a full file path. Default filename: {filename}", diff --git a/custom_components/span_panel/switch.py b/custom_components/span_panel/switch.py index e993a1ce..9e675401 100644 --- a/custom_components/span_panel/switch.py +++ b/custom_components/span_panel/switch.py @@ -242,7 +242,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" client = self.coordinator.client if not hasattr(client, "set_circuit_relay"): - _LOGGER.warning("Circuit relay control not available in simulation mode") + _LOGGER.warning("Client does not support relay control") return await client.set_circuit_relay(self._circuit_id, "CLOSED") @@ -253,7 +253,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" client = self.coordinator.client if not hasattr(client, "set_circuit_relay"): - _LOGGER.warning("Circuit relay control not available in simulation mode") + _LOGGER.warning("Client does not support relay control") return await client.set_circuit_relay(self._circuit_id, "OPEN") diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index f9715792..fb7555d3 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -10,7 +10,7 @@ }, "error": { "cannot_connect": "Failed to connect to Span Panel", - "host_required": "Host is required for non-simulator mode", + "host_required": "Host is required", "invalid_auth": "Invalid authentication", "proximity_failed": "Proximity not proven. Please open and close the panel door 3 times and try again.", "unknown": "Unexpected error" @@ -24,19 +24,17 @@ "user": { "data": { "host": "Host", - "simulator_mode": "Simulator Mode", "power_display_precision": "Power Display Precision", "energy_display_precision": "Energy Display Precision", "enable_energy_dip_compensation": "Auto-Compensate Energy Dips" }, "data_description": { - "host": "IP address or hostname of SPAN Panel (required for real hardware, optional for simulation mode)", - "simulator_mode": "Enable simulator mode for testing (no hardware required). Host field becomes optional when simulation mode is enabled.", + "host": "IP address or hostname of SPAN Panel", "power_display_precision": "Number of decimal places for power values (0-3)", "energy_display_precision": "Number of decimal places for energy values (0-3)", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings, preventing spikes in the energy dashboard." }, - "description": "Enter the connection details for your SPAN Panel. For simulation mode, check the simulator checkbox and the host field becomes optional.", + "description": "Enter the connection details for your SPAN Panel.", "title": "Connect to the Span Panel" }, "choose_auth_type": { @@ -83,28 +81,11 @@ "data_description": { "host": "IP address or hostname of the SPAN Panel" } - }, - "simulator_config": { - "title": "Simulator Configuration", - "description": "Select a simulation configuration and optionally set a custom start time. {config_count} configurations available.", - "data": { - "simulation_config": "Simulation Configuration", - "host": "Host (Serial Number)", - "simulation_start_time": "Simulation Start Time (Optional)", - "entity_naming_pattern": "Entity Naming Pattern" - }, - "data_description": { - "simulation_config": "Choose a simulation configuration that defines circuit templates and energy profiles", - "host": "Custom serial number for the simulated panel (optional, will use config default if empty)", - "simulation_start_time": "Time format: HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS). Leave empty to use current time.", - "entity_naming_pattern": "Choose how circuit entities are named in the simulation" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "Invalid time format. Use HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS)", "directory": "Directory path is required", "base": "Export failed: {error}" }, @@ -114,8 +95,7 @@ "menu_options": { "general_options": "General Options", "export_config": "Export Synthetic Sensor Config", - "simulation_start_time": "Simulation Start Time", - "simulation_offline_minutes": "Simulation Offline Minutes" + "clone_panel_to_simulation": "Clone Panel to Simulation" } }, "general_options": { @@ -129,33 +109,13 @@ "enable_energy_dip_compensation": "Auto-Compensate Energy Dips" }, "data_description": { - "snapshot_update_interval": "How often to rebuild the panel snapshot from MQTT data. Lower values give faster updates but use more CPU. Increase on low-power hardware (e.g., Raspberry Pi). Set to 0 for no debounce. Range: 0\u201315 seconds.", + "snapshot_update_interval": "How often to rebuild the panel snapshot from MQTT data. Lower values give faster updates but use more CPU. Increase on low-power hardware (e.g., Raspberry Pi). Set to 0 for no debounce. Range: 0–15 seconds.", "enable_panel_net_energy_sensors": "Create net energy sensors for panel-level energy flows (main meter and feed-through). Shows true energy balance accounting for bidirectional flows.", "enable_circuit_net_energy_sensors": "Create net energy sensors for individual circuits. Shows true energy consumption accounting for reactive power and regenerative flows.", "energy_reporting_grace_period": "How long energy sensors maintain their last known value when the panel becomes unavailable (0-60 minutes). Helps preserve energy statistics integrity during brief outages. Default: 15 minutes.", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } }, - "simulation_start_time": { - "title": "Simulation Start Time", - "description": "Configure the simulation start time. Changes to simulation time will reload the integration to apply the new time.", - "data": { - "simulation_start_time": "Simulation Start Time" - }, - "data_description": { - "simulation_start_time": "Time format: HH:MM (24h), H:MM (12h), or full ISO datetime (YYYY-MM-DDTHH:MM:SS). Leave empty to use current time. Integration will reload to apply changes." - } - }, - "simulation_offline_minutes": { - "title": "Simulation Offline Minutes", - "description": "Configure how long the panel should appear offline during simulation.", - "data": { - "simulation_offline_minutes": "Offline Minutes" - }, - "data_description": { - "simulation_offline_minutes": "Number of minutes the panel should appear offline during simulation. Set to 0 to disable offline simulation." - } - }, "export_config": { "title": "Export Synthetic Sensor Config", "description": "Export the current synthetic sensor configuration to a YAML file. You can specify either a directory (file will be named automatically) or a full file path. Default filename: {filename}", diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index cb77edaf..ee49063b 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -10,7 +10,7 @@ "cannot_connect": "No se logró establecer conexión con Span Panel", "invalid_auth": "Autenticación invalida", "unknown": "Error inesperado", - "host_required": "Se requiere host para conexiones de panel en vivo" + "host_required": "Se requiere host" }, "flow_title": "Span Panel ({host})", "step": { @@ -21,19 +21,17 @@ "user": { "data": { "host": "Host", - "simulator_mode": "Modo Simulador", "power_display_precision": "Precisión de Visualización de Potencia", "energy_display_precision": "Precisión de Visualización de Energía", "enable_energy_dip_compensation": "Compensar Caídas de Energía Automáticamente" }, "data_description": { - "host": "Dirección IP o nombre de host del SPAN Panel (para simulación: use número de serie como 'sp3-simulation-001' o déjelo vacío para el predeterminado)", - "simulator_mode": "Habilitar modo simulador para pruebas (no se requiere hardware). Deje el host vacío para simulación predeterminada, o ingrese un número de serie personalizado.", + "host": "Dirección IP o nombre de host del SPAN Panel", "power_display_precision": "Número de lugares decimales para valores de potencia (0-3)", "energy_display_precision": "Número de lugares decimales para valores de energía (0-3)", "enable_energy_dip_compensation": "Compensar automáticamente cuando el panel reporta lecturas de energía más bajas, evitando picos en el panel de energía." }, - "description": "Ingrese los detalles de conexión para su SPAN Panel. Para modo simulador, marque la casilla del simulador y opcionalmente ingrese un número de serie personalizado.", + "description": "Ingrese los detalles de conexión para su SPAN Panel.", "title": "Establecer conexión al Span Panel" }, "choose_auth_type": { @@ -63,28 +61,11 @@ "data_description": { "hop_passphrase": "La contraseña utilizada para autenticar con la API v2 del SPAN Panel" } - }, - "simulator_config": { - "title": "Configuración del Simulador", - "description": "Seleccione una configuración de simulación y opcionalmente establezca un tiempo de inicio personalizado. {config_count} configuraciones disponibles.", - "data": { - "simulation_config": "Configuración de Simulación", - "host": "Host (Número de Serie)", - "simulation_start_time": "Tiempo de Inicio de Simulación (Opcional)", - "entity_naming_pattern": "Patrón de Nomenclatura de Entidades" - }, - "data_description": { - "simulation_config": "Elija una configuración de simulación que defina plantillas de circuito y perfiles de energía", - "host": "Número de serie personalizado para el panel simulado (opcional, usará el predeterminado de la configuración si está vacío)", - "simulation_start_time": "Formato de tiempo: HH:MM (24h), H:MM (12h), o datetime ISO completo (YYYY-MM-DDTHH:MM:SS). Deje vacío para usar el tiempo actual.", - "entity_naming_pattern": "Elija cómo se nombran las entidades de circuito en la simulación" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "Formato de fecha/hora inválido. Use HH:MM (24h), H:MM (12h), o datetime ISO completo (YYYY-MM-DDTHH:MM:SS)", "directory": "Se requiere la ruta del archivo", "base": "Falló la exportación: {error}" }, @@ -94,8 +75,7 @@ "menu_options": { "general_options": "Opciones Generales", "export_config": "Exportar Configuración de Sensores Sintéticos", - "simulation_start_time": "Tiempo de Inicio de Simulación", - "simulation_offline_minutes": "Minutos Sin Conexión de Simulación" + "clone_panel_to_simulation": "Clonar Panel a Simulación" } }, "general_options": { @@ -109,7 +89,7 @@ "enable_energy_dip_compensation": "Compensar Caídas de Energía Automáticamente" }, "data_description": { - "snapshot_update_interval": "Con qué frecuencia reconstruir el snapshot del panel desde datos MQTT. Valores más bajos dan actualizaciones más rápidas pero usan más CPU. Aumente en hardware de baja potencia (ej., Raspberry Pi). Establezca en 0 para sin debounce. Rango: 0\u201315 segundos.", + "snapshot_update_interval": "Con qué frecuencia reconstruir el snapshot del panel desde datos MQTT. Valores más bajos dan actualizaciones más rápidas pero usan más CPU. Aumente en hardware de baja potencia (ej., Raspberry Pi). Establezca en 0 para sin debounce. Rango: 0–15 segundos.", "enable_panel_net_energy_sensors": "Crear sensores de energía neta para flujos de energía a nivel de panel (medidor principal y derivación). Muestra el verdadero balance de energía contabilizando flujos bidireccionales.", "enable_circuit_net_energy_sensors": "Crear sensores de energía neta para circuitos individuales. Muestra el verdadero consumo de energía contabilizando potencia reactiva y flujos regenerativos.", "energy_reporting_grace_period": "Cuánto tiempo los sensores de energía mantienen su último valor conocido cuando el panel no está disponible (0-60 minutos). Ayuda a preservar la integridad de las estadísticas de energía durante cortes breves. Predeterminado: 15 minutos.", @@ -125,26 +105,6 @@ "data_description": { "directory": "Una ruta de directorio (el archivo se nombrará automáticamente) o una ruta de archivo completa que termine en .yaml donde se guardará la configuración." } - }, - "simulation_start_time": { - "title": "Tiempo de Inicio de Simulación", - "description": "Configurar el tiempo de inicio de simulación. Los cambios en el tiempo de simulación recargarán la integración para aplicar el nuevo tiempo.", - "data": { - "simulation_start_time": "Tiempo de Inicio de Simulación" - }, - "data_description": { - "simulation_start_time": "Formato de tiempo: HH:MM (24h), H:MM (12h), o datetime ISO completo (YYYY-MM-DDTHH:MM:SS). Deje vacío para usar el tiempo actual. La integración se recargará para aplicar los cambios." - } - }, - "simulation_offline_minutes": { - "title": "Minutos Sin Conexión de Simulación", - "description": "Configurar cuánto tiempo debe aparecer sin conexión el panel durante la simulación.", - "data": { - "simulation_offline_minutes": "Minutos Sin Conexión" - }, - "data_description": { - "simulation_offline_minutes": "Número de minutos que el panel debe aparecer sin conexión durante la simulación. Establezca en 0 para deshabilitar la simulación sin conexión." - } } } }, diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index cd7f8cf6..2336099e 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -10,7 +10,7 @@ "cannot_connect": "Échec de la connexion au Span Panel", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue", - "host_required": "L'hôte est requis pour les connexions de panneau en direct" + "host_required": "L'hôte est requis" }, "flow_title": "Span Panel ({host})", "step": { @@ -21,19 +21,17 @@ "user": { "data": { "host": "Host", - "simulator_mode": "Mode Simulateur", "power_display_precision": "Précision d'Affichage de Puissance", "energy_display_precision": "Précision d'Affichage d'Énergie", "enable_energy_dip_compensation": "Compenser les Baisses d'Énergie Automatiquement" }, "data_description": { - "host": "Adresse IP ou nom d'hôte du SPAN Panel (pour la simulation : utilisez un numéro de série comme 'sp3-simulation-001' ou laissez vide pour la valeur par défaut)", - "simulator_mode": "Activer le mode simulateur pour les tests (aucun matériel requis). Laissez l'hôte vide pour la simulation par défaut, ou entrez un numéro de série personnalisé.", + "host": "Adresse IP ou nom d'hôte du SPAN Panel", "power_display_precision": "Nombre de décimales pour les valeurs de puissance (0-3)", "energy_display_precision": "Nombre de décimales pour les valeurs d'énergie (0-3)", "enable_energy_dip_compensation": "Compenser automatiquement lorsque le panneau signale des lectures d'énergie inférieures, évitant les pics dans le tableau de bord énergétique." }, - "description": "Entrez les détails de connexion pour votre SPAN Panel. Pour le mode simulateur, cochez la case simulateur et entrez optionnellement un numéro de série personnalisé.", + "description": "Entrez les détails de connexion pour votre SPAN Panel.", "title": "Connectez-vous au Span Panel" }, "choose_auth_type": { @@ -63,28 +61,11 @@ "data_description": { "hop_passphrase": "Le mot de passe utilisé pour s'authentifier avec l'API v2 du SPAN Panel" } - }, - "simulator_config": { - "title": "Configuration du Simulateur", - "description": "Sélectionnez une configuration de simulation et définissez optionnellement un heure de début personnalisée. {config_count} configurations disponibles.", - "data": { - "simulation_config": "Configuration de Simulation", - "host": "Host (Numéro de Série)", - "simulation_start_time": "Heure de Début de Simulation (Optionnel)", - "entity_naming_pattern": "Modèle de Nommage des Entités" - }, - "data_description": { - "simulation_config": "Choisissez une configuration de simulation qui définit les modèles de circuit et les profils d'énergie", - "host": "Numéro de série personnalisé pour le panneau simulé (optionnel, utilisera la valeur par défaut de la configuration si vide)", - "simulation_start_time": "Format d'heure : HH:MM (24h), H:MM (12h), ou datetime ISO complet (YYYY-MM-DDTHH:MM:SS). Laissez vide pour utiliser l'heure actuelle.", - "entity_naming_pattern": "Choisissez comment les entités de circuit sont nommées dans la simulation" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "Format de date/heure invalide. Utilisez HH:MM (24h), H:MM (12h), ou datetime ISO complet (YYYY-MM-DDTHH:MM:SS)", "directory": "Le chemin du fichier est requis", "base": "Échec de l'exportation : {error}" }, @@ -94,8 +75,7 @@ "menu_options": { "general_options": "Options Générales", "export_config": "Exporter la Configuration des Capteurs Synthétiques", - "simulation_start_time": "Heure de Début de Simulation", - "simulation_offline_minutes": "Minutes Hors Ligne de Simulation" + "clone_panel_to_simulation": "Cloner le Panneau en Simulation" } }, "general_options": { @@ -109,7 +89,7 @@ "enable_energy_dip_compensation": "Compenser les Baisses d'Énergie Automatiquement" }, "data_description": { - "snapshot_update_interval": "Fréquence de reconstruction du snapshot du panneau à partir des données MQTT. Des valeurs plus basses donnent des mises à jour plus rapides mais utilisent plus de CPU. Augmentez sur du matériel peu puissant (ex., Raspberry Pi). Définissez sur 0 pour désactiver le debounce. Plage : 0\u201315 secondes.", + "snapshot_update_interval": "Fréquence de reconstruction du snapshot du panneau à partir des données MQTT. Des valeurs plus basses donnent des mises à jour plus rapides mais utilisent plus de CPU. Augmentez sur du matériel peu puissant (ex., Raspberry Pi). Définissez sur 0 pour désactiver le debounce. Plage : 0–15 secondes.", "enable_panel_net_energy_sensors": "Créer des capteurs d'énergie nette pour les flux d'énergie au niveau du panneau (compteur principal et dérivation). Affiche le vrai bilan énergétique en tenant compte des flux bidirectionnels.", "enable_circuit_net_energy_sensors": "Créer des capteurs d'énergie nette pour les circuits individuels. Affiche la vraie consommation d'énergie en tenant compte de la puissance réactive et des flux régénératifs.", "energy_reporting_grace_period": "Combien de temps les capteurs d'énergie maintiennent leur dernière valeur connue lorsque le panneau devient indisponible (0-60 minutes). Aide à préserver l'intégrité des statistiques d'énergie pendant les pannes brèves. Par défaut : 15 minutes.", @@ -125,26 +105,6 @@ "data_description": { "directory": "Un chemin de répertoire (le fichier sera nommé automatiquement) ou un chemin de fichier complet se terminant par .yaml où la configuration sera sauvegardée." } - }, - "simulation_start_time": { - "title": "Heure de Début de Simulation", - "description": "Configurer l'heure de début de simulation. Les modifications de l'heure de simulation rechargeront l'intégration pour appliquer la nouvelle heure.", - "data": { - "simulation_start_time": "Heure de Début de Simulation" - }, - "data_description": { - "simulation_start_time": "Format d'heure : HH:MM (24h), H:MM (12h), ou datetime ISO complet (YYYY-MM-DDTHH:MM:SS). Laissez vide pour utiliser l'heure actuelle. L'intégration se rechargera pour appliquer les changements." - } - }, - "simulation_offline_minutes": { - "title": "Minutes Hors Ligne de Simulation", - "description": "Configurer combien de temps le panneau doit apparaître hors ligne pendant la simulation.", - "data": { - "simulation_offline_minutes": "Minutes Hors Ligne" - }, - "data_description": { - "simulation_offline_minutes": "Nombre de minutes pendant lesquelles le panneau doit apparaître hors ligne pendant la simulation. Définissez sur 0 pour désactiver la simulation hors ligne." - } } } }, diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index db886c37..5dfcb391 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -10,7 +10,7 @@ "cannot_connect": "スパンパネルへの接続に失敗しました", "invalid_auth": "認証が無効です", "unknown": "予期しないエラー", - "host_required": "ライブパネル接続にはホストが必要です" + "host_required": "ホストが必要です" }, "flow_title": "スパンパネル ({host})", "step": { @@ -21,19 +21,17 @@ "user": { "data": { "host": "ホスト", - "simulator_mode": "シミュレーターモード", "power_display_precision": "電力表示精度", "energy_display_precision": "エネルギー表示精度", "enable_energy_dip_compensation": "エネルギー低下を自動補正" }, "data_description": { - "host": "SPAN PanelのIPアドレスまたはホスト名(シミュレーション用:'sp3-simulation-001'のようなシリアル番号を使用するか、デフォルトの場合は空にしてください)", - "simulator_mode": "テスト用のシミュレーターモードを有効にします(ハードウェア不要)。デフォルトシミュレーションの場合はホストを空にするか、カスタムシリアル番号を入力してください。", + "host": "SPAN PanelのIPアドレスまたはホスト名", "power_display_precision": "電力値の小数点以下の桁数(0-3)", "energy_display_precision": "エネルギー値の小数点以下の桁数(0-3)", "enable_energy_dip_compensation": "パネルがより低いエネルギー読み取り値を報告した場合に自動的に補正し、エネルギーダッシュボードのスパイクを防止します。" }, - "description": "SPAN Panelの接続詳細を入力してください。シミュレーターモードの場合は、シミュレーターチェックボックスをチェックし、必要に応じてカスタムシリアル番号を入力してください。", + "description": "SPAN Panelの接続詳細を入力してください。", "title": "スパンパネルに接続" }, "choose_auth_type": { @@ -63,28 +61,11 @@ "data_description": { "hop_passphrase": "SPAN Panel v2 APIでの認証に使用するパスフレーズ" } - }, - "simulator_config": { - "title": "シミュレーター設定", - "description": "シミュレーション設定を選択し、必要に応じてカスタム開始時刻を設定します。{config_count}の設定が利用可能です。", - "data": { - "simulation_config": "シミュレーション設定", - "host": "ホスト(シリアル番号)", - "simulation_start_time": "シミュレーション開始時刻(オプション)", - "entity_naming_pattern": "エンティティ命名パターン" - }, - "data_description": { - "simulation_config": "回路テンプレートとエネルギー プロファイルを定義するシミュレーション設定を選択します", - "host": "シミュレートされたパネルのカスタムシリアル番号(オプション、空の場合は設定のデフォルトを使用)", - "simulation_start_time": "時刻形式:HH:MM(24時間)、H:MM(12時間)、または完全なISO日時(YYYY-MM-DDTHH:MM:SS)。空のままにすると現在時刻を使用します。", - "entity_naming_pattern": "シミュレーションで回路エンティティの命名方法を選択します" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "日付/時刻の形式が無効です。HH:MM(24時間)、H:MM(12時間)、または完全なISO日時(YYYY-MM-DDTHH:MM:SS)を使用してください", "directory": "ファイルパスが必要です", "base": "エクスポートに失敗しました:{error}" }, @@ -94,8 +75,7 @@ "menu_options": { "general_options": "一般オプション", "export_config": "合成センサー設定をエクスポート", - "simulation_start_time": "シミュレーション開始時刻", - "simulation_offline_minutes": "シミュレーションオフラインミニッツ" + "clone_panel_to_simulation": "パネルをシミュレーションにクローン" } }, "general_options": { @@ -125,26 +105,6 @@ "data_description": { "directory": "ディレクトリパス(ファイルは自動的に名前が付けられます)または設定が保存される.yamlで終わる完全なファイルパス。" } - }, - "simulation_start_time": { - "title": "シミュレーション開始時刻", - "description": "シミュレーション開始時刻を構成します。シミュレーション時刻の変更により、新しい時刻を適用するために統合が再読み込みされます。", - "data": { - "simulation_start_time": "シミュレーション開始時刻" - }, - "data_description": { - "simulation_start_time": "時刻形式:HH:MM(24時間)、H:MM(12時間)、または完全なISO日時(YYYY-MM-DDTHH:MM:SS)。空のままにすると現在時刻を使用します。統合は変更を適用するために再読み込みされます。" - } - }, - "simulation_offline_minutes": { - "title": "シミュレーションオフラインミニッツ", - "description": "シミュレーション中にパネルがオフラインに表示される時間を構成します。", - "data": { - "simulation_offline_minutes": "オフラインミニッツ" - }, - "data_description": { - "simulation_offline_minutes": "シミュレーション中にパネルがオフラインに表示される分数。オフラインシミュレーションを無効にするには0に設定します。" - } } } }, diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index fa68694e..bc524d1c 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -10,7 +10,7 @@ "cannot_connect": "Falha ao ligar ao Painel Span", "invalid_auth": "Autenticação inválida", "unknown": "Erro inesperado", - "host_required": "O anfitrião é necessário para conexões de painel ao vivo" + "host_required": "O anfitrião é necessário" }, "flow_title": "Painel Span ({host})", "step": { @@ -21,19 +21,17 @@ "user": { "data": { "host": "Anfitrião", - "simulator_mode": "Modo Simulador", "power_display_precision": "Precisão de Exibição de Potência", "energy_display_precision": "Precisão de Exibição de Energia", "enable_energy_dip_compensation": "Compensar Quedas de Energia Automaticamente" }, "data_description": { - "host": "Endereço IP ou nome do host do SPAN Panel (para simulação: use número de série como 'sp3-simulation-001' ou deixe vazio para padrão)", - "simulator_mode": "Ativar modo simulador para testes (não requer hardware). Deixe o host vazio para simulação padrão, ou insira um número de série personalizado.", + "host": "Endereço IP ou nome do host do SPAN Panel", "power_display_precision": "Número de casas decimais para valores de potência (0-3)", "energy_display_precision": "Número de casas decimais para valores de energia (0-3)", "enable_energy_dip_compensation": "Compensar automaticamente quando o painel reporta leituras de energia mais baixas, prevenindo picos no painel de energia." }, - "description": "Insira os detalhes de conexão para seu SPAN Panel. Para modo simulador, marque a caixa do simulador e opcionalmente insira um número de série personalizado.", + "description": "Insira os detalhes de conexão para seu SPAN Panel.", "title": "Ligar ao Painel Span" }, "choose_auth_type": { @@ -63,28 +61,11 @@ "data_description": { "hop_passphrase": "A frase-passe utilizada para autenticar com a API v2 do SPAN Panel" } - }, - "simulator_config": { - "title": "Configuração do Simulador", - "description": "Selecione uma configuração de simulação e opcionalmente defina um horário de início personalizado. {config_count} configurações disponíveis.", - "data": { - "simulation_config": "Configuração de Simulação", - "host": "Anfitrião (Número de Série)", - "simulation_start_time": "Horário de Início da Simulação (Opcional)", - "entity_naming_pattern": "Padrão de Nomenclatura de Entidades" - }, - "data_description": { - "simulation_config": "Escolha uma configuração de simulação que define modelos de circuito e perfis de energia", - "host": "Número de série personalizado para o painel simulado (opcional, usará o padrão da configuração se vazio)", - "simulation_start_time": "Formato de horário: HH:MM (24h), H:MM (12h), ou datetime ISO completo (YYYY-MM-DDTHH:MM:SS). Deixe vazio para usar o horário atual.", - "entity_naming_pattern": "Escolha como as entidades de circuito são nomeadas na simulação" - } } } }, "options": { "error": { - "Invalid date/time format. Use ISO format (YYYY-MM-DDTHH:MM:SS)": "Formato de data/hora inválido. Use HH:MM (24h), H:MM (12h), ou datetime ISO completo (YYYY-MM-DDTHH:MM:SS)", "directory": "O caminho do arquivo é necessário", "base": "Falha na exportação: {error}" }, @@ -94,8 +75,7 @@ "menu_options": { "general_options": "Opções Gerais", "export_config": "Exportar Configuração de Sensores Sintéticos", - "simulation_start_time": "Horário de Início da Simulação", - "simulation_offline_minutes": "Minutos Offline da Simulação" + "clone_panel_to_simulation": "Clonar Painel para Simulação" } }, "general_options": { @@ -109,7 +89,7 @@ "enable_energy_dip_compensation": "Compensar Quedas de Energia Automaticamente" }, "data_description": { - "snapshot_update_interval": "Com que frequência reconstruir o snapshot do painel a partir de dados MQTT. Valores mais baixos dão atualizações mais rápidas mas usam mais CPU. Aumente em hardware de baixa potência (ex., Raspberry Pi). Defina como 0 para sem debounce. Intervalo: 0\u201315 segundos.", + "snapshot_update_interval": "Com que frequência reconstruir o snapshot do painel a partir de dados MQTT. Valores mais baixos dão atualizações mais rápidas mas usam mais CPU. Aumente em hardware de baixa potência (ex., Raspberry Pi). Defina como 0 para sem debounce. Intervalo: 0–15 segundos.", "enable_panel_net_energy_sensors": "Criar sensores de energia líquida para fluxos de energia no nível do painel (medidor principal e derivação). Mostra o verdadeiro balanço energético contabilizando fluxos bidirecionais.", "enable_circuit_net_energy_sensors": "Criar sensores de energia líquida para circuitos individuais. Mostra o verdadeiro consumo de energia contabilizando potência reativa e fluxos regenerativos.", "energy_reporting_grace_period": "Quanto tempo os sensores de energia mantêm seu último valor conhecido quando o painel fica indisponível (0-60 minutos). Ajuda a preservar a integridade das estatísticas de energia durante interrupções breves. Padrão: 15 minutos.", @@ -125,26 +105,6 @@ "data_description": { "directory": "Um caminho de diretório (o arquivo será nomeado automaticamente) ou um caminho de arquivo completo terminando em .yaml onde a configuração será salva." } - }, - "simulation_start_time": { - "title": "Horário de Início da Simulação", - "description": "Configurar o horário de início da simulação. Mudanças no horário de simulação recarregarão a integração para aplicar o novo horário.", - "data": { - "simulation_start_time": "Horário de Início da Simulação" - }, - "data_description": { - "simulation_start_time": "Formato de horário: HH:MM (24h), H:MM (12h), ou datetime ISO completo (YYYY-MM-DDTHH:MM:SS). Deixe vazio para usar o horário atual. A integração será recarregada para aplicar as mudanças." - } - }, - "simulation_offline_minutes": { - "title": "Minutos Offline da Simulação", - "description": "Configurar por quanto tempo o painel deve aparecer offline durante a simulação.", - "data": { - "simulation_offline_minutes": "Minutos Offline" - }, - "data_description": { - "simulation_offline_minutes": "Número de minutos que o painel deve aparecer offline durante a simulação. Defina como 0 para desabilitar a simulação offline." - } } } }, diff --git a/custom_components/span_panel/util.py b/custom_components/span_panel/util.py index 8bab46ec..a42ce0de 100644 --- a/custom_components/span_panel/util.py +++ b/custom_components/span_panel/util.py @@ -3,7 +3,6 @@ import logging from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.util import slugify from span_panel_api import SpanBatterySnapshot, SpanEvseSnapshot, SpanPanelSnapshot from .const import DOMAIN @@ -14,24 +13,13 @@ def snapshot_to_device_info( snapshot: SpanPanelSnapshot, device_name: str | None = None, - is_simulator: bool = False, host: str | None = None, ) -> DeviceInfo: - """Convert a SpanPanelSnapshot to a Home Assistant device info object. - - For simulator entries, use a per-entry identifier derived from the device name - so multiple simulators don't collapse into a single device in the registry. - Live panels continue to use the true serial number identifier. - """ - if is_simulator and device_name: - device_identifier = slugify(device_name) - else: - device_identifier = snapshot.serial_number - + """Convert a SpanPanelSnapshot to a Home Assistant device info object.""" configuration_url = f"http://{host}" if host else None return DeviceInfo( - identifiers={(DOMAIN, device_identifier)}, + identifiers={(DOMAIN, snapshot.serial_number)}, manufacturer="Span", model="SPAN Panel", name=device_name or "Span Panel", diff --git a/tests/conftest.py b/tests/conftest.py index ac61f518..fb98d6f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ """Configure test framework.""" import logging -import os from pathlib import Path import sys import types @@ -9,10 +8,8 @@ import pytest -from tests.test_factories.span_panel_simulation_factory import SpanPanelSimulationFactory - # The real span_panel_api library is used directly (no sys.modules mocking). -# Individual tests mock SpanMqttClient / DynamicSimulationEngine as needed. +# Individual tests mock SpanMqttClient as needed. @pytest.fixture(autouse=True) @@ -68,14 +65,6 @@ def patch_frontend_and_panel_custom(): yield -@pytest.fixture -async def baseline_serial_number(): - """Fixture to provide the serial number from the baseline YAML (friendly_names.yaml).""" - fixtures_dir = os.path.join(os.path.dirname(__file__), "fixtures") - baseline_path = os.path.join(fixtures_dir, "friendly_names.yaml") - return await SpanPanelSimulationFactory.extract_serial_number_from_yaml(baseline_path) - - @pytest.fixture def async_add_entities(): """Mock async_add_entities callback for testing.""" diff --git a/tests/docs/simulation_test_documentation.md b/tests/docs/simulation_test_documentation.md deleted file mode 100644 index c37b62cc..00000000 --- a/tests/docs/simulation_test_documentation.md +++ /dev/null @@ -1,188 +0,0 @@ -# SPAN Panel Simulation Testing Documentation - -## Overview - -The SPAN Panel integration includes a simulation testing mode that allows tests to run against real simulation data instead of mocked responses. This provides -more accurate testing by using the actual SpanPanelApi library with a built-in simulator. - -## Solution: CI-Friendly Simulation Tests - -### Problem Solved - -The main challenge was that simulation tests require `SPAN_USE_REAL_SIMULATION=1` to be set **before** the test module imports, but this made it difficult to -integrate into CI where you want regular `pytest` to work without environment variables. - -### Solution: Module-Level Skip - -Simulation tests now automatically skip when the environment variable isn't set: - -```python -import os -import pytest - -# Skip this test if SPAN_USE_REAL_SIMULATION is not set externally -if not os.environ.get('SPAN_USE_REAL_SIMULATION', '').lower() in ('1', 'true', 'yes'): - pytest.skip("Simulation tests require SPAN_USE_REAL_SIMULATION=1", allow_module_level=True) - -os.environ['SPAN_USE_REAL_SIMULATION'] = '1' -``` - -## How It Works - -### Regular Test Runs (Default) - -```bash -# Regular pytest - simulation tests are automatically skipped -pytest tests/ -pytest tests/test_solar_configuration_with_simulator.py # Shows "SKIPPED" -``` - -### Simulation Test Runs - -```bash -# Run simulation tests with environment variable -SPAN_USE_REAL_SIMULATION=1 pytest tests/test_solar_configuration_with_simulator.py -v - -# Clean output (reduced YAML noise) -SPAN_USE_REAL_SIMULATION=1 python -m pytest tests/test_solar_configuration_with_simulator.py::test_solar_configuration_with_simulator_friendly_names -v -``` - -### Mock vs Simulation Architecture - -#### Regular Tests (Default) - -- `tests/conftest.py` checks for `SPAN_USE_REAL_SIMULATION` environment variable -- If not set, installs mock modules for `span_panel_api` and `span_panel_api.exceptions` -- All API calls are mocked with predefined responses -- Fast execution but limited real-world accuracy - -#### Simulation Tests (With Environment Variable) - -- Environment variable set before module imports -- `tests/conftest.py` sees the variable and skips mock installation -- Real `span_panel_api` library is imported and used -- Integration connects to built-in simulator with realistic data - -## Reduced Logging Output - -The simulation test includes automatic logging configuration to reduce YAML and other verbose output: - -```python -# Configure logging to reduce noise BEFORE other imports -import logging -logging.getLogger("homeassistant.core").setLevel(logging.WARNING) -logging.getLogger("homeassistant.loader").setLevel(logging.WARNING) -logging.getLogger("homeassistant.setup").setLevel(logging.WARNING) -logging.getLogger("homeassistant.components").setLevel(logging.WARNING) -logging.getLogger("aiohttp").setLevel(logging.WARNING) -logging.getLogger("urllib3").setLevel(logging.WARNING) -logging.getLogger("yaml").setLevel(logging.WARNING) -logging.getLogger("homeassistant.helpers").setLevel(logging.WARNING) -logging.getLogger("homeassistant.config_entries").setLevel(logging.WARNING) - -# Keep our own logs visible for debugging -logging.getLogger("custom_components.span_panel").setLevel(logging.INFO) -logging.getLogger("ha_synthetic_sensors").setLevel(logging.INFO) -``` - -## CI Integration - -### GitHub Actions Example - -```yaml -# Regular tests - simulation tests are automatically skipped -- name: Run tests with coverage - run: poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=xml - -# Optional: Run simulation tests separately -- name: Run simulation tests - env: - SPAN_USE_REAL_SIMULATION: 1 - run: poetry run pytest tests/test_solar_configuration_with_simulator.py -v -``` - -### Advantages for CI/CD - -1. **No Configuration Required**: Regular `pytest` just works -2. **Automatic Skipping**: Simulation tests skip gracefully without environment setup -3. **Optional Simulation**: Can run simulation tests in separate CI job if desired -4. **Clean Output**: Reduced logging noise for better CI readability -5. **Flexible**: Can run all tests together or separately - -## Solar Configuration Testing - -### Test Flow for Solar Sensors - -The solar configuration test follows this specific sequence: - -1. **Initial Setup**: Integration loads with native sensors only -2. **Options Change**: Trigger solar configuration to create solar sensors -3. **Reload**: Integration reloads to activate the new solar sensors -4. **Verification**: Solar sensors are verified in the entity registry - -### Why This Sequence is Necessary - -Solar sensors are created via the options flow (when users change settings in the UI), not during initial integration setup. The test simulates this by: - -1. Setting up the integration normally -2. Manually calling `handle_solar_options_change()` -3. Reloading the integration to activate the new sensors - -## Available Simulation Data - -The built-in simulator provides: - -- **Circuits**: Realistic circuit data including tabs 30 and 32 used for solar testing -- **Power Data**: Simulated power consumption and generation values -- **Device Info**: Realistic device metadata and status information -- **Unmapped Tabs**: Circuits not assigned to specific loads (essential for solar) - -## Running Simulation Tests - -### Local Development - -```bash -# Check test status without running -pytest tests/test_solar_configuration_with_simulator.py -v -# Output: SKIPPED [1] Simulation tests require SPAN_USE_REAL_SIMULATION=1 - -# Run simulation tests -SPAN_USE_REAL_SIMULATION=1 pytest tests/test_solar_configuration_with_simulator.py -v - -# Run specific test with clean output -SPAN_USE_REAL_SIMULATION=1 python -m pytest tests/test_solar_configuration_with_simulator.py::test_solar_configuration_with_simulator_friendly_names -v -``` - -### CI/CD Pipeline - -```bash -# Regular tests (simulation automatically skipped) -pytest tests/ --cov=custom_components/span_panel - -# Simulation tests (separate job) -SPAN_USE_REAL_SIMULATION=1 pytest tests/test_solar_configuration_with_simulator.py -v -``` - -## Alternative: Simulation Directory Approach - -We also created an alternative approach with a dedicated `tests/simulation/` directory that has its own `conftest.py`. This approach works but has some fixture -complexity. The module-level skip approach is simpler and more reliable. - -## Advantages of Current Solution - -1. **CI-Friendly**: Works without external environment variables -2. **Automatic**: Simulation tests skip gracefully when environment not set -3. **Clean Output**: Logging configuration reduces YAML noise -4. **Flexible**: Can run regular and simulation tests separately or together -5. **Simple**: Single test file approach is easier to maintain - -## Future Improvements - -The simulation testing mechanism provides a foundation for: - -- Testing edge cases with realistic data -- Validating complex solar configurations -- Testing error recovery scenarios -- Performance testing with large circuit counts - -This documentation should be updated as the simulation capabilities expand. diff --git a/tests/providers/integration_data_provider.py b/tests/providers/integration_data_provider.py deleted file mode 100644 index d1730426..00000000 --- a/tests/providers/integration_data_provider.py +++ /dev/null @@ -1,229 +0,0 @@ -"""Integration Data Provider for bridging simulation data with SPAN Panel integration. - -This module provides data using the integration's actual processing logic, -ensuring all data flows through the same code paths as production. -""" - -from typing import Any -from unittest.mock import MagicMock - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from custom_components.span_panel.coordinator import SpanPanelCoordinator -from tests.factories.span_panel_simulation_factory import SpanPanelSimulationFactory - - -class IntegrationDataProvider: - """Provides data using integration's actual processing logic.""" - - def __init__(self): - """Initialize the data provider.""" - self._simulation_factory = SpanPanelSimulationFactory() - - async def create_coordinator_with_simulation_data( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - simulation_variations: dict | None = None, - scenario_name: str | None = None - ) -> SpanPanelCoordinator: - """Create coordinator with simulation data processed through integration. - - Args: - hass: Home Assistant instance - config_entry: Configuration entry with naming flags - simulation_variations: Direct simulation variations to apply - scenario_name: Named scenario from simulation factory - - Returns: - SpanPanelCoordinator with realistic simulation data - - """ - # Get simulation data - if scenario_name: - sim_data = await self._simulation_factory.get_panel_data_for_scenario(scenario_name) - else: - sim_data = await self._simulation_factory.get_realistic_panel_data( - variations=simulation_variations - ) - - # Create a mock span panel client that returns simulation data - mock_span_panel = self._create_mock_span_panel_from_sim_data(sim_data) - - # Create coordinator using integration's actual initialization - coordinator = SpanPanelCoordinator(hass, mock_span_panel, config_entry) - - # Set data as if it came from real API calls - coordinator.data = mock_span_panel - - return coordinator - - def _create_mock_span_panel_from_sim_data(self, sim_data: dict[str, Any]) -> MagicMock: - """Convert simulation data to coordinator's expected format. - - This ensures the data structure matches exactly what the coordinator expects - by using the same attribute names and structure as the real SpanPanel client. - - Args: - sim_data: Dictionary containing simulation data from span-panel-api - - Returns: - MagicMock configured to match SpanPanel client interface - - """ - mock_panel = MagicMock() - - # Extract data from simulation response - circuits_data = sim_data['circuits'] - panel_state = sim_data['panel_state'] - status_data = sim_data['status'] - storage_data = sim_data['storage'] - - # Set panel-level attributes from panel_state - mock_panel.id = getattr(panel_state, 'panel_id', 'test_panel_123') - mock_panel.name = "Simulation Panel" - mock_panel.model = getattr(panel_state, 'panel_model', '32A') - mock_panel.firmware_version = getattr(panel_state, 'firmware_version', '1.2.3') - mock_panel.main_breaker_size = 200 - - # Power and energy data from panel_state - mock_panel.instant_grid_power_w = getattr(panel_state, 'instant_grid_power_w', 0) - mock_panel.instant_load_power_w = getattr(panel_state, 'instant_load_power_w', 0) - mock_panel.instant_production_power_w = getattr(panel_state, 'instant_production_power_w', 0) - mock_panel.feedthrough_power = getattr(panel_state, 'feedthrough_power_w', 0) - - # Environmental data - mock_panel.env_temp_c = getattr(panel_state, 'env_temp_c', 25.0) - mock_panel.uptime_s = getattr(panel_state, 'uptime_s', 86400) - - # Status data - mock_panel.door_state = getattr(status_data, 'door_state', 'CLOSED') - mock_panel.main_relay_state = getattr(status_data, 'main_relay_state', 'CLOSED') - - # DSM data from panel_state - mock_panel.dsmCurrentRms = getattr(panel_state, 'dsm_current_rms', [120.5, 118.3]) - mock_panel.dsmVoltageRms = getattr(panel_state, 'dsm_voltage_rms', [245.6, 244.1]) - mock_panel.grid_sample_start_ms = getattr(panel_state, 'grid_sample_start_ms', 1234567890123) - - # Convert circuits data to the format the integration expects - mock_circuits = [] - for _circuit_id, circuit_data in circuits_data.circuits.additional_properties.items(): - circuit_mock = MagicMock() - circuit_mock.id = circuit_data.id - circuit_mock.name = circuit_data.name - circuit_mock.instant_power_w = circuit_data.instant_power_w - circuit_mock.produced_energy_wh = circuit_data.produced_energy_wh - circuit_mock.consumed_energy_wh = circuit_data.consumed_energy_wh - circuit_mock.relay_state = circuit_data.relay_state - circuit_mock.priority = circuit_data.priority - circuit_mock.tabs = circuit_data.tabs - circuit_mock.is_user_controllable = circuit_data.is_user_controllable - circuit_mock.is_sheddable = circuit_data.is_sheddable - circuit_mock.is_never_backup = circuit_data.is_never_backup - - # Add properties that integration might expect - circuit_mock.is_main = circuit_data.id == "main" or "main" in circuit_data.name.lower() - circuit_mock.breaker_size = 20 # Default breaker size - - mock_circuits.append(circuit_mock) - - mock_panel.circuits = mock_circuits - - # Storage/battery data if available - if storage_data: - mock_panel.battery_soe = getattr(storage_data, 'soe', 0.5) - mock_panel.max_energy_kwh = getattr(storage_data, 'max_energy_kwh', 10.0) - - return mock_panel - - async def create_config_entry_with_flags( - self, - naming_flags: dict[str, Any], - entry_id: str = "test_entry_id" - ) -> ConfigEntry: - """Create a config entry with specific naming flags. - - Args: - naming_flags: Dictionary of naming configuration flags - entry_id: Unique identifier for the config entry - - Returns: - ConfigEntry configured with the specified flags - - """ - # Import here to avoid circular imports - from tests.common import create_mock_config_entry - - config_entry = create_mock_config_entry() - config_entry.entry_id = entry_id - config_entry.options = { - **config_entry.options, - **naming_flags - } - - return config_entry - - async def create_full_integration_setup( - self, - hass: HomeAssistant, - naming_flags: dict[str, Any], - simulation_variations: dict | None = None, - scenario_name: str | None = None - ) -> tuple[SpanPanelCoordinator, ConfigEntry]: - """Create a complete integration setup with simulation data. - - Args: - hass: Home Assistant instance - naming_flags: Entity naming configuration flags - simulation_variations: Direct simulation variations - scenario_name: Named simulation scenario - - Returns: - Tuple of (coordinator, config_entry) ready for testing - - """ - # Create config entry with naming flags - config_entry = await self.create_config_entry_with_flags(naming_flags) - - # Create coordinator with simulation data - coordinator = await self.create_coordinator_with_simulation_data( - hass, - config_entry, - simulation_variations=simulation_variations, - scenario_name=scenario_name - ) - - return coordinator, config_entry - - async def get_circuit_data_for_naming_tests( - self, - circuit_types: list[str] | None = None - ) -> dict[str, dict]: - """Get circuit data specifically formatted for naming pattern tests. - - Args: - circuit_types: List of circuit types to include (lights, ev_chargers, etc.) - - Returns: - Dictionary with circuit data formatted for naming tests - - """ - # Get all circuit details - circuit_details = await self._simulation_factory.get_circuit_details() - - if circuit_types: - # Filter by circuit types - circuit_ids_by_type = await self._simulation_factory.get_circuit_ids_by_type() - included_ids = set() - for circuit_type in circuit_types: - if circuit_type in circuit_ids_by_type: - included_ids.update(circuit_ids_by_type[circuit_type]) - - circuit_details = { - circuit_id: details - for circuit_id, details in circuit_details.items() - if circuit_id in included_ids - } - - return circuit_details diff --git a/tests/test_dps_and_bess.py b/tests/test_dps_and_bess.py index 4ee90299..12d26770 100644 --- a/tests/test_dps_and_bess.py +++ b/tests/test_dps_and_bess.py @@ -128,8 +128,8 @@ async def test_grid_button_press_publishes_grid(self) -> None: coordinator.async_request_refresh.assert_called_once() @pytest.mark.asyncio - async def test_button_simulation_mode(self) -> None: - """Simulation mode (no set_dominant_power_source method) logs warning.""" + async def test_button_missing_control_method(self) -> None: + """Client without set_dominant_power_source method logs warning.""" from custom_components.span_panel.button import ( GFE_OVERRIDE_DESCRIPTION, SpanPanelGFEOverrideButton, @@ -141,7 +141,7 @@ async def test_button_simulation_mode(self) -> None: ) button.hass = MagicMock() - # Simulation client without set_dominant_power_source + # Client without set_dominant_power_source coordinator.client = MagicMock(spec=[]) await button.async_press() diff --git a/tests/test_factories/span_panel_simulation_factory.py b/tests/test_factories/span_panel_simulation_factory.py deleted file mode 100644 index ac3038f0..00000000 --- a/tests/test_factories/span_panel_simulation_factory.py +++ /dev/null @@ -1,426 +0,0 @@ -"""SPAN Panel Simulation Factory for realistic test data generation. - -This factory leverages the span-panel-api simulation mode with YAML configurations -to generate realistic SPAN panel data that exactly matches what the integration expects, -using actual SPAN panel response structures. -""" - -from __future__ import annotations - -import asyncio -import os -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import yaml - -if TYPE_CHECKING or os.environ.get("SPAN_USE_REAL_SIMULATION", "").lower() in ( - "1", - "true", - "yes", -): - from span_panel_api import SpanPanelClient - - -class SpanPanelSimulationFactory: - """Factory for creating simulation-based SPAN panel data using YAML configurations.""" - - @classmethod - async def _get_config_path(cls, config_name: str = "simulation_config_32_circuit") -> str: - """Get path to a simulation configuration file. - - Args: - config_name: Name of the config file (without .yaml extension) - - Returns: - Full path to the configuration file - - """ - # Look for config in the integration's simulation_configs directory - current_file = Path(__file__) - integration_root = current_file.parent.parent.parent / "custom_components" / "span_panel" - config_path = integration_root / "simulation_configs" / f"{config_name}.yaml" - - if await asyncio.to_thread(config_path.exists): - return str(config_path) - - # Fallback: look in span-panel-api examples - span_api_examples = current_file.parent.parent.parent.parent / "span-panel-api" / "examples" - fallback_path = span_api_examples / f"{config_name}.yaml" - - if await asyncio.to_thread(fallback_path.exists): - return str(fallback_path) - - raise FileNotFoundError(f"Could not find simulation config: {config_name}.yaml") - - @classmethod - async def create_simulation_client( - cls, - host: str = "test-panel-001", - config_name: str = "simulation_config_32_circuit", - **kwargs: Any - ) -> SpanPanelClient: - """Create a simulation client with YAML-based realistic data. - - Args: - host: Host identifier (becomes serial number in simulation mode) - config_name: Name of the YAML config file to use - **kwargs: Additional client configuration parameters - - Returns: - SpanPanelClient configured for simulation mode with YAML config - - """ - config_path = await cls._get_config_path(config_name) - - return SpanPanelClient( - host=host, - simulation_mode=True, - simulation_config_path=config_path, - **kwargs - ) - - @classmethod - async def get_realistic_panel_data( - cls, - host: str = "test-panel-001", - config_name: str = "simulation_config_32_circuit", - circuit_overrides: dict[str, dict] | None = None, - global_overrides: dict[str, Any] | None = None, - ) -> dict[str, Any]: - """Get panel data using YAML-based simulation mode. - - Args: - host: Host identifier for the simulated panel - config_name: Name of the YAML config file to use - circuit_overrides: Per-circuit overrides to apply dynamically - global_overrides: Global overrides (e.g., power_multiplier) - - Returns: - Dictionary containing all panel data types the integration needs - - """ - client = await cls.create_simulation_client(host=host, config_name=config_name) - async with client: - # Apply any dynamic overrides if specified - if circuit_overrides or global_overrides: - await client.set_circuit_overrides( - circuit_overrides=circuit_overrides or {}, - global_overrides=global_overrides or {} - ) - - # Get all data types the integration needs - circuits = await client.get_circuits() - panel_state = await client.get_panel_state() - status = await client.get_status() - storage = await client.get_storage_soe() - - return { - 'circuits': circuits, - 'panel_state': panel_state, - 'status': status, - 'storage': storage - } - - @classmethod - async def get_realistic_circuits_only( - cls, - host: str = "test-circuits-001", - config_name: str = "simulation_config_32_circuit", - circuit_overrides: dict[str, dict] | None = None, - global_overrides: dict[str, Any] | None = None, - ) -> dict[str, Any]: - """Get only circuits data for tests that don't need full panel state. - - Args: - host: Host identifier for the simulated panel - config_name: Name of the YAML config file to use - circuit_overrides: Per-circuit overrides to apply dynamically - global_overrides: Global overrides (e.g., power_multiplier) - - Returns: - CircuitsOut object from simulation - - """ - client = await cls.create_simulation_client(host=host, config_name=config_name) - async with client: - # Apply any dynamic overrides if specified - if circuit_overrides or global_overrides: - await client.set_circuit_overrides( - circuit_overrides=circuit_overrides or {}, - global_overrides=global_overrides or {} - ) - - circuits = await client.get_circuits() - return dict(circuits) if circuits else {} - - @classmethod - def get_preset_scenarios(cls) -> dict[str, dict[str, Any]]: - """Get predefined simulation scenarios for common test cases. - - Returns: - Dictionary of scenario names to simulation parameters - - """ - return { - "normal_operation": { - "config_name": "simulation_config_32_circuit", - "global_overrides": {} - }, - "high_load": { - "config_name": "simulation_config_32_circuit", - "global_overrides": {"power_multiplier": 1.5}, - "circuit_overrides": { - "ev_charger_garage": { - "power_override": 11000.0, # Max EV charging - "relay_state": "CLOSED" - } - } - }, - "circuit_failures": { - "config_name": "simulation_config_32_circuit", - "circuit_overrides": { - "living_room_outlets": {"relay_state": "OPEN"}, - "office_outlets": {"relay_state": "OPEN"} - } - }, - "low_power_stable": { - "config_name": "simple_test_config", # Use simpler config - "global_overrides": {"power_multiplier": 0.3} - }, - "solar_peak": { - "config_name": "simulation_config_32_circuit", - "circuit_overrides": { - "solar_inverter_main": { - "power_override": -8000.0, # Peak solar production - "relay_state": "CLOSED" - } - } - }, - "grid_stress": { - "config_name": "simulation_config_32_circuit", - "global_overrides": {"power_multiplier": 2.0}, - "circuit_overrides": { - "main_hvac": {"relay_state": "OPEN"}, # Load shedding - "heat_pump_backup": {"relay_state": "OPEN"} - } - } - } - - @classmethod - async def get_panel_data_for_scenario(cls, scenario_name: str) -> dict[str, Any]: - """Get panel data for a predefined scenario. - - Args: - scenario_name: Name of the scenario from get_preset_scenarios() - - Returns: - Panel data configured for the specified scenario - - Raises: - ValueError: If scenario_name is not found - - """ - scenarios = cls.get_preset_scenarios() - if scenario_name not in scenarios: - available = ", ".join(scenarios.keys()) - raise ValueError(f"Unknown scenario '{scenario_name}'. Available: {available}") - - scenario_config = scenarios[scenario_name] - return await cls.get_realistic_panel_data(**scenario_config) - - @classmethod - async def get_real_circuit_ids(cls, config_name: str = "simulation_config_32_circuit") -> dict[str, str]: - """Get the actual circuit IDs from YAML simulation config with their names. - - Args: - config_name: Name of the YAML config file to use - - Returns: - Dictionary mapping circuit IDs to their friendly names - - """ - client = await cls.create_simulation_client(config_name=config_name) - async with client: - circuits = await client.get_circuits() - return { - circuit_id: circuit.name - for circuit_id, circuit in circuits.circuits.additional_properties.items() - } - - @classmethod - async def get_circuit_ids_by_type(cls, config_name: str = "simulation_config_32_circuit") -> dict[str, list[str]]: - """Get circuit IDs grouped by appliance type for targeted testing. - - Args: - config_name: Name of the YAML config file to use - - Returns: - Dictionary mapping appliance types to lists of circuit IDs - - """ - # Get all circuits dynamically from the YAML config - circuit_ids = await cls.get_real_circuit_ids(config_name) - - # Categorize circuits based on their names - categorized: dict[str, list[str]] = { - "lights": [], - "ev_chargers": [], - "hvac": [], - "appliances": [], - "outlets": [], - "solar": [], - "pool": [], - "essential": [] - } - - for circuit_id, name in circuit_ids.items(): - name_lower = name.lower() - - # Categorize based on circuit names from YAML config - if "light" in name_lower: - categorized["lights"].append(circuit_id) - elif "ev" in name_lower or "charger" in name_lower: - categorized["ev_chargers"].append(circuit_id) - elif any(term in name_lower for term in ["hvac", "heat pump"]): - categorized["hvac"].append(circuit_id) - elif any(term in name_lower for term in ["dishwasher", "dryer", "microwave", "oven", "refrigerator", "washing"]): - categorized["appliances"].append(circuit_id) - elif "outlet" in name_lower: - categorized["outlets"].append(circuit_id) - elif "solar" in name_lower or "inverter" in name_lower: - categorized["solar"].append(circuit_id) - elif "pool" in name_lower: - categorized["pool"].append(circuit_id) - elif any(term in name_lower for term in ["master", "bedroom", "kitchen", "bathroom"]): - categorized["essential"].append(circuit_id) - else: - # Default to essential for unrecognized circuits - categorized["essential"].append(circuit_id) - - return categorized - - @classmethod - async def find_circuit_ids_by_name( - cls, - name_patterns: str | list[str], - config_name: str = "simulation_config_32_circuit" - ) -> list[str]: - """Find circuit IDs by name patterns. - - Args: - name_patterns: String or list of strings to search for in circuit names (case-insensitive) - config_name: Name of the YAML config file to use - - Returns: - List of circuit IDs matching the patterns - - """ - if isinstance(name_patterns, str): - name_patterns = [name_patterns] - - circuit_ids = await cls.get_real_circuit_ids(config_name) - matching_ids = [] - - for circuit_id, name in circuit_ids.items(): - name_lower = name.lower() - if any(pattern.lower() in name_lower for pattern in name_patterns): - matching_ids.append(circuit_id) - - return matching_ids - - @classmethod - async def get_circuit_details(cls, config_name: str = "simulation_config_32_circuit") -> dict[str, dict[str, Any]]: - """Get detailed information about all circuits from YAML simulation. - - Args: - config_name: Name of the YAML config file to use - - Returns: - Dictionary mapping circuit IDs to their full circuit data - - """ - client = await cls.create_simulation_client(config_name=config_name) - async with client: - circuits = await client.get_circuits() - return { - circuit_id: { - "id": circuit.id, - "name": circuit.name, - "relay_state": circuit.relay_state, - "instant_power_w": circuit.instant_power_w, - "produced_energy_wh": circuit.produced_energy_wh, - "consumed_energy_wh": circuit.consumed_energy_wh, - "tabs": circuit.tabs, - "priority": circuit.priority, - "is_user_controllable": circuit.is_user_controllable, - "is_sheddable": circuit.is_sheddable, - "is_never_backup": circuit.is_never_backup, - } - for circuit_id, circuit in circuits.circuits.additional_properties.items() - } - - @classmethod - async def get_available_configs(cls) -> list[str]: - """Get list of available YAML configuration files. - - Returns: - List of config names (without .yaml extension) - - """ - configs = [] - - # Check integration configs first (this is the primary location) - try: - current_file = Path(__file__) - integration_root = current_file.parent.parent.parent / "custom_components" / "span_panel" - config_dir = integration_root / "simulation_configs" - - if await asyncio.to_thread(config_dir.exists): - files = await asyncio.to_thread(lambda: list(config_dir.glob("*.yaml"))) - for file in files: - configs.append(file.stem) - except Exception: - pass - - # Check span-panel-api examples as fallback - try: - current_file = Path(__file__) - span_api_examples = current_file.parent.parent.parent.parent / "span-panel-api" / "examples" - - if await asyncio.to_thread(span_api_examples.exists): - files = await asyncio.to_thread(lambda: list(span_api_examples.glob("*.yaml"))) - for file in files: - if file.stem not in configs: # Avoid duplicates - configs.append(file.stem) - except Exception: - pass - - return sorted(configs) - - @classmethod - def get_available_configs_with_names(cls) -> dict[str, str]: - """Get available configs with user-friendly display names. - - Returns: - Dictionary mapping config keys to display names - - """ - # Use the same logic as the config flow - from custom_components.span_panel.config_flow import get_available_simulation_configs - return get_available_simulation_configs() - - @staticmethod - def extract_serial_number_from_yaml(yaml_path: str) -> str: - """Extract the serial number from a YAML simulation config file. - - Args: - yaml_path: Path to the YAML configuration file - - Returns: - Serial number from the config file - - """ - content = Path(yaml_path).read_text(encoding="utf-8") - data = yaml.safe_load(content) - return str(data["global_settings"]["device_identifier"]) diff --git a/tests/test_v2_config_flow.py b/tests/test_v2_config_flow.py index 7278eb20..84ffb19d 100644 --- a/tests/test_v2_config_flow.py +++ b/tests/test_v2_config_flow.py @@ -92,7 +92,7 @@ async def test_user_flow_detects_v2_and_shows_auth_choice(hass: HomeAssistant) - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) assert result2["type"] == FlowResultType.MENU @@ -120,7 +120,7 @@ async def test_user_flow_v1_aborts(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) # v1 panels should go through setup_flow which detects v1 @@ -155,7 +155,7 @@ async def test_passphrase_auth_success(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) assert result2["step_id"] == "choose_v2_auth" @@ -198,7 +198,7 @@ async def test_passphrase_auth_bad_passphrase(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) # Select passphrase auth from the menu @@ -240,7 +240,7 @@ async def test_passphrase_auth_connection_error(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) # Select passphrase auth from the menu @@ -286,7 +286,7 @@ async def test_v2_entry_contains_mqtt_credentials(hass: HomeAssistant) -> None: # Step 1: submit host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) # Step 2: choose auth method (passphrase) @@ -356,7 +356,7 @@ async def test_migration_v2_to_v3_live_panel(hass: HomeAssistant) -> None: result = await async_migrate_entry(hass, entry) assert result is True - assert entry.version == 5 + assert entry.version == 6 assert entry.data.get(CONF_API_VERSION) == "v1" @@ -421,18 +421,18 @@ async def test_migration_blocked_when_panel_unreachable(hass: HomeAssistant) -> @pytest.mark.asyncio -async def test_migration_v2_to_v4_simulator(hass: HomeAssistant) -> None: - """Simulator entries migrating from version 2 to 5 should get api_version=simulation.""" +async def test_migration_v5_to_v6_rejects_simulation_entry(hass: HomeAssistant) -> None: + """Simulation entries at v5 should be rejected by v5→v6 migration.""" entry = MockConfigEntry( - version=2, + version=5, minor_version=1, domain=DOMAIN, title="Span Simulator", data={ CONF_HOST: "sim-001", CONF_ACCESS_TOKEN: "simulator_token", + CONF_API_VERSION: "simulation", "simulation_mode": True, - "simulation_config": "simulation_config_32_circuit", }, source=config_entries.SOURCE_USER, options={}, @@ -440,17 +440,11 @@ async def test_migration_v2_to_v4_simulator(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "custom_components.span_panel.migrate_config_entry_sensors", - return_value=True, - ): - from custom_components.span_panel import async_migrate_entry + from custom_components.span_panel import async_migrate_entry - result = await async_migrate_entry(hass, entry) + result = await async_migrate_entry(hass, entry) - assert result is True - assert entry.version == 5 - assert entry.data.get(CONF_API_VERSION) == "simulation" + assert result is False # ---------- zeroconf v2 discovery ---------- @@ -593,7 +587,7 @@ async def test_user_flow_empty_host(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "", "simulator_mode": False}, + {CONF_HOST: ""}, ) assert result2["type"] == FlowResultType.FORM @@ -614,7 +608,7 @@ async def test_user_flow_host_unreachable(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "10.0.0.99", "simulator_mode": False}, + {CONF_HOST: "10.0.0.99"}, ) assert result2["type"] == FlowResultType.FORM @@ -642,14 +636,14 @@ async def test_user_flow_recovery_after_bad_host(hass: HomeAssistant) -> None: # First attempt fails result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "bad-host", "simulator_mode": False}, + {CONF_HOST: "bad-host"}, ) assert result2["errors"] == {"base": "cannot_connect"} # Second attempt succeeds result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) assert result3["type"] == FlowResultType.MENU assert result3["step_id"] == "choose_v2_auth" @@ -677,7 +671,7 @@ async def test_passphrase_auth_empty_passphrase(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) result2b = await hass.config_entries.flow.async_configure( @@ -718,7 +712,7 @@ async def test_passphrase_auth_recovery_after_error(hass: HomeAssistant) -> None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) result2b = await hass.config_entries.flow.async_configure( @@ -768,7 +762,7 @@ async def test_proximity_auth_success(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) assert result2["step_id"] == "choose_v2_auth" @@ -811,7 +805,7 @@ async def test_proximity_auth_failed(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) result2b = await hass.config_entries.flow.async_configure( @@ -852,7 +846,7 @@ async def test_proximity_auth_connection_error(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) result2b = await hass.config_entries.flow.async_configure( @@ -908,7 +902,7 @@ async def test_duplicate_entry_aborts(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: MOCK_HOST, "simulator_mode": False}, + {CONF_HOST: MOCK_HOST}, ) assert result2["type"] == FlowResultType.ABORT From 291a79841af1df3f0daa93f2ec29fa1b7cf63959 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:44:17 -0700 Subject: [PATCH 04/36] Update schema-driven changes plan with versioning model - Add Versioning Model section: library as semantic contract layer, schema-only vs value-change scenarios, human-gated version sequence - Add schema version vs firmware version distinction, note v2 API beta - Add new-fields-require-human-review policy - Remove auto-entity concept from Phase 2 (override table only for reviewed fields) - Update Phase 3 to clarify codegen does not bypass review gate --- docs/dev/schema_driven_changes.md | 81 ++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/docs/dev/schema_driven_changes.md b/docs/dev/schema_driven_changes.md index 15d38fa1..84956fab 100644 --- a/docs/dev/schema_driven_changes.md +++ b/docs/dev/schema_driven_changes.md @@ -26,15 +26,20 @@ schema-driven integration would have propagated the error. Additional blockers: -- **No schema versioning** -- the schema is tied to firmware releases (`rYYYYWW`), with no mechanism to request a specific version or negotiate compatibility. +- **No schema versioning** -- the schema is tied to firmware releases (`rYYYYWW`), which conflates "the software running on the panel" with "the data contract + the panel exposes." A firmware update may change dozens of things without touching the schema, or alter one property's unit declaration without changing the + firmware version format. There is no independent schema version, no mechanism to request a specific version, and no backwards-compatibility guarantee. The + Homie API (`/api/v2/`) is currently in beta, which explains the in-place schema mutations; post-beta breaking changes would be expected under a new endpoint + (e.g. `/api/v3/`). The schema hash computed by Phase 1 drift detection is the best available proxy for a schema version -- a content-addressed identifier for + the exact set of node types, properties, units, and datatypes. - **Irreducible semantic layer** -- sign conventions, derived state machines (`dsm_state`, `current_run_config`), cross-references (EVSE `feed` to circuit), unmapped tab synthesis, and energy dip compensation are domain logic not representable in the Homie schema. - **HA-specific metadata** -- `device_class`, `state_class`, `entity_category`, `suggested_display_precision` have no Homie equivalent. - **User stability** -- HA users build automations and dashboards against stable entity IDs and sensor behaviors. Schema-driven changes that silently alter a sensor's unit or meaning would break installations. -The phased approach below progressively surfaces schema metadata for validation and diagnostic purposes first, then optionally for entity discovery, without -ever trusting the schema blindly for units or semantics. +The phased approach below progressively surfaces schema metadata for validation and diagnostic purposes first, then optionally for reducing entity definition +boilerplate on reviewed fields, without ever trusting the schema blindly for units or semantics and without ever exposing fields to users without human review. ## Phase 1: Schema Metadata Exposure (Validation and Diagnostics) — COMPLETE @@ -115,6 +120,59 @@ attributes, unit cross-check (match, mismatch, missing), unmapped field detectio - Early warning when schema-derived field metadata disagrees with sensor definitions (e.g. the kW/W error). - Foundation for Phase 2 -- the field metadata and mapping are reusable. +## Versioning Model + +The `span-panel-api` library is the gating factor for all schema changes reaching the integration. Even before SPAN corrects known unit declaration errors in +the Homie schema, the library applies the correct interpretation -- the snapshot contract defines the truth, not the schema. This isolation has two +consequences: + +1. **Schema corrections (declaration-only)** -- when SPAN fixes a unit declaration (e.g. `kW` → `W`) without changing actual values, neither repo needs code + changes. The library's `build_field_metadata()` automatically reflects the corrected declaration, and Phase 1 validation mismatches resolve themselves. + +2. **Value changes** -- if SPAN changes actual transmitted values (e.g. starts sending kW-scale values to match a `kW` declaration), the library must apply a + conversion in `_build_snapshot()` to maintain the snapshot contract. The integration bumps the library version; no other changes needed. + +In both cases, the library version pins a specific interpretation of firmware data. The version sequence for a breaking firmware change: + +1. SPAN releases firmware with changed property behavior +2. Library releases a new version with the conversion/adaptation +3. Integration bumps its library dependency +4. User updates the integration -- changelog explains what changed + +Each step is a human decision point. No change reaches users without explicit maintainer review. + +### Schema Version vs Firmware Version + +The correct thing to version against is the schema, not the firmware. The `rYYYYWW` firmware identifier conflates panel software with data contract. Ideally +SPAN would provide a declared `schema_version` field -- a monotonically increasing version or a semver -- so the library can say "I understand schema versions +up to X" rather than "I was built against firmware rXXXXYY." + +The current unit corrections and schema changes being made without a version bump are beta-phase behavior -- the Homie API is served at `/api/v2/` and is not +yet stable. Once the API exits beta, breaking changes to the schema would be expected to land under a new endpoint (e.g. `/api/v3/`), not as in-place mutations +to the v2 schema. This distinction matters: the current churn is not representative of the long-term maintenance burden, and the trigger criteria for later +phases should be evaluated against post-beta stability, not beta-phase corrections. + +Until SPAN provides a declared schema version, the library's schema hash (computed during Phase 1 drift detection) serves as the implicit schema version. The +library could maintain a known-schema-hashes table, mapping each validated hash to the set of corrections it applies. When encountering an unknown hash, it logs +a warning (drift detection already does this) and falls back to existing corrections -- safe-by-default behavior. + +### New Fields Require Human Review + +A new property appearing in the Homie schema must not be automatically exposed to users. The kW/W precedent proves that schema declarations cannot be trusted +for correctness on first appearance. If a field were surfaced automatically, users would build automations on it, and a subsequent correction to its unit or +sign convention would break those automations. + +The path for a new field: + +1. Phase 1 drift detection logs the new property +2. A maintainer reviews the property's actual values against its declared unit and datatype +3. The library adds the field to `_build_snapshot()` and `_PROPERTY_FIELD_MAP` +4. The integration adds a `SensorEntityDescription` with verified HA metadata +5. Both repos release new versions + +This is the same human-gated process used for existing fields. The library absorbs transport details; the integration adds HA semantics; nothing reaches users +without review. + ## Phase 2: Override-Table Entity Creation (Future) **Prerequisite**: Phase 1 complete. Schema metadata proven stable across multiple firmware releases. Schema unit corrections resolved (no outstanding known @@ -124,14 +182,12 @@ Replace the 47+ hardcoded `SensorEntityDescription` instances with: 1. **A declarative override table** mapping snapshot field paths to HA metadata (`device_class`, `state_class`, sign convention, entity category). The library's field metadata provides the base unit and datatype; the override table adds HA-specific semantics. -2. **A generic entity factory** that iterates the library's field metadata, applies overrides where present, and creates entities with sensible defaults - otherwise. -3. **An "unknown field" entity** -- generic sensor, unit from library metadata, no `device_class`, diagnostic category. Surfaces new library fields without - integration code changes. +2. **A generic entity factory** that iterates the library's field metadata, applies overrides where present, and creates entities for fields that have an + override entry. Fields without an override entry are not exposed -- they remain invisible until a maintainer explicitly reviews them and adds an override. -The override table inverts the maintenance model: everything works generically, and the maintainer only writes overrides for HA-specific semantics (device -class, sign convention, etc.). The integration never references Homie node types or property IDs -- it operates entirely in terms of snapshot field paths and -the library's field metadata. +The override table reduces boilerplate for reviewed fields: the maintainer writes only the HA-specific semantics (device class, sign convention, etc.) and the +factory derives the rest from the library's field metadata. But no field is ever exposed without an explicit override entry. The integration never references +Homie node types or property IDs -- it operates entirely in terms of snapshot field paths and the library's field metadata. ### Trigger Criteria for Phase 2 @@ -153,8 +209,9 @@ Auto-generate `span-panel-api` snapshot dataclasses from the schema at build tim 3. Manual `models.py` inherits from generated classes and adds derived fields (`dsm_state`, `current_run_config`, etc.). 4. `mypy` and IDE autocomplete continue working against concrete types. -This means `pip install span-panel-api==X.Y.Z` picks up new fields without integration code changes. The integration's Phase 2 override table handles the -HA-side mapping. +This reduces the library-side work for new fields -- the snapshot dataclass picks up new fields automatically from the schema. However, generated fields still +require human review before they are exposed to the integration. The maintainer must verify the field's actual values against its declared unit and datatype, +then add an override entry in the integration's Phase 2 override table. The codegen eliminates the library boilerplate but does not bypass the review gate. ### Trigger Criteria for Phase 3 From 79d8b1930b955c176f106e4f22b68391d15d332f Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:21:45 -0700 Subject: [PATCH 05/36] Delegate panel cloning to external simulator via WSS Replace the local YAML generation approach with a WebSocket-based flow that delegates clone work to the simulator. The integration now discovers simulators via mDNS (looking for cloneWssPort in _ebus._tcp TXT records), falls back to manual host/port entry, and sends the panel's credentials over WSS. The simulator handles eBus scraping, translation, and config writing. - Rewrite simulation_utils.py with discover_clone_simulators() and execute_clone_via_simulator() backed by aiohttp WSS - Update OptionsFlowHandler clone step: mDNS pre-fill, v2-only gate - Remove simulation_generator.py (dead code) and services.yaml (unimplemented export_synthetic_config service) - Clean up export_config references from strings.json and all translation files; add clone error strings --- custom_components/span_panel/config_flow.py | 101 ++++--- custom_components/span_panel/const.py | 1 + custom_components/span_panel/services.yaml | 11 - .../span_panel/simulation_generator.py | 270 ------------------ .../span_panel/simulation_utils.py | 255 +++++++++++------ custom_components/span_panel/strings.json | 18 +- .../span_panel/translations/en.json | 21 +- .../span_panel/translations/es.json | 17 +- .../span_panel/translations/fr.json | 17 +- .../span_panel/translations/ja.json | 17 +- .../span_panel/translations/pt.json | 17 +- 11 files changed, 279 insertions(+), 466 deletions(-) delete mode 100644 custom_components/span_panel/services.yaml delete mode 100644 custom_components/span_panel/simulation_generator.py diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index 19721e59..16f6ed65 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -15,7 +15,6 @@ ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import callback -from homeassistant.helpers.selector import selector from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util.network import is_ipv4_address from span_panel_api import V2AuthResponse, detect_api_version @@ -40,6 +39,7 @@ CONF_HOP_PASSPHRASE, CONF_HTTP_PORT, CONF_PANEL_SERIAL, + DEFAULT_CLONE_WSS_PORT, DOMAIN, ENABLE_ENERGY_DIP_COMPENSATION, ENTITY_NAMING_PATTERN, @@ -53,7 +53,10 @@ POWER_DISPLAY_PRECISION, SNAPSHOT_UPDATE_INTERVAL, ) -from .simulation_utils import clone_panel_to_simulation +from .simulation_utils import ( + discover_clone_simulators, + execute_clone_via_simulator, +) _LOGGER = logging.getLogger(__name__) @@ -752,10 +755,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Show the main options menu.""" if user_input is None: - menu_options = { + menu_options: dict[str, str] = { "general_options": "General Options", - "clone_panel_to_simulation": "Clone Panel To Simulation", } + # Clone via simulator is only available for v2 panels (eBus) + if self.config_entry.data.get(CONF_API_VERSION) == "v2": + menu_options["clone_panel_to_simulation"] = "Clone Panel To Simulation" return self.async_show_menu( step_id="init", @@ -791,49 +796,71 @@ async def async_step_general_options( async def async_step_clone_panel_to_simulation( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Clone the live panel into a simulation YAML for the standalone simulator.""" - result = await clone_panel_to_simulation(self.hass, self.config_entry, user_input) - - # If result is a ConfigFlowResult, return it directly - if hasattr(result, "type"): - return result # type: ignore[return-value] - - # Otherwise, result is (dest_path, errors) for the form - if isinstance(result, tuple) and len(result) == 2: - dest_path, errors = result - if not isinstance(errors, dict): - errors = {} - else: - # Fallback if result format is unexpected - _LOGGER.error( - "Unexpected result format from clone_panel_to_simulation: %s", type(result) - ) - return self.async_abort(reason="unknown") - - # If user_input was provided and there are no errors, the operation succeeded - if user_input is not None and not errors: - return self.async_create_entry( - title="Simulation Created", - data={}, - description=f"Cloned panel to {dest_path.name}", - ) + """Clone the panel to a simulator discovered via mDNS or entered manually.""" + errors: dict[str, str] = {} + default_host = "" + default_port = DEFAULT_CLONE_WSS_PORT + panel_name = self.config_entry.data.get("device_name", self.config_entry.title) + + if user_input is not None: + sim_host = str(user_input.get("simulator_host", "")).strip() + sim_port = int(user_input.get("clone_wss_port", DEFAULT_CLONE_WSS_PORT)) + + if not sim_host: + errors["simulator_host"] = "host_required" + else: + panel_host = str(self.config_entry.data.get(CONF_HOST, "")) + passphrase: str | None = self.config_entry.data.get(CONF_HOP_PASSPHRASE) + if passphrase == "": + passphrase = None + + result = await execute_clone_via_simulator( + simulator_host=sim_host, + simulator_port=sim_port, + panel_host=panel_host, + panel_passphrase=passphrase, + ) - # Compute device name for form display - device_name = self.config_entry.data.get("device_name", self.config_entry.title) + if result.success: + _LOGGER.info( + "Panel cloned to simulator: %s (%d circuits)", + result.clone_serial, + result.circuits, + ) + return self.async_create_entry( + title="", + data=dict(self.config_entry.options), + ) + + _LOGGER.error("Clone failed at %s: %s", result.error_phase, result.error_message) + errors["base"] = "clone_failed" + + default_host = sim_host + default_port = sim_port + else: + # First visit — try mDNS discovery for simulators + simulators = await discover_clone_simulators(self.hass) + if simulators: + default_host = simulators[0].host + default_port = simulators[0].clone_wss_port + _LOGGER.debug( + "Discovered %d simulator(s) with clone support; using %s:%d", + len(simulators), + default_host, + default_port, + ) - # Confirm form with destination field schema = vol.Schema( { - vol.Required("destination", default=str(dest_path)): selector( - {"text": {"multiline": False}} - ) + vol.Required("simulator_host", default=default_host): str, + vol.Required("clone_wss_port", default=default_port): int, } ) return self.async_show_form( step_id="clone_panel_to_simulation", data_schema=schema, description_placeholders={ - "panel": device_name or "Span Panel", + "panel": str(panel_name) if panel_name else "Span Panel", }, errors=errors, ) diff --git a/custom_components/span_panel/const.py b/custom_components/span_panel/const.py index 49b3f6f9..9899763f 100644 --- a/custom_components/span_panel/const.py +++ b/custom_components/span_panel/const.py @@ -57,6 +57,7 @@ ENABLE_ENERGY_DIP_COMPENSATION = "enable_energy_dip_compensation" DEFAULT_SNAPSHOT_INTERVAL: Final[float] = 5.0 +DEFAULT_CLONE_WSS_PORT: Final[int] = 19443 class CircuitRelayState(enum.Enum): diff --git a/custom_components/span_panel/services.yaml b/custom_components/span_panel/services.yaml deleted file mode 100644 index 3f875223..00000000 --- a/custom_components/span_panel/services.yaml +++ /dev/null @@ -1,11 +0,0 @@ -export_synthetic_config: - name: Export Synthetic Sensor Config - description: Export the current synthetic sensor configuration to a YAML file. You can specify either a directory (file will be named automatically) or a full file path. - fields: - directory: - name: File Path - description: Either a directory path (file will be named automatically) or a full file path ending in .yaml where the configuration will be saved. - required: true - example: "/config/span_panel_sensor_config.yaml" - selector: - text: diff --git a/custom_components/span_panel/simulation_generator.py b/custom_components/span_panel/simulation_generator.py deleted file mode 100644 index 990a787f..00000000 --- a/custom_components/span_panel/simulation_generator.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Build simulation YAML from a live panel snapshot. - -This module inspects the current coordinator data and produces a YAML dict -that matches span_panel_api's simulation reference. It infers templates from -names and seeds energy profiles from current power readings. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - - -@dataclass -class SimulationYamlGenerator: - """Generate YAML from live panel data.""" - - hass: Any - coordinator: Any - - async def build_yaml_from_live_panel(self) -> tuple[dict[str, Any], int]: - """Build YAML from live panel data.""" - data = getattr(self.coordinator, "data", None) - circuits_obj = getattr(data, "circuits", None) - - # Prepare containers - circuit_templates: dict[str, Any] = {} - circuits: list[dict[str, Any]] = [] - mapped_tabs: set[int] = set() - - # Iterate circuits - iter_dict: dict[str, Any] = {} - # Safely extract iterable circuits mapping without accessing attributes on None - if isinstance(circuits_obj, dict): - iter_dict = circuits_obj - elif circuits_obj is not None: - inner_circuits = getattr(circuits_obj, "circuits", None) - if isinstance(inner_circuits, dict): - iter_dict = inner_circuits - - for cid, c in iter_dict.items(): - name = str(getattr(c, "name", cid)) - power_w = float(getattr(c, "instant_power_w", 0.0) or 0.0) - raw_tabs = getattr(c, "tabs", []) if hasattr(c, "tabs") else [] - tabs = ( - list(raw_tabs) - if isinstance(raw_tabs, (list | tuple)) - else ([] if raw_tabs in (None, "UNSET") else [int(raw_tabs)]) - ) - mapped_tabs.update(tabs) - - # Skip unmapped tab circuits - they should be handled as unmapped tabs, not circuits - if str(cid).startswith("unmapped_tab_"): - continue - - template_key = self._infer_template_key(name, power_w, tabs) - if template_key not in circuit_templates: - circuit_templates[template_key] = self._make_template(template_key, power_w, name) - - entry: dict[str, Any] = { - "id": str(cid), - "name": name, - "tabs": tabs, - "template": template_key, - } - if power_w != 0.0: - entry["overrides"] = {"energy_profile": {"typical_power": power_w}} - - circuits.append(entry) - - # Compute total tabs - num_tabs = 32 - if mapped_tabs: - max_tab = max(mapped_tabs) - if max_tab <= 8: - num_tabs = 8 - elif max_tab <= 32: - num_tabs = 32 - else: - num_tabs = 40 - - # Panel config - serial = ( - getattr(getattr(data, "status", None), "serial_number", None) or "span_panel_simulation" - ) - snapshot_yaml: dict[str, Any] = { - "panel_config": { - "serial_number": str(serial), - "total_tabs": num_tabs, - "main_size": 200, - }, - "circuit_templates": circuit_templates, - "circuits": circuits, - "unmapped_tabs": sorted(set(range(1, num_tabs + 1)) - mapped_tabs), - "simulation_params": { - "update_interval": 5, - "time_acceleration": 1.0, - "noise_factor": 0.02, - }, - } - - return snapshot_yaml, num_tabs - - def _infer_template_key(self, name: str, power_w: float, tabs: list[int]) -> str: - lname = name.lower() - if any(k in lname for k in ("light", "lights")): - return "lighting" - if "kitchen" in lname and "outlet" in lname: - return "kitchen_outlets" - if any(k in lname for k in ("hvac", "furnace", "air conditioner", "ac", "heat pump")): - return "hvac" - if any(k in lname for k in ("fridge", "refrigerator", "wine fridge")): - return "refrigerator" - if any(k in lname for k in ("ev", "charger")): - return "ev_charger" - if any(k in lname for k in ("pool", "spa", "fountain")): - return "pool_equipment" - if any(k in lname for k in ("internet", "router", "network", "modem")): - return "always_on" - if len(tabs) >= 2: - return "major_appliance" - if "outlet" in lname: - return "outlets" - if power_w < 0: - return "producer" - return "major_appliance" - - def _make_template(self, key: str, typical: float, name: str) -> dict[str, Any]: - # Base ranges derived from snapshot - if key == "producer" or typical < 0: - pr_min = min(typical * 2.0, -50.0) - profile = { - "mode": "producer", - "power_range": [pr_min, 0.0], - "typical_power": typical, - "power_variation": 0.3, - } - return { - "energy_profile": profile, - "relay_behavior": "non_controllable", - "priority": "MUST_HAVE", - } - - if key == "ev_charger": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 7200.0)], - "typical_power": max(typical, 3000.0), - "power_variation": 0.15, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "NON_ESSENTIAL", - # Prefer night charging and respond to grid stress - "time_of_day_profile": { - "enabled": True, - "peak_hours": [22, 23, 0, 1, 2, 3, 4, 5, 6], - }, - "smart_behavior": {"responds_to_grid": True, "max_power_reduction": 0.6}, - } - - if key == "refrigerator": - profile = { - "mode": "consumer", - "power_range": [50.0, 200.0], - "typical_power": max(typical, 120.0), - "power_variation": 0.2, - } - return { - "energy_profile": profile, - "relay_behavior": "non_controllable", - "priority": "MUST_HAVE", - "cycling_pattern": {"on_duration": 600, "off_duration": 1800}, - } - - if key == "hvac": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 2800.0)], - "typical_power": max(typical, 1800.0), - "power_variation": 0.15, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "MUST_HAVE", - "cycling_pattern": {"on_duration": 1200, "off_duration": 2400}, - } - - if key == "lighting": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 300.0)], - "typical_power": max(typical, 40.0), - "power_variation": 0.1, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "NON_ESSENTIAL", - "time_of_day_profile": {"enabled": True, "peak_hours": [18, 19, 20, 21, 22]}, - } - - if key == "kitchen_outlets": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 2400.0)], - "typical_power": max(typical, 300.0), - "power_variation": 0.4, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "MUST_HAVE", - } - - if key == "outlets": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 1800.0)], - "typical_power": max(typical, 150.0), - "power_variation": 0.4, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "MUST_HAVE", - } - - if key == "always_on": - profile = { - "mode": "consumer", - "power_range": [40.0, 100.0], - "typical_power": max(typical, 60.0), - "power_variation": 0.1, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "MUST_HAVE", - } - - if key == "pool_equipment": - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 1200.0)], - "typical_power": max(typical, 800.0), - "power_variation": 0.1, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "NON_ESSENTIAL", - # Typical pump run: 2h on, 4h off, repeating - "cycling_pattern": {"on_duration": 7200, "off_duration": 14400}, - } - - # major_appliance and fallback - profile = { - "mode": "consumer", - "power_range": [0.0, max(abs(typical) * 2.0, 2500.0)], - "typical_power": max(typical, 800.0), - "power_variation": 0.3, - } - return { - "energy_profile": profile, - "relay_behavior": "controllable", - "priority": "NON_ESSENTIAL", - } diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py index d5c675fb..cbb6cc8a 100644 --- a/custom_components/span_panel/simulation_utils.py +++ b/custom_components/span_panel/simulation_utils.py @@ -1,101 +1,196 @@ -"""Clone panel utilities for SPAN Panel integration.""" +"""Simulator clone utilities for SPAN Panel integration. + +Discovers simulators on the local network via mDNS and delegates panel +cloning to the simulator over its WebSocket endpoint. The simulator +handles eBus scraping, translation, and config writing — the integration +only provides the target panel's address and passphrase. +""" from __future__ import annotations +import asyncio +from dataclasses import dataclass +import json import logging -from pathlib import Path -from typing import TYPE_CHECKING, Any +import ssl -from homeassistant.config_entries import ConfigEntry +import aiohttp +from homeassistant.components import zeroconf as ha_zeroconf from homeassistant.core import HomeAssistant -from homeassistant.util import slugify -import yaml +from zeroconf import ServiceStateChange, Zeroconf +from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo -from .simulation_generator import SimulationYamlGenerator +_LOGGER = logging.getLogger(__name__) -if TYPE_CHECKING: - from .coordinator import SpanPanelCoordinator +EBUS_SERVICE_TYPE = "_ebus._tcp.local." +CLONE_WSS_PORT_PROPERTY = "cloneWssPort" +DISCOVERY_TIMEOUT_SECONDS = 3.0 +CLONE_OPERATION_TIMEOUT_SECONDS = 120 -_LOGGER = logging.getLogger(__name__) +@dataclass +class SimulatorInfo: + """A simulator discovered via mDNS that supports panel cloning.""" -async def clone_panel_to_simulation( - hass: HomeAssistant, - config_entry: ConfigEntry, - user_input: dict[str, Any] | None = None, -) -> tuple[Path, dict[str, str]]: - """Clone the live panel into a simulation YAML for the standalone simulator. + host: str + clone_wss_port: int + name: str - Args: - hass: Home Assistant instance - config_entry: Configuration entry for the SPAN panel - user_input: User input from the config flow form - Returns: - Tuple of (destination_path, errors_dict) +@dataclass +class CloneResult: + """Outcome of a clone-via-simulator operation.""" - """ - errors: dict[str, str] = {} - - # Compute default filename first - device_name = config_entry.data.get("device_name", config_entry.title) - safe_device = slugify(device_name) if isinstance(device_name, str) else "span_panel" - - config_dir = Path(hass.config.config_dir) / "span_panel" / "exports" - base_name = f"simulation_config_{safe_device}.yaml" - dest_path = config_dir / base_name - - # Resolve coordinator from runtime_data - coordinator: SpanPanelCoordinator | None = None - if hasattr(config_entry, "runtime_data") and config_entry.runtime_data is not None: - coordinator = config_entry.runtime_data.coordinator - if coordinator is None: - errors["base"] = "coordinator_unavailable" - return dest_path, errors - - # Suffix if exists: _2, _3, ... - suffix_index = 1 - if await hass.async_add_executor_job(dest_path.exists): - suffix_index = 2 - while True: - candidate = config_dir / f"simulation_config_{safe_device}_{suffix_index}.yaml" - if not await hass.async_add_executor_job(candidate.exists): - dest_path = candidate - break - suffix_index += 1 - - if user_input is not None: - try: - # Use a separate generator to build YAML purely from live data - generator = SimulationYamlGenerator( - hass=hass, - coordinator=coordinator, - ) - snapshot_yaml, num_tabs = await generator.build_yaml_from_live_panel() + success: bool + serial: str = "" + clone_serial: str = "" + filename: str = "" + circuits: int = 0 + error_message: str = "" + error_phase: str = "" - # snapshot_yaml and num_tabs returned by generator - snapshot_yaml["panel_config"]["serial_number"] = f"{safe_device}_simulation" + ( - "" if suffix_index == 1 else f"_{suffix_index}" - ) - # Ensure directory exists and write file - await hass.async_add_executor_job( - lambda: dest_path.parent.mkdir(parents=True, exist_ok=True) - ) +async def discover_clone_simulators(hass: HomeAssistant) -> list[SimulatorInfo]: + """Browse for simulators advertising a clone WSS port via mDNS. - def _write_yaml() -> None: - with dest_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(snapshot_yaml, f, sort_keys=False) + Looks for ``_ebus._tcp.local.`` services whose TXT record contains + ``cloneWssPort``. Discovery runs for a short window and returns + all matching services found. + """ + aiozc = await ha_zeroconf.async_get_async_instance(hass) + zc = aiozc.zeroconf + + discovered_names: list[str] = [] + + def _on_state_change( + zeroconf: Zeroconf, # noqa: ARG001 + service_type: str, # noqa: ARG001 + name: str, + state_change: ServiceStateChange, + ) -> None: + if state_change == ServiceStateChange.Added: + discovered_names.append(name) + + browser = AsyncServiceBrowser(zc, EBUS_SERVICE_TYPE, handlers=[_on_state_change]) + try: + await asyncio.sleep(DISCOVERY_TIMEOUT_SECONDS) + finally: + await browser.async_cancel() + + simulators: list[SimulatorInfo] = [] + + for name in discovered_names: + info = AsyncServiceInfo(EBUS_SERVICE_TYPE, name) + await info.async_request(zc, 3000) + + if not info.properties: + continue + + props: dict[str, str] = {} + for raw_key, raw_val in info.properties.items(): + key = raw_key.decode() if isinstance(raw_key, bytes) else str(raw_key) + val = raw_val.decode() if isinstance(raw_val, bytes) else str(raw_val) + props[key] = val + + port_str = props.get(CLONE_WSS_PORT_PROPERTY) or props.get(CLONE_WSS_PORT_PROPERTY.lower()) + if not port_str: + continue + + addresses = info.parsed_scoped_addresses() + host = addresses[0] if addresses else (info.server or "") + display_name = name.replace(f".{EBUS_SERVICE_TYPE}", "") + + simulators.append( + SimulatorInfo( + host=host.rstrip("."), + clone_wss_port=int(port_str), + name=display_name, + ) + ) - await hass.async_add_executor_job(_write_yaml) - _LOGGER.info("Cloned live panel to simulation YAML at %s", dest_path) + return simulators - # Return success with no errors - return dest_path, {} - except Exception as e: - _LOGGER.error("Clone to simulation failed: %s", e) - errors["base"] = f"Clone failed: {e}" +async def execute_clone_via_simulator( + simulator_host: str, + simulator_port: int, + panel_host: str, + panel_passphrase: str | None, +) -> CloneResult: + """Open a WSS connection to the simulator and run a panel clone. - # Return the destination path for the form - return dest_path, errors + The simulator connects to the real panel's eBus, scrapes retained + messages, translates them into a simulation YAML config, and writes + the file. This function streams status updates to the log and + returns the final result. + """ + url = f"wss://{simulator_host}:{simulator_port}/ws/clone" + + # The simulator uses a self-signed certificate; trust it for + # local-network communication. + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + try: + async with asyncio.timeout(CLONE_OPERATION_TIMEOUT_SECONDS): + async with aiohttp.ClientSession() as session: + async with session.ws_connect(url, ssl=ssl_context) as ws: + await ws.send_json( + { + "type": "clone_panel", + "host": panel_host, + "passphrase": panel_passphrase, + } + ) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + data: dict[str, str | int] = json.loads(msg.data) + msg_type = str(data.get("type", "")) + + if msg_type == "status": + _LOGGER.debug( + "Clone progress: %s — %s", + data.get("phase", ""), + data.get("detail", ""), + ) + elif msg_type == "result": + if data.get("status") == "ok": + return CloneResult( + success=True, + serial=str(data.get("serial", "")), + clone_serial=str(data.get("clone_serial", "")), + filename=str(data.get("filename", "")), + circuits=int(data.get("circuits", 0)), + ) + return CloneResult( + success=False, + error_message=str(data.get("message", "Unknown error")), + error_phase=str(data.get("phase", "")), + ) + + elif msg.type in ( + aiohttp.WSMsgType.ERROR, + aiohttp.WSMsgType.CLOSED, + ): + return CloneResult( + success=False, + error_message="WebSocket connection closed unexpectedly", + ) + + except TimeoutError: + return CloneResult( + success=False, + error_message="Clone operation timed out", + ) + except aiohttp.ClientError as err: + return CloneResult( + success=False, + error_message=f"Cannot connect to simulator: {err}", + ) + + return CloneResult( + success=False, + error_message="Clone completed without receiving a result", + ) diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index 0b778b53..4d4db1d8 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -86,8 +86,10 @@ }, "options": { "error": { - "directory": "Directory path is required", - "base": "Export failed: {error}" + "host_required": "Simulator host is required", + "clone_failed": "Clone failed. Check the simulator and Home Assistant logs for details.", + "clone_connection_failed": "Cannot connect to the simulator", + "clone_timeout": "Clone operation timed out" }, "step": { "init": { @@ -115,14 +117,16 @@ "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } }, - "export_config": { - "title": "Export Synthetic Sensor Config", - "description": "Export the current synthetic sensor configuration to a YAML file. You can specify either a directory (file will be named automatically) or a full file path. Default filename: {filename}", + "clone_panel_to_simulation": { + "title": "Clone Panel to Simulator", + "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", "data": { - "directory": "File Path" + "simulator_host": "Simulator Host", + "clone_wss_port": "Clone WSS Port" }, "data_description": { - "directory": "Either a directory path (file will be named automatically) or a full file path ending in .yaml where the configuration will be saved." + "simulator_host": "IP address or hostname of the SPAN Panel Simulator", + "clone_wss_port": "WebSocket port for the clone endpoint" } } } diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index fb7555d3..5816255e 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -86,16 +86,17 @@ }, "options": { "error": { - "directory": "Directory path is required", - "base": "Export failed: {error}" + "host_required": "Simulator host is required", + "clone_failed": "Clone failed. Check the simulator and Home Assistant logs for details.", + "clone_connection_failed": "Cannot connect to the simulator", + "clone_timeout": "Clone operation timed out" }, "step": { "init": { "title": "Options Menu", "menu_options": { "general_options": "General Options", - "export_config": "Export Synthetic Sensor Config", - "clone_panel_to_simulation": "Clone Panel to Simulation" + "clone_panel_to_simulation": "Clone Panel To Simulation" } }, "general_options": { @@ -116,14 +117,16 @@ "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } }, - "export_config": { - "title": "Export Synthetic Sensor Config", - "description": "Export the current synthetic sensor configuration to a YAML file. You can specify either a directory (file will be named automatically) or a full file path. Default filename: {filename}", + "clone_panel_to_simulation": { + "title": "Clone Panel to Simulator", + "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", "data": { - "directory": "File Path" + "simulator_host": "Simulator Host", + "clone_wss_port": "Clone WSS Port" }, "data_description": { - "directory": "Either a directory path (file will be named automatically) or a full file path ending in .yaml where the configuration will be saved." + "simulator_host": "IP address or hostname of the SPAN Panel Simulator", + "clone_wss_port": "WebSocket port for the clone endpoint" } } } diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index ee49063b..62598645 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -66,15 +66,16 @@ }, "options": { "error": { - "directory": "Se requiere la ruta del archivo", - "base": "Falló la exportación: {error}" + "host_required": "Se requiere el host del simulador", + "clone_failed": "La clonación falló. Verifique los registros del simulador y de Home Assistant para más detalles.", + "clone_connection_failed": "No se puede conectar al simulador", + "clone_timeout": "La operación de clonación expiró" }, "step": { "init": { "title": "Menú de Opciones", "menu_options": { "general_options": "Opciones Generales", - "export_config": "Exportar Configuración de Sensores Sintéticos", "clone_panel_to_simulation": "Clonar Panel a Simulación" } }, @@ -95,16 +96,6 @@ "energy_reporting_grace_period": "Cuánto tiempo los sensores de energía mantienen su último valor conocido cuando el panel no está disponible (0-60 minutos). Ayuda a preservar la integridad de las estadísticas de energía durante cortes breves. Predeterminado: 15 minutos.", "enable_energy_dip_compensation": "Compensar automáticamente cuando el panel reporta lecturas de energía más bajas. Al deshabilitar se borran todas las compensaciones acumuladas." } - }, - "export_config": { - "title": "Exportar Configuración de Sensores Sintéticos", - "description": "Exportar la configuración actual de sensores sintéticos a un archivo YAML. Puede especificar un directorio (el archivo se nombrará automáticamente) o una ruta de archivo completa. Nombre de archivo predeterminado: {filename}", - "data": { - "directory": "Ruta del Archivo" - }, - "data_description": { - "directory": "Una ruta de directorio (el archivo se nombrará automáticamente) o una ruta de archivo completa que termine en .yaml donde se guardará la configuración." - } } } }, diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index 2336099e..272b4758 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -66,15 +66,16 @@ }, "options": { "error": { - "directory": "Le chemin du fichier est requis", - "base": "Échec de l'exportation : {error}" + "host_required": "L'hôte du simulateur est requis", + "clone_failed": "Le clonage a échoué. Vérifiez les journaux du simulateur et de Home Assistant pour plus de détails.", + "clone_connection_failed": "Impossible de se connecter au simulateur", + "clone_timeout": "L'opération de clonage a expiré" }, "step": { "init": { "title": "Menu des Options", "menu_options": { "general_options": "Options Générales", - "export_config": "Exporter la Configuration des Capteurs Synthétiques", "clone_panel_to_simulation": "Cloner le Panneau en Simulation" } }, @@ -95,16 +96,6 @@ "energy_reporting_grace_period": "Combien de temps les capteurs d'énergie maintiennent leur dernière valeur connue lorsque le panneau devient indisponible (0-60 minutes). Aide à préserver l'intégrité des statistiques d'énergie pendant les pannes brèves. Par défaut : 15 minutes.", "enable_energy_dip_compensation": "Compenser automatiquement lorsque le panneau signale des lectures d'énergie inférieures. La désactivation efface toutes les compensations accumulées." } - }, - "export_config": { - "title": "Exporter la Configuration des Capteurs Synthétiques", - "description": "Exporter la configuration actuelle des capteurs synthétiques vers un fichier YAML. Vous pouvez spécifier un répertoire (le fichier sera nommé automatiquement) ou un chemin de fichier complet. Nom de fichier par défaut : {filename}", - "data": { - "directory": "Chemin du Fichier" - }, - "data_description": { - "directory": "Un chemin de répertoire (le fichier sera nommé automatiquement) ou un chemin de fichier complet se terminant par .yaml où la configuration sera sauvegardée." - } } } }, diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index 5dfcb391..79f989ce 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -66,15 +66,16 @@ }, "options": { "error": { - "directory": "ファイルパスが必要です", - "base": "エクスポートに失敗しました:{error}" + "host_required": "シミュレータのホストが必要です", + "clone_failed": "クローンに失敗しました。詳細はシミュレータとHome Assistantのログを確認してください。", + "clone_connection_failed": "シミュレータに接続できません", + "clone_timeout": "クローン操作がタイムアウトしました" }, "step": { "init": { "title": "オプションメニュー", "menu_options": { "general_options": "一般オプション", - "export_config": "合成センサー設定をエクスポート", "clone_panel_to_simulation": "パネルをシミュレーションにクローン" } }, @@ -95,16 +96,6 @@ "energy_reporting_grace_period": "パネルが利用できなくなったときにエネルギーセンサーが最後の既知の値を維持する時間(0-60分)。短時間の停電中にエネルギー統計の整合性を維持するのに役立ちます。デフォルト:15分。", "enable_energy_dip_compensation": "パネルがより低いエネルギー読み取り値を報告した場合に自動的に補正します。無効にすると、蓄積されたすべてのオフセットがクリアされます。" } - }, - "export_config": { - "title": "合成センサー設定をエクスポート", - "description": "現在の合成センサー設定をYAMLファイルにエクスポートします。ディレクトリ(ファイルは自動的に名前が付けられます)または完全なファイルパスを指定できます。デフォルトファイル名:{filename}", - "data": { - "directory": "ファイルパス" - }, - "data_description": { - "directory": "ディレクトリパス(ファイルは自動的に名前が付けられます)または設定が保存される.yamlで終わる完全なファイルパス。" - } } } }, diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index bc524d1c..4081284d 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -66,15 +66,16 @@ }, "options": { "error": { - "directory": "O caminho do arquivo é necessário", - "base": "Falha na exportação: {error}" + "host_required": "O host do simulador é obrigatório", + "clone_failed": "A clonagem falhou. Verifique os logs do simulador e do Home Assistant para mais detalhes.", + "clone_connection_failed": "Não é possível conectar ao simulador", + "clone_timeout": "A operação de clonagem expirou" }, "step": { "init": { "title": "Menu de Opções", "menu_options": { "general_options": "Opções Gerais", - "export_config": "Exportar Configuração de Sensores Sintéticos", "clone_panel_to_simulation": "Clonar Painel para Simulação" } }, @@ -95,16 +96,6 @@ "energy_reporting_grace_period": "Quanto tempo os sensores de energia mantêm seu último valor conhecido quando o painel fica indisponível (0-60 minutos). Ajuda a preservar a integridade das estatísticas de energia durante interrupções breves. Padrão: 15 minutos.", "enable_energy_dip_compensation": "Compensar automaticamente quando o painel reporta leituras de energia mais baixas. Ao desativar, todas as compensações acumuladas são apagadas." } - }, - "export_config": { - "title": "Exportar Configuração de Sensores Sintéticos", - "description": "Exportar a configuração atual de sensores sintéticos para um arquivo YAML. Você pode especificar um diretório (o arquivo será nomeado automaticamente) ou um caminho de arquivo completo. Nome do arquivo padrão: {filename}", - "data": { - "directory": "Caminho do Arquivo" - }, - "data_description": { - "directory": "Um caminho de diretório (o arquivo será nomeado automaticamente) ou um caminho de arquivo completo terminando em .yaml onde a configuração será salva." - } } } }, From 79ecdd119090eb9beaa67036489d75f31611efec Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:24:42 -0700 Subject: [PATCH 06/36] Add eBus panel clone design document for simulator Documents the WebSocket-based clone architecture where the simulator scrapes a real panel's eBus, translates retained messages to YAML, and writes the simulation config. Covers the WSS contract, mDNS discovery, eBus-to-YAML translation rules, and implementation phases. --- docs/dev/simulator_panel_clone.md | 510 ++++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 docs/dev/simulator_panel_clone.md diff --git a/docs/dev/simulator_panel_clone.md b/docs/dev/simulator_panel_clone.md new file mode 100644 index 00000000..4b30ada2 --- /dev/null +++ b/docs/dev/simulator_panel_clone.md @@ -0,0 +1,510 @@ +# eBus Panel Clone — Simulator Feature + +## Context + +The simulator supports BESS/SPAN Drive simulation, breaker ratings, and full Homie v5 eBus publishing. The SPAN integration can already clone the in-memory +simulator config via the dashboard. This feature extends that: given credentials for a **real** SPAN panel, the simulator connects to its eBus, scrapes every +retained topic, and translates the result into a simulator YAML config — a faithful starting point that can then be tuned. + +The integration initiates the clone by contacting the simulator over a WebSocket, passing the target panel's host and passphrase. The simulator handles the +entire scrape-and-translate pipeline, writes the new config, and reloads. + +--- + +## Scope + +**Simulator** (this feature): + +- New WebSocket endpoint accepting clone requests +- Lightweight MQTT scraper (connect to real panel, collect retained messages) +- eBus-to-YAML translation layer +- Config write + hot reload + +**Integration** (future, not covered here): + +- UI/service action to trigger a clone request against the simulator + +--- + +## Architecture + +### Transport: WSS over TLS + +The clone WebSocket runs over TLS (WSS), not plain WS. The simulator already generates a `CertificateBundle` at startup for its MQTTS broker — the clone WSS +endpoint reuses the same certificate and key. This keeps credential management in one place and means the integration's connection to the simulator is encrypted +end-to-end, which matters because the passphrase for the real panel traverses this link. + +The WSS endpoint runs on its own dedicated port, separate from the dashboard HTTP server. This avoids mixing long-lived WebSocket connections with the HTMX +request/response traffic on the dashboard, and allows the clone port to be independently firewalled or exposed. + +### Port configuration + +The clone WSS port follows the same pattern as every other simulator port: + +| Layer | Mechanism | +| ---------------- | ----------------------------------------------------------- | +| **Default** | `CLONE_WSS_PORT = 19443` in `const.py` | +| **CLI arg** | `--clone-wss-port 19443` | +| **Env var** | `CLONE_WSS_PORT=19443` | +| **SimulatorApp** | Plumbed through `__init__` alongside `dashboard_port`, etc. | + +### mDNS discovery + +The integration needs to know which port the clone WSS endpoint is listening on. The simulator's `PanelAdvertiser` already puts custom properties into the +`_ebus._tcp` TXT record — `httpPort` is advertised when it differs from the default. The clone WSS port is advertised the same way: + +```python +# In PanelAdvertiser.register_panel(), alongside the existing httpPort logic: +ebus_properties: dict[str, str] = { + "homie_domain": "ebus", + "homie_version": "5", + "homie_roles": "device", + "mqtt_broker": hostname, + "txtvers": "1", +} +if self._http_port != 80: + ebus_properties["httpPort"] = str(self._http_port) +if self._clone_wss_port: + ebus_properties["cloneWssPort"] = str(self._clone_wss_port) +``` + +The integration reads TXT properties from zeroconf discovery records. When `cloneWssPort` is present, the integration knows this simulator supports panel +cloning and which port to connect to. When absent, clone functionality is unavailable (real panel, older simulator, or clone not configured). + +This also means the `_span._tcp` TXT record does not need changes — `cloneWssPort` is a simulator-only capability advertised on the eBus service type that the +integration already parses. + +### Sequence + +```text +Integration / Dashboard Simulator Real Panel + | | | + |-- WSS: clone_panel ----->| | + | {host, passphrase} | | + | |-- POST /api/v2/auth/register ->| + | |<-- {mqtt_creds, serial} -------| + | |-- GET /api/v2/certificate/ca ->| + | |<-- PEM cert -------------------| + |<-- WSS: "registering" | | + | |== MQTTS connect ===============| + | |-- SUB ebus/5/{serial}/# ------>| + | |<-- $state, $description -------| + | |<-- retained property msgs -----| + |<-- WSS: "scraping" | | + | | (collect until stable) | + | |== MQTT disconnect =============| + |<-- WSS: "translating" | | + | |-- parse $description | + | |-- map properties -> YAML | + | |-- write configs/{serial}-clone.yaml + | |-- trigger reload | + |<-- WSS: "done" | | + | {serial, filename} | | +``` + +### Why WebSocket + +The clone is inherently async — network round-trips to the real panel, waiting for retained messages, translation. A WebSocket lets the simulator stream status +updates back to the caller as each phase completes, and the caller can display progress or abort. + +### Why the simulator scrapes directly + +The simulator already has paho-mqtt infrastructure and runs its own MQTT broker. Having it connect directly to the real panel's broker (as a one-shot client) +avoids routing data through the integration and keeps the translation logic co-located with the config format it produces. The integration's only job is to +provide the panel address and passphrase. + +--- + +## WebSocket Contract + +### Endpoint + +`wss://{simulator_host}:{clone_wss_port}/ws/clone` + +The port is discovered via the `cloneWssPort` TXT property in the simulator's `_ebus._tcp` mDNS record. The TLS certificate is the simulator's self-signed CA — +the same one returned by `GET /api/v2/certificate/ca` on the simulator's bootstrap HTTP server. The integration already fetches and trusts this CA for MQTTS, so +it can reuse the same trust store for the WSS connection. + +### Request message (integration sends) + +```json +{ + "type": "clone_panel", + "host": "192.168.1.100", + "passphrase": "panel-passphrase" +} +``` + +| Field | Type | Required | Description | +| ------------ | -------------- | -------- | ------------------------------------- | +| `type` | string | yes | Must be `"clone_panel"` | +| `host` | string | yes | IP or hostname of the real SPAN panel | +| `passphrase` | string or null | no | Panel passphrase (null = door-bypass) | + +### Status messages (simulator sends) + +```json +{ + "type": "status", + "phase": "registering", + "detail": "Authenticating with panel at 192.168.1.100" +} +``` + +| Phase | Meaning | +| ------------- | ------------------------------------------------------------ | +| `registering` | Calling `/api/v2/auth/register` and `/api/v2/certificate/ca` | +| `connecting` | Opening MQTTS connection to the panel's broker | +| `scraping` | Subscribed to `ebus/5/{serial}/#`, collecting retained msgs | +| `translating` | Parsing `$description` and mapping properties to YAML | +| `writing` | Writing config file and triggering reload | +| `done` | Clone complete | +| `error` | Clone failed | + +### Completion message + +```json +{ + "type": "result", + "status": "ok", + "serial": "nj-2316-XXXX", + "clone_serial": "nj-2316-XXXX-clone", + "filename": "nj-2316-XXXX-clone.yaml", + "circuits": 16, + "has_bess": true, + "has_pv": true, + "has_evse": false +} +``` + +### Error message + +```json +{ + "type": "result", + "status": "error", + "phase": "connecting", + "message": "MQTTS connection refused: bad credentials" +} +``` + +--- + +## eBus Scrape Strategy + +### Authentication + +1. `POST http://{host}/api/v2/auth/register` with `{"name": "sim-clone-{uuid4}", "hopPassphrase": passphrase}` +2. Extract `ebusBrokerUsername`, `ebusBrokerPassword`, `ebusBrokerMqttsPort`, `serialNumber` +3. `GET http://{host}/api/v2/certificate/ca` for TLS trust + +### MQTT collection + +1. Connect via MQTTS (port from auth response, CA cert from step 3) +2. Subscribe to `ebus/5/{serial}/#` with QoS 0 +3. Collect all retained messages into a `dict[str, str]` keyed by full topic +4. Stability gate: stop collecting when no new topics arrive for 5 seconds (retained messages arrive in a burst shortly after subscription) +5. Disconnect cleanly + +### Required topics + +The scraper must receive at minimum: + +| Topic pattern | Purpose | +| ------------------------------- | ------------------------------- | +| `$state` | Confirm panel is `ready` | +| `$description` | Node topology (types, node IDs) | +| `core/breaker-rating` | Main breaker size | +| `core/serial-number` | Panel identity | +| `{circuit-uuid}/name` | Circuit names | +| `{circuit-uuid}/space` | Tab/breaker position | +| `{circuit-uuid}/dipole` | 240V detection | +| `{circuit-uuid}/breaker-rating` | Per-circuit breaker size | +| `{circuit-uuid}/relay` | Current relay state | +| `{circuit-uuid}/shed-priority` | Circuit priority | +| `{circuit-uuid}/active-power` | Current power (W) | + +Optional but used when available: + +| Topic pattern | Purpose | +| -------------------------------- | --------------------------- | +| `{circuit-uuid}/imported-energy` | Seed energy accumulators | +| `{circuit-uuid}/exported-energy` | Seed energy accumulators | +| `bess-0/nameplate-capacity` | Battery sizing | +| `bess-0/soc` | Initial SOC | +| `bess-0/grid-state` | Grid state at clone time | +| `pv-0/nameplate-capacity` | PV system sizing | +| `pv-0/feed` | Which circuit PV feeds | +| `evse-*/feed` | Which circuit EVSE feeds | +| `evse-*/status` | Charger state at clone time | +| `upstream-lugs/active-power` | Grid power reference | +| `power-flows/*` | Validation / sanity check | + +--- + +## eBus-to-YAML Translation + +### Units + +All power values on the eBus are in **watts**. The Homie schema historically declared circuit `active-power` as `kW`, but this is a schema metadata error — +actual published values have always been watts. SPAN firmware 202609 corrects the schema declaration to `W`. The simulator config also uses watts. No unit +conversion is needed anywhere in the pipeline. + +### Panel config + +| eBus source | YAML target | Notes | +| ------------------------------ | ---------------------------- | ----------------------------------- | +| `core/serial-number` | `panel_config.serial_number` | Append `-clone` suffix | +| `core/breaker-rating` | `panel_config.main_size` | Integer amps | +| Panel size from `$description` | `panel_config.total_tabs` | Count circuit space range | +| — | `panel_config.latitude` | Default 37.7 (user adjusts later) | +| — | `panel_config.longitude` | Default -122.4 (user adjusts later) | + +Panel size is derived from the `$description` by examining the Homie schema's circuit `space` property format string (e.g. `"1:32:1"` means 32 spaces). This +matches how span-panel-api determines panel size. + +### Circuit mapping + +For each node in `$description` with type `energy.ebus.device.circuit`: + +| eBus property | YAML target | Derivation | +| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `name` | `circuits[].name` | Direct | +| `space` | `circuits[].tabs` | `[space]` for single-pole; `[space, space+2]` if dipole | +| `dipole` | — | Determines tab count (true = 240V, 2 tabs) | +| `breaker-rating` | `circuits[].breaker_rating` | Integer amps | +| `relay` | `circuit_templates[].relay_behavior` | `OPEN`/`CLOSED` = `controllable`; presence of `always-on: true` = `non_controllable` | +| `shed-priority` | `circuit_templates[].priority` | Direct (`NEVER`, `SOC_THRESHOLD`, `OFF_GRID`) | +| `active-power` | `circuit_templates[].energy_profile.typical_power` | Absolute value in watts | +| `active-power` | `circuit_templates[].energy_profile.power_range` | `[0, breaker_rating * voltage]` | +| `imported-energy` | Energy accumulator seed | Optional: pre-populate consumed Wh | +| `exported-energy` | Energy accumulator seed | Optional: pre-populate produced Wh | + +**Energy profile mode** is inferred from context: + +- Circuits with a `pv-0/feed` reference pointing to them: `mode: "producer"` +- Circuits with a `bess-0/feed` reference pointing to them: `mode: "bidirectional"` +- Circuits with an `evse-*/feed` reference pointing to them: `mode: "bidirectional"`, `device_type: "evse"` +- Everything else: `mode: "consumer"` + +**Circuit ID** in the YAML uses a stable scheme: `circuit_{space}` (e.g. `circuit_5` for space 5, `circuit_7` for a 240V circuit at spaces 7/9). The Homie UUID +is not carried over — the simulator generates its own UUIDs deterministically from circuit IDs. + +### Template strategy + +Each circuit gets its own template named `clone_{space}` (e.g. `clone_5`). While this produces more templates than a hand-authored config, it preserves +per-circuit fidelity from the real panel. The user can consolidate templates later via the dashboard. + +Defaults applied to all cloned templates: + +```yaml +energy_profile: + mode: + power_range: [0, ] + typical_power: + power_variation: 0.1 +relay_behavior: +priority: +``` + +### BESS mapping + +If `$description` contains a node with type `energy.ebus.device.bess`: + +| eBus property | YAML target | +| --------------------------- | ----------------------------------------------- | +| `bess-0/nameplate-capacity` | `battery_behavior.nameplate_capacity_kwh` | +| `bess-0/soc` | Initial SOC (engine start state) | +| `bess-0/feed` | Identifies which circuit is the battery circuit | + +The battery circuit template gets: + +```yaml +battery_behavior: + enabled: true + charge_mode: "custom" + nameplate_capacity_kwh: + backup_reserve_pct: 20.0 + charge_efficiency: 0.95 + discharge_efficiency: 0.95 + max_charge_power: + max_discharge_power: + charge_hours: [0, 1, 2, 3, 4, 5] + discharge_hours: [16, 17, 18, 19, 20, 21] + idle_hours: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 22, 23] +``` + +Charge/discharge schedule uses sensible defaults since the real panel's schedule is not exposed on the eBus. + +### PV mapping + +If `$description` contains a node with type `energy.ebus.device.pv`: + +| eBus property | YAML target | +| ------------------------- | ------------------------------------------ | +| `pv-0/nameplate-capacity` | `energy_profile.nameplate_capacity_w` | +| `pv-0/feed` | Identifies which circuit is the PV circuit | + +The PV circuit template gets: + +```yaml +device_type: "pv" +energy_profile: + mode: "producer" + power_range: [-, 0] + typical_power: <-nameplate * 0.6> + nameplate_capacity_w: +``` + +### EVSE mapping + +If `$description` contains nodes with type `energy.ebus.device.evse`: + +| eBus property | YAML target | +| ------------- | -------------------------------------------- | +| `evse-*/feed` | Identifies which circuit is the EVSE circuit | + +The EVSE circuit template gets: + +```yaml +device_type: "evse" +energy_profile: + mode: "bidirectional" + power_range: [-, ] + typical_power: +time_of_day_profile: + enabled: true + hour_factors: +``` + +### Simulation params + +Cloned configs use conservative defaults: + +```yaml +simulation_params: + update_interval: 5 + time_acceleration: 1.0 + noise_factor: 0.02 + enable_realistic_behaviors: true +``` + +### Serial number + +The clone serial is `{original_serial}-clone`. This ensures: + +- No MQTT topic collision with a real panel on the same broker +- Clear provenance when inspecting configs +- The simulator's mDNS advertisement is distinguishable + +### Output + +Written to `{config_dir}/{original_serial}-clone.yaml`. If a file with that name already exists, it is overwritten (the user explicitly requested a re-clone). +After writing, the simulator triggers a hot reload to pick up the new panel. + +--- + +## Name-based heuristics (future enhancement) + +A later pass could apply smarter template defaults based on circuit names: + +| Name pattern | Applied template behavior | +| -------------------------- | -------------------------------------- | +| `HVAC`, `AC`, `Heat Pump` | `hvac_type`, cycling pattern | +| `Refrigerator`, `Fridge` | Cycling pattern (15min on / 30min off) | +| `Dryer`, `Washer` | Time-of-day profile (daytime usage) | +| `EV Charger`, `SPAN Drive` | EVSE schedule preset | +| `Pool Pump` | Time-of-day profile + cycling | + +This is not part of the initial implementation. The baseline clone captures the topology and sizing accurately; behavioral patterns are tuned via the dashboard. + +--- + +## Implementation plan + +### Phase 1: WSS server and port plumbing + +**Port constant and CLI arg**: + +- Add `CLONE_WSS_PORT = 19443` to `const.py` +- Add `--clone-wss-port` arg to `__main__.py` (with `CLONE_WSS_PORT` env var fallback) +- Plumb through `SimulatorApp.__init__` and store as `self._clone_wss_port` + +**mDNS advertisement**: + +- Add `clone_wss_port` parameter to `PanelAdvertiser.__init__` +- Advertise `cloneWssPort` in `_ebus._tcp` TXT properties when the port is set + +**WSS server lifecycle** (in `SimulatorApp.run()`): + +- Create a dedicated `aiohttp.web.Application` with a single route: `/ws/clone` +- Bind it to an `ssl.SSLContext` using the existing `CertificateBundle` (same cert/key as MQTTS) +- Start as a `web.TCPSite` on `0.0.0.0:{clone_wss_port}` +- Shut down in the `finally` block alongside the dashboard and bootstrap servers + +The WSS handler accepts the WebSocket upgrade, validates the `clone_panel` message, and drives the scrape-translate-write pipeline, sending status messages as +each phase progresses. + +**Files**: `const.py`, `__main__.py`, `app.py`, `discovery.py` + +### Phase 2: eBus scraper + +New module `scraper.py` in the simulator package. Responsibilities: + +1. Call the panel's v2 REST endpoints for auth and CA cert (using `aiohttp.ClientSession`) +2. Connect via paho-mqtt with TLS (reuse existing infrastructure patterns) +3. Subscribe to `ebus/5/{serial}/#` and collect retained messages +4. Return a `ScrapedPanel` dataclass containing the `$description` dict and all property values + +This is a lightweight, purpose-built client — it does not import span-panel-api. It only needs to parse the `$description` JSON and collect string property +values. + +**Files**: `scraper.py` (new) + +### Phase 3: Translation layer + +New module `clone.py` in the simulator package. Responsibilities: + +1. Parse the `$description` to identify node types and IDs +2. Cross-reference `feed` properties to identify PV/BESS/EVSE circuits +3. Map each circuit's properties to a template + circuit definition +4. Build the complete YAML config dict +5. Validate via existing `validate_yaml_config()` +6. Write to the config directory + +**Files**: `clone.py` (new) + +### Phase 4: Integration trigger (separate repo) + +Service action or config flow option in the SPAN integration that opens a WebSocket to the simulator and sends the `clone_panel` message. This is out of scope +for the simulator repo. + +--- + +## Error handling + +| Failure | Behavior | +| ------------------------------- | -------------------------------------------------- | +| Panel unreachable | Error at `registering` phase, WS error message | +| Bad passphrase | Auth returns 401/403, WS error message | +| MQTT connection refused | Error at `connecting` phase | +| No `$description` received | Timeout after 15s, error at `scraping` phase | +| No circuit nodes in description | Error at `translating` phase | +| Config validation fails | Error at `writing` phase, partial config not saved | +| Existing clone file | Overwritten (intentional re-clone) | + +All errors are reported via the WebSocket `result` message with `status: "error"` and a human-readable `message`. The simulator never crashes on a failed clone +attempt. + +--- + +## Testing + +| Test | Validates | +| -------------------------------------- | ----------------------------------------------- | +| Unit: translate `$description` + props | Correct YAML structure from known eBus fixture | +| Unit: tab derivation from space+dipole | Single-pole and 240V mapping | +| Unit: device type inference from feeds | PV/BESS/EVSE circuit detection | +| Unit: serial suffix | `{serial}-clone` naming | +| Integration: full scrape mock | WebSocket flow with mocked MQTT | +| Integration: config roundtrip | Cloned config loads and simulates without error | From f9331ab5a92c30da6b2555326fe73208d4d9e9c8 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:55:14 -0700 Subject: [PATCH 07/36] Pin span-panel-api to ==2.3.0 in manifest.json --- custom_components/span_panel/manifest.json | 2 +- poetry.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 70171b74..7438c841 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -12,7 +12,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api>=2.3.0" + "span-panel-api==2.3.0" ], "version": "2.0.3", "zeroconf": [ diff --git a/poetry.lock b/poetry.lock index 38dbbfce..2c6339b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4520,7 +4520,7 @@ test = ["covdefaults (==2.3.0)", "pytest (==8.4.1)", "pytest-aiohttp (==1.1.0)", [[package]] name = "span-panel-api" -version = "2.2.4" +version = "2.3.0" description = "A client library for SPAN Panel API" optional = false python-versions = ">=3.10,<4.0" From 6ed08f21d0ced54b645efdd32933d7559b1016c7 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:00:15 -0700 Subject: [PATCH 08/36] Fix sync-dependencies to preserve exact version specifier from manifest The script was hardcoding >= and ^ operators when syncing versions from manifest.json to ci.yml, ignoring the actual specifier (e.g. ==2.3.0). Now extracts and writes the full specifier as-is. Also removes unused ci-simulation-example.yml workflow. --- .github/workflows/ci-simulation-example.yml | 62 --------------------- .github/workflows/ci.yml | 2 +- scripts/sync-dependencies.py | 24 ++++---- 3 files changed, 13 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/ci-simulation-example.yml diff --git a/.github/workflows/ci-simulation-example.yml b/.github/workflows/ci-simulation-example.yml deleted file mode 100644 index 83a6353a..00000000 --- a/.github/workflows/ci-simulation-example.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: CI with Simulation Tests - -on: - workflow_call: - -jobs: - lint-and-test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.13", "3.14"] - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Install Poetry - uses: snok/install-poetry@v1 - - - name: Install dependencies - run: | - # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.1"/' pyproject.toml - sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml - # Regenerate lock file with the modified dependencies - poetry lock - poetry install --with dev - # Install bandit with TOML support - poetry run pip install 'bandit[toml]' - - - name: Format check with ruff - run: poetry run ruff format --check custom_components/span_panel - - - name: Lint with ruff - run: poetry run ruff check custom_components/span_panel - - - name: Type check with mypy - run: poetry run mypy custom_components/span_panel - - - name: Security check with bandit - run: poetry run bandit -c pyproject.toml -r custom_components/span_panel - - - name: Check poetry configuration - run: poetry check - - - name: Run pre-commit hooks (for extra validation) - run: poetry run pre-commit run --all-files --show-diff-on-failure - - # Regular tests - simulation tests are automatically skipped - - name: Run tests with coverage - run: poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing - - # Optional: Run simulation tests separately if desired - - name: Run simulation tests - env: - SPAN_USE_REAL_SIMULATION: 1 - run: poetry run pytest tests/test_solar_configuration_with_simulator.py -v diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 199a85f6..b66d3918 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.3.0"/' pyproject.toml + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.0"/' pyproject.toml sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml # Regenerate lock file with the modified dependencies poetry lock diff --git a/scripts/sync-dependencies.py b/scripts/sync-dependencies.py index 40fda34f..e84424f1 100755 --- a/scripts/sync-dependencies.py +++ b/scripts/sync-dependencies.py @@ -29,13 +29,13 @@ def get_manifest_versions(): for req in requirements: if req.startswith("span-panel-api"): - # Extract version from span-panel-api>=2.0.0 or span-panel-api~=1.1.0 - match = re.search(r"span-panel-api[>~=]+([0-9.]+)", req) + # Extract full specifier (e.g. ==2.3.0, >=2.0.0, ~=1.1.0) + match = re.search(r"span-panel-api([>~=!]+[0-9.]+)", req) if match: versions["span-panel-api"] = match.group(1) elif req.startswith("ha-synthetic-sensors"): - # Extract version from ha-synthetic-sensors>=1.0.8 or ~=1.0.8 - match = re.search(r"ha-synthetic-sensors[>~=]+([0-9.]+)", req) + # Extract full specifier (e.g. >=1.0.8, ~=1.0.8) + match = re.search(r"ha-synthetic-sensors([>~=!]+[0-9.]+)", req) if match: versions["ha-synthetic-sensors"] = match.group(1) @@ -58,21 +58,21 @@ def update_ci_workflow(versions): original_content = content - # Update span-panel-api version (handles ^, >=, ~= specifiers) + # Update span-panel-api version — preserve exact specifier from manifest if "span-panel-api" in versions: - span_version = versions["span-panel-api"] + span_spec = versions["span-panel-api"] content = re.sub( - r'span-panel-api = "[>=~^]+[0-9.]+"', - f'span-panel-api = ">={span_version}"', + r'span-panel-api = "[>=~^!]+[0-9.]+"', + f'span-panel-api = "{span_spec}"', content, ) - # Update ha-synthetic-sensors version (handles ^, >=, ~= specifiers) + # Update ha-synthetic-sensors version — preserve exact specifier from manifest if "ha-synthetic-sensors" in versions: - ha_version = versions["ha-synthetic-sensors"] + ha_spec = versions["ha-synthetic-sensors"] content = re.sub( - r'ha-synthetic-sensors = "[>=~^]+[0-9.]+"', - f'ha-synthetic-sensors = "^{ha_version}"', + r'ha-synthetic-sensors = "[>=~^!]+[0-9.]+"', + f'ha-synthetic-sensors = "{ha_spec}"', content, ) From 0e93d17e9468fd9df067c0d55111fd0d288d4a2e Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:54:37 -0700 Subject: [PATCH 09/36] Add python-socketio as dev dependency socketio was only in manifest.json (runtime) but missing from dev dependencies, breaking test collection. --- custom_components/span_panel/config_flow.py | 24 +- custom_components/span_panel/const.py | 1 - custom_components/span_panel/manifest.json | 3 +- .../span_panel/simulation_utils.py | 140 +++-- custom_components/span_panel/strings.json | 4 +- .../span_panel/translations/en.json | 4 +- docs/dev/simulator_panel_clone.md | 510 ------------------ poetry.lock | 91 +++- pyproject.toml | 1 + 9 files changed, 173 insertions(+), 605 deletions(-) delete mode 100644 docs/dev/simulator_panel_clone.md diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index 16f6ed65..eb41cc7e 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -39,7 +39,6 @@ CONF_HOP_PASSPHRASE, CONF_HTTP_PORT, CONF_PANEL_SERIAL, - DEFAULT_CLONE_WSS_PORT, DOMAIN, ENABLE_ENERGY_DIP_COMPENSATION, ENTITY_NAMING_PATTERN, @@ -758,8 +757,11 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Con menu_options: dict[str, str] = { "general_options": "General Options", } - # Clone via simulator is only available for v2 panels (eBus) - if self.config_entry.data.get(CONF_API_VERSION) == "v2": + # Clone via simulator is only available for real v2 panels (eBus). + # Simulator entries have serials prefixed with "sim-". + serial = self.config_entry.unique_id or "" + is_simulator = serial.lower().startswith("sim-") + if self.config_entry.data.get(CONF_API_VERSION) == "v2" and not is_simulator: menu_options["clone_panel_to_simulation"] = "Clone Panel To Simulation" return self.async_show_menu( @@ -799,12 +801,12 @@ async def async_step_clone_panel_to_simulation( """Clone the panel to a simulator discovered via mDNS or entered manually.""" errors: dict[str, str] = {} default_host = "" - default_port = DEFAULT_CLONE_WSS_PORT + default_http_port = 8081 panel_name = self.config_entry.data.get("device_name", self.config_entry.title) if user_input is not None: sim_host = str(user_input.get("simulator_host", "")).strip() - sim_port = int(user_input.get("clone_wss_port", DEFAULT_CLONE_WSS_PORT)) + sim_http_port = int(user_input.get("simulator_http_port", 8081)) if not sim_host: errors["simulator_host"] = "host_required" @@ -816,9 +818,11 @@ async def async_step_clone_panel_to_simulation( result = await execute_clone_via_simulator( simulator_host=sim_host, - simulator_port=sim_port, + simulator_http_port=sim_http_port, panel_host=panel_host, panel_passphrase=passphrase, + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, ) if result.success: @@ -836,24 +840,24 @@ async def async_step_clone_panel_to_simulation( errors["base"] = "clone_failed" default_host = sim_host - default_port = sim_port + default_http_port = sim_http_port else: # First visit — try mDNS discovery for simulators simulators = await discover_clone_simulators(self.hass) if simulators: default_host = simulators[0].host - default_port = simulators[0].clone_wss_port + default_http_port = simulators[0].http_port _LOGGER.debug( "Discovered %d simulator(s) with clone support; using %s:%d", len(simulators), default_host, - default_port, + default_http_port, ) schema = vol.Schema( { vol.Required("simulator_host", default=default_host): str, - vol.Required("clone_wss_port", default=default_port): int, + vol.Required("simulator_http_port", default=default_http_port): int, } ) return self.async_show_form( diff --git a/custom_components/span_panel/const.py b/custom_components/span_panel/const.py index 9899763f..49b3f6f9 100644 --- a/custom_components/span_panel/const.py +++ b/custom_components/span_panel/const.py @@ -57,7 +57,6 @@ ENABLE_ENERGY_DIP_COMPENSATION = "enable_energy_dip_compensation" DEFAULT_SNAPSHOT_INTERVAL: Final[float] = 5.0 -DEFAULT_CLONE_WSS_PORT: Final[int] = 19443 class CircuitRelayState(enum.Enum): diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 7438c841..8dec63db 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -12,7 +12,8 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api==2.3.0" + "span-panel-api==2.3.0", + "python-socketio>=5.0" ], "version": "2.0.3", "zeroconf": [ diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py index cbb6cc8a..b5e50522 100644 --- a/custom_components/span_panel/simulation_utils.py +++ b/custom_components/span_panel/simulation_utils.py @@ -1,39 +1,39 @@ -"""Simulator clone utilities for SPAN Panel integration. +"""Simulator utilities for SPAN Panel integration. Discovers simulators on the local network via mDNS and delegates panel -cloning to the simulator over its WebSocket endpoint. The simulator -handles eBus scraping, translation, and config writing — the integration -only provides the target panel's address and passphrase. +cloning to the simulator over Socket.IO. The simulator handles eBus +scraping, translation, and config writing -- the integration provides +the target panel's address, passphrase, and HA's location so the clone +is configured with the correct timezone and seasonal parameters. """ from __future__ import annotations import asyncio from dataclasses import dataclass -import json import logging -import ssl -import aiohttp from homeassistant.components import zeroconf as ha_zeroconf from homeassistant.core import HomeAssistant +import socketio from zeroconf import ServiceStateChange, Zeroconf from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo _LOGGER = logging.getLogger(__name__) EBUS_SERVICE_TYPE = "_ebus._tcp.local." -CLONE_WSS_PORT_PROPERTY = "cloneWssPort" +HTTP_PORT_PROPERTY = "httpPort" DISCOVERY_TIMEOUT_SECONDS = 3.0 CLONE_OPERATION_TIMEOUT_SECONDS = 120 +SIO_NAMESPACE = "/v1/panel" @dataclass class SimulatorInfo: - """A simulator discovered via mDNS that supports panel cloning.""" + """A simulator discovered via mDNS.""" host: str - clone_wss_port: int + http_port: int name: str @@ -51,11 +51,11 @@ class CloneResult: async def discover_clone_simulators(hass: HomeAssistant) -> list[SimulatorInfo]: - """Browse for simulators advertising a clone WSS port via mDNS. + """Browse for simulators via mDNS. Looks for ``_ebus._tcp.local.`` services whose TXT record contains - ``cloneWssPort``. Discovery runs for a short window and returns - all matching services found. + ``httpPort`` (simulators advertise this; real panels do not). + Discovery runs for a short window and returns all matching services. """ aiozc = await ha_zeroconf.async_get_async_instance(hass) zc = aiozc.zeroconf @@ -92,8 +92,8 @@ def _on_state_change( val = raw_val.decode() if isinstance(raw_val, bytes) else str(raw_val) props[key] = val - port_str = props.get(CLONE_WSS_PORT_PROPERTY) or props.get(CLONE_WSS_PORT_PROPERTY.lower()) - if not port_str: + http_port_str = props.get(HTTP_PORT_PROPERTY) or props.get(HTTP_PORT_PROPERTY.lower()) + if not http_port_str: continue addresses = info.parsed_scoped_addresses() @@ -103,7 +103,7 @@ def _on_state_change( simulators.append( SimulatorInfo( host=host.rstrip("."), - clone_wss_port=int(port_str), + http_port=int(http_port_str), name=display_name, ) ) @@ -113,84 +113,68 @@ def _on_state_change( async def execute_clone_via_simulator( simulator_host: str, - simulator_port: int, + simulator_http_port: int, panel_host: str, panel_passphrase: str | None, + latitude: float, + longitude: float, ) -> CloneResult: - """Open a WSS connection to the simulator and run a panel clone. + """Clone a panel via the simulator's Socket.IO endpoint. - The simulator connects to the real panel's eBus, scrapes retained - messages, translates them into a simulation YAML config, and writes - the file. This function streams status updates to the log and - returns the final result. + Connects to the simulator's ``/v1/panel`` namespace and emits a + ``clone_panel`` event that triggers the scrape-translate-write + pipeline. HA's location is included so the clone config gets the + correct timezone and seasonal parameters. """ - url = f"wss://{simulator_host}:{simulator_port}/ws/clone" - # The simulator uses a self-signed certificate; trust it for - # local-network communication. - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE + url = f"http://{simulator_host}:{simulator_http_port}" + client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() try: async with asyncio.timeout(CLONE_OPERATION_TIMEOUT_SECONDS): - async with aiohttp.ClientSession() as session: - async with session.ws_connect(url, ssl=ssl_context) as ws: - await ws.send_json( - { - "type": "clone_panel", - "host": panel_host, - "passphrase": panel_passphrase, - } - ) - - async for msg in ws: - if msg.type == aiohttp.WSMsgType.TEXT: - data: dict[str, str | int] = json.loads(msg.data) - msg_type = str(data.get("type", "")) - - if msg_type == "status": - _LOGGER.debug( - "Clone progress: %s — %s", - data.get("phase", ""), - data.get("detail", ""), - ) - elif msg_type == "result": - if data.get("status") == "ok": - return CloneResult( - success=True, - serial=str(data.get("serial", "")), - clone_serial=str(data.get("clone_serial", "")), - filename=str(data.get("filename", "")), - circuits=int(data.get("circuits", 0)), - ) - return CloneResult( - success=False, - error_message=str(data.get("message", "Unknown error")), - error_phase=str(data.get("phase", "")), - ) - - elif msg.type in ( - aiohttp.WSMsgType.ERROR, - aiohttp.WSMsgType.CLOSED, - ): - return CloneResult( - success=False, - error_message="WebSocket connection closed unexpectedly", - ) + await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) + + result = await client.call( + "clone_panel", + { + "host": panel_host, + "passphrase": panel_passphrase, + "latitude": latitude, + "longitude": longitude, + }, + ) + + if not isinstance(result, dict): + return CloneResult( + success=False, + error_message="Unexpected response from simulator", + ) + + if result.get("status") == "ok": + return CloneResult( + success=True, + serial=str(result.get("serial", "")), + clone_serial=str(result.get("clone_serial", "")), + filename=str(result.get("filename", "")), + circuits=int(result.get("circuits", 0)), + ) + + return CloneResult( + success=False, + error_message=str(result.get("message", "Unknown error")), + error_phase=str(result.get("phase", "")), + ) except TimeoutError: return CloneResult( success=False, error_message="Clone operation timed out", ) - except aiohttp.ClientError as err: + except Exception as err: return CloneResult( success=False, error_message=f"Cannot connect to simulator: {err}", ) - - return CloneResult( - success=False, - error_message="Clone completed without receiving a result", - ) + finally: + if client.connected: + await client.disconnect() diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index 4d4db1d8..1e98d064 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -122,11 +122,11 @@ "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", "data": { "simulator_host": "Simulator Host", - "clone_wss_port": "Clone WSS Port" + "simulator_http_port": "Simulator HTTP Port" }, "data_description": { "simulator_host": "IP address or hostname of the SPAN Panel Simulator", - "clone_wss_port": "WebSocket port for the clone endpoint" + "simulator_http_port": "HTTP port for the simulator's Socket.IO endpoint" } } } diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index 5816255e..81d679ee 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -122,11 +122,11 @@ "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", "data": { "simulator_host": "Simulator Host", - "clone_wss_port": "Clone WSS Port" + "simulator_http_port": "Simulator HTTP Port" }, "data_description": { "simulator_host": "IP address or hostname of the SPAN Panel Simulator", - "clone_wss_port": "WebSocket port for the clone endpoint" + "simulator_http_port": "HTTP port for the simulator's Socket.IO endpoint" } } } diff --git a/docs/dev/simulator_panel_clone.md b/docs/dev/simulator_panel_clone.md deleted file mode 100644 index 4b30ada2..00000000 --- a/docs/dev/simulator_panel_clone.md +++ /dev/null @@ -1,510 +0,0 @@ -# eBus Panel Clone — Simulator Feature - -## Context - -The simulator supports BESS/SPAN Drive simulation, breaker ratings, and full Homie v5 eBus publishing. The SPAN integration can already clone the in-memory -simulator config via the dashboard. This feature extends that: given credentials for a **real** SPAN panel, the simulator connects to its eBus, scrapes every -retained topic, and translates the result into a simulator YAML config — a faithful starting point that can then be tuned. - -The integration initiates the clone by contacting the simulator over a WebSocket, passing the target panel's host and passphrase. The simulator handles the -entire scrape-and-translate pipeline, writes the new config, and reloads. - ---- - -## Scope - -**Simulator** (this feature): - -- New WebSocket endpoint accepting clone requests -- Lightweight MQTT scraper (connect to real panel, collect retained messages) -- eBus-to-YAML translation layer -- Config write + hot reload - -**Integration** (future, not covered here): - -- UI/service action to trigger a clone request against the simulator - ---- - -## Architecture - -### Transport: WSS over TLS - -The clone WebSocket runs over TLS (WSS), not plain WS. The simulator already generates a `CertificateBundle` at startup for its MQTTS broker — the clone WSS -endpoint reuses the same certificate and key. This keeps credential management in one place and means the integration's connection to the simulator is encrypted -end-to-end, which matters because the passphrase for the real panel traverses this link. - -The WSS endpoint runs on its own dedicated port, separate from the dashboard HTTP server. This avoids mixing long-lived WebSocket connections with the HTMX -request/response traffic on the dashboard, and allows the clone port to be independently firewalled or exposed. - -### Port configuration - -The clone WSS port follows the same pattern as every other simulator port: - -| Layer | Mechanism | -| ---------------- | ----------------------------------------------------------- | -| **Default** | `CLONE_WSS_PORT = 19443` in `const.py` | -| **CLI arg** | `--clone-wss-port 19443` | -| **Env var** | `CLONE_WSS_PORT=19443` | -| **SimulatorApp** | Plumbed through `__init__` alongside `dashboard_port`, etc. | - -### mDNS discovery - -The integration needs to know which port the clone WSS endpoint is listening on. The simulator's `PanelAdvertiser` already puts custom properties into the -`_ebus._tcp` TXT record — `httpPort` is advertised when it differs from the default. The clone WSS port is advertised the same way: - -```python -# In PanelAdvertiser.register_panel(), alongside the existing httpPort logic: -ebus_properties: dict[str, str] = { - "homie_domain": "ebus", - "homie_version": "5", - "homie_roles": "device", - "mqtt_broker": hostname, - "txtvers": "1", -} -if self._http_port != 80: - ebus_properties["httpPort"] = str(self._http_port) -if self._clone_wss_port: - ebus_properties["cloneWssPort"] = str(self._clone_wss_port) -``` - -The integration reads TXT properties from zeroconf discovery records. When `cloneWssPort` is present, the integration knows this simulator supports panel -cloning and which port to connect to. When absent, clone functionality is unavailable (real panel, older simulator, or clone not configured). - -This also means the `_span._tcp` TXT record does not need changes — `cloneWssPort` is a simulator-only capability advertised on the eBus service type that the -integration already parses. - -### Sequence - -```text -Integration / Dashboard Simulator Real Panel - | | | - |-- WSS: clone_panel ----->| | - | {host, passphrase} | | - | |-- POST /api/v2/auth/register ->| - | |<-- {mqtt_creds, serial} -------| - | |-- GET /api/v2/certificate/ca ->| - | |<-- PEM cert -------------------| - |<-- WSS: "registering" | | - | |== MQTTS connect ===============| - | |-- SUB ebus/5/{serial}/# ------>| - | |<-- $state, $description -------| - | |<-- retained property msgs -----| - |<-- WSS: "scraping" | | - | | (collect until stable) | - | |== MQTT disconnect =============| - |<-- WSS: "translating" | | - | |-- parse $description | - | |-- map properties -> YAML | - | |-- write configs/{serial}-clone.yaml - | |-- trigger reload | - |<-- WSS: "done" | | - | {serial, filename} | | -``` - -### Why WebSocket - -The clone is inherently async — network round-trips to the real panel, waiting for retained messages, translation. A WebSocket lets the simulator stream status -updates back to the caller as each phase completes, and the caller can display progress or abort. - -### Why the simulator scrapes directly - -The simulator already has paho-mqtt infrastructure and runs its own MQTT broker. Having it connect directly to the real panel's broker (as a one-shot client) -avoids routing data through the integration and keeps the translation logic co-located with the config format it produces. The integration's only job is to -provide the panel address and passphrase. - ---- - -## WebSocket Contract - -### Endpoint - -`wss://{simulator_host}:{clone_wss_port}/ws/clone` - -The port is discovered via the `cloneWssPort` TXT property in the simulator's `_ebus._tcp` mDNS record. The TLS certificate is the simulator's self-signed CA — -the same one returned by `GET /api/v2/certificate/ca` on the simulator's bootstrap HTTP server. The integration already fetches and trusts this CA for MQTTS, so -it can reuse the same trust store for the WSS connection. - -### Request message (integration sends) - -```json -{ - "type": "clone_panel", - "host": "192.168.1.100", - "passphrase": "panel-passphrase" -} -``` - -| Field | Type | Required | Description | -| ------------ | -------------- | -------- | ------------------------------------- | -| `type` | string | yes | Must be `"clone_panel"` | -| `host` | string | yes | IP or hostname of the real SPAN panel | -| `passphrase` | string or null | no | Panel passphrase (null = door-bypass) | - -### Status messages (simulator sends) - -```json -{ - "type": "status", - "phase": "registering", - "detail": "Authenticating with panel at 192.168.1.100" -} -``` - -| Phase | Meaning | -| ------------- | ------------------------------------------------------------ | -| `registering` | Calling `/api/v2/auth/register` and `/api/v2/certificate/ca` | -| `connecting` | Opening MQTTS connection to the panel's broker | -| `scraping` | Subscribed to `ebus/5/{serial}/#`, collecting retained msgs | -| `translating` | Parsing `$description` and mapping properties to YAML | -| `writing` | Writing config file and triggering reload | -| `done` | Clone complete | -| `error` | Clone failed | - -### Completion message - -```json -{ - "type": "result", - "status": "ok", - "serial": "nj-2316-XXXX", - "clone_serial": "nj-2316-XXXX-clone", - "filename": "nj-2316-XXXX-clone.yaml", - "circuits": 16, - "has_bess": true, - "has_pv": true, - "has_evse": false -} -``` - -### Error message - -```json -{ - "type": "result", - "status": "error", - "phase": "connecting", - "message": "MQTTS connection refused: bad credentials" -} -``` - ---- - -## eBus Scrape Strategy - -### Authentication - -1. `POST http://{host}/api/v2/auth/register` with `{"name": "sim-clone-{uuid4}", "hopPassphrase": passphrase}` -2. Extract `ebusBrokerUsername`, `ebusBrokerPassword`, `ebusBrokerMqttsPort`, `serialNumber` -3. `GET http://{host}/api/v2/certificate/ca` for TLS trust - -### MQTT collection - -1. Connect via MQTTS (port from auth response, CA cert from step 3) -2. Subscribe to `ebus/5/{serial}/#` with QoS 0 -3. Collect all retained messages into a `dict[str, str]` keyed by full topic -4. Stability gate: stop collecting when no new topics arrive for 5 seconds (retained messages arrive in a burst shortly after subscription) -5. Disconnect cleanly - -### Required topics - -The scraper must receive at minimum: - -| Topic pattern | Purpose | -| ------------------------------- | ------------------------------- | -| `$state` | Confirm panel is `ready` | -| `$description` | Node topology (types, node IDs) | -| `core/breaker-rating` | Main breaker size | -| `core/serial-number` | Panel identity | -| `{circuit-uuid}/name` | Circuit names | -| `{circuit-uuid}/space` | Tab/breaker position | -| `{circuit-uuid}/dipole` | 240V detection | -| `{circuit-uuid}/breaker-rating` | Per-circuit breaker size | -| `{circuit-uuid}/relay` | Current relay state | -| `{circuit-uuid}/shed-priority` | Circuit priority | -| `{circuit-uuid}/active-power` | Current power (W) | - -Optional but used when available: - -| Topic pattern | Purpose | -| -------------------------------- | --------------------------- | -| `{circuit-uuid}/imported-energy` | Seed energy accumulators | -| `{circuit-uuid}/exported-energy` | Seed energy accumulators | -| `bess-0/nameplate-capacity` | Battery sizing | -| `bess-0/soc` | Initial SOC | -| `bess-0/grid-state` | Grid state at clone time | -| `pv-0/nameplate-capacity` | PV system sizing | -| `pv-0/feed` | Which circuit PV feeds | -| `evse-*/feed` | Which circuit EVSE feeds | -| `evse-*/status` | Charger state at clone time | -| `upstream-lugs/active-power` | Grid power reference | -| `power-flows/*` | Validation / sanity check | - ---- - -## eBus-to-YAML Translation - -### Units - -All power values on the eBus are in **watts**. The Homie schema historically declared circuit `active-power` as `kW`, but this is a schema metadata error — -actual published values have always been watts. SPAN firmware 202609 corrects the schema declaration to `W`. The simulator config also uses watts. No unit -conversion is needed anywhere in the pipeline. - -### Panel config - -| eBus source | YAML target | Notes | -| ------------------------------ | ---------------------------- | ----------------------------------- | -| `core/serial-number` | `panel_config.serial_number` | Append `-clone` suffix | -| `core/breaker-rating` | `panel_config.main_size` | Integer amps | -| Panel size from `$description` | `panel_config.total_tabs` | Count circuit space range | -| — | `panel_config.latitude` | Default 37.7 (user adjusts later) | -| — | `panel_config.longitude` | Default -122.4 (user adjusts later) | - -Panel size is derived from the `$description` by examining the Homie schema's circuit `space` property format string (e.g. `"1:32:1"` means 32 spaces). This -matches how span-panel-api determines panel size. - -### Circuit mapping - -For each node in `$description` with type `energy.ebus.device.circuit`: - -| eBus property | YAML target | Derivation | -| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------ | -| `name` | `circuits[].name` | Direct | -| `space` | `circuits[].tabs` | `[space]` for single-pole; `[space, space+2]` if dipole | -| `dipole` | — | Determines tab count (true = 240V, 2 tabs) | -| `breaker-rating` | `circuits[].breaker_rating` | Integer amps | -| `relay` | `circuit_templates[].relay_behavior` | `OPEN`/`CLOSED` = `controllable`; presence of `always-on: true` = `non_controllable` | -| `shed-priority` | `circuit_templates[].priority` | Direct (`NEVER`, `SOC_THRESHOLD`, `OFF_GRID`) | -| `active-power` | `circuit_templates[].energy_profile.typical_power` | Absolute value in watts | -| `active-power` | `circuit_templates[].energy_profile.power_range` | `[0, breaker_rating * voltage]` | -| `imported-energy` | Energy accumulator seed | Optional: pre-populate consumed Wh | -| `exported-energy` | Energy accumulator seed | Optional: pre-populate produced Wh | - -**Energy profile mode** is inferred from context: - -- Circuits with a `pv-0/feed` reference pointing to them: `mode: "producer"` -- Circuits with a `bess-0/feed` reference pointing to them: `mode: "bidirectional"` -- Circuits with an `evse-*/feed` reference pointing to them: `mode: "bidirectional"`, `device_type: "evse"` -- Everything else: `mode: "consumer"` - -**Circuit ID** in the YAML uses a stable scheme: `circuit_{space}` (e.g. `circuit_5` for space 5, `circuit_7` for a 240V circuit at spaces 7/9). The Homie UUID -is not carried over — the simulator generates its own UUIDs deterministically from circuit IDs. - -### Template strategy - -Each circuit gets its own template named `clone_{space}` (e.g. `clone_5`). While this produces more templates than a hand-authored config, it preserves -per-circuit fidelity from the real panel. The user can consolidate templates later via the dashboard. - -Defaults applied to all cloned templates: - -```yaml -energy_profile: - mode: - power_range: [0, ] - typical_power: - power_variation: 0.1 -relay_behavior: -priority: -``` - -### BESS mapping - -If `$description` contains a node with type `energy.ebus.device.bess`: - -| eBus property | YAML target | -| --------------------------- | ----------------------------------------------- | -| `bess-0/nameplate-capacity` | `battery_behavior.nameplate_capacity_kwh` | -| `bess-0/soc` | Initial SOC (engine start state) | -| `bess-0/feed` | Identifies which circuit is the battery circuit | - -The battery circuit template gets: - -```yaml -battery_behavior: - enabled: true - charge_mode: "custom" - nameplate_capacity_kwh: - backup_reserve_pct: 20.0 - charge_efficiency: 0.95 - discharge_efficiency: 0.95 - max_charge_power: - max_discharge_power: - charge_hours: [0, 1, 2, 3, 4, 5] - discharge_hours: [16, 17, 18, 19, 20, 21] - idle_hours: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 22, 23] -``` - -Charge/discharge schedule uses sensible defaults since the real panel's schedule is not exposed on the eBus. - -### PV mapping - -If `$description` contains a node with type `energy.ebus.device.pv`: - -| eBus property | YAML target | -| ------------------------- | ------------------------------------------ | -| `pv-0/nameplate-capacity` | `energy_profile.nameplate_capacity_w` | -| `pv-0/feed` | Identifies which circuit is the PV circuit | - -The PV circuit template gets: - -```yaml -device_type: "pv" -energy_profile: - mode: "producer" - power_range: [-, 0] - typical_power: <-nameplate * 0.6> - nameplate_capacity_w: -``` - -### EVSE mapping - -If `$description` contains nodes with type `energy.ebus.device.evse`: - -| eBus property | YAML target | -| ------------- | -------------------------------------------- | -| `evse-*/feed` | Identifies which circuit is the EVSE circuit | - -The EVSE circuit template gets: - -```yaml -device_type: "evse" -energy_profile: - mode: "bidirectional" - power_range: [-, ] - typical_power: -time_of_day_profile: - enabled: true - hour_factors: -``` - -### Simulation params - -Cloned configs use conservative defaults: - -```yaml -simulation_params: - update_interval: 5 - time_acceleration: 1.0 - noise_factor: 0.02 - enable_realistic_behaviors: true -``` - -### Serial number - -The clone serial is `{original_serial}-clone`. This ensures: - -- No MQTT topic collision with a real panel on the same broker -- Clear provenance when inspecting configs -- The simulator's mDNS advertisement is distinguishable - -### Output - -Written to `{config_dir}/{original_serial}-clone.yaml`. If a file with that name already exists, it is overwritten (the user explicitly requested a re-clone). -After writing, the simulator triggers a hot reload to pick up the new panel. - ---- - -## Name-based heuristics (future enhancement) - -A later pass could apply smarter template defaults based on circuit names: - -| Name pattern | Applied template behavior | -| -------------------------- | -------------------------------------- | -| `HVAC`, `AC`, `Heat Pump` | `hvac_type`, cycling pattern | -| `Refrigerator`, `Fridge` | Cycling pattern (15min on / 30min off) | -| `Dryer`, `Washer` | Time-of-day profile (daytime usage) | -| `EV Charger`, `SPAN Drive` | EVSE schedule preset | -| `Pool Pump` | Time-of-day profile + cycling | - -This is not part of the initial implementation. The baseline clone captures the topology and sizing accurately; behavioral patterns are tuned via the dashboard. - ---- - -## Implementation plan - -### Phase 1: WSS server and port plumbing - -**Port constant and CLI arg**: - -- Add `CLONE_WSS_PORT = 19443` to `const.py` -- Add `--clone-wss-port` arg to `__main__.py` (with `CLONE_WSS_PORT` env var fallback) -- Plumb through `SimulatorApp.__init__` and store as `self._clone_wss_port` - -**mDNS advertisement**: - -- Add `clone_wss_port` parameter to `PanelAdvertiser.__init__` -- Advertise `cloneWssPort` in `_ebus._tcp` TXT properties when the port is set - -**WSS server lifecycle** (in `SimulatorApp.run()`): - -- Create a dedicated `aiohttp.web.Application` with a single route: `/ws/clone` -- Bind it to an `ssl.SSLContext` using the existing `CertificateBundle` (same cert/key as MQTTS) -- Start as a `web.TCPSite` on `0.0.0.0:{clone_wss_port}` -- Shut down in the `finally` block alongside the dashboard and bootstrap servers - -The WSS handler accepts the WebSocket upgrade, validates the `clone_panel` message, and drives the scrape-translate-write pipeline, sending status messages as -each phase progresses. - -**Files**: `const.py`, `__main__.py`, `app.py`, `discovery.py` - -### Phase 2: eBus scraper - -New module `scraper.py` in the simulator package. Responsibilities: - -1. Call the panel's v2 REST endpoints for auth and CA cert (using `aiohttp.ClientSession`) -2. Connect via paho-mqtt with TLS (reuse existing infrastructure patterns) -3. Subscribe to `ebus/5/{serial}/#` and collect retained messages -4. Return a `ScrapedPanel` dataclass containing the `$description` dict and all property values - -This is a lightweight, purpose-built client — it does not import span-panel-api. It only needs to parse the `$description` JSON and collect string property -values. - -**Files**: `scraper.py` (new) - -### Phase 3: Translation layer - -New module `clone.py` in the simulator package. Responsibilities: - -1. Parse the `$description` to identify node types and IDs -2. Cross-reference `feed` properties to identify PV/BESS/EVSE circuits -3. Map each circuit's properties to a template + circuit definition -4. Build the complete YAML config dict -5. Validate via existing `validate_yaml_config()` -6. Write to the config directory - -**Files**: `clone.py` (new) - -### Phase 4: Integration trigger (separate repo) - -Service action or config flow option in the SPAN integration that opens a WebSocket to the simulator and sends the `clone_panel` message. This is out of scope -for the simulator repo. - ---- - -## Error handling - -| Failure | Behavior | -| ------------------------------- | -------------------------------------------------- | -| Panel unreachable | Error at `registering` phase, WS error message | -| Bad passphrase | Auth returns 401/403, WS error message | -| MQTT connection refused | Error at `connecting` phase | -| No `$description` received | Timeout after 15s, error at `scraping` phase | -| No circuit nodes in description | Error at `translating` phase | -| Config validation fails | Error at `writing` phase, partial config not saved | -| Existing clone file | Overwritten (intentional re-clone) | - -All errors are reported via the WebSocket `result` message with `status: "error"` and a human-readable `message`. The simulator never crashes on a failed clone -attempt. - ---- - -## Testing - -| Test | Validates | -| -------------------------------------- | ----------------------------------------------- | -| Unit: translate `$description` + props | Correct YAML structure from known eBus fixture | -| Unit: tab derivation from space+dipole | Single-pole and 240V mapping | -| Unit: device type inference from feeds | PV/BESS/EVSE circuit detection | -| Unit: serial suffix | `{serial}-clone` naming | -| Integration: full scrape mock | WebSocket flow with mocked MQTT | -| Integration: config roundtrip | Cloned config loads and simulates without error | diff --git a/poetry.lock b/poetry.lock index 2c6339b0..6b792226 100644 --- a/poetry.lock +++ b/poetry.lock @@ -612,6 +612,18 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "bidict" +version = "0.23.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, + {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, +] + [[package]] name = "bleak" version = "1.1.1" @@ -4085,6 +4097,27 @@ files = [ {file = "python_direnv-0.2.2.tar.gz", hash = "sha256:0fe2fb834c901d675edcacc688689cfcf55cf06d9cf27dc7d3768a6c38c35f00"}, ] +[[package]] +name = "python-engineio" +version = "4.13.1" +description = "Engine.IO server and client for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399"}, + {file = "python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066"}, +] + +[package.dependencies] +simple-websocket = ">=0.10.0" + +[package.extras] +asyncio-client = ["aiohttp (>=3.11)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +dev = ["tox"] +docs = ["furo", "sphinx"] + [[package]] name = "python-slugify" version = "8.0.4" @@ -4103,6 +4136,28 @@ text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] +[[package]] +name = "python-socketio" +version = "5.16.1" +description = "Socket.IO server and client for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35"}, + {file = "python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89"}, +] + +[package.dependencies] +bidict = ">=0.21.0" +python-engineio = ">=4.11.0" + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +dev = ["tox"] +docs = ["furo", "sphinx"] + [[package]] name = "pytz" version = "2025.2" @@ -4474,6 +4529,25 @@ regex = "2024.11.6" [package.extras] dev = ["black (==24.8.0)", "build (==1.2.2)", "flake8 (==7.2.0)", "mypy (==1.14.0)", "pylint (==3.2.7)", "pytest (==8.3.5)", "pytest-asyncio (==1.1.0)", "tox (==4.26.0)"] +[[package]] +name = "simple-websocket" +version = "1.1.0" +description = "Simple WebSocket server and client for Python" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, + {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, +] + +[package.dependencies] +wsproto = "*" + +[package.extras] +dev = ["flake8", "pytest", "pytest-cov", "tox"] +docs = ["sphinx"] + [[package]] name = "six" version = "1.17.0" @@ -5315,6 +5389,21 @@ winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.System[all] (>=3.2.1.0,<3.3.0.0)"] +[[package]] +name = "wsproto" +version = "1.3.2" +description = "Pure-Python WebSocket protocol implementation" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, + {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, +] + +[package.dependencies] +h11 = ">=0.16.0,<1" + [[package]] name = "yarl" version = "1.22.0" @@ -5548,4 +5637,4 @@ ifaddr = ">=0.1.7" [metadata] lock-version = "2.1" python-versions = ">=3.14.2,<3.15" -content-hash = "2db133614283a9dad55d9bcf7a8425f77ed42f2f22bfc80db87cf8b490e88243" +content-hash = "21e41bdd4e4f56ce23181d9634c3da0bd50673f2f4fad9bf225b6b648c91b09f" diff --git a/pyproject.toml b/pyproject.toml index fb4b3fd2..eccc3dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ pylint = "==4.0.5" # For import-outside-toplevel checks pytest = "^9.0.0" # Compatible with Python 3.14 pytest-homeassistant-custom-component = "^0.13.315" # Latest version compatible with HA 2026.2.x isort = "*" +python-socketio = "^5.16.1" [build-system] requires = ["poetry-core"] From a5fcce9415ff14b7d00af3fdadd6d5cf9bf17bbc Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:40:24 -0700 Subject: [PATCH 10/36] Refactor simulation utils for modularity and correctness - Extract shared _sio_call helper to eliminate duplicated Socket.IO connect/call/disconnect pattern in simulation_utils.py - Move clone-then-profile orchestration from simulation_utils into config_flow.py where it belongs (OptionsFlowHandler._apply_usage_profiles) - Replace bare except Exception with narrower exception types - Use DOMAIN constant instead of raw string in simulator_profile_builder - Add Literal types for statistics_during_period parameters - Remove all lazy imports in favor of top-level imports --- custom_components/span_panel/config_flow.py | 57 +++- .../span_panel/simulation_utils.py | 134 +++++++-- .../span_panel/simulator_profile_builder.py | 255 ++++++++++++++++++ 3 files changed, 416 insertions(+), 30 deletions(-) create mode 100644 custom_components/span_panel/simulator_profile_builder.py diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index eb41cc7e..fda6ddc4 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -55,7 +55,9 @@ from .simulation_utils import ( discover_clone_simulators, execute_clone_via_simulator, + send_usage_profiles, ) +from .simulator_profile_builder import build_usage_profiles _LOGGER = logging.getLogger(__name__) @@ -825,20 +827,25 @@ async def async_step_clone_panel_to_simulation( longitude=self.hass.config.longitude, ) - if result.success: + if not result.success: + _LOGGER.error( + "Clone failed at %s: %s", + result.error_phase, + result.error_message, + ) + errors["base"] = "clone_failed" + else: _LOGGER.info( "Panel cloned to simulator: %s (%d circuits)", result.clone_serial, result.circuits, ) + await self._apply_usage_profiles(sim_host, sim_http_port, result.clone_serial) return self.async_create_entry( title="", data=dict(self.config_entry.options), ) - _LOGGER.error("Clone failed at %s: %s", result.error_phase, result.error_message) - errors["base"] = "clone_failed" - default_host = sim_host default_http_port = sim_http_port else: @@ -869,6 +876,48 @@ async def async_step_clone_panel_to_simulation( errors=errors, ) + async def _apply_usage_profiles( + self, + simulator_host: str, + simulator_http_port: int, + clone_serial: str, + ) -> None: + """Build HA-derived usage profiles and send them to the simulator (best-effort). + + Failures are logged but do not affect the clone result. + """ + try: + profiles = await build_usage_profiles(self.hass, self.config_entry) + except (KeyError, ValueError): + _LOGGER.exception("Failed to build usage profiles from recorder data") + return + + if not profiles: + _LOGGER.info("No usage profiles to send (no recorder data)") + return + + try: + profile_result = await send_usage_profiles( + simulator_host=simulator_host, + simulator_http_port=simulator_http_port, + clone_serial=clone_serial, + profiles=profiles, + ) + except (TimeoutError, OSError): + _LOGGER.exception("Failed to deliver usage profiles to simulator") + return + + if profile_result.success: + _LOGGER.info( + "Applied usage profiles: %d templates updated", + profile_result.circuits_updated, + ) + else: + _LOGGER.warning( + "Profile delivery failed: %s", + profile_result.error_message, + ) + # Register the config flow handler config_entries.HANDLERS.register(DOMAIN)(SpanPanelConfigFlow) diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py index b5e50522..54941ff4 100644 --- a/custom_components/span_panel/simulation_utils.py +++ b/custom_components/span_panel/simulation_utils.py @@ -25,6 +25,7 @@ HTTP_PORT_PROPERTY = "httpPort" DISCOVERY_TIMEOUT_SECONDS = 3.0 CLONE_OPERATION_TIMEOUT_SECONDS = 120 +PROFILE_OPERATION_TIMEOUT_SECONDS = 30 SIO_NAMESPACE = "/v1/panel" @@ -50,6 +51,44 @@ class CloneResult: error_phase: str = "" +@dataclass +class ProfileResult: + """Outcome of a usage-profile delivery operation.""" + + success: bool + circuits_updated: int = 0 + error_message: str = "" + + +async def _sio_call( + host: str, + port: int, + event: str, + data: dict[str, object], + timeout_seconds: int, +) -> dict[str, object]: + """Connect to the simulator's Socket.IO namespace, emit an event, and return the response. + + Raises TimeoutError if the operation exceeds ``timeout_seconds``. + Raises ValueError if the response is not a dict. + """ + url = f"http://{host}:{port}" + client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() + + try: + async with asyncio.timeout(timeout_seconds): + await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) + result = await client.call(event, data) + + if not isinstance(result, dict): + raise TypeError("Unexpected response from simulator") + + return result + finally: + if client.connected: + await client.disconnect() + + async def discover_clone_simulators(hass: HomeAssistant) -> list[SimulatorInfo]: """Browse for simulators via mDNS. @@ -126,29 +165,19 @@ async def execute_clone_via_simulator( pipeline. HA's location is included so the clone config gets the correct timezone and seasonal parameters. """ - - url = f"http://{simulator_host}:{simulator_http_port}" - client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() - try: - async with asyncio.timeout(CLONE_OPERATION_TIMEOUT_SECONDS): - await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) - - result = await client.call( - "clone_panel", - { - "host": panel_host, - "passphrase": panel_passphrase, - "latitude": latitude, - "longitude": longitude, - }, - ) - - if not isinstance(result, dict): - return CloneResult( - success=False, - error_message="Unexpected response from simulator", - ) + result = await _sio_call( + host=simulator_host, + port=simulator_http_port, + event="clone_panel", + data={ + "host": panel_host, + "passphrase": panel_passphrase, + "latitude": latitude, + "longitude": longitude, + }, + timeout_seconds=CLONE_OPERATION_TIMEOUT_SECONDS, + ) if result.get("status") == "ok": return CloneResult( @@ -156,7 +185,7 @@ async def execute_clone_via_simulator( serial=str(result.get("serial", "")), clone_serial=str(result.get("clone_serial", "")), filename=str(result.get("filename", "")), - circuits=int(result.get("circuits", 0)), + circuits=int(str(result.get("circuits", 0))), ) return CloneResult( @@ -170,11 +199,64 @@ async def execute_clone_via_simulator( success=False, error_message="Clone operation timed out", ) + except TypeError as err: + return CloneResult( + success=False, + error_message=str(err), + ) except Exception as err: return CloneResult( success=False, error_message=f"Cannot connect to simulator: {err}", ) - finally: - if client.connected: - await client.disconnect() + + +async def send_usage_profiles( + simulator_host: str, + simulator_http_port: int, + clone_serial: str, + profiles: dict[str, dict[str, object]], +) -> ProfileResult: + """Push usage profiles to the simulator via Socket.IO. + + Connects to the simulator's ``/v1/panel`` namespace and emits an + ``apply_usage_profiles`` event with the clone serial and profile data. + """ + try: + result = await _sio_call( + host=simulator_host, + port=simulator_http_port, + event="apply_usage_profiles", + data={ + "clone_serial": clone_serial, + "profiles": profiles, + }, + timeout_seconds=PROFILE_OPERATION_TIMEOUT_SECONDS, + ) + + if result.get("status") == "ok": + return ProfileResult( + success=True, + circuits_updated=int(str(result.get("templates_updated", 0))), + ) + + return ProfileResult( + success=False, + error_message=str(result.get("message", "Unknown error")), + ) + + except TimeoutError: + return ProfileResult( + success=False, + error_message="Profile delivery timed out", + ) + except TypeError as err: + return ProfileResult( + success=False, + error_message=str(err), + ) + except Exception as err: + return ProfileResult( + success=False, + error_message=f"Cannot connect to simulator: {err}", + ) diff --git a/custom_components/span_panel/simulator_profile_builder.py b/custom_components/span_panel/simulator_profile_builder.py new file mode 100644 index 00000000..228cc417 --- /dev/null +++ b/custom_components/span_panel/simulator_profile_builder.py @@ -0,0 +1,255 @@ +"""Build per-circuit usage profiles from HA recorder statistics. + +Queries the recorder's long-term statistics for each circuit's power +sensor, derives time-of-day patterns, monthly seasonality, duty cycle, +and average consumption, then packages them as a dict keyed by simulator +template name (``clone_{tab}``). + +The result is sent to the simulator via Socket.IO so the clone config +reflects real consumption patterns rather than synthetic noise. +""" + +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +import math +from typing import TYPE_CHECKING, Literal + +from homeassistant.components.recorder.statistics import ( + statistics_during_period, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .helpers import build_circuit_unique_id + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + + from .coordinator import SpanPanelCoordinator + +_LOGGER = logging.getLogger(__name__) + +# Minimum hours of recorder data before a circuit is included +_MIN_HOURLY_POINTS = 24 + +# Minimum distinct months before monthly_factors are emitted +_MIN_MONTHS_FOR_SEASONAL = 3 + +# Circuits with duty_cycle >= this are considered always-on; skip the field +_DUTY_CYCLE_CEILING = 0.8 + +# Hardware-driven modes whose power profiles should not be overridden +_SKIP_DEVICE_TYPES = frozenset({"pv", "bess"}) + + +async def build_usage_profiles( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, dict[str, object]]: + """Derive per-circuit usage profiles from recorder statistics. + + Returns a dict keyed by simulator template name (``clone_{tab}``) + with sub-dicts containing any combination of: + + - ``typical_power`` (float, watts) + - ``power_variation`` (float, 0.0–1.0) + - ``hour_factors`` (dict[int, float], 0–23 → 0.0–1.0) + - ``duty_cycle`` (float, 0.0–1.0) + - ``monthly_factors`` (dict[int, float], 1–12 → 0.0–1.0) + """ + coordinator: SpanPanelCoordinator = hass.data[DOMAIN][config_entry.entry_id] + snapshot = coordinator.data + if snapshot is None: + _LOGGER.warning("No snapshot available for profile building") + return {} + + serial = snapshot.serial_number + entity_reg = er.async_get(hass) + + # Map each circuit to (template_name, entity_id) for power sensor lookup + circuit_map: list[tuple[str, str, str]] = [] # (template_name, entity_id, circuit_id) + + for circuit_id, circuit in snapshot.circuits.items(): + if circuit_id.startswith("unmapped_tab_"): + continue + + # Skip hardware-driven device types + if getattr(circuit, "device_type", "circuit") in _SKIP_DEVICE_TYPES: + continue + + tabs = getattr(circuit, "tabs", None) + if not tabs: + continue + + template_name = f"clone_{min(tabs)}" + + # Look up the power sensor entity_id via entity registry + unique_id = build_circuit_unique_id(serial, circuit_id, "instantPowerW") + entity_id = entity_reg.async_get_entity_id("sensor", "span_panel", unique_id) + if entity_id is None: + _LOGGER.debug( + "No power sensor entity for circuit %s (unique_id=%s)", + circuit_id, + unique_id, + ) + continue + + circuit_map.append((template_name, entity_id, circuit_id)) + + if not circuit_map: + _LOGGER.info("No circuits eligible for profile building") + return {} + + stat_ids = {entity_id for _, entity_id, _ in circuit_map} + now = dt_util.utcnow() + + # Query 1: hourly stats for the last 30 days + hourly_start = now - timedelta(days=30) + hourly_stats = await _query_statistics( + hass, hourly_start, now, stat_ids, "hour", {"mean", "min", "max"} + ) + + # Query 2: monthly stats for the last 12 months + monthly_start = now - timedelta(days=365) + monthly_stats = await _query_statistics(hass, monthly_start, now, stat_ids, "month", {"mean"}) + + # Build profiles + profiles: dict[str, dict[str, object]] = {} + + for template_name, entity_id, circuit_id in circuit_map: + hourly_rows = hourly_stats.get(entity_id, []) + if len(hourly_rows) < _MIN_HOURLY_POINTS: + _LOGGER.debug( + "Circuit %s has only %d hourly points, skipping", + circuit_id, + len(hourly_rows), + ) + continue + + profile = _derive_profile(hourly_rows, monthly_stats.get(entity_id, [])) + if profile: + profiles[template_name] = profile + + _LOGGER.info( + "Built usage profiles for %d/%d circuits", + len(profiles), + len(circuit_map), + ) + return profiles + + +_StatPeriod = Literal["5minute", "day", "hour", "week", "month"] +_StatType = Literal["change", "last_reset", "max", "mean", "min", "state", "sum"] + + +async def _query_statistics( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime, + stat_ids: set[str], + period: _StatPeriod, + stat_types: set[_StatType], +) -> dict[str, list[dict[str, object]]]: + """Run statistics_during_period and return the result dict.""" + return await hass.async_add_executor_job( + statistics_during_period, # type: ignore[arg-type] + hass, + start_time, + end_time, + stat_ids, + period, + None, + stat_types, + ) + + +def _derive_profile( + hourly_rows: list[dict[str, object]], + monthly_rows: list[dict[str, object]], +) -> dict[str, object]: + """Compute profile parameters from raw statistics rows.""" + profile: dict[str, object] = {} + + # Extract hourly means and maxes + hourly_means: list[float] = [] + hourly_maxes: list[float] = [] + hour_buckets: dict[int, list[float]] = {h: [] for h in range(24)} + + for row in hourly_rows: + mean_val = row.get("mean") + max_val = row.get("max") + start = row.get("start") + + if mean_val is None or not isinstance(mean_val, int | float): + continue + + abs_mean = abs(float(mean_val)) + hourly_means.append(abs_mean) + + if max_val is not None and isinstance(max_val, int | float): + hourly_maxes.append(abs(float(max_val))) + + # Bucket by hour-of-day + if isinstance(start, datetime): + hour_buckets[start.hour].append(abs_mean) + + if not hourly_means: + return profile + + # typical_power: mean of hourly means + typical_power = sum(hourly_means) / len(hourly_means) + profile["typical_power"] = round(typical_power, 1) + + # power_variation: coefficient of variation, clamped to [0.0, 1.0] + if typical_power > 0: + variance = sum((v - typical_power) ** 2 for v in hourly_means) / len(hourly_means) + stddev = math.sqrt(variance) + cv = stddev / typical_power + profile["power_variation"] = round(min(max(cv, 0.0), 1.0), 3) + + # hour_factors: average by hour-of-day, normalized so peak = 1.0 + hour_averages: dict[int, float] = {} + for h in range(24): + bucket = hour_buckets[h] + if bucket: + hour_averages[h] = sum(bucket) / len(bucket) + else: + hour_averages[h] = 0.0 + + peak_hour = max(hour_averages.values()) if hour_averages else 0.0 + if peak_hour > 0: + hour_factors = {h: round(v / peak_hour, 3) for h, v in hour_averages.items()} + profile["hour_factors"] = hour_factors + + # duty_cycle: mean(hourly_means) / mean(hourly_maxes) + if hourly_maxes: + mean_of_maxes = sum(hourly_maxes) / len(hourly_maxes) + if mean_of_maxes > 0: + duty = typical_power / mean_of_maxes + if duty < _DUTY_CYCLE_CEILING: + profile["duty_cycle"] = round(duty, 3) + + # monthly_factors from monthly stats (requires 3+ distinct months) + if len(monthly_rows) >= _MIN_MONTHS_FOR_SEASONAL: + monthly_means: dict[int, float] = {} + for row in monthly_rows: + mean_val = row.get("mean") + start = row.get("start") + if ( + mean_val is not None + and isinstance(mean_val, int | float) + and isinstance(start, datetime) + ): + monthly_means[start.month] = abs(float(mean_val)) + + if len(monthly_means) >= _MIN_MONTHS_FOR_SEASONAL: + peak_month = max(monthly_means.values()) + if peak_month > 0: + monthly_factors = {m: round(v / peak_month, 3) for m, v in monthly_means.items()} + profile["monthly_factors"] = monthly_factors + + return profile From c651735fa9828d933012dd7ea81d5c70fe59f0c4 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:58:52 -0700 Subject: [PATCH 11/36] Use runtime_data for coordinator access in profile builder The coordinator is stored in config_entry.runtime_data, not hass.data[DOMAIN][entry_id]. Also guard against runtime_data being absent if the entry hasn't completed setup yet. --- .../span_panel/simulator_profile_builder.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/span_panel/simulator_profile_builder.py b/custom_components/span_panel/simulator_profile_builder.py index 228cc417..32c2c252 100644 --- a/custom_components/span_panel/simulator_profile_builder.py +++ b/custom_components/span_panel/simulator_profile_builder.py @@ -22,15 +22,12 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from .const import DOMAIN from .helpers import build_circuit_unique_id if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant - from .coordinator import SpanPanelCoordinator - _LOGGER = logging.getLogger(__name__) # Minimum hours of recorder data before a circuit is included @@ -61,8 +58,12 @@ async def build_usage_profiles( - ``duty_cycle`` (float, 0.0–1.0) - ``monthly_factors`` (dict[int, float], 1–12 → 0.0–1.0) """ - coordinator: SpanPanelCoordinator = hass.data[DOMAIN][config_entry.entry_id] - snapshot = coordinator.data + if not hasattr(config_entry, "runtime_data") or config_entry.runtime_data is None: + _LOGGER.warning( + "Config entry %s has no runtime data (not yet set up?)", config_entry.entry_id + ) + return {} + snapshot = config_entry.runtime_data.coordinator.data if snapshot is None: _LOGGER.warning("No snapshot available for profile building") return {} From a5f1337e98a9249235fcdb6b5515c454651e2108 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:09:14 -0700 Subject: [PATCH 12/36] Use single Socket.IO session for clone and profile delivery Instead of opening separate connections for clone and profiles, keep the connection open after clone_panel and wait for the simulator to emit clone_ready before sending apply_usage_profiles. This eliminates the race where profiles are sent before the simulator has registered the clone config. Profiles are built before connecting and passed into clone_with_profiles so the entire operation runs on one session. --- custom_components/span_panel/config_flow.py | 57 +++--- .../span_panel/simulation_utils.py | 188 +++++++++--------- 2 files changed, 113 insertions(+), 132 deletions(-) diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index fda6ddc4..dae8da0a 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -53,9 +53,8 @@ SNAPSHOT_UPDATE_INTERVAL, ) from .simulation_utils import ( + clone_with_profiles, discover_clone_simulators, - execute_clone_via_simulator, - send_usage_profiles, ) from .simulator_profile_builder import build_usage_profiles @@ -818,13 +817,16 @@ async def async_step_clone_panel_to_simulation( if passphrase == "": passphrase = None - result = await execute_clone_via_simulator( + profiles = await self._build_profiles_best_effort() + + result = await clone_with_profiles( simulator_host=sim_host, simulator_http_port=sim_http_port, panel_host=panel_host, panel_passphrase=passphrase, latitude=self.hass.config.latitude, longitude=self.hass.config.longitude, + profiles=profiles, ) if not result.success: @@ -840,7 +842,17 @@ async def async_step_clone_panel_to_simulation( result.clone_serial, result.circuits, ) - await self._apply_usage_profiles(sim_host, sim_http_port, result.clone_serial) + if result.profile_result is not None: + if result.profile_result.success: + _LOGGER.info( + "Applied usage profiles: %d templates updated", + result.profile_result.circuits_updated, + ) + else: + _LOGGER.debug( + "Profile delivery skipped: %s", + result.profile_result.error_message, + ) return self.async_create_entry( title="", data=dict(self.config_entry.options), @@ -876,47 +888,24 @@ async def async_step_clone_panel_to_simulation( errors=errors, ) - async def _apply_usage_profiles( + async def _build_profiles_best_effort( self, - simulator_host: str, - simulator_http_port: int, - clone_serial: str, - ) -> None: - """Build HA-derived usage profiles and send them to the simulator (best-effort). + ) -> dict[str, dict[str, object]] | None: + """Build HA-derived usage profiles from recorder data (best-effort). - Failures are logged but do not affect the clone result. + Returns ``None`` on failure so the clone can proceed without profiles. """ try: profiles = await build_usage_profiles(self.hass, self.config_entry) except (KeyError, ValueError): _LOGGER.exception("Failed to build usage profiles from recorder data") - return + return None if not profiles: _LOGGER.info("No usage profiles to send (no recorder data)") - return + return None - try: - profile_result = await send_usage_profiles( - simulator_host=simulator_host, - simulator_http_port=simulator_http_port, - clone_serial=clone_serial, - profiles=profiles, - ) - except (TimeoutError, OSError): - _LOGGER.exception("Failed to deliver usage profiles to simulator") - return - - if profile_result.success: - _LOGGER.info( - "Applied usage profiles: %d templates updated", - profile_result.circuits_updated, - ) - else: - _LOGGER.warning( - "Profile delivery failed: %s", - profile_result.error_message, - ) + return profiles # Register the config flow handler diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py index 54941ff4..a01343cb 100644 --- a/custom_components/span_panel/simulation_utils.py +++ b/custom_components/span_panel/simulation_utils.py @@ -10,7 +10,7 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass +from dataclasses import dataclass, field import logging from homeassistant.components import zeroconf as ha_zeroconf @@ -25,7 +25,7 @@ HTTP_PORT_PROPERTY = "httpPort" DISCOVERY_TIMEOUT_SECONDS = 3.0 CLONE_OPERATION_TIMEOUT_SECONDS = 120 -PROFILE_OPERATION_TIMEOUT_SECONDS = 30 +PROFILE_READY_TIMEOUT_SECONDS = 30 SIO_NAMESPACE = "/v1/panel" @@ -38,6 +38,15 @@ class SimulatorInfo: name: str +@dataclass +class ProfileResult: + """Outcome of a usage-profile delivery operation.""" + + success: bool + circuits_updated: int = 0 + error_message: str = "" + + @dataclass class CloneResult: """Outcome of a clone-via-simulator operation.""" @@ -49,44 +58,7 @@ class CloneResult: circuits: int = 0 error_message: str = "" error_phase: str = "" - - -@dataclass -class ProfileResult: - """Outcome of a usage-profile delivery operation.""" - - success: bool - circuits_updated: int = 0 - error_message: str = "" - - -async def _sio_call( - host: str, - port: int, - event: str, - data: dict[str, object], - timeout_seconds: int, -) -> dict[str, object]: - """Connect to the simulator's Socket.IO namespace, emit an event, and return the response. - - Raises TimeoutError if the operation exceeds ``timeout_seconds``. - Raises ValueError if the response is not a dict. - """ - url = f"http://{host}:{port}" - client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() - - try: - async with asyncio.timeout(timeout_seconds): - await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) - result = await client.call(event, data) - - if not isinstance(result, dict): - raise TypeError("Unexpected response from simulator") - - return result - finally: - if client.connected: - await client.disconnect() + profile_result: ProfileResult | None = field(default=None, repr=False) async def discover_clone_simulators(hass: HomeAssistant) -> list[SimulatorInfo]: @@ -150,89 +122,114 @@ def _on_state_change( return simulators -async def execute_clone_via_simulator( +async def clone_with_profiles( simulator_host: str, simulator_http_port: int, panel_host: str, panel_passphrase: str | None, latitude: float, longitude: float, + profiles: dict[str, dict[str, object]] | None = None, ) -> CloneResult: - """Clone a panel via the simulator's Socket.IO endpoint. + """Clone a panel via Socket.IO, optionally applying usage profiles. - Connects to the simulator's ``/v1/panel`` namespace and emits a - ``clone_panel`` event that triggers the scrape-translate-write - pipeline. HA's location is included so the clone config gets the - correct timezone and seasonal parameters. + Runs the entire operation on a single Socket.IO connection: + + 1. Emit ``clone_panel`` and wait for the response. + 2. If the clone succeeded and *profiles* were supplied, wait for the + simulator to emit ``clone_ready`` before sending + ``apply_usage_profiles``. + + Keeping the connection open avoids a race where the clone config has + not yet been registered when profiles are delivered. """ + url = f"http://{simulator_host}:{simulator_http_port}" + client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() + try: - result = await _sio_call( - host=simulator_host, - port=simulator_http_port, - event="clone_panel", - data={ - "host": panel_host, - "passphrase": panel_passphrase, - "latitude": latitude, - "longitude": longitude, - }, - timeout_seconds=CLONE_OPERATION_TIMEOUT_SECONDS, - ) + async with asyncio.timeout(CLONE_OPERATION_TIMEOUT_SECONDS): + await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) - if result.get("status") == "ok": + result = await client.call( + "clone_panel", + { + "host": panel_host, + "passphrase": panel_passphrase, + "latitude": latitude, + "longitude": longitude, + }, + ) + + if not isinstance(result, dict): return CloneResult( - success=True, - serial=str(result.get("serial", "")), - clone_serial=str(result.get("clone_serial", "")), - filename=str(result.get("filename", "")), - circuits=int(str(result.get("circuits", 0))), + success=False, + error_message="Unexpected response from simulator", ) - return CloneResult( - success=False, - error_message=str(result.get("message", "Unknown error")), - error_phase=str(result.get("phase", "")), + if result.get("status") != "ok": + return CloneResult( + success=False, + error_message=str(result.get("message", "Unknown error")), + error_phase=str(result.get("phase", "")), + ) + + clone_result = CloneResult( + success=True, + serial=str(result.get("serial", "")), + clone_serial=str(result.get("clone_serial", "")), + filename=str(result.get("filename", "")), + circuits=int(str(result.get("circuits", 0))), ) + if profiles and clone_result.clone_serial: + clone_result.profile_result = await _wait_ready_and_send_profiles( + client, clone_result.clone_serial, profiles + ) + + return clone_result + except TimeoutError: return CloneResult( success=False, error_message="Clone operation timed out", ) - except TypeError as err: - return CloneResult( - success=False, - error_message=str(err), - ) except Exception as err: return CloneResult( success=False, error_message=f"Cannot connect to simulator: {err}", ) + finally: + if client.connected: + await client.disconnect() -async def send_usage_profiles( - simulator_host: str, - simulator_http_port: int, +async def _wait_ready_and_send_profiles( + client: socketio.AsyncSimpleClient, clone_serial: str, profiles: dict[str, dict[str, object]], ) -> ProfileResult: - """Push usage profiles to the simulator via Socket.IO. - - Connects to the simulator's ``/v1/panel`` namespace and emits an - ``apply_usage_profiles`` event with the clone serial and profile data. - """ + """Wait for ``clone_ready`` then emit ``apply_usage_profiles`` on the same connection.""" try: - result = await _sio_call( - host=simulator_host, - port=simulator_http_port, - event="apply_usage_profiles", - data={ - "clone_serial": clone_serial, - "profiles": profiles, - }, - timeout_seconds=PROFILE_OPERATION_TIMEOUT_SECONDS, - ) + async with asyncio.timeout(PROFILE_READY_TIMEOUT_SECONDS): + # The simulator emits clone_ready when the clone config is registered + while True: + event = await client.receive() + if event[0] == "clone_ready": + break + + result = await client.call( + "apply_usage_profiles", + { + "clone_serial": clone_serial, + "profiles": profiles, + }, + ) + + if not isinstance(result, dict): + return ProfileResult( + success=False, + error_message="Unexpected response from simulator", + ) if result.get("status") == "ok": return ProfileResult( @@ -248,15 +245,10 @@ async def send_usage_profiles( except TimeoutError: return ProfileResult( success=False, - error_message="Profile delivery timed out", - ) - except TypeError as err: - return ProfileResult( - success=False, - error_message=str(err), + error_message="Timed out waiting for simulator clone_ready", ) except Exception as err: return ProfileResult( success=False, - error_message=f"Cannot connect to simulator: {err}", + error_message=f"Profile delivery failed: {err}", ) From 6be314e1d8584b1c323372103ca543232d933b48 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:59:37 -0700 Subject: [PATCH 13/36] Use recorder executor for statistics queries in profile builder HA requires database access to go through the recorder's own executor, not the general hass.async_add_executor_job. Fixes the "Detected code that accesses the database without the database executor" warning. --- custom_components/span_panel/simulator_profile_builder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/span_panel/simulator_profile_builder.py b/custom_components/span_panel/simulator_profile_builder.py index 32c2c252..f1dce179 100644 --- a/custom_components/span_panel/simulator_profile_builder.py +++ b/custom_components/span_panel/simulator_profile_builder.py @@ -16,6 +16,7 @@ import math from typing import TYPE_CHECKING, Literal +from homeassistant.components.recorder import get_instance as get_recorder from homeassistant.components.recorder.statistics import ( statistics_during_period, ) @@ -155,8 +156,8 @@ async def _query_statistics( period: _StatPeriod, stat_types: set[_StatType], ) -> dict[str, list[dict[str, object]]]: - """Run statistics_during_period and return the result dict.""" - return await hass.async_add_executor_job( + """Run statistics_during_period on the recorder's executor.""" + return await get_recorder(hass).async_add_executor_job( statistics_during_period, # type: ignore[arg-type] hass, start_time, From f697f5f68b2c0a11788d4696b16ecadf83addff8 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:27:21 -0700 Subject: [PATCH 14/36] Fix profile builder: timestamp type, timezone, and outlier robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - statistics_during_period returns start as float (Unix timestamp), not datetime — isinstance(start, datetime) silently failed so hour_factors and monthly_factors were never computed - Convert UTC timestamps to HA's local timezone before bucketing by hour, since the simulator engine uses local time - Replace mean with median throughout to handle sensor glitch spikes (e.g. 400kW on a 15A circuit) that destroy mean-based calculations - Use IQR-based power_variation instead of coefficient of variation - Use recorder executor for database queries per HA requirements --- .../span_panel/simulator_profile_builder.py | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/custom_components/span_panel/simulator_profile_builder.py b/custom_components/span_panel/simulator_profile_builder.py index f1dce179..9e2e51e2 100644 --- a/custom_components/span_panel/simulator_profile_builder.py +++ b/custom_components/span_panel/simulator_profile_builder.py @@ -11,9 +11,9 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, tzinfo import logging -import math +import statistics from typing import TYPE_CHECKING, Literal from homeassistant.components.recorder import get_instance as get_recorder @@ -132,7 +132,9 @@ async def build_usage_profiles( ) continue - profile = _derive_profile(hourly_rows, monthly_stats.get(entity_id, [])) + profile = _derive_profile( + hourly_rows, monthly_stats.get(entity_id, []), dt_util.get_default_time_zone() + ) if profile: profiles[template_name] = profile @@ -172,6 +174,7 @@ async def _query_statistics( def _derive_profile( hourly_rows: list[dict[str, object]], monthly_rows: list[dict[str, object]], + local_tz: tzinfo, ) -> dict[str, object]: """Compute profile parameters from raw statistics rows.""" profile: dict[str, object] = {} @@ -195,43 +198,46 @@ def _derive_profile( if max_val is not None and isinstance(max_val, int | float): hourly_maxes.append(abs(float(max_val))) - # Bucket by hour-of-day - if isinstance(start, datetime): - hour_buckets[start.hour].append(abs_mean) + # Bucket by hour-of-day in local time (simulator uses local hours) + if isinstance(start, int | float): + local_hour = datetime.fromtimestamp(start, tz=dt_util.UTC).astimezone(local_tz).hour + hour_buckets[local_hour].append(abs_mean) if not hourly_means: return profile - # typical_power: mean of hourly means - typical_power = sum(hourly_means) / len(hourly_means) + # Use median throughout — robust to sensor glitch spikes that can + # reach 100x the real value and destroy mean-based calculations. + + # typical_power: median of hourly means + typical_power = statistics.median(hourly_means) profile["typical_power"] = round(typical_power, 1) - # power_variation: coefficient of variation, clamped to [0.0, 1.0] - if typical_power > 0: - variance = sum((v - typical_power) ** 2 for v in hourly_means) / len(hourly_means) - stddev = math.sqrt(variance) - cv = stddev / typical_power - profile["power_variation"] = round(min(max(cv, 0.0), 1.0), 3) + # power_variation: IQR-based dispersion relative to median, clamped [0.0, 1.0] + if typical_power > 0 and len(hourly_means) >= 4: + q1, q3 = _quartiles(hourly_means) + iqr_ratio = (q3 - q1) / typical_power + profile["power_variation"] = round(min(max(iqr_ratio, 0.0), 1.0), 3) - # hour_factors: average by hour-of-day, normalized so peak = 1.0 - hour_averages: dict[int, float] = {} + # hour_factors: median by hour-of-day, normalized so peak = 1.0 + hour_medians: dict[int, float] = {} for h in range(24): bucket = hour_buckets[h] if bucket: - hour_averages[h] = sum(bucket) / len(bucket) + hour_medians[h] = statistics.median(bucket) else: - hour_averages[h] = 0.0 + hour_medians[h] = 0.0 - peak_hour = max(hour_averages.values()) if hour_averages else 0.0 + peak_hour = max(hour_medians.values()) if hour_medians else 0.0 if peak_hour > 0: - hour_factors = {h: round(v / peak_hour, 3) for h, v in hour_averages.items()} + hour_factors = {h: round(v / peak_hour, 3) for h, v in hour_medians.items()} profile["hour_factors"] = hour_factors - # duty_cycle: mean(hourly_means) / mean(hourly_maxes) + # duty_cycle: median(hourly_means) / median(hourly_maxes) if hourly_maxes: - mean_of_maxes = sum(hourly_maxes) / len(hourly_maxes) - if mean_of_maxes > 0: - duty = typical_power / mean_of_maxes + median_of_maxes = statistics.median(hourly_maxes) + if median_of_maxes > 0: + duty = typical_power / median_of_maxes if duty < _DUTY_CYCLE_CEILING: profile["duty_cycle"] = round(duty, 3) @@ -244,9 +250,10 @@ def _derive_profile( if ( mean_val is not None and isinstance(mean_val, int | float) - and isinstance(start, datetime) + and isinstance(start, int | float) ): - monthly_means[start.month] = abs(float(mean_val)) + month = datetime.fromtimestamp(start, tz=dt_util.UTC).month + monthly_means[month] = abs(float(mean_val)) if len(monthly_means) >= _MIN_MONTHS_FOR_SEASONAL: peak_month = max(monthly_means.values()) @@ -255,3 +262,15 @@ def _derive_profile( profile["monthly_factors"] = monthly_factors return profile + + +def _quartiles(values: list[float]) -> tuple[float, float]: + """Return (Q1, Q3) for a list of values.""" + sorted_vals = sorted(values) + n = len(sorted_vals) + mid = n // 2 + lower = sorted_vals[:mid] + upper = sorted_vals[mid + (n % 2) :] + q1 = statistics.median(lower) if lower else sorted_vals[0] + q3 = statistics.median(upper) if upper else sorted_vals[-1] + return q1, q3 From b7c9914f30ac546a6f37101002c53d0982a836ac Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:07:32 -0700 Subject: [PATCH 15/36] Add export_circuit_manifest service for simulator add-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes an authoritative circuit-to-entity manifest so the simulator add-on can query HA recorder stats and build usage profiles without reverse-engineering entity IDs from the states API. The service iterates all loaded SPAN panel config entries, resolves each circuit's power sensor entity_id via the entity registry, computes clone template names from breaker tabs, and returns a flat list per panel — ready for the add-on to consume directly. --- custom_components/span_panel/__init__.py | 78 ++++- custom_components/span_panel/services.yaml | 5 + tests/test_circuit_manifest_service.py | 390 +++++++++++++++++++++ 3 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 custom_components/span_panel/services.yaml create mode 100644 tests/test_circuit_manifest_service.py diff --git a/custom_components/span_panel/__init__.py b/custom_components/span_panel/__init__.py index 930476c9..b8790b31 100644 --- a/custom_components/span_panel/__init__.py +++ b/custom_components/span_panel/__init__.py @@ -5,11 +5,18 @@ import asyncio from dataclasses import dataclass import logging +from typing import cast from homeassistant.components.persistent_notification import async_create as pn_create from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import ( + CoreState, + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -41,6 +48,7 @@ DOMAIN, ) from .coordinator import SpanPanelCoordinator +from .helpers import build_circuit_unique_id from .migration import migrate_config_entry_sensors from .options import SNAPSHOT_UPDATE_INTERVAL from .util import snapshot_to_device_info @@ -69,6 +77,9 @@ class SpanPanelRuntimeData: # Config entry version — bumped to 6 for simulation removal CURRENT_CONFIG_VERSION = 6 +# Map internal device_type values to external manifest format +_DEVICE_TYPE_MAP: dict[str, str] = {"bess": "battery"} + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate config entry through successive versions.""" @@ -261,11 +272,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> """Set up Span Panel from a config entry.""" _LOGGER.debug("Setting up entry %s (version %s)", entry.entry_id, entry.version) - # Register WebSocket commands once per HA instance + # Register WebSocket commands and services once per HA instance domain_data: dict[str, bool] = hass.data.setdefault(DOMAIN, {}) if not domain_data.get("websocket_registered"): domain_data["websocket_registered"] = True async_register_commands(hass) + if not domain_data.get("service_registered"): + domain_data["service_registered"] = True + _async_register_services(hass) config = entry.data api_version = config.get(CONF_API_VERSION, "v1") @@ -454,3 +468,63 @@ async def ensure_device_registered( else: device_info = snapshot_to_device_info(snapshot, device_name, host=host) device_registry.async_get_or_create(config_entry_id=entry.entry_id, **device_info) + + +def _async_register_services(hass: HomeAssistant) -> None: + """Register domain-level services (called once per HA instance).""" + + async def async_handle_export_manifest( + _call: ServiceCall, + ) -> ServiceResponse: + """Export circuit manifest for all configured SPAN panels.""" + entity_reg = er.async_get(hass) + panels = [] + + for entry in hass.config_entries.async_entries(DOMAIN): + if not hasattr(entry, "runtime_data") or not isinstance( + entry.runtime_data, SpanPanelRuntimeData + ): + continue + + snapshot = entry.runtime_data.coordinator.data + if snapshot is None: + continue + + serial = snapshot.serial_number + circuits = [] + + for circuit_id, circuit in snapshot.circuits.items(): + if circuit_id.startswith("unmapped_tab_"): + continue + + tabs = getattr(circuit, "tabs", None) + if not tabs: + continue + + unique_id = build_circuit_unique_id(serial, circuit_id, "instantPowerW") + entity_id = entity_reg.async_get_entity_id("sensor", DOMAIN, unique_id) + if entity_id is None: + continue + + raw_type = getattr(circuit, "device_type", "circuit") + + circuits.append( + { + "entity_id": entity_id, + "template": f"clone_{min(tabs)}", + "device_type": _DEVICE_TYPE_MAP.get(raw_type, raw_type), + "tabs": list(tabs), + } + ) + + if circuits: + panels.append({"serial": serial, "circuits": circuits}) + + return cast(ServiceResponse, {"panels": panels}) + + hass.services.async_register( + DOMAIN, + "export_circuit_manifest", + async_handle_export_manifest, + supports_response=SupportsResponse.ONLY, + ) diff --git a/custom_components/span_panel/services.yaml b/custom_components/span_panel/services.yaml new file mode 100644 index 00000000..783a238c --- /dev/null +++ b/custom_components/span_panel/services.yaml @@ -0,0 +1,5 @@ +export_circuit_manifest: + description: >- + Export an authoritative circuit-to-entity manifest for all configured + SPAN panels. Returns panel serial numbers, circuit power sensor entity + IDs, simulator template names, device types, and breaker tab numbers. diff --git a/tests/test_circuit_manifest_service.py b/tests/test_circuit_manifest_service.py new file mode 100644 index 00000000..bf7a825b --- /dev/null +++ b/tests/test_circuit_manifest_service.py @@ -0,0 +1,390 @@ +"""Tests for the export_circuit_manifest service.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from custom_components.span_panel import ( + SpanPanelRuntimeData, + _async_register_services, +) +from custom_components.span_panel.const import DOMAIN + +from tests.factories import ( + SpanCircuitSnapshotFactory, + SpanPanelSnapshotFactory, +) + + +def _make_coordinator(snapshot): + """Create a mock coordinator wrapping a snapshot.""" + coordinator = MagicMock() + coordinator.data = snapshot + return coordinator + + +def _register_power_entity( + hass: HomeAssistant, + config_entry_id: str, + serial: str, + circuit_id: str, + suggested_entity_id: str, +) -> er.RegistryEntry: + """Register a circuit power sensor entity in the entity registry.""" + config_entry = hass.config_entries.async_get_entry(config_entry_id) + entity_registry = er.async_get(hass) + unique_id = f"span_{serial.lower()}_{circuit_id}_power" + return entity_registry.async_get_or_create( + "sensor", + DOMAIN, + unique_id, + config_entry=config_entry, + suggested_object_id=suggested_entity_id.split(".", 1)[1], + ) + + +async def _call_manifest_service(hass: HomeAssistant): + """Call the export_circuit_manifest service and return the response.""" + return await hass.services.async_call( + DOMAIN, + "export_circuit_manifest", + {}, + blocking=True, + return_response=True, + ) + + +class TestExportCircuitManifest: + """Tests for the export_circuit_manifest service.""" + + @pytest.mark.asyncio + async def test_basic_manifest(self, hass: HomeAssistant): + """Returns correct manifest for a panel with circuits.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + kitchen = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_kitchen", name="Kitchen", tabs=[2, 3], + ) + bedroom = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_bedroom", name="Bedroom", tabs=[5], + ) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-test-001", + circuits={"uuid_kitchen": kitchen, "uuid_bedroom": bedroom}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-test-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity( + hass, "span_entry", "sp3-test-001", "uuid_kitchen", + "sensor.span_panel_kitchen_power", + ) + _register_power_entity( + hass, "span_entry", "sp3-test-001", "uuid_bedroom", + "sensor.span_panel_bedroom_power", + ) + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result is not None + assert len(result["panels"]) == 1 + + panel = result["panels"][0] + assert panel["serial"] == "sp3-test-001" + assert len(panel["circuits"]) == 2 + + by_template = {c["template"]: c for c in panel["circuits"]} + + assert by_template["clone_2"]["device_type"] == "circuit" + assert by_template["clone_2"]["tabs"] == [2, 3] + assert by_template["clone_2"]["entity_id"].endswith("_kitchen_power") + + assert by_template["clone_5"]["device_type"] == "circuit" + assert by_template["clone_5"]["tabs"] == [5] + assert by_template["clone_5"]["entity_id"].endswith("_bedroom_power") + + @pytest.mark.asyncio + async def test_multiple_panels(self, hass: HomeAssistant): + """Returns manifests for all loaded panels.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + circuit_a = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_a", tabs=[1], + ) + circuit_b = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_b", tabs=[3], + ) + snapshot_a = SpanPanelSnapshotFactory.create( + serial_number="serial-aaa", circuits={"uuid_a": circuit_a}, + ) + snapshot_b = SpanPanelSnapshotFactory.create( + serial_number="serial-bbb", circuits={"uuid_b": circuit_b}, + ) + + entry_a = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="entry_a", unique_id="serial-aaa", + ) + entry_b = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="entry_b", unique_id="serial-bbb", + ) + entry_a.add_to_hass(hass) + entry_b.add_to_hass(hass) + entry_a.mock_state(hass, ConfigEntryState.LOADED) + entry_b.mock_state(hass, ConfigEntryState.LOADED) + entry_a.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot_a)) + entry_b.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot_b)) + + _register_power_entity(hass, "entry_a", "serial-aaa", "uuid_a", "sensor.panel_a_power") + _register_power_entity(hass, "entry_b", "serial-bbb", "uuid_b", "sensor.panel_b_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result is not None + serials = {p["serial"] for p in result["panels"]} + assert serials == {"serial-aaa", "serial-bbb"} + + @pytest.mark.asyncio + async def test_unmapped_tabs_excluded(self, hass: HomeAssistant): + """Unmapped tab circuits are excluded from the manifest.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + real = SpanCircuitSnapshotFactory.create(circuit_id="uuid_real", tabs=[1]) + unmapped = SpanCircuitSnapshotFactory.create(circuit_id="unmapped_tab_5", tabs=[5]) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", + circuits={"uuid_real": real, "unmapped_tab_5": unmapped}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_real", "sensor.real_power") + _register_power_entity( + hass, "span_entry", "sp3-001", "unmapped_tab_5", "sensor.unmapped_power", + ) + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + panel = result["panels"][0] + templates = [c["template"] for c in panel["circuits"]] + assert "clone_1" in templates + assert "clone_5" not in templates + + @pytest.mark.asyncio + async def test_circuit_without_entity_excluded(self, hass: HomeAssistant): + """Circuits with no registered power entity are excluded.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + registered = SpanCircuitSnapshotFactory.create(circuit_id="uuid_reg", tabs=[1]) + unregistered = SpanCircuitSnapshotFactory.create(circuit_id="uuid_unreg", tabs=[3]) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", + circuits={"uuid_reg": registered, "uuid_unreg": unregistered}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + # Only register entity for one circuit + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_reg", "sensor.reg_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + panel = result["panels"][0] + assert len(panel["circuits"]) == 1 + assert panel["circuits"][0]["template"] == "clone_1" + + @pytest.mark.asyncio + async def test_entry_without_runtime_data_skipped(self, hass: HomeAssistant): + """Entries not yet loaded (no runtime_data) are silently omitted.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + # NOT setting runtime_data + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result is not None + assert result["panels"] == [] + + @pytest.mark.asyncio + async def test_no_entries_returns_empty(self, hass: HomeAssistant): + """No SPAN entries returns empty panels list.""" + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result is not None + assert result["panels"] == [] + + @pytest.mark.asyncio + async def test_all_device_types_included(self, hass: HomeAssistant): + """All device types are included — PV, BESS, EVSE, and regular circuits.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + regular = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_reg", tabs=[1], device_type="circuit", + ) + pv = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_pv", tabs=[5], device_type="pv", + ) + evse = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_evse", tabs=[10, 12], device_type="evse", + ) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", + circuits={ + "uuid_reg": regular, + "uuid_pv": pv, + "uuid_evse": evse, + }, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_reg", "sensor.reg_power") + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_pv", "sensor.pv_power") + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_evse", "sensor.evse_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + panel = result["panels"][0] + assert len(panel["circuits"]) == 3 + + by_template = {c["template"]: c for c in panel["circuits"]} + assert by_template["clone_1"]["device_type"] == "circuit" + assert by_template["clone_5"]["device_type"] == "pv" + assert by_template["clone_10"]["device_type"] == "evse" + + @pytest.mark.asyncio + async def test_bess_device_type_mapped_to_battery(self, hass: HomeAssistant): + """Internal 'bess' device type is mapped to 'battery' in the manifest.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + bess = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_bess", tabs=[14, 16], device_type="bess", + ) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", circuits={"uuid_bess": bess}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_bess", "sensor.bess_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + panel = result["panels"][0] + assert len(panel["circuits"]) == 1 + assert panel["circuits"][0]["device_type"] == "battery" + assert panel["circuits"][0]["template"] == "clone_14" + assert panel["circuits"][0]["tabs"] == [14, 16] + + @pytest.mark.asyncio + async def test_panel_with_no_resolvable_circuits_omitted(self, hass: HomeAssistant): + """Panel where no circuits have registered entities is omitted.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + circuit = SpanCircuitSnapshotFactory.create(circuit_id="uuid_orphan", tabs=[1]) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", circuits={"uuid_orphan": circuit}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + # No entities registered + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result["panels"] == [] + + @pytest.mark.asyncio + async def test_coordinator_with_no_snapshot_skipped(self, hass: HomeAssistant): + """Entry whose coordinator has no data (None snapshot) is skipped.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + coordinator = MagicMock() + coordinator.data = None + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=coordinator) + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result["panels"] == [] + + @pytest.mark.asyncio + async def test_template_uses_min_tab(self, hass: HomeAssistant): + """Template name is clone_{min(tabs)}, not max or first.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + circuit = SpanCircuitSnapshotFactory.create( + circuit_id="uuid_240v", tabs=[8, 6], + ) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", circuits={"uuid_240v": circuit}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_240v", "sensor.c_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + circuit_data = result["panels"][0]["circuits"][0] + assert circuit_data["template"] == "clone_6" + assert circuit_data["tabs"] == [8, 6] From 3e63c4d22b442adb765f659ad00323c80f7ff253 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:07:59 -0700 Subject: [PATCH 16/36] bump versioon to 2.0.4 --- custom_components/span_panel/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 8dec63db..716590a4 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -15,7 +15,7 @@ "span-panel-api==2.3.0", "python-socketio>=5.0" ], - "version": "2.0.3", + "version": "2.0.4", "zeroconf": [ { "type": "_span._tcp.local." From efb2b3e7a243c6a9ed89e4d2e2899cf63a271cba Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:08:09 -0700 Subject: [PATCH 17/36] Register export_circuit_manifest in async_setup per HA core standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move service registration from async_setup_entry to async_setup so the service is available even when no config entries are loaded. The handler raises ServiceValidationError with an informative message instead of silently not existing. Remove service unregistration from async_unload_entry — services stay registered for the lifetime of the domain per HA quality-scale action-setup rule. Also add host field to per-panel manifest response (entry.data[CONF_HOST]) and iterate async_loaded_entries instead of async_entries. --- custom_components/span_panel/__init__.py | 29 +++++-- tests/test_circuit_manifest_service.py | 99 +++++++++++++++++------- 2 files changed, 93 insertions(+), 35 deletions(-) diff --git a/custom_components/span_panel/__init__.py b/custom_components/span_panel/__init__.py index b8790b31..5e4befcd 100644 --- a/custom_components/span_panel/__init__.py +++ b/custom_components/span_panel/__init__.py @@ -21,8 +21,10 @@ ConfigEntryAuthFailed, ConfigEntryError, ConfigEntryNotReady, + ServiceValidationError, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.typing import ConfigType from span_panel_api import ( SpanMqttClient, SpanPanelSnapshot, @@ -81,6 +83,12 @@ class SpanPanelRuntimeData: _DEVICE_TYPE_MAP: dict[str, str] = {"bess": "battery"} +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Span Panel integration (domain-level, called once).""" + _async_register_services(hass) + return True + + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate config entry through successive versions.""" @@ -272,14 +280,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> """Set up Span Panel from a config entry.""" _LOGGER.debug("Setting up entry %s (version %s)", entry.entry_id, entry.version) - # Register WebSocket commands and services once per HA instance + # Register WebSocket commands once per HA instance domain_data: dict[str, bool] = hass.data.setdefault(DOMAIN, {}) if not domain_data.get("websocket_registered"): domain_data["websocket_registered"] = True async_register_commands(hass) - if not domain_data.get("service_registered"): - domain_data["service_registered"] = True - _async_register_services(hass) config = entry.data api_version = config.get(CONF_API_VERSION, "v1") @@ -477,10 +482,16 @@ async def async_handle_export_manifest( _call: ServiceCall, ) -> ServiceResponse: """Export circuit manifest for all configured SPAN panels.""" + if not hass.config_entries.async_loaded_entries(DOMAIN): + raise ServiceValidationError( + "No SPAN panel config entries are loaded. " + "Add and configure a SPAN panel before calling this service." + ) + entity_reg = er.async_get(hass) panels = [] - for entry in hass.config_entries.async_entries(DOMAIN): + for entry in hass.config_entries.async_loaded_entries(DOMAIN): if not hasattr(entry, "runtime_data") or not isinstance( entry.runtime_data, SpanPanelRuntimeData ): @@ -518,7 +529,13 @@ async def async_handle_export_manifest( ) if circuits: - panels.append({"serial": serial, "circuits": circuits}) + panels.append( + { + "serial": serial, + "host": entry.data[CONF_HOST], + "circuits": circuits, + } + ) return cast(ServiceResponse, {"panels": panels}) diff --git a/tests/test_circuit_manifest_service.py b/tests/test_circuit_manifest_service.py index bf7a825b..473295ac 100644 --- a/tests/test_circuit_manifest_service.py +++ b/tests/test_circuit_manifest_service.py @@ -7,6 +7,7 @@ import pytest from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -80,7 +81,10 @@ async def test_basic_manifest(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-test-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.100"}, + entry_id="span_entry", + unique_id="sp3-test-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -103,6 +107,7 @@ async def test_basic_manifest(self, hass: HomeAssistant): panel = result["panels"][0] assert panel["serial"] == "sp3-test-001" + assert panel["host"] == "192.168.1.100" assert len(panel["circuits"]) == 2 by_template = {c["template"]: c for c in panel["circuits"]} @@ -134,10 +139,16 @@ async def test_multiple_panels(self, hass: HomeAssistant): ) entry_a = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="entry_a", unique_id="serial-aaa", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.100"}, + entry_id="entry_a", + unique_id="serial-aaa", ) entry_b = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="entry_b", unique_id="serial-bbb", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.101"}, + entry_id="entry_b", + unique_id="serial-bbb", ) entry_a.add_to_hass(hass) entry_b.add_to_hass(hass) @@ -156,6 +167,10 @@ async def test_multiple_panels(self, hass: HomeAssistant): serials = {p["serial"] for p in result["panels"]} assert serials == {"serial-aaa", "serial-bbb"} + by_serial = {p["serial"]: p for p in result["panels"]} + assert by_serial["serial-aaa"]["host"] == "192.168.1.100" + assert by_serial["serial-bbb"]["host"] == "192.168.1.101" + @pytest.mark.asyncio async def test_unmapped_tabs_excluded(self, hass: HomeAssistant): """Unmapped tab circuits are excluded from the manifest.""" @@ -169,7 +184,10 @@ async def test_unmapped_tabs_excluded(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.1"}, + entry_id="span_entry", + unique_id="sp3-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -201,7 +219,10 @@ async def test_circuit_without_entity_excluded(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.1"}, + entry_id="span_entry", + unique_id="sp3-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -218,30 +239,14 @@ async def test_circuit_without_entity_excluded(self, hass: HomeAssistant): assert panel["circuits"][0]["template"] == "clone_1" @pytest.mark.asyncio - async def test_entry_without_runtime_data_skipped(self, hass: HomeAssistant): - """Entries not yet loaded (no runtime_data) are silently omitted.""" - from pytest_homeassistant_custom_component.common import MockConfigEntry - - entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", - ) - entry.add_to_hass(hass) - # NOT setting runtime_data - - _async_register_services(hass) - result = await _call_manifest_service(hass) - - assert result is not None - assert result["panels"] == [] + async def test_no_loaded_entries_raises_validation_error(self, hass: HomeAssistant): + """Raises ServiceValidationError when no config entries are loaded.""" + from homeassistant.exceptions import ServiceValidationError - @pytest.mark.asyncio - async def test_no_entries_returns_empty(self, hass: HomeAssistant): - """No SPAN entries returns empty panels list.""" _async_register_services(hass) - result = await _call_manifest_service(hass) - assert result is not None - assert result["panels"] == [] + with pytest.raises(ServiceValidationError): + await _call_manifest_service(hass) @pytest.mark.asyncio async def test_all_device_types_included(self, hass: HomeAssistant): @@ -267,7 +272,10 @@ async def test_all_device_types_included(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.1"}, + entry_id="span_entry", + unique_id="sp3-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -301,7 +309,10 @@ async def test_bess_device_type_mapped_to_battery(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.1"}, + entry_id="span_entry", + unique_id="sp3-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -374,7 +385,10 @@ async def test_template_uses_min_tab(self, hass: HomeAssistant): ) entry = MockConfigEntry( - domain=DOMAIN, data={}, entry_id="span_entry", unique_id="sp3-001", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.1"}, + entry_id="span_entry", + unique_id="sp3-001", ) entry.add_to_hass(hass) entry.mock_state(hass, ConfigEntryState.LOADED) @@ -388,3 +402,30 @@ async def test_template_uses_min_tab(self, hass: HomeAssistant): circuit_data = result["panels"][0]["circuits"][0] assert circuit_data["template"] == "clone_6" assert circuit_data["tabs"] == [8, 6] + + @pytest.mark.asyncio + async def test_host_included_from_config_entry(self, hass: HomeAssistant): + """Host field is populated from config entry data.""" + from pytest_homeassistant_custom_component.common import MockConfigEntry + + circuit = SpanCircuitSnapshotFactory.create(circuit_id="uuid_a", tabs=[1]) + snapshot = SpanPanelSnapshotFactory.create( + serial_number="sp3-001", circuits={"uuid_a": circuit}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "10.0.0.50"}, + entry_id="span_entry", + unique_id="sp3-001", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, ConfigEntryState.LOADED) + entry.runtime_data = SpanPanelRuntimeData(coordinator=_make_coordinator(snapshot)) + + _register_power_entity(hass, "span_entry", "sp3-001", "uuid_a", "sensor.a_power") + + _async_register_services(hass) + result = await _call_manifest_service(hass) + + assert result["panels"][0]["host"] == "10.0.0.50" From f63d0a0bce075617aab09c1e468043aa4148f5c2 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:21:53 -0700 Subject: [PATCH 18/36] Remove clone-to-simulation from options flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panel cloning is now handled by the export_circuit_manifest service for the simulator add-on. With clone removed, general options is the only option so the menu is eliminated — the options gear goes straight to the general options form. Also adds missing v2.0.3 changelog entry. --- CHANGELOG.md | 12 + custom_components/span_panel/config_flow.py | 136 +-------- custom_components/span_panel/manifest.json | 6 +- .../span_panel/simulation_utils.py | 254 ---------------- .../span_panel/simulator_profile_builder.py | 276 ------------------ custom_components/span_panel/strings.json | 25 -- .../span_panel/translations/en.json | 25 -- .../span_panel/translations/es.json | 13 - .../span_panel/translations/fr.json | 13 - .../span_panel/translations/ja.json | 13 - .../span_panel/translations/pt.json | 13 - 11 files changed, 15 insertions(+), 771 deletions(-) delete mode 100644 custom_components/span_panel/simulation_utils.py delete mode 100644 custom_components/span_panel/simulator_profile_builder.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f41487f8..106d470e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [2.0.3] - 3/2026 + +**Important** 2.0.1 cautions still apply — read those carefully if not already on 2.0.1 BEFORE proceeding: + +- Requires firmware `spanos2/r202603/05` or later (v2 eBus MQTT) +- You _must_ already be on v1.3.x or later of the SpanPanel/span integration if upgrading + +### Fixed + +- **Force dependency re-resolution** — Version bump to ensure HACS re-installs `span-panel-api` for users who had the earlier 2.0.2 release. Users upgrading HA + without re-downloading the integration could be left with a stale library missing required imports. (#191) + ## [2.0.2] - 3/2026 **Important** 2.0.1 cautions still apply — read those carefully if not already on 2.0.1 BEFORE proceeding: diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index dae8da0a..7808846e 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -52,11 +52,6 @@ POWER_DISPLAY_PRECISION, SNAPSHOT_UPDATE_INTERVAL, ) -from .simulation_utils import ( - clone_with_profiles, - discover_clone_simulators, -) -from .simulator_profile_builder import build_usage_profiles _LOGGER = logging.getLogger(__name__) @@ -753,24 +748,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle the options flow for Span Panel.""" async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: - """Show the main options menu.""" - if user_input is None: - menu_options: dict[str, str] = { - "general_options": "General Options", - } - # Clone via simulator is only available for real v2 panels (eBus). - # Simulator entries have serials prefixed with "sim-". - serial = self.config_entry.unique_id or "" - is_simulator = serial.lower().startswith("sim-") - if self.config_entry.data.get(CONF_API_VERSION) == "v2" and not is_simulator: - menu_options["clone_panel_to_simulation"] = "Clone Panel To Simulation" - - return self.async_show_menu( - step_id="init", - menu_options=menu_options, - ) - - return self.async_abort(reason="unknown") + """Start the options flow with general options directly.""" + return await self.async_step_general_options(user_input) async def async_step_general_options( self, user_input: dict[str, Any] | None = None @@ -796,117 +775,6 @@ async def async_step_general_options( errors=errors, ) - async def async_step_clone_panel_to_simulation( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Clone the panel to a simulator discovered via mDNS or entered manually.""" - errors: dict[str, str] = {} - default_host = "" - default_http_port = 8081 - panel_name = self.config_entry.data.get("device_name", self.config_entry.title) - - if user_input is not None: - sim_host = str(user_input.get("simulator_host", "")).strip() - sim_http_port = int(user_input.get("simulator_http_port", 8081)) - - if not sim_host: - errors["simulator_host"] = "host_required" - else: - panel_host = str(self.config_entry.data.get(CONF_HOST, "")) - passphrase: str | None = self.config_entry.data.get(CONF_HOP_PASSPHRASE) - if passphrase == "": - passphrase = None - - profiles = await self._build_profiles_best_effort() - - result = await clone_with_profiles( - simulator_host=sim_host, - simulator_http_port=sim_http_port, - panel_host=panel_host, - panel_passphrase=passphrase, - latitude=self.hass.config.latitude, - longitude=self.hass.config.longitude, - profiles=profiles, - ) - - if not result.success: - _LOGGER.error( - "Clone failed at %s: %s", - result.error_phase, - result.error_message, - ) - errors["base"] = "clone_failed" - else: - _LOGGER.info( - "Panel cloned to simulator: %s (%d circuits)", - result.clone_serial, - result.circuits, - ) - if result.profile_result is not None: - if result.profile_result.success: - _LOGGER.info( - "Applied usage profiles: %d templates updated", - result.profile_result.circuits_updated, - ) - else: - _LOGGER.debug( - "Profile delivery skipped: %s", - result.profile_result.error_message, - ) - return self.async_create_entry( - title="", - data=dict(self.config_entry.options), - ) - - default_host = sim_host - default_http_port = sim_http_port - else: - # First visit — try mDNS discovery for simulators - simulators = await discover_clone_simulators(self.hass) - if simulators: - default_host = simulators[0].host - default_http_port = simulators[0].http_port - _LOGGER.debug( - "Discovered %d simulator(s) with clone support; using %s:%d", - len(simulators), - default_host, - default_http_port, - ) - - schema = vol.Schema( - { - vol.Required("simulator_host", default=default_host): str, - vol.Required("simulator_http_port", default=default_http_port): int, - } - ) - return self.async_show_form( - step_id="clone_panel_to_simulation", - data_schema=schema, - description_placeholders={ - "panel": str(panel_name) if panel_name else "Span Panel", - }, - errors=errors, - ) - - async def _build_profiles_best_effort( - self, - ) -> dict[str, dict[str, object]] | None: - """Build HA-derived usage profiles from recorder data (best-effort). - - Returns ``None`` on failure so the clone can proceed without profiles. - """ - try: - profiles = await build_usage_profiles(self.hass, self.config_entry) - except (KeyError, ValueError): - _LOGGER.exception("Failed to build usage profiles from recorder data") - return None - - if not profiles: - _LOGGER.info("No usage profiles to send (no recorder data)") - return None - - return profiles - # Register the config flow handler config_entries.HANDLERS.register(DOMAIN)(SpanPanelConfigFlow) diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 716590a4..2877cf56 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -1,9 +1,6 @@ { "domain": "span_panel", "name": "Span Panel", - "after_dependencies": [ - "recorder" - ], "codeowners": [ "@SpanPanel" ], @@ -12,8 +9,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api==2.3.0", - "python-socketio>=5.0" + "span-panel-api==2.3.0" ], "version": "2.0.4", "zeroconf": [ diff --git a/custom_components/span_panel/simulation_utils.py b/custom_components/span_panel/simulation_utils.py deleted file mode 100644 index a01343cb..00000000 --- a/custom_components/span_panel/simulation_utils.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Simulator utilities for SPAN Panel integration. - -Discovers simulators on the local network via mDNS and delegates panel -cloning to the simulator over Socket.IO. The simulator handles eBus -scraping, translation, and config writing -- the integration provides -the target panel's address, passphrase, and HA's location so the clone -is configured with the correct timezone and seasonal parameters. -""" - -from __future__ import annotations - -import asyncio -from dataclasses import dataclass, field -import logging - -from homeassistant.components import zeroconf as ha_zeroconf -from homeassistant.core import HomeAssistant -import socketio -from zeroconf import ServiceStateChange, Zeroconf -from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo - -_LOGGER = logging.getLogger(__name__) - -EBUS_SERVICE_TYPE = "_ebus._tcp.local." -HTTP_PORT_PROPERTY = "httpPort" -DISCOVERY_TIMEOUT_SECONDS = 3.0 -CLONE_OPERATION_TIMEOUT_SECONDS = 120 -PROFILE_READY_TIMEOUT_SECONDS = 30 -SIO_NAMESPACE = "/v1/panel" - - -@dataclass -class SimulatorInfo: - """A simulator discovered via mDNS.""" - - host: str - http_port: int - name: str - - -@dataclass -class ProfileResult: - """Outcome of a usage-profile delivery operation.""" - - success: bool - circuits_updated: int = 0 - error_message: str = "" - - -@dataclass -class CloneResult: - """Outcome of a clone-via-simulator operation.""" - - success: bool - serial: str = "" - clone_serial: str = "" - filename: str = "" - circuits: int = 0 - error_message: str = "" - error_phase: str = "" - profile_result: ProfileResult | None = field(default=None, repr=False) - - -async def discover_clone_simulators(hass: HomeAssistant) -> list[SimulatorInfo]: - """Browse for simulators via mDNS. - - Looks for ``_ebus._tcp.local.`` services whose TXT record contains - ``httpPort`` (simulators advertise this; real panels do not). - Discovery runs for a short window and returns all matching services. - """ - aiozc = await ha_zeroconf.async_get_async_instance(hass) - zc = aiozc.zeroconf - - discovered_names: list[str] = [] - - def _on_state_change( - zeroconf: Zeroconf, # noqa: ARG001 - service_type: str, # noqa: ARG001 - name: str, - state_change: ServiceStateChange, - ) -> None: - if state_change == ServiceStateChange.Added: - discovered_names.append(name) - - browser = AsyncServiceBrowser(zc, EBUS_SERVICE_TYPE, handlers=[_on_state_change]) - try: - await asyncio.sleep(DISCOVERY_TIMEOUT_SECONDS) - finally: - await browser.async_cancel() - - simulators: list[SimulatorInfo] = [] - - for name in discovered_names: - info = AsyncServiceInfo(EBUS_SERVICE_TYPE, name) - await info.async_request(zc, 3000) - - if not info.properties: - continue - - props: dict[str, str] = {} - for raw_key, raw_val in info.properties.items(): - key = raw_key.decode() if isinstance(raw_key, bytes) else str(raw_key) - val = raw_val.decode() if isinstance(raw_val, bytes) else str(raw_val) - props[key] = val - - http_port_str = props.get(HTTP_PORT_PROPERTY) or props.get(HTTP_PORT_PROPERTY.lower()) - if not http_port_str: - continue - - addresses = info.parsed_scoped_addresses() - host = addresses[0] if addresses else (info.server or "") - display_name = name.replace(f".{EBUS_SERVICE_TYPE}", "") - - simulators.append( - SimulatorInfo( - host=host.rstrip("."), - http_port=int(http_port_str), - name=display_name, - ) - ) - - return simulators - - -async def clone_with_profiles( - simulator_host: str, - simulator_http_port: int, - panel_host: str, - panel_passphrase: str | None, - latitude: float, - longitude: float, - profiles: dict[str, dict[str, object]] | None = None, -) -> CloneResult: - """Clone a panel via Socket.IO, optionally applying usage profiles. - - Runs the entire operation on a single Socket.IO connection: - - 1. Emit ``clone_panel`` and wait for the response. - 2. If the clone succeeded and *profiles* were supplied, wait for the - simulator to emit ``clone_ready`` before sending - ``apply_usage_profiles``. - - Keeping the connection open avoids a race where the clone config has - not yet been registered when profiles are delivered. - """ - url = f"http://{simulator_host}:{simulator_http_port}" - client: socketio.AsyncSimpleClient = socketio.AsyncSimpleClient() - - try: - async with asyncio.timeout(CLONE_OPERATION_TIMEOUT_SECONDS): - await client.connect(url, namespace=SIO_NAMESPACE, wait_timeout=10) - - result = await client.call( - "clone_panel", - { - "host": panel_host, - "passphrase": panel_passphrase, - "latitude": latitude, - "longitude": longitude, - }, - ) - - if not isinstance(result, dict): - return CloneResult( - success=False, - error_message="Unexpected response from simulator", - ) - - if result.get("status") != "ok": - return CloneResult( - success=False, - error_message=str(result.get("message", "Unknown error")), - error_phase=str(result.get("phase", "")), - ) - - clone_result = CloneResult( - success=True, - serial=str(result.get("serial", "")), - clone_serial=str(result.get("clone_serial", "")), - filename=str(result.get("filename", "")), - circuits=int(str(result.get("circuits", 0))), - ) - - if profiles and clone_result.clone_serial: - clone_result.profile_result = await _wait_ready_and_send_profiles( - client, clone_result.clone_serial, profiles - ) - - return clone_result - - except TimeoutError: - return CloneResult( - success=False, - error_message="Clone operation timed out", - ) - except Exception as err: - return CloneResult( - success=False, - error_message=f"Cannot connect to simulator: {err}", - ) - finally: - if client.connected: - await client.disconnect() - - -async def _wait_ready_and_send_profiles( - client: socketio.AsyncSimpleClient, - clone_serial: str, - profiles: dict[str, dict[str, object]], -) -> ProfileResult: - """Wait for ``clone_ready`` then emit ``apply_usage_profiles`` on the same connection.""" - try: - async with asyncio.timeout(PROFILE_READY_TIMEOUT_SECONDS): - # The simulator emits clone_ready when the clone config is registered - while True: - event = await client.receive() - if event[0] == "clone_ready": - break - - result = await client.call( - "apply_usage_profiles", - { - "clone_serial": clone_serial, - "profiles": profiles, - }, - ) - - if not isinstance(result, dict): - return ProfileResult( - success=False, - error_message="Unexpected response from simulator", - ) - - if result.get("status") == "ok": - return ProfileResult( - success=True, - circuits_updated=int(str(result.get("templates_updated", 0))), - ) - - return ProfileResult( - success=False, - error_message=str(result.get("message", "Unknown error")), - ) - - except TimeoutError: - return ProfileResult( - success=False, - error_message="Timed out waiting for simulator clone_ready", - ) - except Exception as err: - return ProfileResult( - success=False, - error_message=f"Profile delivery failed: {err}", - ) diff --git a/custom_components/span_panel/simulator_profile_builder.py b/custom_components/span_panel/simulator_profile_builder.py deleted file mode 100644 index 9e2e51e2..00000000 --- a/custom_components/span_panel/simulator_profile_builder.py +++ /dev/null @@ -1,276 +0,0 @@ -"""Build per-circuit usage profiles from HA recorder statistics. - -Queries the recorder's long-term statistics for each circuit's power -sensor, derives time-of-day patterns, monthly seasonality, duty cycle, -and average consumption, then packages them as a dict keyed by simulator -template name (``clone_{tab}``). - -The result is sent to the simulator via Socket.IO so the clone config -reflects real consumption patterns rather than synthetic noise. -""" - -from __future__ import annotations - -from datetime import datetime, timedelta, tzinfo -import logging -import statistics -from typing import TYPE_CHECKING, Literal - -from homeassistant.components.recorder import get_instance as get_recorder -from homeassistant.components.recorder.statistics import ( - statistics_during_period, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util - -from .helpers import build_circuit_unique_id - -if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntry - from homeassistant.core import HomeAssistant - -_LOGGER = logging.getLogger(__name__) - -# Minimum hours of recorder data before a circuit is included -_MIN_HOURLY_POINTS = 24 - -# Minimum distinct months before monthly_factors are emitted -_MIN_MONTHS_FOR_SEASONAL = 3 - -# Circuits with duty_cycle >= this are considered always-on; skip the field -_DUTY_CYCLE_CEILING = 0.8 - -# Hardware-driven modes whose power profiles should not be overridden -_SKIP_DEVICE_TYPES = frozenset({"pv", "bess"}) - - -async def build_usage_profiles( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> dict[str, dict[str, object]]: - """Derive per-circuit usage profiles from recorder statistics. - - Returns a dict keyed by simulator template name (``clone_{tab}``) - with sub-dicts containing any combination of: - - - ``typical_power`` (float, watts) - - ``power_variation`` (float, 0.0–1.0) - - ``hour_factors`` (dict[int, float], 0–23 → 0.0–1.0) - - ``duty_cycle`` (float, 0.0–1.0) - - ``monthly_factors`` (dict[int, float], 1–12 → 0.0–1.0) - """ - if not hasattr(config_entry, "runtime_data") or config_entry.runtime_data is None: - _LOGGER.warning( - "Config entry %s has no runtime data (not yet set up?)", config_entry.entry_id - ) - return {} - snapshot = config_entry.runtime_data.coordinator.data - if snapshot is None: - _LOGGER.warning("No snapshot available for profile building") - return {} - - serial = snapshot.serial_number - entity_reg = er.async_get(hass) - - # Map each circuit to (template_name, entity_id) for power sensor lookup - circuit_map: list[tuple[str, str, str]] = [] # (template_name, entity_id, circuit_id) - - for circuit_id, circuit in snapshot.circuits.items(): - if circuit_id.startswith("unmapped_tab_"): - continue - - # Skip hardware-driven device types - if getattr(circuit, "device_type", "circuit") in _SKIP_DEVICE_TYPES: - continue - - tabs = getattr(circuit, "tabs", None) - if not tabs: - continue - - template_name = f"clone_{min(tabs)}" - - # Look up the power sensor entity_id via entity registry - unique_id = build_circuit_unique_id(serial, circuit_id, "instantPowerW") - entity_id = entity_reg.async_get_entity_id("sensor", "span_panel", unique_id) - if entity_id is None: - _LOGGER.debug( - "No power sensor entity for circuit %s (unique_id=%s)", - circuit_id, - unique_id, - ) - continue - - circuit_map.append((template_name, entity_id, circuit_id)) - - if not circuit_map: - _LOGGER.info("No circuits eligible for profile building") - return {} - - stat_ids = {entity_id for _, entity_id, _ in circuit_map} - now = dt_util.utcnow() - - # Query 1: hourly stats for the last 30 days - hourly_start = now - timedelta(days=30) - hourly_stats = await _query_statistics( - hass, hourly_start, now, stat_ids, "hour", {"mean", "min", "max"} - ) - - # Query 2: monthly stats for the last 12 months - monthly_start = now - timedelta(days=365) - monthly_stats = await _query_statistics(hass, monthly_start, now, stat_ids, "month", {"mean"}) - - # Build profiles - profiles: dict[str, dict[str, object]] = {} - - for template_name, entity_id, circuit_id in circuit_map: - hourly_rows = hourly_stats.get(entity_id, []) - if len(hourly_rows) < _MIN_HOURLY_POINTS: - _LOGGER.debug( - "Circuit %s has only %d hourly points, skipping", - circuit_id, - len(hourly_rows), - ) - continue - - profile = _derive_profile( - hourly_rows, monthly_stats.get(entity_id, []), dt_util.get_default_time_zone() - ) - if profile: - profiles[template_name] = profile - - _LOGGER.info( - "Built usage profiles for %d/%d circuits", - len(profiles), - len(circuit_map), - ) - return profiles - - -_StatPeriod = Literal["5minute", "day", "hour", "week", "month"] -_StatType = Literal["change", "last_reset", "max", "mean", "min", "state", "sum"] - - -async def _query_statistics( - hass: HomeAssistant, - start_time: datetime, - end_time: datetime, - stat_ids: set[str], - period: _StatPeriod, - stat_types: set[_StatType], -) -> dict[str, list[dict[str, object]]]: - """Run statistics_during_period on the recorder's executor.""" - return await get_recorder(hass).async_add_executor_job( - statistics_during_period, # type: ignore[arg-type] - hass, - start_time, - end_time, - stat_ids, - period, - None, - stat_types, - ) - - -def _derive_profile( - hourly_rows: list[dict[str, object]], - monthly_rows: list[dict[str, object]], - local_tz: tzinfo, -) -> dict[str, object]: - """Compute profile parameters from raw statistics rows.""" - profile: dict[str, object] = {} - - # Extract hourly means and maxes - hourly_means: list[float] = [] - hourly_maxes: list[float] = [] - hour_buckets: dict[int, list[float]] = {h: [] for h in range(24)} - - for row in hourly_rows: - mean_val = row.get("mean") - max_val = row.get("max") - start = row.get("start") - - if mean_val is None or not isinstance(mean_val, int | float): - continue - - abs_mean = abs(float(mean_val)) - hourly_means.append(abs_mean) - - if max_val is not None and isinstance(max_val, int | float): - hourly_maxes.append(abs(float(max_val))) - - # Bucket by hour-of-day in local time (simulator uses local hours) - if isinstance(start, int | float): - local_hour = datetime.fromtimestamp(start, tz=dt_util.UTC).astimezone(local_tz).hour - hour_buckets[local_hour].append(abs_mean) - - if not hourly_means: - return profile - - # Use median throughout — robust to sensor glitch spikes that can - # reach 100x the real value and destroy mean-based calculations. - - # typical_power: median of hourly means - typical_power = statistics.median(hourly_means) - profile["typical_power"] = round(typical_power, 1) - - # power_variation: IQR-based dispersion relative to median, clamped [0.0, 1.0] - if typical_power > 0 and len(hourly_means) >= 4: - q1, q3 = _quartiles(hourly_means) - iqr_ratio = (q3 - q1) / typical_power - profile["power_variation"] = round(min(max(iqr_ratio, 0.0), 1.0), 3) - - # hour_factors: median by hour-of-day, normalized so peak = 1.0 - hour_medians: dict[int, float] = {} - for h in range(24): - bucket = hour_buckets[h] - if bucket: - hour_medians[h] = statistics.median(bucket) - else: - hour_medians[h] = 0.0 - - peak_hour = max(hour_medians.values()) if hour_medians else 0.0 - if peak_hour > 0: - hour_factors = {h: round(v / peak_hour, 3) for h, v in hour_medians.items()} - profile["hour_factors"] = hour_factors - - # duty_cycle: median(hourly_means) / median(hourly_maxes) - if hourly_maxes: - median_of_maxes = statistics.median(hourly_maxes) - if median_of_maxes > 0: - duty = typical_power / median_of_maxes - if duty < _DUTY_CYCLE_CEILING: - profile["duty_cycle"] = round(duty, 3) - - # monthly_factors from monthly stats (requires 3+ distinct months) - if len(monthly_rows) >= _MIN_MONTHS_FOR_SEASONAL: - monthly_means: dict[int, float] = {} - for row in monthly_rows: - mean_val = row.get("mean") - start = row.get("start") - if ( - mean_val is not None - and isinstance(mean_val, int | float) - and isinstance(start, int | float) - ): - month = datetime.fromtimestamp(start, tz=dt_util.UTC).month - monthly_means[month] = abs(float(mean_val)) - - if len(monthly_means) >= _MIN_MONTHS_FOR_SEASONAL: - peak_month = max(monthly_means.values()) - if peak_month > 0: - monthly_factors = {m: round(v / peak_month, 3) for m, v in monthly_means.items()} - profile["monthly_factors"] = monthly_factors - - return profile - - -def _quartiles(values: list[float]) -> tuple[float, float]: - """Return (Q1, Q3) for a list of values.""" - sorted_vals = sorted(values) - n = len(sorted_vals) - mid = n // 2 - lower = sorted_vals[:mid] - upper = sorted_vals[mid + (n % 2) :] - q1 = statistics.median(lower) if lower else sorted_vals[0] - q3 = statistics.median(upper) if upper else sorted_vals[-1] - return q1, q3 diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index 1e98d064..2bd7c717 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -85,20 +85,7 @@ } }, "options": { - "error": { - "host_required": "Simulator host is required", - "clone_failed": "Clone failed. Check the simulator and Home Assistant logs for details.", - "clone_connection_failed": "Cannot connect to the simulator", - "clone_timeout": "Clone operation timed out" - }, "step": { - "init": { - "title": "Options Menu", - "menu_options": { - "general_options": "General Options", - "clone_panel_to_simulation": "Clone Panel To Simulation" - } - }, "general_options": { "title": "General Options", "description": "Configure SPAN Panel integration settings.", @@ -116,18 +103,6 @@ "energy_reporting_grace_period": "How long energy sensors maintain their last known value when the panel becomes unavailable (0-60 minutes). Helps preserve energy statistics integrity during brief outages. Default: 15 minutes.", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } - }, - "clone_panel_to_simulation": { - "title": "Clone Panel to Simulator", - "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", - "data": { - "simulator_host": "Simulator Host", - "simulator_http_port": "Simulator HTTP Port" - }, - "data_description": { - "simulator_host": "IP address or hostname of the SPAN Panel Simulator", - "simulator_http_port": "HTTP port for the simulator's Socket.IO endpoint" - } } } }, diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index 81d679ee..f917ba82 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -85,20 +85,7 @@ } }, "options": { - "error": { - "host_required": "Simulator host is required", - "clone_failed": "Clone failed. Check the simulator and Home Assistant logs for details.", - "clone_connection_failed": "Cannot connect to the simulator", - "clone_timeout": "Clone operation timed out" - }, "step": { - "init": { - "title": "Options Menu", - "menu_options": { - "general_options": "General Options", - "clone_panel_to_simulation": "Clone Panel To Simulation" - } - }, "general_options": { "title": "General Options", "description": "Configure SPAN Panel integration settings.", @@ -116,18 +103,6 @@ "energy_reporting_grace_period": "How long energy sensors maintain their last known value when the panel becomes unavailable (0-60 minutes). Helps preserve energy statistics integrity during brief outages. Default: 15 minutes.", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings. Disabling clears all accumulated offsets." } - }, - "clone_panel_to_simulation": { - "title": "Clone Panel to Simulator", - "description": "Clone **{panel}** to the SPAN Panel Simulator. The simulator will connect to the panel's eBus and create a simulation configuration. If a simulator was discovered on the network, the address is pre-filled.", - "data": { - "simulator_host": "Simulator Host", - "simulator_http_port": "Simulator HTTP Port" - }, - "data_description": { - "simulator_host": "IP address or hostname of the SPAN Panel Simulator", - "simulator_http_port": "HTTP port for the simulator's Socket.IO endpoint" - } } } }, diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index 62598645..700a3853 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -65,20 +65,7 @@ } }, "options": { - "error": { - "host_required": "Se requiere el host del simulador", - "clone_failed": "La clonación falló. Verifique los registros del simulador y de Home Assistant para más detalles.", - "clone_connection_failed": "No se puede conectar al simulador", - "clone_timeout": "La operación de clonación expiró" - }, "step": { - "init": { - "title": "Menú de Opciones", - "menu_options": { - "general_options": "Opciones Generales", - "clone_panel_to_simulation": "Clonar Panel a Simulación" - } - }, "general_options": { "title": "Opciones Generales", "description": "Configurar los ajustes de la integración SPAN Panel.", diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index 272b4758..8517778a 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -65,20 +65,7 @@ } }, "options": { - "error": { - "host_required": "L'hôte du simulateur est requis", - "clone_failed": "Le clonage a échoué. Vérifiez les journaux du simulateur et de Home Assistant pour plus de détails.", - "clone_connection_failed": "Impossible de se connecter au simulateur", - "clone_timeout": "L'opération de clonage a expiré" - }, "step": { - "init": { - "title": "Menu des Options", - "menu_options": { - "general_options": "Options Générales", - "clone_panel_to_simulation": "Cloner le Panneau en Simulation" - } - }, "general_options": { "title": "Options Générales", "description": "Configurer les paramètres de l'intégration SPAN Panel.", diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index 79f989ce..ae019398 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -65,20 +65,7 @@ } }, "options": { - "error": { - "host_required": "シミュレータのホストが必要です", - "clone_failed": "クローンに失敗しました。詳細はシミュレータとHome Assistantのログを確認してください。", - "clone_connection_failed": "シミュレータに接続できません", - "clone_timeout": "クローン操作がタイムアウトしました" - }, "step": { - "init": { - "title": "オプションメニュー", - "menu_options": { - "general_options": "一般オプション", - "clone_panel_to_simulation": "パネルをシミュレーションにクローン" - } - }, "general_options": { "title": "一般オプション", "description": "SPAN Panel統合の設定を構成します。", diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index 4081284d..1f2bde8b 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -65,20 +65,7 @@ } }, "options": { - "error": { - "host_required": "O host do simulador é obrigatório", - "clone_failed": "A clonagem falhou. Verifique os logs do simulador e do Home Assistant para mais detalhes.", - "clone_connection_failed": "Não é possível conectar ao simulador", - "clone_timeout": "A operação de clonagem expirou" - }, "step": { - "init": { - "title": "Menu de Opções", - "menu_options": { - "general_options": "Opções Gerais", - "clone_panel_to_simulation": "Clonar Painel para Simulação" - } - }, "general_options": { "title": "Opções Gerais", "description": "Configurar os ajustes da integração SPAN Panel.", From 103a7fb0da7d6355bcd12b3c74707d2b4f477dfd Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:24:06 -0700 Subject: [PATCH 19/36] Note the remoal of the simulation config flow export --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106d470e..0ebb8ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. +## [2.0.4] - 3/2026 + +**Important** 2.0.1 cautions still apply — read those carefully if not already on 2.0.1 BEFORE proceeding: + +- Requires firmware `spanos2/r202603/05` or later (v2 eBus MQTT) +- You _must_ already be on v1.3.x or later of the SpanPanel/span integration if upgrading + +### Changed + +- **Simulation moved to dedicated add-on** — Panel cloning and simulation are no longer part of the integration's options flow. A new `export_circuit_manifest` + service provides panel parameters to the standalone SPAN Panel Simulator add-on. + +### Fixed + +- **PV nameplate capacity unit** — Corrected the PV nameplate capacity sensor unit to watts. +- **Schema validation cross-check** — Added Phase 1 schema validation that cross-checks sensor definitions against the Homie schema metadata at startup. + ## [2.0.3] - 3/2026 **Important** 2.0.1 cautions still apply — read those carefully if not already on 2.0.1 BEFORE proceeding: From 7d7931c0cc8c3f52e7a57e93e8ba5135675c1cde Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:49:17 -0700 Subject: [PATCH 20/36] Use configured panel host for MQTT broker connection The MQTT broker runs on the panel itself. The panel advertises its own mDNS hostname (.local) as ebusBrokerHost, but mDNS does not resolve across VLAN boundaries. Use the user-configured panel host (IP or FQDN) which is known reachable. Fixes #193 --- custom_components/span_panel/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/custom_components/span_panel/__init__.py b/custom_components/span_panel/__init__.py index 5e4befcd..3872eaa5 100644 --- a/custom_components/span_panel/__init__.py +++ b/custom_components/span_panel/__init__.py @@ -319,8 +319,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> if not serial_number: raise ConfigEntryNotReady("Config entry has no unique_id (serial number)") + # The MQTT broker runs on the panel itself. The panel advertises + # its own mDNS hostname (.local) as ebusBrokerHost, but mDNS + # does not resolve across VLAN boundaries. Use the user-configured + # panel host (IP or FQDN) which is known reachable. + advertised_broker = config[CONF_EBUS_BROKER_HOST] + if advertised_broker != host: + _LOGGER.debug( + "Panel advertised broker host '%s' differs from configured " + "host '%s'; using configured host for MQTT connection", + advertised_broker, + host, + ) + broker_config = MqttClientConfig( - broker_host=config[CONF_EBUS_BROKER_HOST], + broker_host=host, username=config[CONF_EBUS_BROKER_USERNAME], password=config[CONF_EBUS_BROKER_PASSWORD], mqtts_port=int(config[CONF_EBUS_BROKER_PORT]), From 96199b1f01a28ad195c7b31a523b0cb1bd74203d Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:54:37 -0700 Subject: [PATCH 21/36] Add changelog entry for MQTT broker host resolution fix (#193) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebb8ffa..b4db5034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file. ### Fixed +- **MQTT broker hostname resolution across VLANs** — The panel advertises its own mDNS hostname (`.local`) as the MQTT broker address, but mDNS does not resolve + across VLAN boundaries. The integration now uses the user-configured panel host (IP or FQDN) for the MQTT broker connection, since the broker runs on the panel + itself. (#193) - **PV nameplate capacity unit** — Corrected the PV nameplate capacity sensor unit to watts. - **Schema validation cross-check** — Added Phase 1 schema validation that cross-checks sensor definitions against the Homie schema metadata at startup. From 9eb32206a4ddb3b803d79ef1beeb73c5785c3d77 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:12:05 -0700 Subject: [PATCH 22/36] bump span-panel-api to 2.3.1 --- .github/workflows/ci.yml | 2 +- custom_components/span_panel/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b66d3918..c6f1b967 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.0"/' pyproject.toml + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.1"/' pyproject.toml sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml # Regenerate lock file with the modified dependencies poetry lock diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index 2877cf56..b33056ed 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api==2.3.0" + "span-panel-api==2.3.1" ], "version": "2.0.4", "zeroconf": [ From b7c58df90b8661216c615ce3ca238296e76f9e97 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:49:39 -0700 Subject: [PATCH 23/36] Handle malformed mDNS httpPort and add http_port translation strings Guard against non-numeric httpPort values in zeroconf TXT records that would crash discovery with a ValueError. Add missing UI label and description for the http_port field in the user config step. --- custom_components/span_panel/config_flow.py | 5 ++++- custom_components/span_panel/strings.json | 2 ++ custom_components/span_panel/translations/en.json | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index 7808846e..f3231b1d 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -198,7 +198,10 @@ async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo) -> Conf # Read optional httpPort from mDNS TXT records (non-standard port) props = discovery_info.properties or {} http_port_str = props.get("httpPort", props.get("httpport", "")) - http_port = int(http_port_str) if http_port_str else 80 + try: + http_port = int(http_port_str) if http_port_str else 80 + except (ValueError, TypeError): + http_port = 80 self._http_port = http_port detection = await detect_api_version(discovery_info.host, port=http_port) diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index 2bd7c717..a4e67423 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -24,12 +24,14 @@ "user": { "data": { "host": "Host", + "http_port": "HTTP Port", "power_display_precision": "Power Display Precision", "energy_display_precision": "Energy Display Precision", "enable_energy_dip_compensation": "Auto-Compensate Energy Dips" }, "data_description": { "host": "IP address or hostname of SPAN Panel", + "http_port": "HTTP port for the panel API (default: 80)", "power_display_precision": "Number of decimal places for power values (0-3)", "energy_display_precision": "Number of decimal places for energy values (0-3)", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings, preventing spikes in the energy dashboard." diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index f917ba82..b0c235fa 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -24,12 +24,14 @@ "user": { "data": { "host": "Host", + "http_port": "HTTP Port", "power_display_precision": "Power Display Precision", "energy_display_precision": "Energy Display Precision", "enable_energy_dip_compensation": "Auto-Compensate Energy Dips" }, "data_description": { "host": "IP address or hostname of SPAN Panel", + "http_port": "HTTP port for the panel API (default: 80)", "power_display_precision": "Number of decimal places for power values (0-3)", "energy_display_precision": "Number of decimal places for energy values (0-3)", "enable_energy_dip_compensation": "Automatically compensate when the panel reports lower energy readings, preventing spikes in the energy dashboard." From 7bcf69bbc39e7a6d932d650372c2014856ed0005 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:03:01 -0700 Subject: [PATCH 24/36] Remove unused python-socketio dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No integration code imports socketio — it was left over from removed functionality. Also drops 4 transitive dependencies. --- poetry.lock | 95 ++------------------------------------------------ pyproject.toml | 1 - 2 files changed, 3 insertions(+), 93 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6b792226..7f748825 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "acme" @@ -612,18 +612,6 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] -[[package]] -name = "bidict" -version = "0.23.1" -description = "The bidirectional mapping library for Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, - {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, -] - [[package]] name = "bleak" version = "1.1.1" @@ -4097,27 +4085,6 @@ files = [ {file = "python_direnv-0.2.2.tar.gz", hash = "sha256:0fe2fb834c901d675edcacc688689cfcf55cf06d9cf27dc7d3768a6c38c35f00"}, ] -[[package]] -name = "python-engineio" -version = "4.13.1" -description = "Engine.IO server and client for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399"}, - {file = "python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066"}, -] - -[package.dependencies] -simple-websocket = ">=0.10.0" - -[package.extras] -asyncio-client = ["aiohttp (>=3.11)"] -client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] -dev = ["tox"] -docs = ["furo", "sphinx"] - [[package]] name = "python-slugify" version = "8.0.4" @@ -4136,28 +4103,6 @@ text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] -[[package]] -name = "python-socketio" -version = "5.16.1" -description = "Socket.IO server and client for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35"}, - {file = "python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89"}, -] - -[package.dependencies] -bidict = ">=0.21.0" -python-engineio = ">=4.11.0" - -[package.extras] -asyncio-client = ["aiohttp (>=3.4)"] -client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] -dev = ["tox"] -docs = ["furo", "sphinx"] - [[package]] name = "pytz" version = "2025.2" @@ -4529,25 +4474,6 @@ regex = "2024.11.6" [package.extras] dev = ["black (==24.8.0)", "build (==1.2.2)", "flake8 (==7.2.0)", "mypy (==1.14.0)", "pylint (==3.2.7)", "pytest (==8.3.5)", "pytest-asyncio (==1.1.0)", "tox (==4.26.0)"] -[[package]] -name = "simple-websocket" -version = "1.1.0" -description = "Simple WebSocket server and client for Python" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, - {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, -] - -[package.dependencies] -wsproto = "*" - -[package.extras] -dev = ["flake8", "pytest", "pytest-cov", "tox"] -docs = ["sphinx"] - [[package]] name = "six" version = "1.17.0" @@ -4594,7 +4520,7 @@ test = ["covdefaults (==2.3.0)", "pytest (==8.4.1)", "pytest-aiohttp (==1.1.0)", [[package]] name = "span-panel-api" -version = "2.3.0" +version = "2.3.1" description = "A client library for SPAN Panel API" optional = false python-versions = ">=3.10,<4.0" @@ -5389,21 +5315,6 @@ winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.System[all] (>=3.2.1.0,<3.3.0.0)"] -[[package]] -name = "wsproto" -version = "1.3.2" -description = "Pure-Python WebSocket protocol implementation" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, - {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, -] - -[package.dependencies] -h11 = ">=0.16.0,<1" - [[package]] name = "yarl" version = "1.22.0" @@ -5637,4 +5548,4 @@ ifaddr = ">=0.1.7" [metadata] lock-version = "2.1" python-versions = ">=3.14.2,<3.15" -content-hash = "21e41bdd4e4f56ce23181d9634c3da0bd50673f2f4fad9bf225b6b648c91b09f" +content-hash = "2db133614283a9dad55d9bcf7a8425f77ed42f2f22bfc80db87cf8b490e88243" diff --git a/pyproject.toml b/pyproject.toml index eccc3dfe..fb4b3fd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ pylint = "==4.0.5" # For import-outside-toplevel checks pytest = "^9.0.0" # Compatible with Python 3.14 pytest-homeassistant-custom-component = "^0.13.315" # Latest version compatible with HA 2026.2.x isort = "*" -python-socketio = "^5.16.1" [build-system] requires = ["poetry-core"] From ca8ab394cf303eee321325959a4092b205071989 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:33:49 -0700 Subject: [PATCH 25/36] Add Grid Power Sensor --- CHANGELOG.md | 6 ++++++ README.md | 3 ++- custom_components/span_panel/entity_summary.py | 1 + custom_components/span_panel/helpers.py | 2 ++ custom_components/span_panel/icons.json | 3 +++ custom_components/span_panel/schema_expectations.py | 1 + custom_components/span_panel/schema_validation.py | 2 ++ custom_components/span_panel/sensor.py | 2 ++ custom_components/span_panel/sensor_definitions.py | 11 +++++++++++ custom_components/span_panel/strings.json | 3 +++ custom_components/span_panel/translations/en.json | 3 +++ tests/test_schema_validation.py | 2 ++ 12 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4db5034..1e2aed3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. - Requires firmware `spanos2/r202603/05` or later (v2 eBus MQTT) - You _must_ already be on v1.3.x or later of the SpanPanel/span integration if upgrading +### Added + +- **Grid Power sensor** — New `Grid Power`. Previously + only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own grid power accounting alongside Battery Power, PV Power, + and Site Power. Without BESS `Grid Power` is the same as `Current Power`. + ### Changed - **Simulation moved to dedicated add-on** — Panel cloning and simulation are no longer part of the integration's options flow. A new `export_circuit_manifest` diff --git a/README.md b/README.md index 6e26ffb2..52777139 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ If you encounter issues, restore from your backup or check the [troubleshooting | ------------- | ------------ | ---- | --------------------------------------------------------------------------- | | Battery Power | Power | W | Battery charge/discharge (+discharge, -charge). Only when BESS commissioned | | PV Power | Power | W | PV generation (+producing). Only when PV commissioned | +| Grid Power | Power | W | Computed grid power flow. Only when power-flows node active | | Site Power | Power | W | Total site power (grid + PV + battery). Only when power-flows node active | ### PV Metadata Sensors (v2 only, on main panel device) @@ -144,7 +145,7 @@ If you encounter issues, restore from your backup or check the [troubleshooting ### Power Sensor Attributes -Applies to Current Power, Feed Through Power, Battery Power, PV Power, and Site Power sensors. +Applies to Current Power, Feed Through Power, Battery Power, PV Power, Grid Power, and Site Power sensors. | Attribute | Type | Notes | | ---------- | ------ | ------------------------------------ | diff --git a/custom_components/span_panel/entity_summary.py b/custom_components/span_panel/entity_summary.py index c21b3b97..6343e976 100644 --- a/custom_components/span_panel/entity_summary.py +++ b/custom_components/span_panel/entity_summary.py @@ -65,6 +65,7 @@ def log_entity_summary(coordinator: SpanPanelCoordinator) -> None: if pv_present: power_flow_sensors += 1 # PV power if power_flows_present: + power_flow_sensors += 1 # Grid power flow power_flow_sensors += 1 # Site power # Synthetic sensors (now handled by template system - counts are estimates) diff --git a/custom_components/span_panel/helpers.py b/custom_components/span_panel/helpers.py index d372af59..28b4b660 100644 --- a/custom_components/span_panel/helpers.py +++ b/custom_components/span_panel/helpers.py @@ -45,6 +45,7 @@ "feedthroughPowerW": "feed_through_power", "batteryPowerW": "battery_power", "pvPowerW": "pv_power", + "gridPowerFlowW": "grid_power_flow", "sitePowerW": "site_power", "mainMeterEnergyProducedWh": "main_meter_energy_produced", # Consistent naming "mainMeterEnergyConsumedWh": "main_meter_energy_consumed", # Consistent naming @@ -63,6 +64,7 @@ "feedthroughPowerW": "feed_through_power", "batteryPowerW": "battery_power", "pvPowerW": "pv_power", + "gridPowerFlowW": "grid_power_flow", "sitePowerW": "site_power", "mainMeterEnergyProducedWh": "main_meter_produced_energy", "mainMeterEnergyConsumedWh": "main_meter_consumed_energy", diff --git a/custom_components/span_panel/icons.json b/custom_components/span_panel/icons.json index 60ffe3da..8330205e 100644 --- a/custom_components/span_panel/icons.json +++ b/custom_components/span_panel/icons.json @@ -62,6 +62,9 @@ "pv_power": { "default": "mdi:solar-power" }, + "grid_power_flow": { + "default": "mdi:transmission-tower" + }, "site_power": { "default": "mdi:home-lightning-bolt" }, diff --git a/custom_components/span_panel/schema_expectations.py b/custom_components/span_panel/schema_expectations.py index ae2ab87b..ed4d2050 100644 --- a/custom_components/span_panel/schema_expectations.py +++ b/custom_components/span_panel/schema_expectations.py @@ -39,6 +39,7 @@ "feedthroughPowerW": "panel.feedthrough_power_w", "batteryPowerW": "panel.power_flow_battery", "pvPowerW": "panel.power_flow_pv", + "gridPowerFlowW": "panel.power_flow_grid", "sitePowerW": "panel.power_flow_site", # --- Panel energy sensors ------------------------------------------------ "mainMeterEnergyProducedWh": "panel.main_meter_energy_produced_wh", diff --git a/custom_components/span_panel/schema_validation.py b/custom_components/span_panel/schema_validation.py index 8f50e249..4f39724e 100644 --- a/custom_components/span_panel/schema_validation.py +++ b/custom_components/span_panel/schema_validation.py @@ -35,6 +35,7 @@ DOWNSTREAM_L1_CURRENT_SENSOR, DOWNSTREAM_L2_CURRENT_SENSOR, EVSE_SENSORS, + GRID_POWER_FLOW_SENSOR, L1_VOLTAGE_SENSOR, L2_VOLTAGE_SENSOR, MAIN_BREAKER_RATING_SENSOR, @@ -165,6 +166,7 @@ def collect_sensor_definitions() -> dict[str, SensorEntityDescription]: *PANEL_POWER_SENSORS, BATTERY_POWER_SENSOR, PV_POWER_SENSOR, + GRID_POWER_FLOW_SENSOR, SITE_POWER_SENSOR, *PANEL_ENERGY_SENSORS, *CIRCUIT_SENSORS, diff --git a/custom_components/span_panel/sensor.py b/custom_components/span_panel/sensor.py index aa9cfd71..622db17c 100644 --- a/custom_components/span_panel/sensor.py +++ b/custom_components/span_panel/sensor.py @@ -37,6 +37,7 @@ DOWNSTREAM_L1_CURRENT_SENSOR, DOWNSTREAM_L2_CURRENT_SENSOR, EVSE_SENSORS, + GRID_POWER_FLOW_SENSOR, L1_VOLTAGE_SENSOR, L2_VOLTAGE_SENSOR, MAIN_BREAKER_RATING_SENSOR, @@ -341,6 +342,7 @@ def create_power_flow_sensors( entities.append(SpanPVMetadataSensor(coordinator, desc, snapshot)) if has_power_flows(snapshot): + entities.append(SpanPanelPowerSensor(coordinator, GRID_POWER_FLOW_SENSOR, snapshot)) entities.append(SpanPanelPowerSensor(coordinator, SITE_POWER_SENSOR, snapshot)) return entities diff --git a/custom_components/span_panel/sensor_definitions.py b/custom_components/span_panel/sensor_definitions.py index 1eb3262d..5aeea891 100644 --- a/custom_components/span_panel/sensor_definitions.py +++ b/custom_components/span_panel/sensor_definitions.py @@ -497,6 +497,17 @@ class SpanPVMetadataSensorEntityDescription( value_fn=lambda s: (-s.power_flow_pv or 0.0) if s.power_flow_pv is not None else 0.0, ) +# Grid power flow sensor (conditionally created when power-flows data is available) +GRID_POWER_FLOW_SENSOR: SpanPanelDataSensorEntityDescription = SpanPanelDataSensorEntityDescription( + key="gridPowerFlowW", + translation_key="grid_power_flow", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + device_class=SensorDeviceClass.POWER, + value_fn=lambda s: s.power_flow_grid if s.power_flow_grid is not None else 0.0, +) + # Site power sensor (conditionally created when power-flows data is available) SITE_POWER_SENSOR: SpanPanelDataSensorEntityDescription = SpanPanelDataSensorEntityDescription( key="sitePowerW", diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index a4e67423..dad336d8 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -197,6 +197,9 @@ "pv_power": { "name": "PV Power" }, + "grid_power_flow": { + "name": "Grid Power" + }, "site_power": { "name": "Site Power" }, diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index b0c235fa..915731bd 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -197,6 +197,9 @@ "pv_power": { "name": "PV Power" }, + "grid_power_flow": { + "name": "Grid Power" + }, "site_power": { "name": "Site Power" }, diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py index 3b50bb57..ee45f05c 100644 --- a/tests/test_schema_validation.py +++ b/tests/test_schema_validation.py @@ -89,6 +89,7 @@ def test_sensor_keys_exist_in_definitions(self) -> None: DOWNSTREAM_L1_CURRENT_SENSOR, DOWNSTREAM_L2_CURRENT_SENSOR, EVSE_SENSORS, + GRID_POWER_FLOW_SENSOR, L1_VOLTAGE_SENSOR, L2_VOLTAGE_SENSOR, MAIN_BREAKER_RATING_SENSOR, @@ -123,6 +124,7 @@ def test_sensor_keys_exist_in_definitions(self) -> None: *PANEL_POWER_SENSORS, BATTERY_POWER_SENSOR, PV_POWER_SENSOR, + GRID_POWER_FLOW_SENSOR, SITE_POWER_SENSOR, *PANEL_ENERGY_SENSORS, *CIRCUIT_SENSORS, From b0030a3fd5f4a777df6978f561151226b15a2fec Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:02:16 -0700 Subject: [PATCH 26/36] Add warning to readme and update changelog --- CHANGELOG.md | 10 ++++------ README.md | 7 +++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2aed3f..36abbb28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,8 @@ All notable changes to this project will be documented in this file. ### Added -- **Grid Power sensor** — New `Grid Power`. Previously - only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own grid power accounting alongside Battery Power, PV Power, - and Site Power. Without BESS `Grid Power` is the same as `Current Power`. +- **Grid Power sensor** — New `Grid Power`. Previously only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own + grid power accounting alongside Battery Power, PV Power, and Site Power. Without BESS `Grid Power` is the same as `Current Power`. ### Changed @@ -23,10 +22,9 @@ All notable changes to this project will be documented in this file. ### Fixed - **MQTT broker hostname resolution across VLANs** — The panel advertises its own mDNS hostname (`.local`) as the MQTT broker address, but mDNS does not resolve - across VLAN boundaries. The integration now uses the user-configured panel host (IP or FQDN) for the MQTT broker connection, since the broker runs on the panel - itself. (#193) + across VLAN boundaries. The integration now uses the user-configured panel host (IP or FQDN) for the MQTT broker connection, since the broker runs on the + panel itself. (#193) - **PV nameplate capacity unit** — Corrected the PV nameplate capacity sensor unit to watts. -- **Schema validation cross-check** — Added Phase 1 schema validation that cross-checks sensor definitions against the Homie schema metadata at startup. ## [2.0.3] - 3/2026 diff --git a/README.md b/README.md index 52777139..0b9f0e32 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ execute these actions without user presence; design them with the same care you safety device and must not be relied upon for life-safety applications. Use this software at your own risk. If you cannot accept that risk, do not use this software. See [LICENSE](LICENSE) for the full warranty disclaimer. +The SPAN Client documentation has warnings regarding the use of the API (the API used by this integration) which should be headed just as if you were using that +API directly: + +> An API client that attempts to implement its own load-shedding decisions, grid-state detection, or other critical automation is operating outside the scope of +> what SPAN API was designed and engineered for. Such use is entirely at the client developer's and homeowner's own risk and may void the SPAN Panel Limited +> Warranty. See the SPAN API Scope & Responsibility Model in the [SPAN API documentation](https://github.com/spanio/SPAN-API-Client-Docs). + This integration provides sensors and controls for understanding an installation's power consumption, energy usage, and controlling user-manageable panel circuits. You can optionally use the [span-card](https://github.com/SpanPanel/span-card) Lovelace card for visualization and switch control. From 427f6b9c704176b2b56b1b673f63143a1b1e29fc Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:40:32 -0700 Subject: [PATCH 27/36] Add FQDN support to config flow validation --- .github/workflows/ci.yml | 2 +- custom_components/span_panel/config_flow.py | 162 +++++++++++++++++- .../span_panel/config_flow_utils/__init__.py | 4 + .../config_flow_utils/validation.py | 65 ++++++- custom_components/span_panel/const.py | 1 + custom_components/span_panel/manifest.json | 2 +- custom_components/span_panel/strings.json | 23 ++- .../span_panel/translations/en.json | 23 ++- .../span_panel/translations/es.json | 23 ++- .../span_panel/translations/fr.json | 23 ++- .../span_panel/translations/ja.json | 23 ++- .../span_panel/translations/pt.json | 23 ++- poetry.lock | 4 +- 13 files changed, 360 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6f1b967..d883c305 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.1"/' pyproject.toml + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.2"/' pyproject.toml sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml # Regenerate lock file with the modified dependencies poetry lock diff --git a/custom_components/span_panel/config_flow.py b/custom_components/span_panel/config_flow.py index f3231b1d..71423572 100644 --- a/custom_components/span_panel/config_flow.py +++ b/custom_components/span_panel/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Mapping import enum import logging @@ -17,13 +18,15 @@ from homeassistant.core import callback from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util.network import is_ipv4_address -from span_panel_api import V2AuthResponse, detect_api_version +from span_panel_api import V2AuthResponse, delete_fqdn, detect_api_version, register_fqdn from span_panel_api.exceptions import SpanPanelAuthError, SpanPanelConnectionError import voluptuous as vol from .config_flow_utils import ( build_general_options_schema, + check_fqdn_tls_ready, get_general_options_defaults, + is_fqdn, process_general_options_input, validate_auth_token, validate_host, @@ -39,6 +42,7 @@ CONF_HOP_PASSPHRASE, CONF_HTTP_PORT, CONF_PANEL_SERIAL, + CONF_REGISTERED_FQDN, DOMAIN, ENABLE_ENERGY_DIP_COMPENSATION, ENTITY_NAMING_PATTERN, @@ -130,6 +134,9 @@ def __init__(self) -> None: self._http_port: int = 80 # Energy dip compensation default for fresh installs self._enable_dip_compensation: bool = True + # FQDN registration task (async_show_progress) + self._fqdn_task: asyncio.Task[None] | None = None + self._reconfigure_fqdn_task: asyncio.Task[None] | None = None async def setup_flow(self, trigger_type: TriggerFlowType, host: str) -> None: """Set up the flow by detecting the panel API version and serial number.""" @@ -164,12 +171,14 @@ def ensure_flow_is_set_up(self) -> None: _LOGGER.error("Flow method called before setup") raise ConfigFlowError("Flow is not set up") - async def ensure_not_already_configured(self) -> None: + async def ensure_not_already_configured(self, raise_on_progress: bool = True) -> None: """Ensure the panel is not already configured.""" self.ensure_flow_is_set_up() - # Abort if we had already set this panel up - await self.async_set_unique_id(self.serial_number) + # Abort if we had already set this panel up. + # User-initiated flows pass raise_on_progress=False so they can + # proceed when a zeroconf discovery flow is already running. + await self.async_set_unique_id(self.serial_number, raise_on_progress=raise_on_progress) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo) -> ConfigFlowResult: @@ -284,13 +293,13 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Con }, } self._is_flow_setup = True - await self.ensure_not_already_configured() + await self.ensure_not_already_configured(raise_on_progress=False) return await self.async_step_choose_v2_auth() # v1 path: probe via the existing setup_flow if not self._is_flow_setup: await self.setup_flow(TriggerFlowType.CREATE_ENTRY, host) - await self.ensure_not_already_configured() + await self.ensure_not_already_configured(raise_on_progress=False) return await self.async_step_choose_auth_type() @@ -500,8 +509,71 @@ async def _async_finalize_v2_auth(self) -> ConfigFlowResult: if "entry_id" not in self.context: raise ValueError("Entry ID is missing from context") return self._update_v2_entry(self.context["entry_id"]) + # If host is an FQDN, register it with the panel for TLS cert SAN inclusion + if self.host and is_fqdn(self.host): + return await self.async_step_register_fqdn() return await self.async_step_choose_entity_naming_initial() + async def async_step_register_fqdn( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Register FQDN with the panel and wait for TLS certificate update.""" + if not self._fqdn_task: + self._fqdn_task = self.hass.async_create_task( + self._async_register_fqdn_and_wait(), + "span_panel_register_fqdn", + ) + + if not self._fqdn_task.done(): + return self.async_show_progress( + step_id="register_fqdn", + progress_action="registering_fqdn", + progress_task=self._fqdn_task, + ) + + try: + self._fqdn_task.result() + except Exception: + _LOGGER.exception("FQDN registration failed for %s", self.host) + self._fqdn_task = None + return self.async_show_progress_done(next_step_id="fqdn_failed") + + self._fqdn_task = None + return self.async_show_progress_done(next_step_id="choose_entity_naming_initial") + + async def _async_register_fqdn_and_wait(self) -> None: + """Register the FQDN and poll until the TLS cert includes it.""" + if not self.host or not self.access_token: + raise ConfigFlowError("Host and access token required for FQDN registration") + + await register_fqdn(self.host, self.access_token, self.host, port=self._http_port) + + mqtts_port = self._v2_broker_port or 8883 + max_attempts = 30 + for attempt in range(max_attempts): + await asyncio.sleep(2) + if await check_fqdn_tls_ready(self.host, mqtts_port, http_port=self._http_port): + _LOGGER.debug( + "FQDN %s found in TLS cert SAN after %d attempts", + self.host, + attempt + 1, + ) + return + + raise ConfigFlowError(f"Timed out waiting for TLS certificate to include FQDN {self.host}") + + async def async_step_fqdn_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle FQDN registration failure — user may continue without it.""" + if user_input is not None: + return await self.async_step_choose_entity_naming_initial() + return self.async_show_form( + step_id="fqdn_failed", + data_schema=vol.Schema({}), + errors={"base": "fqdn_registration_failed"}, + ) + async def async_step_resolve_entity( self, entry_data: dict[str, Any] | None = None, @@ -572,6 +644,8 @@ def create_new_entry( entry_data[CONF_PANEL_SERIAL] = self._v2_panel_serial if self._http_port != 80: entry_data[CONF_HTTP_PORT] = self._http_port + if is_fqdn(host): + entry_data[CONF_REGISTERED_FQDN] = host return self.async_create_entry( title=device_name, @@ -722,9 +796,83 @@ async def async_step_reconfigure( await self.async_set_unique_id(detection.status_info.serial_number) self._abort_if_unique_id_mismatch(reason="unique_id_mismatch") + if is_fqdn(host): + # New host is FQDN — register it (replaces any existing FQDN on the panel) + self.host = host + self.access_token = str(reconfigure_entry.data.get(CONF_ACCESS_TOKEN, "")) + self._http_port = http_port + self._v2_broker_port = int(reconfigure_entry.data.get(CONF_EBUS_BROKER_PORT, 8883)) + return await self.async_step_reconfigure_register_fqdn() + + # New host is not an FQDN — simple update + data_updates: dict[str, Any] = {CONF_HOST: host} + old_fqdn = str(reconfigure_entry.data.get(CONF_REGISTERED_FQDN, "")) + if old_fqdn: + # Switching from FQDN to IP — clean up old registration + access_token = str(reconfigure_entry.data.get(CONF_ACCESS_TOKEN, "")) + try: + await delete_fqdn(host, access_token, port=http_port) + except Exception: + _LOGGER.warning("Failed to delete old FQDN registration: %s", old_fqdn) + data_updates[CONF_REGISTERED_FQDN] = "" + return self.async_update_reload_and_abort( reconfigure_entry, - data_updates={CONF_HOST: host}, + data_updates=data_updates, + ) + + async def async_step_reconfigure_register_fqdn( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Register FQDN during reconfiguration and wait for TLS cert update.""" + if not self._reconfigure_fqdn_task: + self._reconfigure_fqdn_task = self.hass.async_create_task( + self._async_register_fqdn_and_wait(), + "span_panel_reconfigure_fqdn", + ) + + if not self._reconfigure_fqdn_task.done(): + return self.async_show_progress( + step_id="reconfigure_register_fqdn", + progress_action="registering_fqdn", + progress_task=self._reconfigure_fqdn_task, + ) + + try: + self._reconfigure_fqdn_task.result() + except Exception: + _LOGGER.exception("FQDN registration failed during reconfigure for %s", self.host) + self._reconfigure_fqdn_task = None + return self.async_show_progress_done(next_step_id="reconfigure_fqdn_failed") + + self._reconfigure_fqdn_task = None + return self.async_show_progress_done(next_step_id="reconfigure_fqdn_done") + + async def async_step_reconfigure_fqdn_done( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Complete reconfiguration after successful FQDN registration.""" + reconfigure_entry = self._get_reconfigure_entry() + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={CONF_HOST: self.host or "", CONF_REGISTERED_FQDN: self.host or ""}, + ) + + async def async_step_reconfigure_fqdn_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle FQDN registration failure during reconfigure.""" + if user_input is not None: + # User chose to continue anyway — update host without FQDN registration + reconfigure_entry = self._get_reconfigure_entry() + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={CONF_HOST: self.host or ""}, + ) + return self.async_show_form( + step_id="reconfigure_fqdn_failed", + data_schema=vol.Schema({}), + errors={"base": "fqdn_registration_failed"}, ) @staticmethod diff --git a/custom_components/span_panel/config_flow_utils/__init__.py b/custom_components/span_panel/config_flow_utils/__init__.py index e5e9e2c2..919d3cc9 100644 --- a/custom_components/span_panel/config_flow_utils/__init__.py +++ b/custom_components/span_panel/config_flow_utils/__init__.py @@ -7,6 +7,8 @@ process_general_options_input, ) from .validation import ( + check_fqdn_tls_ready, + is_fqdn, validate_auth_token, validate_host, validate_ipv4_address, @@ -16,6 +18,8 @@ __all__ = [ # Validation + "check_fqdn_tls_ready", + "is_fqdn", "validate_auth_token", "validate_host", "validate_ipv4_address", diff --git a/custom_components/span_panel/config_flow_utils/validation.py b/custom_components/span_panel/config_flow_utils/validation.py index 97acc7a0..3d9aa42e 100644 --- a/custom_components/span_panel/config_flow_utils/validation.py +++ b/custom_components/span_panel/config_flow_utils/validation.py @@ -2,11 +2,17 @@ from __future__ import annotations +import asyncio +import ipaddress import logging +from pathlib import Path +import socket +import ssl +import tempfile from homeassistant.core import HomeAssistant from homeassistant.util.network import is_ipv4_address -from span_panel_api import V2AuthResponse, detect_api_version, register_v2 +from span_panel_api import V2AuthResponse, detect_api_version, download_ca_cert, register_v2 _LOGGER = logging.getLogger(__name__) @@ -54,6 +60,63 @@ async def validate_v2_passphrase(host: str, passphrase: str, port: int = 80) -> return await register_v2(host, "Home Assistant", passphrase, port=port) +def is_fqdn(host: str) -> bool: + """Determine if host is a Fully Qualified Domain Name (not IP, not mDNS). + + Returns True for domain names like 'span.home.lan' or 'panel.example.com'. + Returns False for IP addresses, mDNS (.local) names, and single-label hostnames. + """ + if is_ipv4_address(host): + return False + try: + ipaddress.ip_address(host) + return False + except ValueError: + pass + if host.endswith(".local") or host.endswith(".local."): + return False + return "." in host + + +async def check_fqdn_tls_ready(fqdn: str, mqtts_port: int, http_port: int = 80) -> bool: + """Check if the MQTTS server certificate includes the FQDN in its SAN. + + Downloads the CA certificate from the panel via HTTP, then attempts + a TLS connection to the MQTTS port using the FQDN as server_hostname. + If the TLS handshake succeeds with hostname verification, the panel + has regenerated its certificate to include the FQDN. + """ + try: + ca_pem = await download_ca_cert(fqdn, port=http_port) + except Exception: + return False + + loop = asyncio.get_running_loop() + + def _check() -> bool: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = True + ctx.verify_mode = ssl.CERT_REQUIRED + + tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) # noqa: SIM115 + tmp.write(ca_pem) + tmp.close() + ca_path = Path(tmp.name) + try: + ctx.load_verify_locations(str(ca_path)) + with ( + socket.create_connection((fqdn, mqtts_port), timeout=5) as sock, + ctx.wrap_socket(sock, server_hostname=fqdn), + ): + return True + except (ssl.SSLCertVerificationError, ssl.SSLError, OSError, TimeoutError): + return False + finally: + ca_path.unlink(missing_ok=True) + + return await loop.run_in_executor(None, _check) + + async def validate_v2_proximity(host: str, port: int = 80) -> V2AuthResponse: """Validate v2 panel proximity (door bypass) and return MQTT credentials. diff --git a/custom_components/span_panel/const.py b/custom_components/span_panel/const.py index 49b3f6f9..39ff4a62 100644 --- a/custom_components/span_panel/const.py +++ b/custom_components/span_panel/const.py @@ -18,6 +18,7 @@ CONF_HOP_PASSPHRASE = "hop_passphrase" CONF_HTTP_PORT = "http_port" CONF_PANEL_SERIAL = "panel_serial" +CONF_REGISTERED_FQDN = "registered_fqdn" # Binary sensor / status field keys (used in entity definitions) SYSTEM_DOOR_STATE = "doorState" diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json index b33056ed..123f1cb8 100644 --- a/custom_components/span_panel/manifest.json +++ b/custom_components/span_panel/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/SpanPanel/span/issues", "requirements": [ - "span-panel-api==2.3.1" + "span-panel-api==2.3.2" ], "version": "2.0.4", "zeroconf": [ diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json index dad336d8..1b47985f 100644 --- a/custom_components/span_panel/strings.json +++ b/custom_components/span_panel/strings.json @@ -13,7 +13,11 @@ "host_required": "Host is required", "invalid_auth": "Invalid authentication", "proximity_failed": "Proximity not proven. Please open and close the panel door 3 times and try again.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "Span Panel ({host})", "step": { @@ -83,6 +87,23 @@ "data_description": { "host": "IP address or hostname of the SPAN Panel" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json index 915731bd..c950ae20 100644 --- a/custom_components/span_panel/translations/en.json +++ b/custom_components/span_panel/translations/en.json @@ -13,7 +13,11 @@ "host_required": "Host is required", "invalid_auth": "Invalid authentication", "proximity_failed": "Proximity not proven. Please open and close the panel door 3 times and try again.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "Span Panel ({host})", "step": { @@ -83,6 +87,23 @@ "data_description": { "host": "IP address or hostname of the SPAN Panel" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index 700a3853..27372e98 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -10,7 +10,11 @@ "cannot_connect": "No se logró establecer conexión con Span Panel", "invalid_auth": "Autenticación invalida", "unknown": "Error inesperado", - "host_required": "Se requiere host" + "host_required": "Se requiere host", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "Span Panel ({host})", "step": { @@ -61,6 +65,23 @@ "data_description": { "hop_passphrase": "La contraseña utilizada para autenticar con la API v2 del SPAN Panel" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index 8517778a..9288afae 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -10,7 +10,11 @@ "cannot_connect": "Échec de la connexion au Span Panel", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue", - "host_required": "L'hôte est requis" + "host_required": "L'hôte est requis", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "Span Panel ({host})", "step": { @@ -61,6 +65,23 @@ "data_description": { "hop_passphrase": "Le mot de passe utilisé pour s'authentifier avec l'API v2 du SPAN Panel" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index ae019398..9b035061 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -10,7 +10,11 @@ "cannot_connect": "スパンパネルへの接続に失敗しました", "invalid_auth": "認証が無効です", "unknown": "予期しないエラー", - "host_required": "ホストが必要です" + "host_required": "ホストが必要です", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "スパンパネル ({host})", "step": { @@ -61,6 +65,23 @@ "data_description": { "hop_passphrase": "SPAN Panel v2 APIでの認証に使用するパスフレーズ" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index 1f2bde8b..49477cec 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -10,7 +10,11 @@ "cannot_connect": "Falha ao ligar ao Painel Span", "invalid_auth": "Autenticação inválida", "unknown": "Erro inesperado", - "host_required": "O anfitrião é necessário" + "host_required": "O anfitrião é necessário", + "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + }, + "progress": { + "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." }, "flow_title": "Painel Span ({host})", "step": { @@ -61,6 +65,23 @@ "data_description": { "hop_passphrase": "A frase-passe utilizada para autenticar com a API v2 do SPAN Panel" } + }, + "register_fqdn": { + "title": "Registering Domain Name" + }, + "fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_register_fqdn": { + "title": "Registering Domain Name" + }, + "reconfigure_fqdn_failed": { + "title": "Domain Registration", + "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." + }, + "reconfigure_fqdn_done": { + "title": "Reconfiguration Complete" } } }, diff --git a/poetry.lock b/poetry.lock index 7f748825..17fe08be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "acme" @@ -4520,7 +4520,7 @@ test = ["covdefaults (==2.3.0)", "pytest (==8.4.1)", "pytest-aiohttp (==1.1.0)", [[package]] name = "span-panel-api" -version = "2.3.1" +version = "2.3.2" description = "A client library for SPAN Panel API" optional = false python-versions = ">=3.10,<4.0" From 56e66d0d1b1a4acda6689f27a2601d1c60cb4cc7 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:05:27 -0700 Subject: [PATCH 28/36] Fix Grid Power sensor sign and misc cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Negate power_flow_grid to match PV/Battery convention — the panel reports negative for power flowing into the panel (grid import), so inversion is needed for positive-import user expectation. Also fix "headed" → "heeded" typo in README and translate FQDN registration strings in es/fr/ja/pt locale files. --- README.md | 2 +- custom_components/span_panel/sensor_definitions.py | 2 +- custom_components/span_panel/translations/es.json | 4 ++-- custom_components/span_panel/translations/fr.json | 4 ++-- custom_components/span_panel/translations/ja.json | 4 ++-- custom_components/span_panel/translations/pt.json | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0b9f0e32..98977510 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ execute these actions without user presence; design them with the same care you safety device and must not be relied upon for life-safety applications. Use this software at your own risk. If you cannot accept that risk, do not use this software. See [LICENSE](LICENSE) for the full warranty disclaimer. -The SPAN Client documentation has warnings regarding the use of the API (the API used by this integration) which should be headed just as if you were using that +The SPAN Client documentation has warnings regarding the use of the API (the API used by this integration) which should be heeded just as if you were using that API directly: > An API client that attempts to implement its own load-shedding decisions, grid-state detection, or other critical automation is operating outside the scope of diff --git a/custom_components/span_panel/sensor_definitions.py b/custom_components/span_panel/sensor_definitions.py index 5aeea891..612db880 100644 --- a/custom_components/span_panel/sensor_definitions.py +++ b/custom_components/span_panel/sensor_definitions.py @@ -505,7 +505,7 @@ class SpanPVMetadataSensorEntityDescription( state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, device_class=SensorDeviceClass.POWER, - value_fn=lambda s: s.power_flow_grid if s.power_flow_grid is not None else 0.0, + value_fn=lambda s: (-s.power_flow_grid or 0.0) if s.power_flow_grid is not None else 0.0, ) # Site power sensor (conditionally created when power-flows data is available) diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index 27372e98..c8692178 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -11,10 +11,10 @@ "invalid_auth": "Autenticación invalida", "unknown": "Error inesperado", "host_required": "Se requiere host", - "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + "fqdn_registration_failed": "No se pudo registrar el nombre de dominio en el panel o el certificado TLS no se actualizó a tiempo." }, "progress": { - "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." + "registering_fqdn": "Registrando el nombre de dominio en el panel y esperando la actualización del certificado TLS. Esto puede tardar hasta 60 segundos..." }, "flow_title": "Span Panel ({host})", "step": { diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index 9288afae..0124e7c5 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -11,10 +11,10 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue", "host_required": "L'hôte est requis", - "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + "fqdn_registration_failed": "Impossible d'enregistrer le nom de domaine auprès du panneau ou le certificat TLS n'a pas été mis à jour à temps." }, "progress": { - "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." + "registering_fqdn": "Enregistrement du nom de domaine auprès du panneau et attente de la mise à jour du certificat TLS. Cela peut prendre jusqu'à 60 secondes..." }, "flow_title": "Span Panel ({host})", "step": { diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index 9b035061..e56dc67c 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -11,10 +11,10 @@ "invalid_auth": "認証が無効です", "unknown": "予期しないエラー", "host_required": "ホストが必要です", - "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + "fqdn_registration_failed": "パネルにドメイン名を登録できなかったか、TLS証明書が時間内に更新されませんでした。" }, "progress": { - "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." + "registering_fqdn": "パネルにドメイン名を登録し、TLS証明書の更新を待っています。最大60秒かかる場合があります..." }, "flow_title": "スパンパネル ({host})", "step": { diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index 49477cec..c243a3c5 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -11,10 +11,10 @@ "invalid_auth": "Autenticação inválida", "unknown": "Erro inesperado", "host_required": "O anfitrião é necessário", - "fqdn_registration_failed": "Could not register the domain name with the panel or the TLS certificate was not updated in time." + "fqdn_registration_failed": "Não foi possível registar o nome de domínio no painel ou o certificado TLS não foi atualizado a tempo." }, "progress": { - "registering_fqdn": "Registering domain name with the panel and waiting for TLS certificate update. This may take up to 60 seconds..." + "registering_fqdn": "A registar o nome de domínio no painel e a aguardar a atualização do certificado TLS. Isto pode demorar até 60 segundos..." }, "flow_title": "Painel Span ({host})", "step": { From f6cc94bcb0201a1268cfc72ec1b842e46816ac48 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:54 -0700 Subject: [PATCH 29/36] Add FQDN registration changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36abbb28..5dd0afbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All notable changes to this project will be documented in this file. - **Grid Power sensor** — New `Grid Power`. Previously only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own grid power accounting alongside Battery Power, PV Power, and Site Power. Without BESS `Grid Power` is the same as `Current Power`. +- **FQDN registration support** — Config flow detects FQDN-based connections and registers the domain with the panel for TLS certificate SAN inclusion. + Blocked by an upstream API permission issue ([SPAN-API-Client-Docs#10](https://github.com/spanio/SPAN-API-Client-Docs/issues/10)); the integration falls back + to IP-based connections until resolved. ### Changed From 03fb7852e11e7ec8cd200281dacd844fd596a909 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:06:49 -0700 Subject: [PATCH 30/36] Update README: power flow sensors are always available in eBus Grid Power and Site Power are standard eBus device properties, not conditional on specific hardware. Remove "only when power-flows node active" qualifiers and drop "conditional" from the section heading. Battery Power and PV Power remain correctly gated on BESS/PV presence. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98977510..f6b31ce0 100644 --- a/README.md +++ b/README.md @@ -127,14 +127,14 @@ If you encounter issues, restore from your backup or check the [troubleshooting | Downstream L2 Current | Current | A | Downstream lugs L2 current | | Main Breaker Rating | Current | A | Main breaker amperage | -### Power Flow Sensors (v2 only, conditional) +### Power Flow Sensors (v2 only) | Sensor | Device Class | Unit | Notes | | ------------- | ------------ | ---- | --------------------------------------------------------------------------- | +| Grid Power | Power | W | Grid power flow | +| Site Power | Power | W | Total site power (grid + PV + battery) | | Battery Power | Power | W | Battery charge/discharge (+discharge, -charge). Only when BESS commissioned | | PV Power | Power | W | PV generation (+producing). Only when PV commissioned | -| Grid Power | Power | W | Computed grid power flow. Only when power-flows node active | -| Site Power | Power | W | Total site power (grid + PV + battery). Only when power-flows node active | ### PV Metadata Sensors (v2 only, on main panel device) From 14451e8fc02be33515d147640b95f1270406cb06 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:19:03 -0700 Subject: [PATCH 31/36] Migrate from pre-commit to prek and add BESS documentation Replace pre-commit with prek (Rust-based drop-in replacement) for faster hook execution. Convert .pre-commit-config.yaml to prek.toml, update CI to install prek binary, and update all documentation references. Remove copilot-instructions.md from tracking. Add BESS grid management documentation with topology diagrams. --- .github/copilot-instructions.md | 266 ------------------ .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 7 +- .gitignore | 1 + .pre-commit-config.yaml | 140 --------- CHANGELOG.md | 6 +- README.md | 174 +++++++----- bess-grid-management.md | 167 +++++++++++ docs/developer.md | 12 +- docs/images/.gitignore | 2 + docs/images/bess-topology-integrated.drawio | 117 ++++++++ docs/images/bess-topology-integrated.svg | 3 + .../bess-topology-non-integrated.drawio | 92 ++++++ docs/images/bess-topology-non-integrated.svg | 3 + prek.toml | 169 +++++++++++ pyproject.toml | 2 +- 16 files changed, 679 insertions(+), 484 deletions(-) delete mode 100644 .github/copilot-instructions.md delete mode 100644 .pre-commit-config.yaml create mode 100644 bess-grid-management.md create mode 100644 docs/images/.gitignore create mode 100644 docs/images/bess-topology-integrated.drawio create mode 100644 docs/images/bess-topology-integrated.svg create mode 100644 docs/images/bess-topology-non-integrated.drawio create mode 100644 docs/images/bess-topology-non-integrated.svg create mode 100644 prek.toml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 11d3098b..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,266 +0,0 @@ -# Copilot Instructions for SPAN Panel Integration - -You are working on a Home Assistant custom integration for SPAN Panel, a smart electrical panel that provides circuit-level monitoring and control. - -## Project Context - -This is a **Home Assistant custom integration** (not a standalone application) that: - -- Connects to SPAN Panel hardware via REST API -- Provides sensors, switches, and controls for Home Assistant -- Manages circuit-level power monitoring and energy tracking -- Handles authentication, data coordination, and state management - -## Tech Stack - -- **Python**: 3.13.2+ (strictly required) -- **Framework**: Home Assistant 2025.12.4 -- **Package Manager**: Poetry (not pip) -- **Type Checking**: MyPy, Pyright -- **Linting/Formatting**: Ruff (primary), Pylint (import checks only) -- **Code Quality**: Bandit (security), Radon (complexity) -- **Testing**: pytest with pytest-homeassistant-custom-component -- **Pre-commit**: Enforced hooks for all commits - -## Project Structure - -```text -custom_components/span_panel/ # Main integration code -├── __init__.py # Integration setup -├── config_flow.py # Configuration UI -├── coordinator.py # Data update coordinator -├── sensor.py # Sensor platform -├── switch.py # Switch platform -├── select.py # Select platform -├── binary_sensor.py # Binary sensor platform -├── services/ # Custom services -├── sensors/ # Sensor definitions -└── ... -tests/ # Test files -scripts/ # Development scripts -``` - -## Coding Standards and Conventions - -### Code Style - -1. **Imports**: Use absolute imports. No relative imports like `from .module import X`. Always import from `custom_components.span_panel` or use top-level - imports -2. **Imports Location**: ALL imports MUST be at the top of the file (enforced by Pylint). No `import-outside-toplevel` violations allowed -3. **Type Hints**: Required for all functions and methods (enforced by MyPy with strict settings) -4. **Docstrings**: Required for all public modules, classes, and functions (Google style, enforced by Ruff D-rules) -5. **Line Length**: 100 characters maximum (enforced by Ruff) -6. **String Quotes**: Double quotes `"` preferred (enforced by Ruff formatter) -7. **Complexity**: Maximum cyclomatic complexity of 25 (enforced by Radon) - -### Home Assistant Specific - -1. **Entity IDs**: Support both friendly names and circuit numbers patterns (see `entity_id_naming_patterns.py`) -2. **Async**: All I/O operations must be async (Home Assistant requirement) -3. **Coordinator Pattern**: Use `DataUpdateCoordinator` for polling data -4. **Config Flow**: Use config flow for UI-based configuration (no YAML config) -5. **Services**: Define in `services.yaml` with proper schema validation -6. **Translations**: Add UI strings to `strings.json` and `translations/en.json` - -### Python Patterns - -1. **Exception Handling**: Use specific exceptions from `exceptions.py` -2. **Constants**: Define in `const.py`, use UPPER_CASE naming -3. **Data Classes**: Use `@dataclass` with type hints -4. **None Checks**: Use `if value is None:` not `if not value:` -5. **Dictionary Access**: Use `.get()` with defaults instead of try/except for missing keys - -## Build, Test, and Lint Commands - -### Setup (First Time) - -```bash -# Install dependencies -poetry install --with dev - -# Install pre-commit hooks -poetry run pre-commit install -``` - -### Development Workflow - -```bash -# Run all pre-commit checks manually -poetry run pre-commit run --all-files - -# Run tests with coverage -poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=term-missing -v - -# Run specific test file -poetry run pytest tests/test_config_flow.py -v - -# Type checking -poetry run mypy custom_components/span_panel/ - -# Linting only -poetry run ruff check custom_components/span_panel/ - -# Format code -poetry run ruff format custom_components/span_panel/ - -# Security check -poetry run bandit -c pyproject.toml -r custom_components/span_panel/ - -# Check complexity -poetry run radon cc --min=B custom_components/span_panel/ -``` - -### Before Committing - -Pre-commit hooks will automatically run when you commit. If hooks modify files, review changes, re-stage, and commit again. - -## Boundaries and Precautions - -### NEVER Do These Things - -1. **DO NOT** use `pip install` - always use `poetry add` or `poetry add --group dev` -2. **DO NOT** add imports inside functions (violates `import-outside-toplevel`) -3. **DO NOT** modify files in `.github/workflows/` without understanding CI implications -4. **DO NOT** change the Home Assistant version in `pyproject.toml` without testing -5. **DO NOT** add type: ignore comments without specific error codes (enforced by MyPy) -6. **DO NOT** commit secrets, tokens, or API keys -7. **DO NOT** modify the SPAN Panel API contract (it's external hardware) -8. **DO NOT** break backward compatibility with existing entity IDs without migration -9. **DO NOT** add print statements in production code (use logging via `_LOGGER`) -10. **DO NOT** modify translation files for other languages (only `en.json`) - -### Be Careful With - -1. **Entity Migrations**: Changes to entity IDs require migration logic (see `migration.py`) -2. **Energy Statistics**: SPAN panels can reset causing data issues (see spike cleanup service) -3. **Authentication**: Door proximity auth is time-limited (15 minutes) -4. **Async Operations**: All Home Assistant platform methods must be async -5. **Test Coverage**: Maintain or improve coverage percentage - -### Files You Should NOT Modify - -- `.github/workflows/*.yml` - CI/CD configuration (unless explicitly asked) -- `poetry.lock` - Managed by Poetry (use `poetry lock` to update) -- `custom_components/span_panel/manifest.json` - Version and metadata (coordinate with release process) -- `custom_components/span_panel/translations/*.json` - Non-English translations - -## File Naming and Organization - -### New Python Files - -- Test files: `test_*.py` in `tests/` directory -- Service files: `*_service.py` or module in `services/` directory -- Sensor definitions: In `sensors/` directory -- Utilities: `*_utils.py` or in existing utility modules - -### Naming Conventions - -- Classes: `PascalCase` (e.g., `SpanPanelCoordinator`) -- Functions/Methods: `snake_case` (e.g., `async_setup_entry`) -- Constants: `UPPER_SNAKE_CASE` (e.g., `DOMAIN`, `SCAN_INTERVAL`) -- Private members: Prefix with `_` (e.g., `_async_update_data`) -- Type variables: `PascalCase` with `T` suffix (e.g., `DataT`) - -## Testing Guidelines - -1. **Test Location**: All tests in `tests/` directory -2. **Fixtures**: Use pytest fixtures from `conftest.py` -3. **Factories**: Use test factories from `factories.py` for test data -4. **Mocking**: Use pytest-homeassistant-custom-component mocks -5. **Coverage**: Aim for >80% coverage for new code -6. **Test Types**: Unit tests, integration tests with Home Assistant test harness - -### Test File Structure - -```python -"""Test module for X functionality.""" -import pytest -from homeassistant.core import HomeAssistant -# ... other imports - -async def test_something(hass: HomeAssistant) -> None: - """Test that something works correctly.""" - # Arrange - # Act - # Assert -``` - -## Common Patterns in This Codebase - -### Logging - -```python -import logging -_LOGGER = logging.getLogger(__name__) - -_LOGGER.debug("Debug message") -_LOGGER.info("Info message") -_LOGGER.warning("Warning message") -_LOGGER.error("Error message") -``` - -### Data Coordinator - -```python -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -class SpanPanelCoordinator(DataUpdateCoordinator[dict]): - """Class to manage fetching SPAN Panel data.""" - - async def _async_update_data(self) -> dict: - """Fetch data from API.""" -``` - -### Config Flow - -```python -from homeassistant import config_entries - -class SpanPanelConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow.""" -``` - -## When Making Changes - -### For Bug Fixes - -1. Write a failing test that reproduces the bug -2. Fix the bug with minimal changes -3. Verify the test passes -4. Run the full test suite -5. Run pre-commit hooks - -### For New Features - -1. Check if the feature fits Home Assistant patterns -2. Update `const.py` with any new constants -3. Add type hints and docstrings -4. Write comprehensive tests -5. Update `strings.json` for any new UI elements -6. Update README.md if it's a user-facing feature - -### For Refactoring - -1. Ensure tests exist and pass before refactoring -2. Make changes incrementally -3. Run tests after each change -4. Maintain or improve type coverage -5. Keep the same external API/behavior - -## Useful References - -- [Home Assistant Developer Docs](https://developers.home-assistant.io/) -- [Home Assistant Integration Quality Scale](https://www.home-assistant.io/docs/quality_scale/) -- Repository README.md - User-facing documentation -- `developer_attribute_readme.md` - Developer notes on attributes - -## Questions to Ask Before Starting - -1. Does this change affect entity IDs? (Need migration logic?) -2. Does this change affect the API integration? (Breaking change?) -3. Is this a user-facing change? (Update README and strings?) -4. Does this require new tests? -5. Does this change affect Home Assistant compatibility? - -Remember: This is a custom integration that users install in their Home Assistant instance. Breaking changes can affect real home automation systems, so test -thoroughly and maintain backward compatibility when possible. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 84538b78..20d5d5d0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,7 +18,7 @@ updates: - "mypy" - "ruff" - "bandit" - - "pre-commit" + - "prek" - "isort" - "pylint" - "radon" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d883c305..5a75ce74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: - name: Install Poetry uses: snok/install-poetry@v1 + - name: Install prek + run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/latest/download/prek-installer.sh | sh + - name: Install dependencies run: | # Replace path dependencies with PyPI versions for CI @@ -48,8 +51,8 @@ jobs: - name: Check poetry configuration run: poetry check - - name: Run pre-commit hooks (for extra validation) - run: poetry run pre-commit run --all-files --show-diff-on-failure + - name: Run prek hooks (for extra validation) + run: prek run --all-files --show-diff-on-failure env: SKIP: poetry-lock,poetry-check diff --git a/.gitignore b/.gitignore index dc0e7eba..47930c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ env/ htmlcov/ .pytest_cache/ docs/agile-readme.md +.github/copilot-instructions.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index fb5695ec..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,140 +0,0 @@ -repos: - # Pre-commit hooks for essential file checks only - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: check-yaml - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: check-toml - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: check-json - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: check-added-large-files - - id: check-merge-conflict - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: debug-statements - exclude: '^tests/.*|\..*_cache/.*|dist/.*|venv/.*' - - id: trailing-whitespace - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: end-of-file-fixer - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - id: mixed-line-ending - args: ['--fix=lf'] - exclude: '\..*_cache/.*|dist/.*|venv/.*' - - # Pylint check for import-outside-toplevel (run before ruff) - - repo: local - hooks: - - id: pylint-import-check - name: pylint import-outside-toplevel check - entry: poetry run pylint - language: system - args: ['--disable=all', '--enable=import-outside-toplevel', '--score=no', 'custom_components/span_panel/'] - pass_filenames: false - files: \.py$ - exclude: '^tests/.*|^scripts/.*' - - # Ruff for linting, import sorting, and primary formatting - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 - hooks: - - id: ruff-format - exclude: '^tests/.*|scripts/.*|\..*_cache/.*|dist/.*|venv/.*' - - id: ruff-check - args: ['--fix'] - exclude: '^tests/.*|scripts/.*|\..*_cache/.*|dist/.*|venv/.*' - - # MyPy for type checking - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 - hooks: - - id: mypy - additional_dependencies: - - httpx - - pydantic - - typing-extensions - - pytest - - homeassistant-stubs - - types-PyYAML - - types-aiofiles - args: ['--config-file=pyproject.toml'] - exclude: '^tests/.*|^scripts/.*|docs/.*|\..*_cache/.*|dist/.*|venv/.*' - - # Markdownlint for markdown files - - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.18.1 - hooks: - - id: markdownlint-cli2 - args: ['--config', '.markdownlint-cli2.jsonc'] - exclude: '\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*|tests/.*' - - # Markdown formatting using shared script (local only) - - repo: local - hooks: - - id: markdown-format - name: Format markdown files - entry: scripts/fix-markdown.sh - language: system - args: [.] - types: [markdown] - pass_filenames: false - always_run: true - stages: [manual] - - # Check for common security issues - - repo: https://github.com/PyCQA/bandit - rev: 1.8.3 - hooks: - - id: bandit - args: ['-c', 'pyproject.toml'] - additional_dependencies: ['bandit[toml]'] - exclude: '^tests/.*|^scripts/.*|\..*_cache/.*|dist/.*|venv/.*' - - # Poetry check for pyproject.toml validation - - repo: https://github.com/python-poetry/poetry - rev: 2.1.3 - hooks: - - id: poetry-check - - id: poetry-lock - - # Radon for code metrics and maintainability (local) - - repo: local - hooks: - - id: radon-complexity - name: radon complexity check - entry: poetry run radon - language: system - args: ['cc', '--min=B', '--show-complexity', 'custom_components/span_panel/'] - pass_filenames: false - files: \.py$ - - id: radon-maintainability - name: radon maintainability index - entry: poetry run radon - language: system - args: ['mi', '--min=B', '--show', 'custom_components/span_panel/'] - pass_filenames: false - files: \.py$ - - # Coverage check with pytest output and coverage report - - repo: local - hooks: - - id: pytest-cov-summary - name: coverage summary - entry: bash - language: system - args: ['-c', 'echo "Running tests with coverage..."; poetry run pytest tests/ --cov=custom_components/span_panel --cov-config=pyproject.toml --cov-report=term-missing:skip-covered -v; exit_code=$?; echo; if [ $exit_code -eq 0 ]; then echo "✅ Tests passed with coverage report above"; else echo "❌ Tests failed"; fi; exit $exit_code'] - pass_filenames: false - always_run: true - verbose: true - - # Sync dependencies from manifest.json to CI workflow (run last) - - repo: local - hooks: - - id: sync-dependencies - name: sync dependency versions between manifest.json and CI workflow - entry: python scripts/sync-dependencies.py - language: system - pass_filenames: false - always_run: true - stages: [pre-commit] - verbose: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd0afbb..f29746e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,9 @@ All notable changes to this project will be documented in this file. - **Grid Power sensor** — New `Grid Power`. Previously only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own grid power accounting alongside Battery Power, PV Power, and Site Power. Without BESS `Grid Power` is the same as `Current Power`. -- **FQDN registration support** — Config flow detects FQDN-based connections and registers the domain with the panel for TLS certificate SAN inclusion. - Blocked by an upstream API permission issue ([SPAN-API-Client-Docs#10](https://github.com/spanio/SPAN-API-Client-Docs/issues/10)); the integration falls back - to IP-based connections until resolved. +- **FQDN registration support** — Config flow detects FQDN-based connections and registers the domain with the panel for TLS certificate SAN inclusion. Blocked + by an upstream API permission issue ([SPAN-API-Client-Docs#10](https://github.com/spanio/SPAN-API-Client-Docs/issues/10)); the integration falls back to + IP-based connections until resolved. ### Changed diff --git a/README.md b/README.md index f6b31ce0..affd1dc5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ monitoring and control of your home's electrical system. [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) [![Mypy](https://img.shields.io/badge/mypy-checked-blue)](http://mypy-lang.org/) [![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![prek](https://img.shields.io/badge/prek-enabled-brightgreen)](https://github.com/j178/prek) The software is provided as-is with no warranty or guarantee of performance or suitability to your particular setting. @@ -94,6 +94,21 @@ When upgrading through HACS: If you encounter issues, restore from your backup or check the [troubleshooting section](#troubleshooting) below. +## Key Terms + +The following terms appear throughout this document and in the integration's sensors: + +- **Grid-forming entity (GFE)** — The power source that sets the voltage and frequency reference for the home. When the utility grid is up, it is the GFE. When + islanded on battery, the battery inverter becomes the GFE. +- **Islanded** — The home is electrically disconnected from the utility grid and running on its own power source, typically battery. Circuits may be shed to + conserve battery life. +- **Microgrid** — When the home is islanded, the battery inverter creates a small, self-contained electrical grid for the home. This local grid functions + independently of the utility — the inverter generates AC power at the correct voltage and frequency, and the home's circuits run on it just as they would on + utility power. +- **Microgrid Interconnect Device (MID)** — A switch, part of or alongside the battery system, that disconnects the home from the utility grid during an outage. + While open, the panel's sensors can only see the home side. +- **Shedding** — Automatically turning off lower-priority circuits to conserve battery during an outage, based on each circuit's configured shed priority. + ## Entity Reference ### Panel-Level Sensors @@ -166,6 +181,50 @@ Applies to Current Power, Feed Through Power, Battery Power, PV Power, Grid Powe | `panel_size` | int | Total breaker spaces (e.g., 32, 40) | | `wifi_ssid` | string | Current Wi-Fi network | +### EVSE (EV Charger) Entities + +Created automatically when a SPAN Drive or other EVSE is commissioned on the panel. Each EVSE appears as a separate sub-device linked to the panel via +`via_device`. Vendor, product, serial number, and software version are surfaced as device info attributes — not separate entities. + +#### EVSE Device Naming + +The EVSE device name includes the panel device name prefix for collision avoidance across multi-panel installations and to support HA's bulk device rename +feature. A display suffix differentiates multiple chargers on the same panel: + +- **Friendly names** (`USE_CIRCUIT_NUMBERS=False`): suffix is the fed circuit's panel name (e.g., "Garage") +- **Circuit numbers** (`USE_CIRCUIT_NUMBERS=True`): suffix is the EVSE serial number (e.g., "SN-EVSE-001") +- **No suffix available**: the display suffix is omitted entirely (no empty parentheses) + +| Naming Mode | Example Device Name | Example Entity ID | +| --------------- | ------------------------------------- | --------------------------------------------------------- | +| Friendly names | `Main House SPAN Drive (Garage)` | `sensor.main_house_span_drive_garage_charger_status` | +| Circuit numbers | `Main House SPAN Drive (SN-EVSE-001)` | `sensor.main_house_span_drive_sn_evse_001_charger_status` | +| No suffix | `Main House SPAN Drive` | `sensor.main_house_span_drive_charger_status` | + +#### EVSE Sensors (per charger) + +| Sensor | Device Class | Unit | Notes | +| ------------------ | ------------ | ---- | -------------------------------------------------------------------------------- | +| Charger Status | Enum | — | OCPP-based states: AVAILABLE, PREPARING, CHARGING, SUSPENDED_EV, etc. Translated | +| Advertised Current | Current | A | Amps offered to the vehicle | +| Lock State | Enum | — | LOCKED, UNLOCKED, UNKNOWN. Translated | + +#### EVSE Binary Sensors (per charger) + +| Sensor | Device Class | Notes | +| ------------ | ---------------- | ------------------------------------------------------------------ | +| Charging | Battery Charging | ON when status is CHARGING | +| EV Connected | Plug | ON when status is PREPARING, CHARGING, SUSPENDED\_\*, or FINISHING | + +#### EVSE Device Info Attributes + +| Attribute | Source | +| ---------------- | ------------------ | +| Manufacturer | `vendor-name` | +| Model | `product-name` | +| Serial Number | `serial-number` | +| Software Version | `software-version` | + ### BESS Sub-Device (v2 only, conditional) When a Battery Energy Storage System (BESS) is commissioned, the integration creates a separate BESS sub-device linked to the panel via `via_device`. The BESS @@ -228,50 +287,6 @@ Applies to Main Meter and Feed Through energy sensors. | `tabs` | string | Breaker slot position(s) | | `voltage` | string | 120 or 240 (derived from tab count) | -### EVSE (EV Charger) Entities - -Created automatically when a SPAN Drive or other EVSE is commissioned on the panel. Each EVSE appears as a separate sub-device linked to the panel via -`via_device`. Vendor, product, serial number, and software version are surfaced as device info attributes — not separate entities. - -#### EVSE Device Naming - -The EVSE device name includes the panel device name prefix for collision avoidance across multi-panel installations and to support HA's bulk device rename -feature. A display suffix differentiates multiple chargers on the same panel: - -- **Friendly names** (`USE_CIRCUIT_NUMBERS=False`): suffix is the fed circuit's panel name (e.g., "Garage") -- **Circuit numbers** (`USE_CIRCUIT_NUMBERS=True`): suffix is the EVSE serial number (e.g., "SN-EVSE-001") -- **No suffix available**: the display suffix is omitted entirely (no empty parentheses) - -| Naming Mode | Example Device Name | Example Entity ID | -| --------------- | ------------------------------------- | --------------------------------------------------------- | -| Friendly names | `Main House SPAN Drive (Garage)` | `sensor.main_house_span_drive_garage_charger_status` | -| Circuit numbers | `Main House SPAN Drive (SN-EVSE-001)` | `sensor.main_house_span_drive_sn_evse_001_charger_status` | -| No suffix | `Main House SPAN Drive` | `sensor.main_house_span_drive_charger_status` | - -#### EVSE Sensors (per charger) - -| Sensor | Device Class | Unit | Notes | -| ------------------ | ------------ | ---- | -------------------------------------------------------------------------------- | -| Charger Status | Enum | — | OCPP-based states: AVAILABLE, PREPARING, CHARGING, SUSPENDED_EV, etc. Translated | -| Advertised Current | Current | A | Amps offered to the vehicle | -| Lock State | Enum | — | LOCKED, UNLOCKED, UNKNOWN. Translated | - -#### EVSE Binary Sensors (per charger) - -| Sensor | Device Class | Notes | -| ------------ | ---------------- | ------------------------------------------------------------------ | -| Charging | Battery Charging | ON when status is CHARGING | -| EV Connected | Plug | ON when status is PREPARING, CHARGING, SUSPENDED\_\*, or FINISHING | - -#### EVSE Device Info Attributes - -| Attribute | Source | -| ---------------- | ------------------ | -| Manufacturer | `vendor-name` | -| Model | `product-name` | -| Serial Number | `serial-number` | -| Software Version | `software-version` | - ### Binary Sensors | Sensor | Device Class | Notes | @@ -311,38 +326,67 @@ Labels match the SPAN Home On-Premise app. Translations are provided for all sup | ---------------------------- | ------ | ------------------------------------------------------------------------------------ | | GFE Override: Grid Connected | Button | (v2) Tell the panel the grid is up. Only present on MQTT-connected panels. See below | -### Grid Forming Entity +### BESS & Grid Management + +This section explains how the SPAN panel manages power sources and load shedding when a Battery Energy Storage System (BESS) is installed, and what the +integration can and cannot tell you about grid status. -The Grid Forming Entity (GFE) identifies which power source provides the frequency and voltage reference for the home. When GFE is Grid, the utility grid sets -the reference and all circuits remain on. When GFE is Battery, the battery inverter is the reference and circuits are shed based on each circuit's configured -shed priority. +#### Grid Forming Entity -When a battery system (BESS) is installed, the panel relies on the BESS to determine whether the grid is online and to set the GFE accordingly. If BESS -communication is lost while the panel is islanded, the GFE value becomes stale — it may show Battery when the grid has actually been restored, causing -unnecessary shedding to continue. +The Grid Forming Entity (GFE) sensor identifies which power source provides the voltage and frequency reference for the home — not which source is producing the +most watts. When GFE is Grid, the utility grid sets the reference and all circuits remain on, even if 100% of consumption comes from solar. When GFE is Battery, +the battery inverter is the reference and circuits are shed based on each circuit's configured shed priority. -The panel cannot detect grid restoration while islanded because the Microgrid Interconnect Device (MID) is open. The panel's power sensors are on the home side -of the open switch and measure only battery-supplied power — grid restoration on the utility side is invisible to any panel-side measurement. This is a physical -limitation, not a software gap. The `DSM State` sensor inherits the same blind spot for the same reason. +| GFE Value | Meaning | +| --------- | ----------------------------------------------------------------- | +| GRID | Panel is grid-connected (includes generator power, see deep dive) | +| BATTERY | Panel is islanded, running on battery | +| PV | Panel is islanded, running on solar (future) | +| GENERATOR | Panel is islanded, running on generator (future) | +| NONE | Panel is islanded with no power source | +| UNKNOWN | State not yet determined or fault condition | -The **GFE Override: Grid Connected** button exists for this scenario. It publishes a temporary `GRID` command to the panel telling it the grid is back and -shedding can stop. When the BESS restores communication, it automatically reclaims control and the override is superseded. +When a BESS is installed, the panel relies on the BESS to determine whether the grid is online and to set the GFE accordingly. If BESS communication is lost +while the panel is islanded, the GFE value becomes stale — it may show Battery when the grid has actually been restored, causing unnecessary shedding to +continue. -#### Detecting grid restoration +#### What the Panel Can Detect -The panel requires an external signal to know the grid is back while islanded. Options include: +**Grid loss** — The panel independently detects grid loss via its own voltage monitoring, even if BESS communication is already lost. The MID is still closed at +this point, so the panel's sensors see the real voltage drop and respond immediately. -- An Automatic Transfer Switch (ATS) or Manual Transfer Switch (MTS) with a utility-side contact closure, integrated into Home Assistant as a binary sensor -- Utility notification, neighbor confirmation, or physical observation +**Grid restoration while islanded** — Not detectable by the panel. While the MID is open, the panel's sensors are on the home side and measure only +battery-supplied power. Grid restoration on the utility side of the open MID is invisible to any panel-side measurement. This is a physical limitation, not a +software gap. A utility-side sensor — such as a current clamp (e.g., Emporia Vue), ATS/MTS contact closure, or any device that can see the grid side of the MID +— integrated into Home Assistant as a binary sensor can provide this signal. -**WARNING** - Do _not_ automate the GFE override button based on `dsm_state` — it will read `DSM_OFF_GRID` even after the grid is restored because the panel's -sensors cannot see past the open MID. Manual confirmation or an external sensor is required before pressing the button. +#### DSM State Sensor -Pressing "GFE Override: Grid Connected" when actually off-grid will prevent shedding and drain the battery faster. The battery protects itself by disconnecting -when depleted, so there is no overload risk, but runtime will be reduced. +The integration's `DSM State` sensor combines multiple panel signals to provide defense-in-depth for grid status detection. It corroborates the Grid Forming +Entity with BESS grid state and power measurements, which adds confidence during transient inconsistencies and detects some edge cases — for example, when BESS +communication is lost while on-grid and the grid subsequently drops, the panel self-corrects via voltage detection and the corroborating signals confirm it. + +However, when the panel is islanded and the MID is open, all of the panel's signals measure the home side. No combination of panel-sourced data can detect grid +restoration in this state. Only an external signal (utility-side sensor) or manual confirmation via the GFE Override button can resolve it. + +#### GFE Override Button + +The **GFE Override: Grid Connected** button tells the panel that the grid is back and shedding can stop. When the BESS restores communication, it automatically +reclaims control and the override is superseded — no manual undo is needed. + +**Risk asymmetry** — Telling the panel to shed (conservative direction) is low-risk; worst case is unnecessary circuit disruption. Telling the panel the grid is +back when it is not means unmanaged battery drain and reduced runtime, which could affect critical equipment. The battery protects itself by disconnecting when +depleted, so there is no overload risk, but runtime will be reduced. Use the override button only with confidence that the grid has actually been restored — via +a utility-side sensor or manual confirmation. + +**WARNING** — Do _not_ automate the GFE override button based on `DSM State` — it inherits the same MID blind spot described above and will read `dsm_off_grid` +even after the grid is restored. Manual confirmation or an external sensor is required before pressing the button. When `bess_connected` returns to on, no action is needed — firmware resumes normal GFE management automatically. +For a detailed discussion of failure scenarios, the MID topology, generator and non-integrated BESS behavior, and `/set` risk analysis, see +[BESS & Grid Management Deep Dive](bess-grid-management.md). + ## Configuration Options ### Snapshot Update Interval diff --git a/bess-grid-management.md b/bess-grid-management.md new file mode 100644 index 00000000..f2810b8c --- /dev/null +++ b/bess-grid-management.md @@ -0,0 +1,167 @@ +# BESS & Grid Management Deep Dive + +This document provides detailed technical context for how the SPAN panel manages grid status, load shedding, and battery system interaction. It supplements the +[BESS & Grid Management](README.md#bess--grid-management) section in the README. + +## What You Should Know + +**The panel detects outages on its own.** When the utility grid drops, the panel sees the voltage sag and responds immediately — even if BESS communication is +already lost. No action is needed from you. + +**The panel cannot detect grid restoration while islanded.** Once the MID (Microgrid Interconnect Device) opens to isolate the home, every sensor on the panel +measures battery-supplied power. Grid restoration on the utility side of the open MID is invisible. This is a physical limitation — no combination of panel +sensors or integration logic can work around it without an upstream electrical current clamp or Automatic Transfer Switch (ATS) signal. + +**If BESS communication is lost while islanded, shedding continues indefinitely** — even after the grid comes back. The panel's last-known state was "off-grid" +and it has no way to learn otherwise until either the BESS reconnects or you intervene. + +**The GFE Override button is the manual fix.** It tells the panel the grid is back and stops shedding. But only press it when you are confident the grid has +actually been restored — pressing it while truly off-grid means unmanaged battery drain and reduced runtime. See +[GFE Override — Risk by Direction](#gfe-override--risk-by-direction) for details. + +**An external utility-side sensor is the best long-term solution.** A current clamp (e.g., Emporia Vue), ATS/MTS contact closure, or any device on the grid side +of the MID, integrated into Home Assistant as a binary sensor, can detect grid restoration and trigger automations that the panel's own sensors cannot. + +**The `DSM State` sensor adds confidence but shares the blind spot.** It corroborates the Grid Forming Entity with additional signals and catches some edge +cases (like the panel self-correcting after detecting grid loss). But for the core problem — grid restored while islanded with BESS comms lost — it cannot help. +All its inputs measure the home side of the open MID. + +**Generators appear as grid power.** The panel cannot distinguish between utility and generator power. GFE reports GRID when a generator is running. No +automatic load shedding is available with generators — that requires a +[compatible integrated BESS](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN). + +**Non-integrated battery systems provide no panel awareness.** If the BESS is not on the +[compatible integrations list](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN), the panel does not know it is +islanded during an outage. GFE reports GRID, power flows show battery as grid, and no automatic shedding is available. + +## System Topology + +The diagrams below show the key components and where sensors can observe power. The critical insight is that the panel's sensors are on the **home side** of the +MID — they cannot see what is happening on the utility side when the MID is open. + +![Integrated BESS Topology](docs/images/bess-topology-integrated.svg) + +Editable source: [docs/images/bess-topology-integrated.drawio](docs/images/bess-topology-integrated.drawio) + +**Key observations:** + +- When the MID is **closed** (on-grid), the panel's sensors see real grid power and can detect changes. +- When the MID is **open** (islanded), the panel's sensors see battery-supplied power only. The utility side is invisible. +- A **utility-side sensor** (current clamp, ATS contact closure, etc.) is the only way to detect grid restoration while islanded. +- **Generators** connect upstream of the MID via an ATS/MTS. The panel sees generator power as grid power — it cannot distinguish between the two. + +### Non-Integrated BESS Topology + +When the battery system is not on the +[compatible integrations list](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN), there is no communication link +between the BESS and the panel. The panel cannot distinguish battery power from grid power and has no awareness that it is islanded during an outage. No +automatic load shedding is available. + +![Non-Integrated BESS Topology](docs/images/bess-topology-non-integrated.svg) + +Editable source: [docs/images/bess-topology-non-integrated.drawio](docs/images/bess-topology-non-integrated.drawio) + +**Key differences from the integrated topology:** + +- **No comms link** — The panel has no communication with the BESS and cannot receive grid state updates. +- **GFE reports GRID** even when islanded — the panel does not know it is running on battery. +- **No automatic shedding** — Without BESS communication, the panel cannot manage circuits during an outage. +- **Power flows show battery as grid** — All incoming power appears as grid power in sensors and dashboards. + +## Technical Details + +### How the Panel Determines Grid Status + +The Grid Forming Entity (GFE) is determined from multiple inputs: + +1. **BESS reporting** — The BESS communicates grid state to the panel and is authoritative when connected. This is the primary source for grid status. +2. **Panel voltage monitoring** — The panel monitors voltage on the main conductors and can independently detect grid loss (voltage sag/collapse) even if BESS + communication is already lost. + +The panel is **not** entirely BESS-dependent for detecting outages. However, for detecting grid **restoration** while islanded, the BESS is the only source — +the panel's voltage monitoring cannot distinguish BESS-generated power from utility power on the home side of an open MID. + +### Stale GFE Scenarios + +When BESS communication is lost, the GFE value reflects the last-known state. The following matrix shows what happens in each failure sequence and whether the +actual state is detectable. + +| # | Sequence | GFE (stale) | Actual State | Impact | Detectable? | +| --- | -------------------------------------------------- | ----------- | ------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| 1 | BESS comms drop while on-grid, grid stays up | GRID | On-grid | None — stale value happens to be correct | N/A | +| 2 | BESS comms drop while off-grid, grid stays down | BATTERY | Off-grid | None — stale value happens to be correct | N/A | +| 3 | Grid restored, BESS comms still down | BATTERY | On-grid | Unnecessary shedding continues | **No** — MID is open, all panel sensors show battery power. Only correctable via GFE Override | +| 4 | BESS comms drop while on-grid, then grid drops | GRID | Off-grid | No shedding — battery drains faster, reduced runtime | **Yes** — MID still closed, panel detects voltage sag independently and self-corrects | +| 5 | BESS comms drop while off-grid, then grid restores | BATTERY | On-grid | Unnecessary shedding continues (same as #3) | **No** — MID is open, same blind spot as #3. Only correctable via GFE Override | +| 6 | BESS itself fails (comms + battery), grid up | Stale | On-grid | Shedding state depends on last GFE; `bess_connected` goes false | **Yes** — `bess_connected` = false is a distinct signal, but GFE may still be stale | + +Scenario 4 self-corrects because the MID is still closed and the panel can see the real voltage change. Scenarios 3 and 5 cannot self-correct because the MID is +open and all panel-side signals measure battery power. The GFE Override button or an external utility-side sensor is the only resolution. + +### DSM State Sensor — What It Can and Cannot Do + +The integration's `DSM State` sensor corroborates GFE with additional signals: + +- `bess/grid-state` — The BESS's own view of grid connectivity +- Power flow measurements — Grid power flow and upstream lugs active power + +This multi-signal approach adds genuine value for: + +- **Transient inconsistencies** during normal BESS communication +- **Scenario 4** — Grid drops while on-grid with BESS comms already lost. The MID is still closed, so power measurements reflect reality and confirm the panel's + self-correction. +- **Scenario 6** — BESS failure detection via `bess_connected` +- **Defense-in-depth** for edge cases not yet enumerated + +**Limitation:** For scenarios 3 and 5 (grid restored while islanded, BESS comms lost, MID open), no combination of panel-sourced signals can help. All signals +measure the home side of the open MID. Only the GFE Override button or an external utility-side sensor can resolve the stale state. + +### GFE Override — Risk by Direction + +The GFE Override button allows a user or automation to tell the panel what power regime it should operate in. Not all directions carry equal risk. + +| Direction | Action | Risk | Automation Safe? | +| ------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------- | +| GRID → BATTERY | Triggers shedding | **Low** — conservative, extends runtime. Worst case is unnecessary circuit disruption. | Yes | +| BATTERY → GRID | Stops shedding | **Moderate** — if actually off-grid, unmanaged battery drain reduces runtime, could affect critical loads. | User confirmation recommended | +| Any → UNKNOWN | Behavior undefined | **Unknown** | No | +| Any → PV / GENERATOR | Future / undefined | **Unknown** | No | + +**Firmware reclaim behavior:** When the BESS restores communication, it reasserts its authoritative grid state. This produces a new state transition that +overrides any previous GFE Override command. The override is a temporary measure for the BESS-communication-loss window, not a persistent latch. + +### Generator Systems + +SPAN is always installed downstream of a transfer switch (ATS or MTS) in generator installations. There is no communication wiring between the panel and the +generator — the panel sees whatever voltage the transfer switch feeds it. When a generator is running, the panel reports GFE as GRID because it literally cannot +distinguish generator power from utility power. + +Implications: + +- **GFE = GRID does not necessarily mean utility power.** On panels with a generator, it could mean generator power. +- **No automatic load shedding with generators.** Shedding requires a + [compatible integrated BESS](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN). +- **Power flow sensors report generator power as grid power.** This is expected behavior, not a bug. +- The `GENERATOR` GFE value is reserved for possible future use. Distinguishing generator from utility would require either communication with the + generator/transfer switch or a grid-side voltage sensor, neither of which exists in current configurations. + +### Non-Integrated BESS + +For battery systems that are not on the +[compatible integrations list](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN), the panel has no communication +with the battery system. During an outage with a non-integrated BESS: + +- GFE reports **GRID** even when actually off-grid — the panel does not know it is islanded. +- **No automatic load shedding** is available. +- Power flow sensors report all incoming power as grid power. + +This underscores a broader point: GFE reflects what the panel **knows**, which depends on what systems are communicating with it. A non-integrated BESS provides +backup power but no panel awareness. + +## Reference Links + +- [Storage System Integrations with SPAN](https://support.span.io/hc/en-us/articles/4412059545111-Storage-System-Integrations-with-SPAN) +- [Adding Battery Backup to your SPAN Panel](https://support.span.io/hc/en-us/articles/4574797073303-Adding-Battery-Backup-to-your-SPAN-Panel) +- [Can I install SPAN with a standby generator?](https://support.span.io/hc/en-us/articles/6064757711255-Can-I-install-SPAN-with-a-standby-generator) +- [SPAN API Scope & Responsibility Model](https://github.com/spanio/SPAN-API-Client-Docs#span-api-scope--responsibility-model) +- [GitHub Discussion: Migration Guide — v1 dsmState to v2 dominant-power-source](https://github.com/spanio/SPAN-API-Client-Docs/discussions/8) diff --git a/docs/developer.md b/docs/developer.md index 9ea0cc63..ef877b45 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -3,22 +3,22 @@ ## Developer Prerequisites - Poetry -- Pre-commit +- prek - Python 3.13.2+ This project uses [poetry](https://python-poetry.org/) for dependency management. Linting and type checking are accomplished using -[pre-commit](https://pre-commit.com/) which is installed by poetry. +[prek](https://github.com/j178/prek), a fast Rust-based pre-commit framework installed by poetry. ## Developer Setup 1. Install [poetry](https://python-poetry.org/). 2. In the project root run `poetry install --with dev` to install dependencies. -3. Run `poetry run pre-commit install` to install pre-commit hooks. -4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `poetry run pre-commit run --all-files` from the terminal to - manually run pre-commit hooks on files locally in your environment as you make changes. +3. Run `prek install` to install pre-commit hooks. +4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `prek run --all-files` from the terminal to + manually run hooks on files locally in your environment as you make changes. The linters may make changes to files when you try to commit, for example to sort imports. Files that are changed or fail tests will be unstaged. After -reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the pre-commit hook run succeeds, your +reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the prek hook run succeeds, your commit can proceed. ## VS Code diff --git a/docs/images/.gitignore b/docs/images/.gitignore new file mode 100644 index 00000000..753c0f2a --- /dev/null +++ b/docs/images/.gitignore @@ -0,0 +1,2 @@ +*.bkp +*.drawio.svg diff --git a/docs/images/bess-topology-integrated.drawio b/docs/images/bess-topology-integrated.drawio new file mode 100644 index 00000000..f5eadb68 --- /dev/null +++ b/docs/images/bess-topology-integrated.drawio @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/bess-topology-integrated.svg b/docs/images/bess-topology-integrated.svg new file mode 100644 index 00000000..744a943f --- /dev/null +++ b/docs/images/bess-topology-integrated.svg @@ -0,0 +1,3 @@ + + +
UTILITY SIDE — Invisible to panel when MID is open
HOME SIDE
All panel sensors measure here
UTILITY GRID
Utility Meter /
Service Entrance
Generator
+ ATS/MTS
Utility-Side Sensor
e.g. Emporia Vue, ATS
contact closure, clamp
Opens during outage, isolating
home from grid. Built into or
alongside the BESS.
MID
Microgrid Interconnect Device
Upstream Lugs
lugs current / power
PV Inverter
(Solar)
Downstream
Lugs
Sub-Panel or
Second Panel
SPAN Panel
Circuit
Circuit
Circuit
...
BESS / Hybrid Inverter
Battery Pack(s)
commscomms
Text is not SVG - cannot display
diff --git a/docs/images/bess-topology-non-integrated.drawio b/docs/images/bess-topology-non-integrated.drawio new file mode 100644 index 00000000..8e0e0c36 --- /dev/null +++ b/docs/images/bess-topology-non-integrated.drawio @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/bess-topology-non-integrated.svg b/docs/images/bess-topology-non-integrated.svg new file mode 100644 index 00000000..ddb579fa --- /dev/null +++ b/docs/images/bess-topology-non-integrated.svg @@ -0,0 +1,3 @@ + + +
HOME SIDE
All panel sensors measure here
UTILITY GRID
Utility Meter /
Service Entrance
MID
Microgrid Interconnect Device
BESS controls MID but does
NOT report state to panel
Non-Integrated
BESS / Inverter
Battery Pack(s)
✖ NO COMMS
Panel has no awareness of BESS
Upstream Lugs
sees battery power as "grid"
PV Inverter
(Solar)
comms
Downstream
Lugs
SPAN Panel
Circuit
Circuit
Circuit
...
Sub-Panel or
Second Panel
Text is not SVG - cannot display
diff --git a/prek.toml b/prek.toml new file mode 100644 index 00000000..d0618b19 --- /dev/null +++ b/prek.toml @@ -0,0 +1,169 @@ +default_stages = ["pre-commit"] + +[[repos]] +repo = "https://github.com/pre-commit/pre-commit-hooks" +rev = "v5.0.0" +hooks = [ + { id = "check-yaml", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "check-toml", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "check-json", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "check-added-large-files" }, + { id = "check-merge-conflict", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "debug-statements", exclude = '^tests/.*|\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "trailing-whitespace", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "end-of-file-fixer", exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "mixed-line-ending", args = ["--fix=lf"], exclude = '\.\S*_cache/.*|dist/.*|venv/.*' }, +] + +# Pylint check for import-outside-toplevel (run before ruff) +[[repos]] +repo = "local" +hooks = [ + { + id = "pylint-import-check", + name = "pylint import-outside-toplevel check", + entry = "poetry run pylint", + language = "system", + args = ["--disable=all", "--enable=import-outside-toplevel", "--score=no", "custom_components/span_panel/"], + pass_filenames = false, + files = '\.py$', + exclude = '^tests/.*|^scripts/.*', + }, +] + +# Ruff for linting, import sorting, and primary formatting +[[repos]] +repo = "https://github.com/astral-sh/ruff-pre-commit" +rev = "v0.15.1" +hooks = [ + { id = "ruff-format", exclude = '^tests/.*|scripts/.*|\.\S*_cache/.*|dist/.*|venv/.*' }, + { id = "ruff-check", args = ["--fix"], exclude = '^tests/.*|scripts/.*|\.\S*_cache/.*|dist/.*|venv/.*' }, +] + +# MyPy for type checking +[[repos]] +repo = "https://github.com/pre-commit/mirrors-mypy" +rev = "v1.16.0" +hooks = [ + { + id = "mypy", + additional_dependencies = [ + "httpx", + "pydantic", + "typing-extensions", + "pytest", + "homeassistant-stubs", + "types-PyYAML", + "types-aiofiles", + ], + args = ["--config-file=pyproject.toml"], + exclude = '^tests/.*|^scripts/.*|docs/.*|\.\S*_cache/.*|dist/.*|venv/.*', + }, +] + +# Markdownlint for markdown files +[[repos]] +repo = "https://github.com/DavidAnson/markdownlint-cli2" +rev = "v0.18.1" +hooks = [ + { + id = "markdownlint-cli2", + args = ["--config", ".markdownlint-cli2.jsonc"], + exclude = '\.\S*_cache/.*|dist/.*|venv/.*|\.venv/.*|node_modules/.*|htmlcov/.*|tests/.*', + }, +] + +# Markdown formatting using shared script (local only) +[[repos]] +repo = "local" +hooks = [ + { + id = "markdown-format", + name = "Format markdown files", + entry = "scripts/fix-markdown.sh", + language = "system", + args = ["."], + types = ["markdown"], + pass_filenames = false, + always_run = true, + stages = ["manual"], + }, +] + +# Check for common security issues +[[repos]] +repo = "https://github.com/PyCQA/bandit" +rev = "1.8.3" +hooks = [ + { + id = "bandit", + args = ["-c", "pyproject.toml"], + additional_dependencies = ["bandit[toml]"], + exclude = '^tests/.*|^scripts/.*|\.\S*_cache/.*|dist/.*|venv/.*', + }, +] + +# Poetry check for pyproject.toml validation +[[repos]] +repo = "https://github.com/python-poetry/poetry" +rev = "2.1.3" +hooks = [ + { id = "poetry-check" }, + { id = "poetry-lock" }, +] + +# Radon for code metrics and maintainability (local) +[[repos]] +repo = "local" +hooks = [ + { + id = "radon-complexity", + name = "radon complexity check", + entry = "poetry run radon", + language = "system", + args = ["cc", "--min=B", "--show-complexity", "custom_components/span_panel/"], + pass_filenames = false, + files = '\.py$', + }, + { + id = "radon-maintainability", + name = "radon maintainability index", + entry = "poetry run radon", + language = "system", + args = ["mi", "--min=B", "--show", "custom_components/span_panel/"], + pass_filenames = false, + files = '\.py$', + }, +] + +# Coverage check with pytest output and coverage report +[[repos]] +repo = "local" +hooks = [ + { + id = "pytest-cov-summary", + name = "coverage summary", + entry = "bash", + language = "system", + args = ["-c", 'echo "Running tests with coverage..."; poetry run pytest tests/ --cov=custom_components/span_panel --cov-config=pyproject.toml --cov-report=term-missing:skip-covered -v; exit_code=$?; echo; if [ $exit_code -eq 0 ]; then echo "Tests passed with coverage report above"; else echo "Tests failed"; fi; exit $exit_code'], + pass_filenames = false, + always_run = true, + verbose = true, + }, +] + +# Sync dependencies from manifest.json to CI workflow (run last) +[[repos]] +repo = "local" +hooks = [ + { + id = "sync-dependencies", + name = "sync dependency versions between manifest.json and CI workflow", + entry = "python scripts/sync-dependencies.py", + language = "system", + pass_filenames = false, + always_run = true, + stages = ["pre-commit"], + verbose = true, + }, +] diff --git a/pyproject.toml b/pyproject.toml index fb4b3fd2..d230bd5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ mypy = "==1.19.1" pyright = "==1.1.405" ruff = "==0.15.1" bandit = "==1.8.6" -pre-commit = "==4.3.0" +prek = ">=0.3.6" voluptuous-stubs = "*" python-direnv = "*" prettier = "*" From 83929863c4747e838ca65e16b18bf6c898390fe1 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:27:45 -0700 Subject: [PATCH 32/36] Migrate from Poetry to uv Replace Poetry with uv for dependency management across the project: - Convert pyproject.toml to PEP 621 [project] + PEP 735 [dependency-groups] + [tool.uv] - Replace poetry.lock with uv.lock - Update prek.toml hooks to use uv run, remove poetry-check/poetry-lock hooks - Update CI workflow to use astral-sh/setup-uv and uv commands - Update all scripts (run-in-env.sh, run_mypy.py, sync-ha-deps.py, sync-dependencies.py) - Update developer docs and dependabot config Also update CHANGELOG entries for BESS Grid Power caveat and MQTT broker description. --- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 31 +- CHANGELOG.md | 10 +- docs/developer.md | 20 +- .../2026-03-20-poetry-to-uv-migration.md | 521 ++ poetry.lock | 5551 ----------------- prek.toml | 17 +- pyproject.toml | 70 +- scripts/run-in-env.sh | 12 +- scripts/run_mypy.py | 2 +- scripts/sync-dependencies.py | 37 +- scripts/sync-ha-deps.py | 31 +- setup-hooks.sh | 4 +- uv.lock | 2910 +++++++++ 14 files changed, 3539 insertions(+), 5679 deletions(-) create mode 100644 docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 20d5d5d0..1fc4de93 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - # Python dependencies managed by Poetry + # Python dependencies managed by uv - package-ecosystem: "pip" directory: "/" schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a75ce74..65e3f1e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,45 +19,38 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Install prek run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/latest/download/prek-installer.sh | sh - name: Install dependencies run: | - # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.2"/' pyproject.toml - sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml - # Regenerate lock file with the modified dependencies - poetry lock - poetry install --with dev - # Install bandit with TOML support - poetry run pip install 'bandit[toml]' + # Remove local path source overrides so uv resolves from PyPI + sed -i '/^\[tool\.uv\.sources\]/,/^$/d' pyproject.toml + uv lock + uv sync - name: Format check with ruff - run: poetry run ruff format --check custom_components/span_panel + run: uv run ruff format --check custom_components/span_panel - name: Lint with ruff - run: poetry run ruff check custom_components/span_panel + run: uv run ruff check custom_components/span_panel - name: Type check with mypy - run: poetry run mypy custom_components/span_panel + run: uv run mypy custom_components/span_panel - name: Security check with bandit - run: poetry run bandit -c pyproject.toml -r custom_components/span_panel - - - name: Check poetry configuration - run: poetry check + run: uv run bandit -c pyproject.toml -r custom_components/span_panel - name: Run prek hooks (for extra validation) run: prek run --all-files --show-diff-on-failure env: - SKIP: poetry-lock,poetry-check + SKIP: "" - name: Run tests with coverage - run: poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing + run: uv run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index f29746e6..a445c891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ All notable changes to this project will be documented in this file. ### Added - **Grid Power sensor** — New `Grid Power`. Previously only `Current Power` (upstream lugs measurement) was available; the new sensor surfaces the panel's own - grid power accounting alongside Battery Power, PV Power, and Site Power. Without BESS `Grid Power` is the same as `Current Power`. + grid power accounting alongside Battery Power, PV Power, and Site Power. Without BESS `Grid Power` is the same as `Current Power`. Note that if your panel has + an integrated BESS and the BESS loses communication with the panel the Grid Power sensor is not accurate. In such a case HA would need a current clamp + upstream of the BESS to accurately reflect whether the Grid is up. - **FQDN registration support** — Config flow detects FQDN-based connections and registers the domain with the panel for TLS certificate SAN inclusion. Blocked by an upstream API permission issue ([SPAN-API-Client-Docs#10](https://github.com/spanio/SPAN-API-Client-Docs/issues/10)); the integration falls back to IP-based connections until resolved. @@ -24,9 +26,9 @@ All notable changes to this project will be documented in this file. ### Fixed -- **MQTT broker hostname resolution across VLANs** — The panel advertises its own mDNS hostname (`.local`) as the MQTT broker address, but mDNS does not resolve - across VLAN boundaries. The integration now uses the user-configured panel host (IP or FQDN) for the MQTT broker connection, since the broker runs on the - panel itself. (#193) +- **MQTT broker connection** — The eBus broker connection now uses the panel host from zeroconf discovery or user configuration instead of the panel-advertised + `.local` address, which may not resolve in all HA environments (#193). + - **PV nameplate capacity unit** — Corrected the PV nameplate capacity sensor unit to watts. ## [2.0.3] - 3/2026 diff --git a/docs/developer.md b/docs/developer.md index ef877b45..f7438241 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,24 +2,24 @@ ## Developer Prerequisites -- Poetry +- uv - prek -- Python 3.13.2+ +- Python 3.14.2+ -This project uses [poetry](https://python-poetry.org/) for dependency management. Linting and type checking are accomplished using -[prek](https://github.com/j178/prek), a fast Rust-based pre-commit framework installed by poetry. +This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Linting and type checking are accomplished using +[prek](https://github.com/j178/prek), a fast Rust-based pre-commit framework. ## Developer Setup -1. Install [poetry](https://python-poetry.org/). -2. In the project root run `poetry install --with dev` to install dependencies. +1. Install [uv](https://docs.astral.sh/uv/). +2. In the project root run `uv sync` to install dependencies. 3. Run `prek install` to install pre-commit hooks. -4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `prek run --all-files` from the terminal to - manually run hooks on files locally in your environment as you make changes. +4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `prek run --all-files` from the terminal to manually run + hooks on files locally in your environment as you make changes. The linters may make changes to files when you try to commit, for example to sort imports. Files that are changed or fail tests will be unstaged. After -reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the prek hook run succeeds, your -commit can proceed. +reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the prek hook run succeeds, your commit can +proceed. ## VS Code diff --git a/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md b/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md new file mode 100644 index 00000000..059c3c46 --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md @@ -0,0 +1,521 @@ +# Poetry to uv Migration Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan +> task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace Poetry with uv as the Python package manager and task runner across the entire project. + +**Architecture:** Convert pyproject.toml from Poetry-specific format to PEP 621 (`[project]`) + PEP 735 (`[dependency-groups]`) + `[tool.uv]`. All `poetry run` +invocations become `uv run`. CI uses `astral-sh/setup-uv@v5`. Local path dependency uses `[tool.uv.sources]` which CI strips to resolve from PyPI. + +**Tech Stack:** uv, PEP 621/735, astral-sh/setup-uv GitHub Action + +--- + +## File Map + +| Action | File | Responsibility | +| -------- | --------------------------------- | --------------------------------------------------------------------------------------------------------- | +| Rewrite | `pyproject.toml` | Project metadata, dependencies, tool config | +| Delete | `poetry.lock` | Replaced by `uv.lock` | +| Generate | `uv.lock` | New lock file from `uv lock` | +| Modify | `prek.toml` | Replace `poetry run` entries, remove poetry hooks | +| Modify | `.github/workflows/ci.yml` | uv setup, uv sync, uv run | +| Modify | `scripts/run-in-env.sh` | Replace poetry env detection with uv | +| Modify | `scripts/run_mypy.py` | `poetry run mypy` -> `uv run mypy` | +| Modify | `scripts/sync-ha-deps.py` | `poetry show` -> `uv pip show` / parse uv.lock | +| Modify | `scripts/sync-dependencies.py` | Update to sync manifest.json versions into pyproject.toml `[project]` deps instead of ci.yml sed commands | +| Modify | `docs/developer.md` | Update prerequisites and setup instructions | +| Modify | `.github/copilot-instructions.md` | Replace all poetry references | + +--- + +### Task 1: Convert pyproject.toml + +**Files:** + +- Modify: `pyproject.toml` + +Replace the Poetry-specific sections with PEP 621 / PEP 735 / uv equivalents. All `[tool.*]` sections (mypy, ruff, pyright, bandit, etc.) are unchanged. + +- [ ] **Step 1: Replace `[tool.poetry]` + `[tool.poetry.dependencies]` + `[tool.poetry.group.dev.dependencies]` + `[build-system]`** + +Remove: + +```toml +[tool.poetry] +name = "span" +# ... +package-mode = false + +[tool.poetry.dependencies] +python = ">=3.14.2,<3.15" +homeassistant = "2026.2.2" +span-panel-api = {path = "../span-panel-api", develop = true} + +[tool.poetry.group.dev.dependencies] +# all dev deps... + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +``` + +Replace with: + +```toml +[project] +name = "span" +version = "0.0.0" +description = "Span Panel Custom Integration for Home Assistant" +authors = [{name = "SpanPanel"}] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.14.2,<3.15" +dependencies = [ + "homeassistant==2026.2.2", + "span-panel-api==2.3.2", +] + +[dependency-groups] +dev = [ + "homeassistant-stubs==2026.2.2", + "types-requests", + "types-PyYAML", + "mypy==1.19.1", + "pyright==1.1.405", + "ruff==0.15.1", + "bandit[toml]==1.8.6", + "prek>=0.3.6", + "voluptuous-stubs", + "python-direnv", + "prettier", + "radon==6.0.1", + "pylint==4.0.5", + "pytest>=9.0.0", + "pytest-homeassistant-custom-component>=0.13.315", + "isort", +] + +[tool.uv] +package = false + +[tool.uv.sources] +span-panel-api = { path = "../span-panel-api", editable = true } +``` + +Notes: + +- `package = false` is the uv equivalent of Poetry's `package-mode = false` +- No `[build-system]` needed for virtual (non-package) projects +- `bandit[toml]` includes the TOML extra directly (eliminates the separate `pip install` in CI) +- Poetry `*` becomes unconstrained (just package name) +- Poetry `^X` becomes `>=X` +- `develop = true` becomes `editable = true` in `[tool.uv.sources]` +- `span-panel-api==2.3.2` version must match `manifest.json` + +- [ ] **Step 2: Verify pyproject.toml is valid** + +Run: `cd /Users/bflood/projects/HA/span && uv lock` Expected: Lock file generated without errors + +- [ ] **Step 3: Install dependencies with uv** + +Run: `cd /Users/bflood/projects/HA/span && uv sync` Expected: All dependencies installed, `.venv` created/updated + +- [ ] **Step 4: Verify tools work** + +Run: `cd /Users/bflood/projects/HA/span && uv run ruff --version && uv run mypy --version && uv run pytest --version` Expected: All tools report their versions + +--- + +### Task 2: Delete poetry.lock + +**Files:** + +- Delete: `poetry.lock` + +- [ ] **Step 1: Remove poetry.lock from repo** + +Run: `cd /Users/bflood/projects/HA/span && git rm poetry.lock` Expected: File staged for deletion + +--- + +### Task 3: Update prek.toml + +**Files:** + +- Modify: `prek.toml` + +Three changes: replace `poetry run` in local hooks, remove poetry-check/poetry-lock hooks. + +- [ ] **Step 1: Replace `poetry run pylint` with `uv run pylint`** + +Line 25: `entry = "poetry run pylint"` -> `entry = "uv run pylint"` + +- [ ] **Step 2: Replace `poetry run radon` with `uv run radon`** (two hooks) + +Line 122: `entry = "poetry run radon"` -> `entry = "uv run radon"` Line 131 (radon-maintainability): same replacement + +- [ ] **Step 3: Replace `poetry run pytest` with `uv run pytest`** + +Line 148: the pytest-cov-summary entry argument string: Replace `poetry run pytest tests/ --cov=...` with `uv run pytest tests/ --cov=...` + +- [ ] **Step 4: Remove poetry-check and poetry-lock hooks** + +Delete lines 106-113 (the entire poetry repo block): + +```toml +# Poetry check for pyproject.toml validation +[[repos]] +repo = "https://github.com/python-poetry/poetry" +rev = "2.1.3" +hooks = [ + { id = "poetry-check" }, + { id = "poetry-lock" }, +] +``` + +- [ ] **Step 5: Verify prek hooks run** + +Run: `cd /Users/bflood/projects/HA/span && prek run --all-files` Expected: All hooks pass (poetry-check and poetry-lock no longer appear) + +--- + +### Task 4: Update CI workflow + +**Files:** + +- Modify: `.github/workflows/ci.yml` + +- [ ] **Step 1: Replace Poetry setup with uv setup** + +Replace: + +```yaml +- name: Install Poetry + uses: snok/install-poetry@v1 +``` + +With: + +```yaml +- name: Install uv + uses: astral-sh/setup-uv@v5 +``` + +- [ ] **Step 2: Replace dependency installation step** + +Replace: + +```yaml +- name: Install dependencies + run: | + # Replace path dependencies with PyPI versions for CI + sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.2"/' pyproject.toml + sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml + # Regenerate lock file with the modified dependencies + poetry lock + poetry install --with dev + # Install bandit with TOML support + poetry run pip install 'bandit[toml]' +``` + +With: + +```yaml +- name: Install dependencies + run: | + # Remove local path source overrides so uv resolves from PyPI + sed -i '/^\[tool\.uv\.sources\]/,/^$/d' pyproject.toml + uv lock + uv sync +``` + +Notes: + +- The `sed` removes the `[tool.uv.sources]` block through the next blank line, so uv resolves `span-panel-api==2.3.2` from PyPI +- `uv sync` installs all dependency groups (including dev) by default +- `bandit[toml]` is already in `[dependency-groups]` dev, no separate pip install needed + +- [ ] **Step 3: Replace all `poetry run` with `uv run`** + +```yaml +- name: Format check with ruff + run: uv run ruff format --check custom_components/span_panel + +- name: Lint with ruff + run: uv run ruff check custom_components/span_panel + +- name: Type check with mypy + run: uv run mypy custom_components/span_panel + +- name: Security check with bandit + run: uv run bandit -c pyproject.toml -r custom_components/span_panel +``` + +- [ ] **Step 4: Remove `poetry check` step** + +Delete: + +```yaml +- name: Check poetry configuration + run: poetry check +``` + +- [ ] **Step 5: Update prek SKIP env var** + +Replace: + +```yaml +env: + SKIP: poetry-lock,poetry-check +``` + +With (remove the env block entirely or clear the SKIP list if no other hooks need skipping): + +```yaml +env: + SKIP: "" +``` + +Or remove the `env:` block entirely if all hooks should run. + +- [ ] **Step 6: Replace test runner** + +Replace: + +```yaml +- name: Run tests with coverage + run: poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing +``` + +With: + +```yaml +- name: Run tests with coverage + run: uv run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing +``` + +--- + +### Task 5: Update scripts + +**Files:** + +- Modify: `scripts/run-in-env.sh` +- Modify: `scripts/run_mypy.py` +- Modify: `scripts/sync-ha-deps.py` +- Modify: `scripts/sync-dependencies.py` + +- [ ] **Step 1: Update run-in-env.sh** + +Replace the poetry venv detection and install logic: + +```bash +VENV_PATHS=( + ".venv" + "venv" + ".env" + "env" + "$(poetry env info --path 2>/dev/null)" # Try to get Poetry's venv path +) +``` + +With (remove the poetry line since uv uses `.venv` by default): + +```bash +VENV_PATHS=( + ".venv" + "venv" + ".env" + "env" +) +``` + +Replace the poetry install fallback: + +```bash +# If poetry is available, ensure dependencies +if command -v poetry &> /dev/null && [ -f "pyproject.toml" ]; then + # Check if pylint is missing + if ! command -v pylint &> /dev/null; then + echo "pylint not found, installing dependencies with poetry..." + poetry install --only dev + fi +fi +``` + +With: + +```bash +# If uv is available, ensure dependencies +if command -v uv &> /dev/null && [ -f "pyproject.toml" ]; then + if ! command -v pylint &> /dev/null; then + echo "pylint not found, installing dependencies with uv..." + uv sync + fi +fi +``` + +Update the comment on line 4: `# Handles pyenv/virtualenv/poetry activation if needed` -> `# Handles pyenv/virtualenv/uv activation if needed` + +- [ ] **Step 2: Update run_mypy.py** + +Replace line 12: + +```python +result = subprocess.check_call(["poetry", "run", "mypy"] + sys.argv[1:]) # nosec B603 +``` + +With: + +```python +result = subprocess.check_call(["uv", "run", "mypy"] + sys.argv[1:]) # nosec B603 +``` + +- [ ] **Step 3: Update sync-ha-deps.py** + +Replace the `get_ha_dependencies()` function. Change from `poetry show homeassistant --format json` to `uv pip show homeassistant --format json`: + +```python +def get_ha_dependencies(): + """Get HomeAssistant's dependency pins from uv pip show.""" + try: + result = subprocess.run( + ["uv", "pip", "show", "homeassistant", "--format", "json"], + capture_output=True, + text=True, + check=True, + ) + ha_info = json.loads(result.stdout) + return {dep["name"]: dep["version"] for dep in ha_info.get("dependencies", [])} + except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError): + return {} +``` + +Also update `update_pyproject_constraints()` to work with PEP 621 `[project]` format instead of `[tool.poetry]`: + +Replace: + +```python +deps = ( + pyproject.setdefault("tool", {}).setdefault("poetry", {}).setdefault("dependencies", {}) +) +``` + +With logic that reads/updates the `[project]` dependencies list (which is a list of PEP 508 strings, not a dict). + +- [ ] **Step 4: Update sync-dependencies.py** + +The CI workflow no longer has version-specific sed commands. Instead, the version lives in `[project]` dependencies. Update this script to sync versions from +manifest.json into pyproject.toml `[project]` dependencies instead of ci.yml sed commands. + +Replace `update_ci_workflow()` with `update_pyproject_dependencies()` that reads pyproject.toml, finds the dependency string (e.g., `"span-panel-api==2.3.2"`), +and updates the version to match manifest.json. + +--- + +### Task 6: Update documentation + +**Files:** + +- Modify: `docs/developer.md` +- Modify: `.github/copilot-instructions.md` + +- [ ] **Step 1: Rewrite docs/developer.md** + +Replace full content with: + +```markdown +# Development Notes + +## Developer Prerequisites + +- uv +- prek +- Python 3.14.2+ + +This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Linting and type checking are accomplished using +[prek](https://github.com/j178/prek), a fast Rust-based pre-commit framework. + +## Developer Setup + +1. Install [uv](https://docs.astral.sh/uv/). +2. In the project root run `uv sync` to install dependencies. +3. Run `prek install` to install pre-commit hooks. +4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `prek run --all-files` from the terminal to manually run + hooks on files locally in your environment as you make changes. + +The linters may make changes to files when you try to commit, for example to sort imports. Files that are changed or fail tests will be unstaged. After +reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the prek hook run succeeds, your commit can +proceed. + +## VS Code + +See the .vscode/settings.json.example file for starter settings +``` + +- [ ] **Step 2: Update .github/copilot-instructions.md** + +Replace all `poetry` references: + +- Line 18: `Poetry (not pip)` -> `uv (not pip)` +- Lines 78-79: `poetry install --with dev` -> `uv sync` +- Line 92: `poetry run pytest` -> `uv run pytest` +- Lines 95-96: `poetry run pytest` -> `uv run pytest` +- Lines 98-99: `poetry run mypy` -> `uv run mypy` +- Lines 101-102: `poetry run ruff check` -> `uv run ruff check` +- Lines 104-105: `poetry run ruff format` -> `uv run ruff format` +- Lines 107-108: `poetry run bandit` -> `uv run bandit` +- Lines 110-111: `poetry run radon` -> `uv run radon` +- Line 121: `poetry add` / `poetry add --group dev` -> `uv add` / `uv add --group dev` +- Line 143: `poetry.lock` managed by Poetry -> `uv.lock` managed by uv, use `uv lock` to update + +- [ ] **Step 3: Commit** + +```bash +git add -A +git commit -m "Migrate from Poetry to uv" +``` + +--- + +### Task 7: Verify end-to-end + +- [ ] **Step 1: Clean install from scratch** + +```bash +cd /Users/bflood/projects/HA/span +rm -rf .venv +uv sync +``` + +Expected: Fresh `.venv` created with all deps + +- [ ] **Step 2: Run all tools** + +```bash +uv run ruff format --check custom_components/span_panel +uv run ruff check custom_components/span_panel +uv run mypy custom_components/span_panel +uv run bandit -c pyproject.toml -r custom_components/span_panel +uv run pytest tests/ -q +``` + +Expected: All pass + +- [ ] **Step 3: Run prek hooks** + +```bash +prek run --all-files +``` + +Expected: All hooks pass + +- [ ] **Step 4: Verify no poetry references remain** + +```bash +grep -r "poetry" --include="*.py" --include="*.toml" --include="*.yml" --include="*.yaml" --include="*.md" --include="*.sh" . +``` + +Expected: No matches (except possibly in git history references or this plan file) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 17fe08be..00000000 --- a/poetry.lock +++ /dev/null @@ -1,5551 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. - -[[package]] -name = "acme" -version = "5.2.2" -description = "ACME protocol implementation in Python" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "acme-5.2.2-py3-none-any.whl", hash = "sha256:354ef66cf226b2bef02006311778e97123237207b4febe8829ded9860784ee64"}, - {file = "acme-5.2.2.tar.gz", hash = "sha256:7702d5b99149d5cd9cd48a9270c04693e925730c023ca3e1b853ab43746a9d01"}, -] - -[package.dependencies] -cryptography = ">=43.0.0" -josepy = ">=2.0.0" -PyOpenSSL = ">=25.0.0" -pyrfc3339 = "*" -requests = ">=2.25.1" - -[package.extras] -docs = ["Sphinx (>=1.0)", "sphinx_rtd_theme"] -test = ["pytest", "pytest-xdist", "typing-extensions"] - -[[package]] -name = "aiodns" -version = "4.0.0" -description = "Simple DNS resolver for asyncio" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "aiodns-4.0.0-py3-none-any.whl", hash = "sha256:a188a75fb8b2b7862ac8f84811a231402fb74f5b4e6f10766dc8a4544b0cf989"}, - {file = "aiodns-4.0.0.tar.gz", hash = "sha256:17be26a936ba788c849ba5fd20e0ba69d8c46e6273e846eb5430eae2630ce5b1"}, -] - -[package.dependencies] -pycares = ">=5.0.0,<6" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohasupervisor" -version = "0.3.3" -description = "Asynchronous python client for Home Assistant Supervisor." -optional = false -python-versions = ">=3.12.0" -groups = ["main", "dev"] -files = [ - {file = "aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f"}, - {file = "aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8"}, -] - -[package.dependencies] -aiohttp = ">=3.3.0,<4.0.0" -mashumaro = ">=3.11,<4.0" -orjson = ">=3.6.1,<4.0.0" - -[package.extras] -dev = ["aiohttp (==3.12.15)", "aioresponses (==0.7.8)", "codespell (==2.4.1)", "coverage (==7.10.7)", "mashumaro (==3.16)", "mypy (==1.18.2)", "orjson (==3.11.3)", "pre-commit (==4.3.0)", "pytest (==8.4.2)", "pytest-aiohttp (==1.1.0)", "pytest-cov (==7.0.0)", "pytest-timeout (==2.4.0)", "ruff (==0.13.2)", "yamllint (==1.37.1)"] - -[[package]] -name = "aiohttp" -version = "3.13.3" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, - {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, - {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, - {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, - {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, - {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, - {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, - {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, - {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, - {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, - {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, - {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, - {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, - {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aiohttp-asyncmdnsresolver" -version = "0.1.1" -description = "An async resolver for aiohttp that supports MDNS" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e"}, - {file = "aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81"}, -] - -[package.dependencies] -aiodns = ">=3.2.0" -aiohttp = ">=3.10.0" -zeroconf = ">=0.142.0" - -[[package]] -name = "aiohttp-cors" -version = "0.8.1" -description = "CORS support for aiohttp" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d"}, - {file = "aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403"}, -] - -[package.dependencies] -aiohttp = ">=3.9" - -[[package]] -name = "aiohttp-fast-zlib" -version = "0.3.0" -description = "Use the fastest installed zlib compatible library with aiohttp" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e"}, - {file = "aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28"}, -] - -[package.dependencies] -aiohttp = ">=3.9.0" - -[package.extras] -isal = ["isal (>=1.6.1)"] -zlib-ng = ["zlib_ng (>=0.4.3)"] - -[[package]] -name = "aiooui" -version = "0.1.9" -description = "Async OUI lookups" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiooui-0.1.9-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:64d904b43f14dd1d8d9fcf1684d9e2f558bc5e0bd68dc10023c93355c9027907"}, - {file = "aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5"}, - {file = "aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8"}, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "aiozoneinfo" -version = "0.2.3" -description = "Tools to fetch zoneinfo with asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661"}, - {file = "aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3"}, -] - -[package.dependencies] -tzdata = ">=2024.1" - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "annotatedyaml" -version = "1.0.2" -description = "Annotated YAML that supports secrets for Python" -optional = false -python-versions = ">=3.13" -groups = ["main", "dev"] -files = [ - {file = "annotatedyaml-1.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:359a964daf3fccbb4818e6f08478d2e6712a2417a261cbd6472826ce5e8f1503"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6d2dcf741bdedf893d04f958f3f1ad0b5b12b1fe27746f9918a24e2f347eac1"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:139533a395301f219bfd4ba2265b7a8c55cb4931aac7f730a8ff204a465e76d3"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:623f571e0d3819a3cbadce592a2c691274ccd46b09ad770f9271201d7476ea88"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75927ec682f188efe25309e259c115e3976b702900ce1be93a971b328c87a10a"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:106ac5eaa022df4dfa42e307932aa2a197a19151de3bb41e98840cfc7f1745e1"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:139d2626fe8faccba9cc79b4d8dca25a4d59e4a274508612842d78945bddeebe"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6f2fb86c18064f0dcfb01e3d1096f0575cdff509a24b748c2994f97eb0b70156"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56d7235147b58d155b4ee93f2a92920b4c0be6a6852dc3fd810c67f6e56f8c15"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-win32.whl", hash = "sha256:e53c74051a82c4cbd68db1371918a6399650f165579a2bf1f7e0a2ed58300564"}, - {file = "annotatedyaml-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:13ca5d3a325103fd0a0c5a27af05f22118935f3e731e2df26f620ee85b56e85b"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c42f385c3f04f425d5948c16afbb94a876da867be276dbf2c2e7436b9a80792d"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5d9d24ba907fd2e905eac69c88e651310c480980a17aa57faf0599ff21f586f"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2572b7c3c630dae1dd163d6c6ba847493a7f987437941b32d0ad8354615f358a"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b28fe13eb0014a0dd06c9a292466feed0cd298ab10525ef9a37089df01c7d333"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987f73a13f121c775bcdb082214c17f114447fee7dad37db2f86b035893ad58d"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cb4ee79c08da2b8f4f24b1775732ca6c497682f3c9b3fd65dee4ea084fc925c"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:da065a8c29556219fce1aa81b406e84f73bc2181067658e57428a8b2e662fc1b"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ba9937418c1b189b267540b47fa0dc24c148292739d06a6ca31c2ca8482f16"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-win32.whl", hash = "sha256:003e16e91b40176dd8fe77d56c6c936106b408b62953e88ce3506e8ba10bf4e1"}, - {file = "annotatedyaml-1.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:17e64a7dde47a678db8aa4e934c3ed8da9a52ab1bc6946d12be86f323e6bd8c7"}, - {file = "annotatedyaml-1.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8698bbbd1d38f8c9ba95a107d7597f5af3f2ba295d1d14227f85b62377998ffc"}, - {file = "annotatedyaml-1.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cbc661dbc7c5f7ddf69fbf879da6a96745b8cd39ae1338dab3a0aa8eb208367"}, - {file = "annotatedyaml-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db1c3ca021bbd390354037ede5c255657afb2a7544b7cfa0e091b62b888aa462"}, - {file = "annotatedyaml-1.0.2.tar.gz", hash = "sha256:f9a49952994ef1952ca17d27bb6478342eb1189d2c28e4c0ddbbb32065471fb0"}, -] - -[package.dependencies] -propcache = ">0.1" -pyyaml = ">=6.0.1" -voluptuous = ">0.15" - -[[package]] -name = "anyio" -version = "4.11.0" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -trio = ["trio (>=0.31.0)"] - -[[package]] -name = "astral" -version = "2.2" -description = "Calculations for the position of the sun and moon." -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a"}, - {file = "astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe"}, -] - -[package.dependencies] -pytz = "*" - -[[package]] -name = "astroid" -version = "4.0.4" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.10.0" -groups = ["dev"] -files = [ - {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, - {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, -] - -[[package]] -name = "async-interrupt" -version = "1.2.2" -description = "Context manager to raise an exception when a future is done" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c"}, - {file = "async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7"}, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "atomicwrites-homeassistant" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main", "dev"] -files = [ - {file = "atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f"}, - {file = "atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d"}, -] - -[[package]] -name = "attrs" -version = "25.4.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, -] - -[[package]] -name = "audioop-lts" -version = "0.2.1" -description = "LTS Port of Python audioop" -optional = false -python-versions = ">=3.13" -groups = ["main", "dev"] -files = [ - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0"}, - {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, -] - -[[package]] -name = "awesomeversion" -version = "25.8.0" -description = "One version package to rule them all, One version package to find them, One version package to bring them all, and in the darkness bind them." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "awesomeversion-25.8.0-py3-none-any.whl", hash = "sha256:1c314683abfcc3e26c62af9e609b585bbcbf2ec19568df2f60ff1034fb1dae28"}, - {file = "awesomeversion-25.8.0.tar.gz", hash = "sha256:e6cd08c90292a11f30b8de401863dcde7bc66a671d8173f9066ebd15d9310453"}, -] - -[package.extras] -dev = ["black (>=25.1)", "isort (>=6.0.1)", "mypy (>=1.16)", "pylint (>=3.3.7)", "pytest (>=8.4.1)", "pytest-codspeed (>=3.2.0)", "pytest-cov (>=6.2.1)", "pytest-snapshot (>=0.9.0)", "pytest-timeout (>=2.4.0)"] - -[[package]] -name = "bandit" -version = "1.8.6" -description = "Security oriented static analyser for python code." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"}, - {file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] -yaml = ["PyYAML"] - -[[package]] -name = "bcrypt" -version = "5.0.0" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83"}, - {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746"}, - {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e"}, - {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d"}, - {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba"}, - {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41"}, - {file = "bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861"}, - {file = "bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e"}, - {file = "bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5"}, - {file = "bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493"}, - {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b"}, - {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c"}, - {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4"}, - {file = "bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e"}, - {file = "bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d"}, - {file = "bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993"}, - {file = "bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75"}, - {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff"}, - {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4"}, - {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb"}, - {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c"}, - {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb"}, - {file = "bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538"}, - {file = "bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9"}, - {file = "bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980"}, - {file = "bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8"}, - {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a"}, - {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1"}, - {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42"}, - {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10"}, - {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172"}, - {file = "bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683"}, - {file = "bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2"}, - {file = "bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927"}, - {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534"}, - {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4"}, - {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911"}, - {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4"}, - {file = "bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - -[[package]] -name = "bleak" -version = "1.1.1" -description = "Bluetooth Low Energy platform Agnostic Klient" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "bleak-1.1.1-py3-none-any.whl", hash = "sha256:e601371396e357d95ee3c256db65b7da624c94ef6f051d47dfce93ea8361c22e"}, - {file = "bleak-1.1.1.tar.gz", hash = "sha256:eeef18053eb3bd569a25bff62cd4eb9ee56be4d84f5321023a7c4920943e6ccb"}, -] - -[package.dependencies] -dbus-fast = {version = ">=1.83.0", markers = "platform_system == \"Linux\""} -pyobjc-core = {version = ">=10.3", markers = "platform_system == \"Darwin\""} -pyobjc-framework-CoreBluetooth = {version = ">=10.3", markers = "platform_system == \"Darwin\""} -pyobjc-framework-libdispatch = {version = ">=10.3", markers = "platform_system == \"Darwin\""} -winrt-runtime = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Devices.Bluetooth" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Devices.Enumeration" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Foundation" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Foundation.Collections" = {version = ">=3.1", markers = "platform_system == \"Windows\""} -"winrt-Windows.Storage.Streams" = {version = ">=3.1", markers = "platform_system == \"Windows\""} - -[package.extras] -pythonista = ["bleak-pythonista (>=0.1.1)"] - -[[package]] -name = "bleak-retry-connector" -version = "4.4.3" -description = "A connector for Bleak Clients that handles transient connection failures" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "bleak_retry_connector-4.4.3-py3-none-any.whl", hash = "sha256:17a478d525706488973b181fc789e960bc3fb4bcd94ccb0eee7b7b682442577b"}, - {file = "bleak_retry_connector-4.4.3.tar.gz", hash = "sha256:70aa305dbd26eaf0586dd24723daac93ee3dd6a465e9782bf02b711fcbc4a527"}, -] - -[package.dependencies] -dbus-fast = {version = ">=1.14.0", markers = "platform_system == \"Linux\""} - -[[package]] -name = "bluetooth-adapters" -version = "2.1.1" -description = "Tools to enumerate and find Bluetooth Adapters" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "bluetooth_adapters-2.1.1-py3-none-any.whl", hash = "sha256:1f93026e530dcb2f4515a92955fa6f85934f928b009a181ee57edc8b4affd25c"}, - {file = "bluetooth_adapters-2.1.1.tar.gz", hash = "sha256:f289e0f08814f74252a28862f488283680584744430d7eac45820f9c20ba041a"}, -] - -[package.dependencies] -aiooui = ">=0.1.1" -bleak = ">=1" -dbus-fast = {version = ">=1.21.0", markers = "platform_system == \"Linux\""} -uart-devices = ">=0.1.0" -usb-devices = ">=0.4.5" - -[package.extras] -docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] - -[[package]] -name = "bluetooth-auto-recovery" -version = "1.5.3" -description = "Recover bluetooth adapters that are in an stuck state" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "bluetooth_auto_recovery-1.5.3-py3-none-any.whl", hash = "sha256:5d66b859a54ef20fdf1bd3cf6762f153e86651babe716836770da9d9c47b01c4"}, - {file = "bluetooth_auto_recovery-1.5.3.tar.gz", hash = "sha256:0b36aa6be84474fff81c1ce328f016a6553272ac47050b1fa60f03e36a8db46d"}, -] - -[package.dependencies] -bluetooth-adapters = ">=0.16.0" -btsocket = ">=0.2.0" -PyRIC = ">=0.1.6.3" -usb-devices = ">=0.4.1" - -[package.extras] -docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] - -[[package]] -name = "bluetooth-data-tools" -version = "1.28.2" -description = "Tools for converting bluetooth data and packets" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59de191b17a6d7ab23f00b375638667556424c53f08efd288fc9694cf347edb8"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12498ee9387680205aaa856f964d70517a1ac2d4d64bbb78c35cb1f152137bf1"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a559839fc1fd5f84439608b3df598ed583354a671306ff2cd4ef9e667cba855"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:191b8e40b35cfd4201f01d7638bd9e845cb42479a6cc6943fd3cbcd2c978d434"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b7bb1268d5e56a658efd666242a0b7b407f460a3257512b77a2fd3ed0bd9f7"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04d2d0310eae1577554edf2b3306f9ed18b9621edff57e153204430cfa83a541"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:82283c317e465dd998af4ddb5553f79992213b4c1055d0ed1977e6786a5401d8"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d26a2398886c37771e35fa4f2c36e965da771fe3e984e76c078a98bcc5eb3733"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab975c1567aa3fd20e787c8ddc9d55984d0883a251c7d65c55dc3a173f9ef796"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c8454962e005d48e23af3fe240235e16c81590805dfe0b27b08b82337df16a8"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-win32.whl", hash = "sha256:8e492b786ac561f628bf964f522d669259b608d1f434102d003afdc46dcab473"}, - {file = "bluetooth_data_tools-1.28.2-cp310-cp310-win_amd64.whl", hash = "sha256:567dae42c2e1d7da5aeca1ef181bd660db8fc2f3fe3af7e999e82a030a1a422d"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4042b36f1c50bcdc0afb6be78a0bac11a8be6a73a3284825502ce6e82661423e"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:728cfb9fd366b01eec9eaa226353cdd43e099288974f6e9273dc90a577fc970a"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3a2c9fba041b8486f1a4d4098b40241168a2d79ab0f5cdbccafadac7e41747"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3cd8bb926742f01ced40b635a860c1cf5a6b82c3ce88e8431ee77e1062b7d6"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a791a07d60ae70e2fc174688abd1e28fdc54a0c337b6306ce6c6a66c06e41db7"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2049946c4a489db86c7f5cc32ef50894952244be07a5919a58f8b4b03f742b1"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b3344421a012641f21ffbc7e3d5955dbc19150ad48d800b4b61a7a2cb4ee0d87"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9e068aa7b0c147f8bae735298c041058693e8ee3763d769ca6cbd938210ed715"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0eed475d3dfe8af519348047f150b52dcaa5aa84c5a5bae023a6df3005fe95b1"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dfffdaab077173886c64c5c89329d00c7140302fd7f537e3f7bd00d8b851c9d8"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-win32.whl", hash = "sha256:02045a0dc566122f3e30e4ac5861f35c5749cd52eda4abd5a3918c27bfff28fb"}, - {file = "bluetooth_data_tools-1.28.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed619bb4fd4a3794c9c11c140c59a727bb1f63d0888e135da8382d2dbc8c18f9"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8eef61057a754293e2ca54e153ba804bc313257881bd8360de091524b3dffc0"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1567e45ed6b4987db1e70e7f59d7499a57f8eec2e4a38131eeedd8c5b52bc81"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094a6a4aeaf82a6e939318f716294fa7d65a8f025f8b10471e2145320a57b412"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59398ab013e33539a08cc0dcf19f0fc4a7dc6311d5dc5bfb7af03906fc3a902f"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:010564351a0b269371b5f5dc4ed1084b9524028f106cce49208db5b1bc236e84"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:639f89a57936fb65a881ea7dd60dc6d8bc5b859e740d2c4e699f28f28f3da992"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60dac54c9fe308a9dc4b7bc5c94d174c6e93931ac27cdfba8b2cd5c19d73f126"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:50493246908a5aaa305d00df86a83d915a00cc1a783e05d6666ea3ff293cc137"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7fcbf974dc3a7d1a00cebacc424dcf1d18d4eac4bccecba1d8ff5a1e8eb0b348"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a5e9eb874c9823d330dd0eadca9176a8a713395908f865c19156a233da7b5c2"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-win32.whl", hash = "sha256:d3346ef76577f5060955a80c46ae16004f76673d844f8c44a416bd8177d6695c"}, - {file = "bluetooth_data_tools-1.28.2-cp312-cp312-win_amd64.whl", hash = "sha256:872c31ba9042ed614aad459fd0358893554c67e2fa5c2d78d525cd96af3e31da"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71df3e6221ee472cb38fd625cecc6e0a8733e093e40c08e80638e9387349b43b"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b2925335caf40bb9872a8733d823bb8e97bac2bc7ce988a695452e4a39507e29"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535c037b3ccd86a5df890b338b901eea3e974692ae07b591c1f99e787d629170"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:080668765dc7d04d6b78a7bc0feaffd14b45ccee58b5c005a22b78e3730934fd"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c2947f86112fc308973df735f030ede800473dd61f9e32d62d55bfb5c00748"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d74c6b9187b444e548cd01ce56c74eb0c1ba592043b9a1f48a9c2ed19a8a236a"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ad09f0dbc343e51c34f32672aa877373d747eebe956c640117ce9472c86f1cb2"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c833481774fe319ef239351bb8a028cc2efe44ad7cf23681bd2cd2a4dfb71599"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a989a4a5e8e4d70410fd9bba7b03f970bed7b8f79531087565931314437420be"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6f30e619ca3b46716a7f8c2bde35776d36e6b98e1922f0642034618e1056b3b3"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cf3714c9e27aaa7db0800816bf766919cd1ac18080bac0102c2ad466db02f47a"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-win32.whl", hash = "sha256:8f28eeee5fecaebeb9fc1012e4220bc3c1ee6ee82bf8a17b9183995933f6d938"}, - {file = "bluetooth_data_tools-1.28.2-cp313-cp313-win_amd64.whl", hash = "sha256:e748587be85a8133b0a43e34e2c6f65dbf5113765a03d4f89c26039b8289decb"}, - {file = "bluetooth_data_tools-1.28.2.tar.gz", hash = "sha256:2afa97695fc61c8d55d19ffa9485a498051410f399a183852d1bf29f675c3537"}, -] - -[package.dependencies] -cryptography = ">=41.0.3" - -[package.extras] -docs = ["Sphinx (>=5,<9)", "myst-parser (>=0.18,<4.1)", "sphinx-rtd-theme (>=1,<4)"] - -[[package]] -name = "boolean-py" -version = "5.0" -description = "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9"}, - {file = "boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95"}, -] - -[package.extras] -dev = ["build", "twine"] -docs = ["Sphinx (>=3.3.1)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)", "sphinxcontrib-apidoc (>=0.3.0)"] -linting = ["black", "isort", "pycodestyle"] -testing = ["pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)"] - -[[package]] -name = "boto3" -version = "1.40.38" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "boto3-1.40.38-py3-none-any.whl", hash = "sha256:fac337b4f0615e4d6ceee44686e662f51d8e57916ed2bc763468e3e8c611a658"}, - {file = "boto3-1.40.38.tar.gz", hash = "sha256:932ebdd8dbf8ab5694d233df86d5d0950291e0b146c27cb46da8adb4f00f6ca4"}, -] - -[package.dependencies] -botocore = ">=1.40.38,<1.41.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.14.0,<0.15.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.40.38" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "botocore-1.40.38-py3-none-any.whl", hash = "sha256:7d60a7557db3a58f9394e7ecec1f6b87495ce947eb713f29d53aee83a6e9dc71"}, - {file = "botocore-1.40.38.tar.gz", hash = "sha256:18039009e1eca2bff12e576e8dd3c80cd9b312294f1469c831de03169582ad59"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.27.6)"] - -[[package]] -name = "btsocket" -version = "0.3.0" -description = "Python library for BlueZ Bluetooth Management API" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c"}, - {file = "btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d"}, -] - -[package.extras] -dev = ["bumpversion", "coverage", "pycodestyle", "pygments", "sphinx", "sphinx-rtd-theme", "twine"] -docs = ["pygments", "sphinx", "sphinx-rtd-theme"] -rel = ["bumpversion", "twine"] -test = ["coverage", "pycodestyle"] - -[[package]] -name = "certifi" -version = "2025.8.3" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, - {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, -] - -[[package]] -name = "ciso8601" -version = "2.3.3" -description = "Fast ISO8601 date time parser for Python written in C" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "ciso8601-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf67a1d47a52dad19aaffb136de63263910dcab6e50d428f27416733ce81f183"}, - {file = "ciso8601-2.3.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:67316d2a2d278fad3d569771b032e9bd8484c8aab842e1a2524f6433260cf9ac"}, - {file = "ciso8601-2.3.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:48e0ac5d411d186865fdf0d30529fb7ae6df7c8d622540d5274b453f0e7b935a"}, - {file = "ciso8601-2.3.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9063aa362b291a72d395980e1b6479366061ec77d98ae7375aa5891abe0c6b9d"}, - {file = "ciso8601-2.3.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe7b832298a70ac39ef0b3cd1ce860289a2b45d2fdca2c2acd26551e29273487"}, - {file = "ciso8601-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0e81268f84f6ed5a8f07026abed8ffa4fa54953e5763802b259e170f7bd7fb0"}, - {file = "ciso8601-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44fdb272acdc59e94282f6155eacbff8cd9687a2a84df0bbbed2b1bd53fa8406"}, - {file = "ciso8601-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:74b14ffaddb890a48d03b3b97cc3f56875a4a93b3116b023add408e45b010c22"}, - {file = "ciso8601-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f068fb60b801640b4d729a3cf79f5b3075c071f0dad3a08e5bf68b89ca41aef7"}, - {file = "ciso8601-2.3.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:2f347401756cdd552420a4596a0535a4f8193298ff401e41fb31603e182ae302"}, - {file = "ciso8601-2.3.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:77e8e691ade14dd0e2ae1bcdd98475c25cd76be34b1cf43d9138bbb7ea7a8a37"}, - {file = "ciso8601-2.3.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a5839ea7d2edf22e0199587e2ea71bc082b0e7ffce90389c7bdd407c05dbf230"}, - {file = "ciso8601-2.3.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de0476ced02b965ef82c20191757f26e14878c76ce8d32a94c1e9ee14658ec6e"}, - {file = "ciso8601-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe9303131af07e3596583e9d7faebb755d44c52c16f8077beeea1b297541fb61"}, - {file = "ciso8601-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c443761b899e4e350a647b3439f8e999d6c925dc4e83887b3063b13c2a9b195"}, - {file = "ciso8601-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:e3a395ebc5932982a72841820a6bf6e5cd1d41a760cd15ffafd1d4e963c9b802"}, - {file = "ciso8601-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7ef14610446211c4102bf6c67f32619ab341e56db15bad6884385b43c12b064"}, - {file = "ciso8601-2.3.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:523901aec6b0ccdf255c863ef161f476197f177c5cd33f2fbb35955c5f97fdb4"}, - {file = "ciso8601-2.3.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:45f8254d1fb0a41e20f98e93075db7b56504adddf65e4c8b397671feba4861ca"}, - {file = "ciso8601-2.3.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:202ca99077577683e6a84d394ff2677ec19d9f406fbf35734f68be85d2bcd3f1"}, - {file = "ciso8601-2.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7cec4e31c363e87221f2561e7083ce055a82de041e822e7c3775f8ce6250a7e"}, - {file = "ciso8601-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:389fef3ccc3065fa21cb6ef7d03aee63ab980591b5d87b9f0bbe349f52b16bdc"}, - {file = "ciso8601-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4499cfbe4da092dea95ab81aefc78b98e2d7464518e6e80107cf2b9b1f65fa2"}, - {file = "ciso8601-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:1df1ca3791c6f2d543f091d88e728a60a31681ff900d9eb02f1403cf31e9c177"}, - {file = "ciso8601-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8afa073802c926c3244e1e5fcc5818afd3acb90fb7826a90f91ddbda0636ea70"}, - {file = "ciso8601-2.3.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8a04e518b4adf8e35e030feaecdb4a835d39b9bb44d207e926aea8ce3447ad7c"}, - {file = "ciso8601-2.3.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f79ad8372463ba4265981016d1648bc05f4922bc8044c4243fcbaef7a12ee9f7"}, - {file = "ciso8601-2.3.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5894a33f119b5ac1082df187dc58c74fe13c9c092e19ba36495c2b7cee3540b"}, - {file = "ciso8601-2.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09deebf3e326ec59d80019b4ad35175c90b99cde789c644b1496811fe3340587"}, - {file = "ciso8601-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3aa43ed59b2117baccc5bb760e5e53dad77cacba671d757c1e82e0a367b1f42a"}, - {file = "ciso8601-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:289515aa3a3b86a9c3450bf482f634138b98788332d136751507bfdfe46e6031"}, - {file = "ciso8601-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:e7288068a5bffbcc50cbe9cdaf3971f541fcd209c194fa6a59ad06066a3dcff0"}, - {file = "ciso8601-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82db4047d74d8b1d129e7a8da578518729912c3bd19cb71541b147e41f426381"}, - {file = "ciso8601-2.3.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a553f3fc03a2ed5ca6f5716de0b314fa166461df01b45d8b36043ccac3a5e79f"}, - {file = "ciso8601-2.3.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:ff59c26083b7bef6df4f0d96e4b649b484806d3d7bcc2de14ad43147c3aafb04"}, - {file = "ciso8601-2.3.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99a1fa5a730790431d0bfcd1f3a6387f60cddc6853d8dcc5c2e140cd4d67a928"}, - {file = "ciso8601-2.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c35265c1b0bd2ac30ed29b49818dd38b0d1dfda43086af605d8b91722727dec0"}, - {file = "ciso8601-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa9df2f84ab25454f14df92b2dd4f9aae03dbfa581565a716b3e89b8e2110c03"}, - {file = "ciso8601-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32e06a35eb251cfc4bbe01a858c598da0a160e4ad7f42ff52477157ceaf48061"}, - {file = "ciso8601-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:7657ba9730dc1340d73b9e61eca14f341c41dd308128c808b8b084d2b85bc03e"}, - {file = "ciso8601-2.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8377c9e0c4ddab6a50bf7b55ad867d4ffacdcfe85fa9aaab78fe878e62565f8"}, - {file = "ciso8601-2.3.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:354fde847522b0092052867748a5fd235b26fe947c9081f3e0b7d4f69e5403cd"}, - {file = "ciso8601-2.3.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:3770e40139292b7464e78b7c98aa4b9d65830fc5c410830b1ed61bedf2c4b9b8"}, - {file = "ciso8601-2.3.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4817f258d3cea15a82e1e65d1cb9ac8d6fff8d6e09a9a801a8de8a2d9a36b3b"}, - {file = "ciso8601-2.3.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b2842f1fdc8061a9c37311f87896285ebe2a5ceb5bc486c1248add98c0deba"}, - {file = "ciso8601-2.3.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a68f4ad734eb1f8415a88c4563cbebc086da61327ca880a5d622bf210347804e"}, - {file = "ciso8601-2.3.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc1ebb2d34b2e47a4533bad6d3672e18d27dc4b53bea589404afdc4eae102193"}, - {file = "ciso8601-2.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:aebe909c8965c44644cee40d6bd1ecc4987a7be59963e95d6f62f6229c5cc7ab"}, - {file = "ciso8601-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc1d96d46d144bef8f59ec6a63b1f5d3cd93f95242fbebc990b68e17b23c2cc8"}, - {file = "ciso8601-2.3.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:fbdcd1a6515bced4b97ddfe21da921952367953c27cf567e154982ca4dbff867"}, - {file = "ciso8601-2.3.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:27863fa85067059363592b60c9e1c00f3e04cf627e38fa530dfa332a3d0afb92"}, - {file = "ciso8601-2.3.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9305f5b519548e1ae4f2817659ff8c3d75a625f34cbda749bf0be43e39d2844a"}, - {file = "ciso8601-2.3.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e3d0f9633e894e975a9ac4e048db5c930c837c43b4d9524be3cd65ddf017bea"}, - {file = "ciso8601-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f5f6c8febe2b656a6acab6e6c78a3dd411334e161c643475bc50d0f37b642d05"}, - {file = "ciso8601-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3ad0925c2ca305d12796a4b6300a37b098094ffe24cb0407c65c4fef4b5298cc"}, - {file = "ciso8601-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:44741daf5c46f51458d42dfa097eb46409659fc0b2824cdcab699cb43b135313"}, - {file = "ciso8601-2.3.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d88ab28ecb3626e3417c564e8aec9d0245b4eb75e773d2e7f3f095ea9897ded"}, - {file = "ciso8601-2.3.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d5a37798bf0cab6144daa2b6d07657ab1a63df540de24c23a809fb2bdf36149"}, - {file = "ciso8601-2.3.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5b18c75c66499ef22cb47b429e3b5a137db5a68674365b9ca3cd0e4488d229f"}, - {file = "ciso8601-2.3.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58799673ffdf621fe138fb8af6a89daf4ddefdf7ca4a10777ad8d55f3f171b6e"}, - {file = "ciso8601-2.3.3-pp38-pypy38_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16a0bc10783e9f06f46357ef77afb74f9b6a250bee7dbc00d51850d5894cc543"}, - {file = "ciso8601-2.3.3-pp38-pypy38_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ced7b8675d94583b242ba976dbd9b1fd6ab18613f02d6d32361e718839282740"}, - {file = "ciso8601-2.3.3-pp39-pypy39_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:25c834e6a963951a2ac908d0844ca0562972285de1c9a3dc198fc850fcca5458"}, - {file = "ciso8601-2.3.3-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:475583568c06a5bc23a4de8c0521c39c2a46c2e189bae9a6c5efc25ab0605372"}, - {file = "ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.10.6" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}, - {file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}, - {file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}, - {file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}, - {file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}, - {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}, - {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}, - {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}, - {file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}, - {file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}, - {file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}, - {file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}, - {file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}, - {file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}, - {file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}, - {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}, - {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}, - {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}, - {file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}, - {file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}, - {file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}, - {file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}, - {file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}, - {file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}, - {file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}, - {file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}, - {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}, - {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}, - {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}, - {file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}, - {file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}, - {file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}, - {file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}, - {file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}, - {file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}, - {file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}, - {file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}, - {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}, - {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}, - {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}, - {file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}, - {file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}, - {file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}, - {file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}, - {file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}, - {file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}, - {file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}, - {file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}, - {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}, - {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}, - {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}, - {file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}, - {file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}, - {file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}, - {file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}, - {file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}, - {file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}, - {file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}, - {file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}, - {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}, - {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}, - {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}, - {file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}, - {file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}, - {file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}, - {file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}, - {file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}, - {file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}, - {file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}, - {file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}, - {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}, - {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}, - {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}, - {file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}, - {file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}, - {file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}, - {file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"}, - {file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"}, - {file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"}, - {file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"}, - {file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"}, - {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"}, - {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"}, - {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"}, - {file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"}, - {file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"}, - {file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}, - {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, -] - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "cronsim" -version = "2.7" -description = "Cron expression parser and evaluator" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e"}, -] - -[[package]] -name = "cryptography" -version = "46.0.5" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["main", "dev"] -files = [ - {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, - {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, - {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, - {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, - {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, - {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, - {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, - {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, - {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, - {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "dbus-fast" -version = "2.44.3" -description = "A faster version of dbus-next" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "dbus_fast-2.44.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:644880d8db53a6d92e88015f6ac6e0d9a5c1bfdacbc5356de816212cca33c629"}, - {file = "dbus_fast-2.44.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be7e2e39bc6a5e0fe758d9d7abb19f91a7540e3b45124764f318147b74c9b2e6"}, - {file = "dbus_fast-2.44.3-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:049236a2cacddc6f1f8583371d8fa54d0a01e2081c8f1311a6ad71b27b1512aa"}, - {file = "dbus_fast-2.44.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bb4820259e0969ae79585ffc98409bf781589c138a90d4799d5751c83ed04a"}, - {file = "dbus_fast-2.44.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86350a3fc4304f50c56730b64bd3d709458648fa1b23f8e9449dfcce206defe4"}, - {file = "dbus_fast-2.44.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:89c418c8f18fff8eb17143184d4e0f68216c4d702f16cba4323a6b6be6aaab2a"}, - {file = "dbus_fast-2.44.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c700cdb06e74a6c462d180eff146105fe08f0dc4a8f1f8ff93022175c8e6fe76"}, - {file = "dbus_fast-2.44.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9018568987b878e577bc3e692f2eef6b7a4482490a373ec00098578fa919076c"}, - {file = "dbus_fast-2.44.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3879fb6d6e9260b310fed33457835e11b83e96144bfcf2cbb9abcd3e740c2836"}, - {file = "dbus_fast-2.44.3-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0c68f14d5a329bd494a2da561da961ddfb3f3351d41225dcf0e59106f32bf5d6"}, - {file = "dbus_fast-2.44.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f10ee6ba45f37d067775c0719d072bc4a7e0bdc9a0411f5c7c93af0bfd9958"}, - {file = "dbus_fast-2.44.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bec6cb61d9ce56715410e17e6e6d935df6d39bc01e0aae691135229a0d69072"}, - {file = "dbus_fast-2.44.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94ae76e470c5cf6eb507e2a92e698a9183b3558e3a09efcb7fe2152b92dd300b"}, - {file = "dbus_fast-2.44.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3f1df8582723ee1b1689243663f4e93fc406f0966ff3e9c26a21cb498de3b9ca"}, - {file = "dbus_fast-2.44.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:861352c19f57087e9b2ff7e16a1bab0cfb2e7dc982ce0249aad2a36e1af8f110"}, - {file = "dbus_fast-2.44.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aafa42df91e17023885c508539df2f6312abb9d050f56e39345175cef05bfbb"}, - {file = "dbus_fast-2.44.3-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4e5c2515bdc159eaa9ac9e99115016af65261cb4d1d237162295966ad1d8cac0"}, - {file = "dbus_fast-2.44.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dab3b4802e1c518b8f3d98bfefe1f696125c00016faf1b6f1fd5170efc06d7e"}, - {file = "dbus_fast-2.44.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:42842e8f396be5d938c60cb449600df811373efd57dc630bb40d6d36f4e710a4"}, - {file = "dbus_fast-2.44.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:93ea055c644bdfd7c70614f7c860db9f5234736a15992df9e4a723fa55ef7622"}, - {file = "dbus_fast-2.44.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9764e4188e21ad4a9f65856f3adacfc83d583a950d4dabc5ec5856db387784b"}, - {file = "dbus_fast-2.44.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d967a751cc2dd530d5b756a22bf67a603ebeca13c6f72d8b1cb8575b872caa16"}, - {file = "dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da0910f813350b951efe4964a19d7f4aaf253b6c1021b0d68340160a990dc2fc"}, - {file = "dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:253ad2417b0651ba32325661bb559228ceaedea9fb75d238972087a5f66551fd"}, - {file = "dbus_fast-2.44.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb4c56bef8f69e4e2606eb29a5c137ba448cf7d6958f4f2fba263d74623bd06"}, - {file = "dbus_fast-2.44.3-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6e0a6a27a1f53b32259d0789bca6f53decd88dec52722cac9a93327f8b7670c3"}, - {file = "dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a990390c5d019e8e4d41268a3ead0eb6e48e977173d7685b0f5b5b3d0695c2f"}, - {file = "dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5aca3c940eddb99f19bd3f0c6c50cd566fd98396dd9516d35dbf12af25b7a2c6"}, - {file = "dbus_fast-2.44.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0046e74c25b79ffb6ea5b07f33b5da0bdc2a75ad6aede3f7836654485239121d"}, - {file = "dbus_fast-2.44.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fce364e03b98a6acb4694f1c24b05bfc33d10045af1469378a25ffe4fa046f40"}, - {file = "dbus_fast-2.44.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd955b153622df80cc420fe53c265cd43b7c559100a9e52c83ab0425bc083604"}, - {file = "dbus_fast-2.44.3-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6b00eef5437d27917d55d04b3edea60c12a3e2a94fd82e81b396311ff7bb1c88"}, - {file = "dbus_fast-2.44.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8468df924e5a3870b1e23377ea573e4b43a22ab1730084eab1b838fd18c9a589"}, - {file = "dbus_fast-2.44.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e4dd64813f175403fac894b5f6f6ff028127ea3c6ca8eda41770f39ba9815572"}, - {file = "dbus_fast-2.44.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f36183af2c6d3a00bd555e7d871d8c3214bb91c42439428dfcf7cc664081182a"}, - {file = "dbus_fast-2.44.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0bb0dfc386ae246def7ee64ce058d099b1bc8c35cd5325e6cd80f57b8115fec7"}, - {file = "dbus_fast-2.44.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b853f75e10b34bb2ba76706d10fdab5ba0cef9ebc1faec1969c84e5b155b3b8"}, - {file = "dbus_fast-2.44.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de00d3d7731b2f915ac3f4ed2119442f3054efeb84c5bdd21717b92241b68f82"}, - {file = "dbus_fast-2.44.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:92377b4f274e3e70b9fcffd9a0e37a9808748f8df4b9d510a81f36b9e8c0f42f"}, - {file = "dbus_fast-2.44.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6ee1dc3e05b47e89b6be5b45d345b57a85b822f3a55299b569766384e74d0f9"}, - {file = "dbus_fast-2.44.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:780c960c546fe509dd2b7a8c7f5eeef3a88f99cdea77225a400a47411b9aea17"}, - {file = "dbus_fast-2.44.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4200a3c33684df692a545b16f72f52e70ecd68e8226273e828fc12fbcdde88"}, - {file = "dbus_fast-2.44.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e1643f9d47450e29fd14e62c583c71f332337dc157e9536692e5c0cd5e70ec53"}, - {file = "dbus_fast-2.44.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1f3c673c40a3f82388b377d492aa31f9ba66c20ba1183f1bcd8f9b64eda599c"}, - {file = "dbus_fast-2.44.3.tar.gz", hash = "sha256:962b36abbe885159e31135c57a7d9659997c61a13d55ecb070a61dc502dbd87e"}, -] - -[[package]] -name = "dill" -version = "0.4.0" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - -[[package]] -name = "envs" -version = "1.4" -description = "Easy access of environment variables from Python with support for strings, booleans, list, tuples, and dicts." -optional = false -python-versions = ">=3.6,<4.0" -groups = ["main", "dev"] -files = [ - {file = "envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1"}, - {file = "envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398"}, -] - -[package.extras] -cli = ["Jinja2[cli] (>=3.0.3,<4.0.0)", "click[cli] (>=8.0.3,<9.0.0)", "terminaltables[cli] (>=3.1.10,<4.0.0)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "filelock" -version = "3.19.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, - {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, -] - -[[package]] -name = "fnv-hash-fast" -version = "1.6.0" -description = "A fast version of fnv1a" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "fnv_hash_fast-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ee5d5fcdf1f24c5adb5c2e009f3f930c992bc94649bba06123cce6bbf1653e6"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1af1b4280df65626c96d83253c7266f850d77c82247c4934158f94df9f2019e"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:56b74fe49032c4fc677a535693e2bd2e8758e98b8e9f9e718488eaa52ab45d78"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:332fd17375357d6a83278d5cd264de3ece1063defd9af342d4f29178fba10cc9"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6fad7a0ab855fc3c15899622ed0230b16a8ddf2a2d7bec23705fa08e2d3d2d1e"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6feab601350657ea6b4a96c0efd6b4ce25d6599df131cbd418dd4b66bf48a183"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1f9d46414a7c856dc57002eda9bcf13427deb18ef285f371fc3319a7ac7a3e4f"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e3c978f8bf86a470d077b3087afc7b736a7a02f724377f14db45843dd3b3f701"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-win32.whl", hash = "sha256:0ddb793228e7d659307218a037acee06260e1188a2971c2d4ddc7113b8cd8948"}, - {file = "fnv_hash_fast-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:09d4a5beb160af10ea2cb816a7f7d4c593caf6fdadcbba8db756b531b5180443"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8054c711283a4d598d516c17445d6c12304c93e48f7e97aba970549e0d5b413b"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:135bc62ec5d61a9a38222afce83d3d0c9d4d52fa8a2670f6acc86ee6818d2bfa"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34cd7e776fd515dfcab4cf84bdb63d879653cc3f77f6f086822ed9c50645f75c"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9189a9a1b820658c791728b4558b4aadad6b35429b1784bcaf57183788d3f489"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893306251f69bf9591b51e75dfac2bac9703c4156b9c7e78dd2930e33b184f1f"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7107073d69dcf68b602bddf37a6ab2af6e9afcfc31239e215b75dcc5049667ae"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f0b9cbb8538e981d4e0895f01af6510bae5dd6bcb1a8bf3db8cecd9877079e66"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11b93131512147038d54c92106a4035b1ae71488abe730952bdeb55a9d7dcb18"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-win32.whl", hash = "sha256:df7666d14e01352a22351344fdbcb8916b873ea91d598b53e07736b0e64fb66c"}, - {file = "fnv_hash_fast-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:60d7c0a89ee63076de139f0b619f5cc55378f3c4ed67488dde456dbf93479530"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34e4f2acc41aacd97877d396948b38efc7197a2dd91c15e818c049c4d48b0a0"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1a1fe55163d38052ec90aaf16f190bb807342aa09f9680185b9772ce0407b62"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d7c3a18e7aa483d18ff569554b07b5238403775f8e401245ab8b3c27bcb34cf"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8fdeee59431cc03afdb8a04c3c46b452dc2ded85973953b7077715e897a85b"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6d8284c7ad0339def03252905f3456195ec9d77d8329225e5b09b226e3eb79ec"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:03642803cc4567dada952d7b1490d6eedd97cd960a83ebbb4a4b7c545629f33f"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a540bae99086d3942a2976c16480916cb86d9f06a632023176fe4fa56d298b5"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9d6a447404ddfc0035a52de80747c36dce8ba0cc24c27610ca4be9c0ba46d783"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d016ee85cd9faccb2f958e5017eb60c8c6410b1700f85052f5dbf2b34084c7ef"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-win32.whl", hash = "sha256:9a3751dc38c33b0be4fc4a5a5947ab6d9acbdb1017dfeff55ab3d1fa3ed6c03e"}, - {file = "fnv_hash_fast-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:1e8fb4c1cd62bc8d559dabeaf69fb25ba647232d980ffdb8e5f679d4aef8d03a"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:07bb79eaa44f91db2aab3b641194f68dc4ddd15701756f687c1a7a294bfa9c06"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4176315430f9fcf5346a0339b0f55982e1715452345d70c2887755bfd5aa2b64"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31db9d944c91d286475870855b9203f4fb4794cb0674de5458e9d1231e07f37"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fc1bbec5871060c6efa6a444a554496f372f1f4a7e83b99989be5ea6b97435f"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:91ed6df63ab2082b5b48a6b8f5d7eb7b51d39c2eeffd64821301bf6d9662ff11"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d34541e15bbc3877da7541f059fb1eadf53031abe7fc4318b28421e02eff383"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-win32.whl", hash = "sha256:74320b9033c13e851174edf959c167619907eb985176e795d17d7fbe29cf3a45"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:540670ff837824939d2af90dd89cddbd02d238d778999a403cdb4a4de8c65a73"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:83aa2d791193e3b3f4132741c4dc09eed4f7df8000d76ad77fb9d24db8e59a88"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b8d33f002bb336f9f0949a32d7da07cc9d340a9d07e4f16cc9ece982842eb4e0"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0042af2a1cb7ffae412ec3cb6ae8c581a73610fd523f7e17ed58a5359505ffec"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73308e11c0e5a2dba433fc5645672de4756a52b323de1dab20e45d4fe5e83994"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96282ecb75bec190af0111e82ddd38afc98e9cb867a1689e873ab6802af951b7"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cae16753c1d85ed358df13824bd8a474bfa9da34daddc1a90c72b25ff4177f51"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:e2efb5953475a5a0529ca9757d6782c5174a3b8a3fbdc4e1c1273ac1d293316b"}, - {file = "fnv_hash_fast-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6eb03cd17c134d412fed9f05dc6f9ff9a8aa3b8e69c0135603a521e77720c93"}, - {file = "fnv_hash_fast-1.6.0.tar.gz", hash = "sha256:a09feefad2c827192dc4306826df3ffb7c6288f25ab7976d4588fdae9cbb7661"}, -] - -[package.dependencies] -fnvhash = ">=0.1,<0.3" - -[[package]] -name = "fnvhash" -version = "0.1.0" -description = "Pure Python FNV hash implementation" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e"}, -] - -[[package]] -name = "freezegun" -version = "1.5.2" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b"}, - {file = "freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181"}, -] - -[package.dependencies] -python-dateutil = ">=2.7" - -[[package]] -name = "frozenlist" -version = "1.7.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, - {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, - {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, - {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, - {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, - {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, - {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, - {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, - {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, - {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, - {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, - {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, -] - -[[package]] -name = "grpcio" -version = "1.76.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"}, - {file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990"}, - {file = "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6"}, - {file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3"}, - {file = "grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b"}, - {file = "grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b"}, - {file = "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a"}, - {file = "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48"}, - {file = "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749"}, - {file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00"}, - {file = "grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054"}, - {file = "grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d"}, - {file = "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8"}, - {file = "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11"}, - {file = "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980"}, - {file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882"}, - {file = "grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958"}, - {file = "grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347"}, - {file = "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2"}, - {file = "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb"}, - {file = "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03"}, - {file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42"}, - {file = "grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f"}, - {file = "grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8"}, - {file = "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62"}, - {file = "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a"}, - {file = "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc"}, - {file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc"}, - {file = "grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e"}, - {file = "grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e"}, - {file = "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783"}, - {file = "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378"}, - {file = "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c"}, - {file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886"}, - {file = "grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f"}, - {file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"}, - {file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"}, -] - -[package.dependencies] -typing-extensions = ">=4.12,<5.0" - -[package.extras] -protobuf = ["grpcio-tools (>=1.76.0)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "habluetooth" -version = "5.6.4" -description = "High availability Bluetooth" -optional = false -python-versions = ">=3.11" -groups = ["main", "dev"] -files = [ - {file = "habluetooth-5.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8f4ebcae127e186c392905a68452292beb03fb4bc7733e569520044a8d177eb"}, - {file = "habluetooth-5.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd5b083955e47a8c6490779c98a0e5443ec3e7eb527bcf23b26be6ef99406f7e"}, - {file = "habluetooth-5.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4ebc03a1f1c4fa7cd443765cb9ef8706d74240522dce377651f616d5ad4ad5b"}, - {file = "habluetooth-5.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:24eb60c9c96c6ec71e993fe45877f197d110ad33cf12606853c9517ccef529ec"}, - {file = "habluetooth-5.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:419ee7f6ae0247d485b0f5f8f8665ea680bdd6c2ec62a290ab14e4354c82a994"}, - {file = "habluetooth-5.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e8686f591442629852a6f83cda824c0c2ca519d972f959a992fc2665d17fafde"}, - {file = "habluetooth-5.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:68860a521c355963f2d3dc3f9654eca5b05416277ebc905dc0b650b6dcf2e60a"}, - {file = "habluetooth-5.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c0e3f6717a2a961217b7786b4c00bb27f7aab4706449186d1ea8977bca44523"}, - {file = "habluetooth-5.6.4-cp311-cp311-win32.whl", hash = "sha256:a3755d11d10ea715de717503faf7c6342d2b46ea79ef8f76a3165330d1f64143"}, - {file = "habluetooth-5.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:bddb194ef1ce7e7280a654bb47d1e59d7f2546c755fa6ea66292216d88cec731"}, - {file = "habluetooth-5.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5d538a95eeac3f13071ffbedc0362de36f501fcf3804cadcee1b3a5b3af43e4"}, - {file = "habluetooth-5.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f744e49935d4537a10300b25298df11f8bd3c1d17207be6a7dc1cdcf2b235316"}, - {file = "habluetooth-5.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e2aff14b04a71c3553602b99bd86392e7f7097a78182304886996505143e5d1"}, - {file = "habluetooth-5.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5c456fa7110e38c60872ac7f881644ce1b0229b19dd41a01dcebc31bc20c8b8f"}, - {file = "habluetooth-5.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f9ae64297ae906dd1b2d744f12d24474134aedeb5b1cfbb7b1ccd39a57447f1"}, - {file = "habluetooth-5.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee94cf96ad6244cb9ae7aa3fa0e9d67bf1d98cfe833d0dbfbca4a2ae76e5f928"}, - {file = "habluetooth-5.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eb782568aa210e3a09a373743e9621ed7a1e96568ecf97b024a88af1f3f58cb7"}, - {file = "habluetooth-5.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d865288fe6c7a51478e2688ebfab2107b07cd42de6b54c08d44a8b57dfcf586"}, - {file = "habluetooth-5.6.4-cp312-cp312-win32.whl", hash = "sha256:409c320a2ad3214c1879674d9cc59fc136f8bb19b2e0ac1a6d24f316654012e7"}, - {file = "habluetooth-5.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:b6602e61be6a21fb3cddcda588611278247d5a1fa42bc3590e76c0b554924b29"}, - {file = "habluetooth-5.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5cf1914a6c9db804e1da93a33552dbecbde7e2706a4224029ef15931e044781d"}, - {file = "habluetooth-5.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2262dcc0a9d7d7561afd05f98ac8cb471b5e006d9d09a6661b309c1877964175"}, - {file = "habluetooth-5.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f605273127378a28e02ada14f122933f4061c89600aa9ca67a798f6091dae29"}, - {file = "habluetooth-5.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1827ec0ed938f39ef7745c053f78c3320870a454f29a8ce82b02933846adeb70"}, - {file = "habluetooth-5.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21be74c2dfed3411dd52c58c8d614e9187e13da2308d42ffd9590630ef6cd2b3"}, - {file = "habluetooth-5.6.4-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:83fc0f21d165c99f3cba74a0c67becd0fdf911c31dc70370a1f0668a5cbfef6a"}, - {file = "habluetooth-5.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f69fe990a4e0bac6c1755cdace1839ca305076cf364984aa3eb59eb054936df3"}, - {file = "habluetooth-5.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:69ea4a09cea97af776ec5d933e86da356af9565a19885ef8743d7fd57e8cf0b2"}, - {file = "habluetooth-5.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5250bbd8216b34838c34ad469aefd052db41b1c9af21fb9a61c3884f82e46c83"}, - {file = "habluetooth-5.6.4-cp313-cp313-win32.whl", hash = "sha256:a27cd4487943d9b4218453a221d7a89ffc96f048b1301e984c04de90d249fc4c"}, - {file = "habluetooth-5.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:c09d1bf5e648579306c4b1a328a05b654e8492ab97502912a72d241995f9289b"}, - {file = "habluetooth-5.6.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cab943f42bfc573492a4dc044f6872186c8c7b412f470748bcb0e00855884276"}, - {file = "habluetooth-5.6.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:872735ae9ffb045acc3a87b55233177ac02c68c38c8fe8dc8dc527bd2627a300"}, - {file = "habluetooth-5.6.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ead8368ff986da9d4167812e55c79c69dbf5c7f707ebfdd31e9e6afc87aca70"}, - {file = "habluetooth-5.6.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d2c05f812a44c955e58bde1639a23f05ee0feb48222596e0c364d4e07185989"}, - {file = "habluetooth-5.6.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add79f79f1442f5a887d0874bfa3ffcf3ccc6916bf16747c449b306dedb88dd3"}, - {file = "habluetooth-5.6.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fe70a41ccdf305106ebdbe9d4d4dbfcb4039bc15463b37fe994ba3c5a7988a78"}, - {file = "habluetooth-5.6.4-cp314-cp314-win32.whl", hash = "sha256:0af0ce0005949be15e7f629a38167d76aff171e189a4372f67f42a5b4da33031"}, - {file = "habluetooth-5.6.4-cp314-cp314-win_amd64.whl", hash = "sha256:a136c3b31c971ff34319b0177b4b3226f439f6555c043571a17a197218c10ce5"}, - {file = "habluetooth-5.6.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9354c1e4b15187ee46f8d67bdde91feb683474c6c7258dd982443d0e368199ec"}, - {file = "habluetooth-5.6.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3337c57aa32f7256b712160854053bf8e86af958fa33a40a1225ba638735cba8"}, - {file = "habluetooth-5.6.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adc204fb01efa57ad7772bac9a69ad78a44ef11fe59dafffe780d6cc657a9612"}, - {file = "habluetooth-5.6.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1760ebeb50242a7dcff86a3df90bb403f93b3df91d150c03791dbf7bcef9ac7f"}, - {file = "habluetooth-5.6.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab386898fd63b3fe52343fcb5d99bca0b15269574f1d897a3fde27e23d12f91c"}, - {file = "habluetooth-5.6.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5adb089d1d0da72d064c54fbb4251690dc91333003b62d954e0fa47b746c59d9"}, - {file = "habluetooth-5.6.4-cp314-cp314t-win32.whl", hash = "sha256:20f56d3bd1b20fde5a5989dd6039ab36afb43fd82ac464f276a776f9d3bdad79"}, - {file = "habluetooth-5.6.4-cp314-cp314t-win_amd64.whl", hash = "sha256:88c4eeb1696a34697b36e8a36c482f728e6bbc494a2047786bfd7f39aeb08249"}, - {file = "habluetooth-5.6.4.tar.gz", hash = "sha256:8dce896b19bcb5991c17b9836361e4bf740101bee8cbf028902f96be3010c06c"}, -] - -[package.dependencies] -async-interrupt = ">=1.1.1" -bleak = ">=1.0.1" -bleak-retry-connector = ">=4.2.0" -bluetooth-adapters = ">=2.1.0" -bluetooth-auto-recovery = ">=1.5.1" -bluetooth-data-tools = ">=1.28.0" -btsocket = ">=0.3.0" -dbus-fast = {version = ">=2.30.2", markers = "platform_system == \"Linux\""} - -[[package]] -name = "hass-nabucasa" -version = "1.12.0" -description = "Home Assistant cloud integration by Nabu Casa, Inc." -optional = false -python-versions = ">=3.13" -groups = ["main", "dev"] -files = [ - {file = "hass_nabucasa-1.12.0-py3-none-any.whl", hash = "sha256:90debd3efa2bdf6bca03e20f1a61e15441b260661ed17106dca6141b005ef788"}, - {file = "hass_nabucasa-1.12.0.tar.gz", hash = "sha256:06bc4ebe89ffd08b744aa6540a2ebc44a82f60e2e74645e3b7498385c88d722c"}, -] - -[package.dependencies] -acme = "5.2.2" -aiohttp = ">=3.6.1" -async_timeout = ">=4" -atomicwrites-homeassistant = "1.4.1" -attrs = ">=19.3" -ciso8601 = ">=2.3.0" -cryptography = ">=42.0.0" -grpcio = ">=1.75.1,<2" -icmplib = ">=3,<4" -josepy = ">=2,<3" -pycognito = "2024.5.1" -PyJWT = ">=2.8.0" -sentence-stream = ">=1.2.0,<2" -snitun = "0.45.1" -voluptuous = ">=0.15" -webrtc-models = "<1.0.0" -yarl = ">=1.20,<2" - -[package.extras] -test = ["codespell (==2.4.1)", "freezegun (==1.5.5)", "mypy (==1.19.1)", "pre-commit (==4.5.1)", "pre-commit-hooks (==6.0.0)", "pylint (==4.0.4)", "pytest (==9.0.2)", "pytest-aiohttp (==1.1.0)", "pytest-socket (==0.7.0)", "pytest-timeout (==2.4.0)", "ruff (==0.14.14)", "syrupy (==5.1.0)", "tomli (==2.4.0)", "types_atomicwrites (==1.4.5.1)", "types_pyOpenSSL (==24.1.0.20240722)", "xmltodict (==1.0.2)"] - -[[package]] -name = "home-assistant-bluetooth" -version = "1.13.1" -description = "Home Assistant Bluetooth Models and Helpers" -optional = false -python-versions = ">=3.11" -groups = ["main", "dev"] -files = [ - {file = "home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7"}, - {file = "home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e"}, -] - -[package.dependencies] -habluetooth = ">=3.0" - -[[package]] -name = "homeassistant" -version = "2026.2.2" -description = "Open-source home automation platform running on Python 3." -optional = false -python-versions = ">=3.13.2" -groups = ["main", "dev"] -files = [ - {file = "homeassistant-2026.2.2-py3-none-any.whl", hash = "sha256:ef3e6a4e1cf96f4ad36062163b24b08488c3fe32f6115c2e02a0cc30ba65b30a"}, - {file = "homeassistant-2026.2.2.tar.gz", hash = "sha256:418a5f375bda07d9136ef256a7b1a8fc7c3b891f00e63da59c829cafba7d32ce"}, -] - -[package.dependencies] -aiodns = "4.0.0" -aiohasupervisor = "0.3.3" -aiohttp = "3.13.3" -aiohttp-asyncmdnsresolver = "0.1.1" -aiohttp_cors = "0.8.1" -aiohttp-fast-zlib = "0.3.0" -aiozoneinfo = "0.2.3" -annotatedyaml = "1.0.2" -astral = "2.2" -async-interrupt = "1.2.2" -atomicwrites-homeassistant = "1.4.1" -attrs = "25.4.0" -audioop-lts = "0.2.1" -awesomeversion = "25.8.0" -bcrypt = "5.0.0" -certifi = ">=2021.5.30" -ciso8601 = "2.3.3" -cronsim = "2.7" -cryptography = "46.0.5" -fnv-hash-fast = "1.6.0" -hass-nabucasa = "1.12.0" -home-assistant-bluetooth = "1.13.1" -httpx = "0.28.1" -ifaddr = "0.2.0" -Jinja2 = "3.1.6" -lru-dict = "1.3.0" -orjson = "3.11.5" -packaging = ">=23.1" -Pillow = "12.0.0" -propcache = "0.4.1" -psutil-home-assistant = "0.0.1" -PyJWT = "2.10.1" -pyOpenSSL = "25.3.0" -python-slugify = "8.0.4" -PyYAML = "6.0.3" -requests = "2.32.5" -securetar = "2025.2.1" -SQLAlchemy = "2.0.41" -standard-aifc = "3.13.0" -standard-telnetlib = "3.13.0" -typing-extensions = ">=4.15.0,<5.0" -ulid-transform = "1.5.2" -urllib3 = ">=2.0" -uv = "0.9.26" -voluptuous = "0.15.2" -voluptuous-openapi = "0.2.0" -voluptuous-serialize = "2.7.0" -webrtc-models = "0.3.0" -yarl = "1.22.0" -zeroconf = "0.148.0" - -[[package]] -name = "homeassistant-stubs" -version = "2026.2.2" -description = "PEP 484 typing stubs for Home Assistant Core" -optional = false -python-versions = ">=3.13.2" -groups = ["dev"] -files = [ - {file = "homeassistant_stubs-2026.2.2-py3-none-any.whl", hash = "sha256:5cb40c3f07c1d102edbf2e8452f6ef279524482fc6efd6cbd716d01c47c83113"}, - {file = "homeassistant_stubs-2026.2.2.tar.gz", hash = "sha256:c660c84f8b81038f77dfb644e098d0d86901d19945364d927a4640414110ca1a"}, -] - -[package.dependencies] -homeassistant = "2026.2.2" - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "icmplib" -version = "3.0.4" -description = "Easily forge ICMP packets and make your own ping and traceroute." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "icmplib-3.0.4-py3-none-any.whl", hash = "sha256:336b75c6c23c5ce99ddec33f718fab09661f6ad698e35b6f1fc7cc0ecf809398"}, - {file = "icmplib-3.0.4.tar.gz", hash = "sha256:57868f2cdb011418c0e1d5586b16d1fabd206569fe9652654c27b6b2d6a316de"}, -] - -[[package]] -name = "identify" -version = "2.6.14" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e"}, - {file = "identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "ifaddr" -version = "0.2.0" -description = "Cross-platform network interface and IP address enumeration library" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, - {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "josepy" -version = "2.1.0" -description = "JOSE protocol implementation in Python" -optional = false -python-versions = "<4.0,>=3.9.2" -groups = ["main", "dev"] -files = [ - {file = "josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd"}, - {file = "josepy-2.1.0.tar.gz", hash = "sha256:9beafbaa107ec7128e6c21d86b2bc2aea2f590158e50aca972dca3753046091f"}, -] - -[package.dependencies] -cryptography = ">=1.5" - -[package.extras] -docs = ["sphinx (>=4.3.0)", "sphinx-rtd-theme (>=1.0)"] - -[[package]] -name = "librt" -version = "0.8.1" -description = "Mypyc runtime library" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, - {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, - {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, - {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, - {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, - {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, - {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, - {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, - {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, - {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, - {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, - {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, - {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, - {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, - {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, - {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, - {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, - {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, - {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, - {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, - {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, - {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, - {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, - {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, - {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, - {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, - {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, - {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, - {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, - {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, - {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, - {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, - {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, - {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, - {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, - {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, - {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, - {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, - {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, - {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, - {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, - {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, - {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, -] - -[[package]] -name = "license-expression" -version = "30.4.3" -description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280"}, - {file = "license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2"}, -] - -[package.dependencies] -"boolean.py" = ">=4.0" - -[package.extras] -dev = ["Sphinx (>=5.0.2)", "doc8 (>=0.11.2)", "pytest (>=7.0.1)", "pytest-xdist (>=2)", "ruff", "sphinx-autobuild", "sphinx-copybutton", "sphinx-reredirects (>=0.1.2)", "sphinx-rtd-dark-mode (>=1.3.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-apidoc (>=0.4.0)", "twine"] - -[[package]] -name = "lru-dict" -version = "1.3.0" -description = "An Dict like LRU container." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4073333894db9840f066226d50e6f914a2240711c87d60885d8c940b69a6673f"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0ad6361e4dd63b47b2fc8eab344198f37387e1da3dcfacfee19bafac3ec9f1eb"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c637ab54b8cd9802fe19b260261e38820d748adf7606e34045d3c799b6dde813"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fce5f95489ca1fc158cc9fe0f4866db9cec82c2be0470926a9080570392beaf"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2bf2e24cf5f19c3ff69bf639306e83dced273e6fa775b04e190d7f5cd16f794"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90059f7701bef3c4da073d6e0434a9c7dc551d5adce30e6b99ef86b186f4b4a"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ecb7ae557239c64077e9b26a142eb88e63cddb104111a5122de7bebbbd00098"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6af36166d22dba851e06a13e35bbf33845d3dd88872e6aebbc8e3e7db70f4682"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ee38d420c77eed548df47b7d74b5169a98e71c9e975596e31ab808e76d11f09"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0e1845024c31e6ff246c9eb5e6f6f1a8bb564c06f8a7d6d031220044c081090b"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ca5474b1649555d014be1104e5558a92497509021a5ba5ea6e9b492303eb66b"}, - {file = "lru_dict-1.3.0-cp310-cp310-win32.whl", hash = "sha256:ebb03a9bd50c2ed86d4f72a54e0aae156d35a14075485b2127c4b01a3f4a63fa"}, - {file = "lru_dict-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:04cda617f4e4c27009005d0a8185ef02829b14b776d2791f5c994cc9d668bc24"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a"}, - {file = "lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4"}, - {file = "lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df"}, - {file = "lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc"}, - {file = "lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c265f16c936a8ff3bb4b8a4bda0be94c15ec28b63e99fdb1439c1ffe4cd437db"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:784ca9d3b0730b3ec199c0a58f66264c63dd5d438119c739c349a6a9be8e5f6e"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13b2f58f647178470adaa14603bb64cc02eeed32601772ccea30e198252883c"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ffbce5c2e80f57937679553c8f27e61ec327c962bf7ea0b15f1d74277fd5363"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7969cb034b3ccc707aff877c73c225c32d7e2a7981baa8f92f5dd4d468fe8c33"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9ab676609cce85dd65d91c275e47da676d13d77faa72de286fbea30fbaa596"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27c078b5d75989952acbf9b77e14c3dadc468a4aafe85174d548afbc5efc38b"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6123aefe97762ad74215d05320a7f389f196f0594c8813534284d4eafeca1a96"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cd869cadba9a63e1e7fe2dced4a5747d735135b86016b0a63e8c9e324ab629ac"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:40a8daddc29c7edb09dfe44292cf111f1e93a8344349778721d430d336b50505"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a03170e4152836987a88dcebde61aaeb73ab7099a00bb86509d45b3fe424230"}, - {file = "lru_dict-1.3.0-cp38-cp38-win32.whl", hash = "sha256:3b4f121afe10f5a82b8e317626eb1e1c325b3f104af56c9756064cd833b1950b"}, - {file = "lru_dict-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:1470f5828c7410e16c24b5150eb649647986e78924816e6fb0264049dea14a2b"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c9f746a9917e784fffcedeac4c8c47a3dbd90cbe13b69e9140182ad97ce4b7"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2789296819525a1f3204072dfcf3df6db8bcf69a8fc740ffd3de43a684ea7002"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:170b66d29945391460351588a7bd8210a95407ae82efe0b855e945398a1d24ea"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774ca88501a9effe8797c3db5a6685cf20978c9cb0fe836b6813cfe1ca60d8c9"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df2e119c6ae412d2fd641a55f8a1e2e51f45a3de3449c18b1b86c319ab79e0c4"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28aa1ea42a7e48174bf513dc2416fea7511a547961e678dc6f5670ca987c18cb"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9537e1cee6fa582cb68f2fb9ce82d51faf2ccc0a638b275d033fdcb1478eb80b"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:64545fca797fe2c68c5168efb5f976c6e1459e058cab02445207a079180a3557"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a193a14c66cfc0c259d05dddc5e566a4b09e8f1765e941503d065008feebea9d"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3cb1de0ce4137b060abaafed8474cc0ebd12cedd88aaa7f7b3ebb1ddfba86ae0"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8551ccab1349d4bebedab333dfc8693c74ff728f4b565fe15a6bf7d296bd7ea9"}, - {file = "lru_dict-1.3.0-cp39-cp39-win32.whl", hash = "sha256:6cb0be5e79c3f34d69b90d8559f0221e374b974b809a22377122c4b1a610ff67"}, - {file = "lru_dict-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9f725f2a0bdf1c18735372d5807af4ea3b77888208590394d4660e3d07971f21"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f8f7824db5a64581180ab9d09842e6dd9fcdc46aac9cb592a0807cd37ea55680"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd04b7e7b0c0c192d738df9c317093335e7282c64c9d1bb6b7ebb54674b4e24"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5c20f236f27551e3f0adbf1a987673fb1e9c38d6d284502cd38f5a3845ef681"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca3703ff03b03a1848c563bc2663d0ad813c1cd42c4d9cf75b623716d4415d9a"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a9fb71ba262c6058a0017ce83d343370d0a0dbe2ae62c2eef38241ec13219330"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f5b88a7c39e307739a3701194993455968fcffe437d1facab93546b1b8a334c1"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2682bfca24656fb7a643621520d57b7fe684ed5fa7be008704c1235d38e16a32"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96fc87ddf569181827458ec5ad8fa446c4690cffacda66667de780f9fcefd44d"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcec98e2c7da7631f0811730303abc4bdfe70d013f7a11e174a2ccd5612a7c59"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6bba2863060caeaedd8386b0c8ee9a7ce4d57a7cb80ceeddf440b4eff2d013ba"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c497fb60279f1e1d7dfbe150b1b069eaa43f7e172dab03f206282f4994676c5"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9509d817a47597988615c1a322580c10100acad10c98dfcf3abb41e0e5877f"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0213ab4e3d9a8d386c18e485ad7b14b615cb6f05df6ef44fb2a0746c6ea9278b"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b50fbd69cd3287196796ab4d50e4cc741eb5b5a01f89d8e930df08da3010c385"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5247d1f011f92666010942434020ddc5a60951fefd5d12a594f0e5d9f43e3b3b"}, -] - -[package.extras] -test = ["pytest"] - -[[package]] -name = "mando" -version = "0.7.1" -description = "Create Python CLI apps with little to no effort at all!" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a"}, - {file = "mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500"}, -] - -[package.dependencies] -six = "*" - -[package.extras] -restructuredtext = ["rst2ansi"] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mashumaro" -version = "3.20" -description = "Fast and well tested serialization library" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "mashumaro-3.20-py3-none-any.whl", hash = "sha256:648bc326f64c55447988eab67d6bfe3b7958c0961c83590709b1f950f88f4a3c"}, - {file = "mashumaro-3.20.tar.gz", hash = "sha256:af4573f14ae61be3fbc3a473158ddfc1420f345410385809fd782e0d79e9215c"}, -] - -[package.dependencies] -typing_extensions = ">=4.14.0" - -[package.extras] -msgpack = ["msgpack (>=0.5.6)"] -orjson = ["orjson"] -toml = ["tomli (>=1.1.0) ; python_version < \"3.11\"", "tomli-w (>=1.0)"] -yaml = ["pyyaml (>=3.13)"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mock-open" -version = "1.4.0" -description = "A better mock for file I/O" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mock-open-1.4.0.tar.gz", hash = "sha256:c3ecb6b8c32a5899a4f5bf4495083b598b520c698bba00e1ce2ace6e9c239100"}, -] - -[[package]] -name = "multidict" -version = "6.6.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, - {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, - {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, - {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, - {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, - {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, - {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, - {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, - {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, - {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, - {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, - {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, - {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, - {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, - {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, - {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, - {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, - {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, - {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, - {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, - {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, - {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, - {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, -] - -[[package]] -name = "mypy" -version = "1.19.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, -] - -[package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "numpy" -version = "2.3.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["dev"] -files = [ - {file = "numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8"}, - {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d"}, - {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3"}, - {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f"}, - {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097"}, - {file = "numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220"}, - {file = "numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170"}, - {file = "numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b"}, - {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370"}, - {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73"}, - {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc"}, - {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be"}, - {file = "numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036"}, - {file = "numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f"}, - {file = "numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089"}, - {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2"}, - {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f"}, - {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee"}, - {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6"}, - {file = "numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b"}, - {file = "numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56"}, - {file = "numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286"}, - {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8"}, - {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a"}, - {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91"}, - {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5"}, - {file = "numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5"}, - {file = "numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450"}, - {file = "numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19"}, - {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f"}, - {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5"}, - {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58"}, - {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0"}, - {file = "numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2"}, - {file = "numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b"}, - {file = "numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2"}, - {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0"}, - {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0"}, - {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2"}, - {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf"}, - {file = "numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1"}, - {file = "numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b"}, - {file = "numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619"}, - {file = "numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48"}, -] - -[[package]] -name = "orjson" -version = "3.11.5" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e"}, - {file = "orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7"}, - {file = "orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401"}, - {file = "orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8"}, - {file = "orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167"}, - {file = "orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8"}, - {file = "orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef"}, - {file = "orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5"}, - {file = "orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880"}, - {file = "orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d"}, - {file = "orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1"}, - {file = "orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c"}, - {file = "orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d"}, - {file = "orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa"}, - {file = "orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3"}, - {file = "orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca"}, - {file = "orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98"}, - {file = "orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875"}, - {file = "orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe"}, - {file = "orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629"}, - {file = "orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706"}, - {file = "orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2"}, - {file = "orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05"}, - {file = "orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef"}, - {file = "orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583"}, - {file = "orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287"}, - {file = "orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0"}, - {file = "orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4"}, - {file = "orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d"}, - {file = "orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439"}, - {file = "orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499"}, - {file = "orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310"}, - {file = "orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5"}, - {file = "orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5"}, - {file = "orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8"}, - {file = "orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a"}, - {file = "orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1"}, - {file = "orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30"}, - {file = "orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "paho-mqtt" -version = "2.1.0" -description = "MQTT version 5.0/3.1.1 client class" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee"}, - {file = "paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834"}, -] - -[package.extras] -proxy = ["pysocks"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pillow" -version = "12.0.0" -description = "Python Imaging Library (fork)" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, - {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, - {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, - {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, - {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, - {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, - {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, - {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, - {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, - {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, - {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, - {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, - {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, - {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, - {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, - {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, - {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, - {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, - {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, - {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, - {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, - {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, - {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, - {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -xmp = ["defusedxml"] - -[[package]] -name = "pip" -version = "25.2" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717"}, - {file = "pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2"}, -] - -[[package]] -name = "pipdeptree" -version = "2.26.1" -description = "Command line utility to show dependency tree of packages." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pipdeptree-2.26.1-py3-none-any.whl", hash = "sha256:3849d62a2ed641256afac3058c4f9b85ac4a47e9d8c991ee17a8f3d230c5cffb"}, - {file = "pipdeptree-2.26.1.tar.gz", hash = "sha256:92a8f37ab79235dacb46af107e691a1309ca4a429315ba2a1df97d1cd56e27ac"}, -] - -[package.dependencies] -packaging = ">=24.1" -pip = ">=24.2" - -[package.extras] -graphviz = ["graphviz (>=0.20.3)"] -test = ["covdefaults (>=2.3)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-console-scripts (>=1.4.1)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "virtualenv (>=20.26.4,<21)"] - -[[package]] -name = "platformdirs" -version = "4.4.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "4.3.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, - {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "prettier" -version = "0.0.7" -description = "Properly pprint of nested objects" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "prettier-0.0.7-py3-none-any.whl", hash = "sha256:20e76791de41cafe481328dd49552303f29ca192151cee1b120c26f66cae9bfc"}, - {file = "prettier-0.0.7.tar.gz", hash = "sha256:6c34b8cd09fd9c8956c05d6395ea3f575e0122dce494ba57685c07065abed427"}, -] - -[[package]] -name = "propcache" -version = "0.4.1" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, - {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, - {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, - {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, - {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, - {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, - {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, - {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, - {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, - {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, - {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, - {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, - {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, - {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, - {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, - {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, - {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, - {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, - {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, - {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, - {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, - {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, - {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, - {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, - {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, - {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, - {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, -] - -[[package]] -name = "psutil" -version = "7.1.0" -description = "Cross-platform lib for process and system monitoring." -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13"}, - {file = "psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d"}, - {file = "psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca"}, - {file = "psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d"}, - {file = "psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07"}, - {file = "psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] -test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] - -[[package]] -name = "psutil-home-assistant" -version = "0.0.1" -description = "Wrapper for psutil to allow it to be used several times in the same process." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8"}, - {file = "psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41"}, -] - -[package.dependencies] -psutil = "*" - -[[package]] -name = "pycares" -version = "5.0.1" -description = "Python interface for c-ares" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pycares-5.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:adc592534a10fe24fd1a801173c46769f75b97c440c9162f5d402ee1ba3eaf51"}, - {file = "pycares-5.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8848bbea6b5c2a0f7c9d0231ee455c3ce976c5c85904e014b2e9aa636a34140e"}, - {file = "pycares-5.0.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5003cbbae0a943f49089cc149996c3d078cef482971d834535032d53558f4d48"}, - {file = "pycares-5.0.1-cp310-cp310-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc0cdeadb2892e7f0ab30b6a288a357441c21dcff2ce16e91fccbc9fae9d1e2a"}, - {file = "pycares-5.0.1-cp310-cp310-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:faa093af3bea365947325ec23ed24efe81dcb0efc1be7e19a36ba37108945237"}, - {file = "pycares-5.0.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dedd6d41bd09dbed7eeea84a30b4b6fd1cacf9523a3047e088b5e692cff13d84"}, - {file = "pycares-5.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d3eb5e6ba290efd8b543a2cb77ad938c3494250e6e0302ee2aa55c06bbe153cd"}, - {file = "pycares-5.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:58634f83992c81f438987b572d371825dae187d3a09d6e213edbe71fbb4ba18c"}, - {file = "pycares-5.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe9ce4361809903261c4b055284ba91d94adedfd2202e0257920b9085d505e37"}, - {file = "pycares-5.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:965ec648814829788233155ef3f6d4d7e7d6183460d10f9c71859c504f8f488b"}, - {file = "pycares-5.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:171182baa32951fffd1568ba9b934a76f36ed86c6248855ec6f82bbb3954d604"}, - {file = "pycares-5.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:48ac858124728b8bac0591aa8361c683064fefe35794c29b3a954818c59f1e9b"}, - {file = "pycares-5.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c29ca77ff9712e20787201ca8e76ad89384771c0e058a0a4f3dc05afbc4b32de"}, - {file = "pycares-5.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f11424bf5cf6226d0b136ed47daa58434e377c61b62d0100d1de7793f8e34a72"}, - {file = "pycares-5.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d765afb52d579879f5c4f005763827d3b1eb86b23139e9614e6089c9f98db017"}, - {file = "pycares-5.0.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ea0d57ba5add4bfbcc40cbdfa92bbb8a5ef0c4c21881e26c7229d9bdc92a4533"}, - {file = "pycares-5.0.1-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9ec2aa3553d33e6220aeb1a05f4853fb83fce4cec3e0dea2dc970338ea47dc"}, - {file = "pycares-5.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c63fb2498b05e9f5670a1bf3b900c5d09343b3b6d5001a9714d593f9eb54de1"}, - {file = "pycares-5.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71316f7a87c15a8d32127ff01374dc2c969c37410693cc0cf6532590b7f18e7a"}, - {file = "pycares-5.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a2117dffbb78615bfdb41ad77b17038689e4e01c66f153649e80d268c6228b4f"}, - {file = "pycares-5.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7d7c4f5d8b88b586ef2288142b806250020e6490b9f2bd8fd5f634a78fd20fcf"}, - {file = "pycares-5.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433b9a4b5a7e10ef8aef0b957e6cd0bfc1bb5bc730d2729f04e93c91c25979c0"}, - {file = "pycares-5.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:cf2699883b88713670d3f9c0a1e44ac24c70aeace9f8c6aa7f0b9f222d5b08a5"}, - {file = "pycares-5.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:9528dc11749e5e098c996475b60f879e1db5a6cb3dd0cdc747530620bb1a8941"}, - {file = "pycares-5.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ee551be4f3f3ac814ac8547586c464c9035e914f5122a534d25de147fa745e1"}, - {file = "pycares-5.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:252d4e5a52a68f825eaa90e16b595f9baee22c760f51e286ab612c6829b96de3"}, - {file = "pycares-5.0.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c1aa549b8c2f2e224215c793d660270778dcba9abc3b85abbc7c41eabe4f1e5"}, - {file = "pycares-5.0.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db7c9c9f16e8311998667a7488e817f8cbeedec2447bac827c71804663f1437e"}, - {file = "pycares-5.0.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9c4c8bb69bab863f677fa166653bb872bfa5d5a742f1f30bebc2d53b6e71db"}, - {file = "pycares-5.0.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09ef90da8da3026fcba4ed223bd71e8057608d5b3fec4f5990b52ae1e8c855cc"}, - {file = "pycares-5.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ce193ebd54f4c74538b751ebb0923a9208c234ff180589d4d3cec134c001840e"}, - {file = "pycares-5.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36b9ff18ef231277f99a846feade50b417187a96f742689a9d08b9594e386de4"}, - {file = "pycares-5.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5e40ea4a0ef0c01a02ef7f7390a58c62d237d5ad48d36bc3245e9c2ac181cc22"}, - {file = "pycares-5.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f323b0ddfd2c7896af6fba4f8851d34d3d13387566aa573d93330fb01cb1038"}, - {file = "pycares-5.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bdc6bcafb72a97b3cdd529fc87210e59e67feb647a7e138110656023599b84da"}, - {file = "pycares-5.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:f8ef4c70c1edaf022875a8f9ff6c0c064f82831225acc91aa1b4f4d389e2e03a"}, - {file = "pycares-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d1b2c6b152c65f14d0e12d741fabb78a487f0f0d22773eede8d8cfc97af612b"}, - {file = "pycares-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c8ffcc9a48cfc296fe1aefc07d2c8e29a7f97e4bb366ce17effea6a38825f70"}, - {file = "pycares-5.0.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8efc38c2703e3530b823a4165a7b28d7ce0fdcf41960fb7a4ca834a0f8cfe79"}, - {file = "pycares-5.0.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e380bf6eff42c260f829a0a14547e13375e949053a966c23ca204a13647ef265"}, - {file = "pycares-5.0.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:35dd5858ee1246bd092a212b5e85a8ef70853f7cfaf16b99569bf4af3ae4695d"}, - {file = "pycares-5.0.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c257c6e7bf310cdb5823aa9d9a28f1e370fed8c653a968d38a954a8f8e0375ce"}, - {file = "pycares-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07711acb0ef75758f081fb7436acaccc91e8afd5ae34fd35d4edc44297e81f27"}, - {file = "pycares-5.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:30e5db1ae85cffb031dd8bc1b37903cd74c6d37eb737643bbca3ff2cd4bc6ae2"}, - {file = "pycares-5.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:efbe7f89425a14edbc94787042309be77cb3674415eb6079b356e1f9552ba747"}, - {file = "pycares-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5de9e7ce52d638d78723c24704eb032e60b96fbb6fe90c6b3110882987251377"}, - {file = "pycares-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e99af0a1ce015ab6cc6bd85ce158d95ed89fb3b654515f1d0989d1afcf11026"}, - {file = "pycares-5.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a511c9f3b11b7ce9f159c956ea1b8f2de7f419d7ca9fa24528d582cb015dbf9"}, - {file = "pycares-5.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e330e3561be259ad7a1b7b0ce282c872938625f76587fae7ac8d6bc5af1d0c3d"}, - {file = "pycares-5.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82bd37fec2a3fa62add30d4a3854720f7b051386e2f18e6e8f4ee94b89b5a7b0"}, - {file = "pycares-5.0.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:258c38aaa82ad1d565b4591cdb93d2c191be8e0a2c70926999c8e0b717a01f2a"}, - {file = "pycares-5.0.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ccc1b2df8a09ca20eefbe20b9f7a484d376525c0fb173cfadd692320013c6bc5"}, - {file = "pycares-5.0.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c4dfc80cc8b43dc79e02a15486c58eead5cae0a40906d6be64e2522285b5b39"}, - {file = "pycares-5.0.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f498a6606247bfe896c2a4d837db711eb7b0ba23e409e16e4b23def4bada4b9d"}, - {file = "pycares-5.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a7d197835cdb4b202a3b12562b32799e27bb132262d4aa1ac3ee9d440e8ec22c"}, - {file = "pycares-5.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f78ab823732b050d658eb735d553726663c9bccdeeee0653247533a23eb2e255"}, - {file = "pycares-5.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f444ab7f318e9b2c209b45496fb07bff5e7ada606e15d5253a162964aa078527"}, - {file = "pycares-5.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de80997de7538619b7dd28ec4371e5172e3f9480e4fc648726d3d5ba661ca05"}, - {file = "pycares-5.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:206ce9f3cb9d51f5065c81b23c22996230fbc2cf58ae22834c623631b2b473aa"}, - {file = "pycares-5.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:45fb3b07231120e8cb5b75be7f15f16115003e9251991dc37a3e5c63733d63b5"}, - {file = "pycares-5.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:602f3eac4b880a2527d21f52b2319cb10fde9225d103d338c4d0b2b07f136849"}, - {file = "pycares-5.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1c3736deef003f0c57bc4e7f94d54270d0824350a8f5ceaba3a20b2ce8fb427"}, - {file = "pycares-5.0.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e63328df86d37150ce697fb5d9313d1d468dd4dddee1d09342cb2ed241ce6ad9"}, - {file = "pycares-5.0.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57f6fd696213329d9a69b9664a68b1ff2a71ccbdc1fc928a42c9a92858c1ec5d"}, - {file = "pycares-5.0.1-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d0878edabfbecb48a29e8769284003d8dbc05936122fe361849cd5fa52722e0"}, - {file = "pycares-5.0.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50e21f27a91be122e066ddd78c2d0d2769e547561481d8342a9d652a345b89f7"}, - {file = "pycares-5.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:97ceda969f5a5d5c6b15558b658c29e4301b3a2c4615523797b5f9d4ac74772e"}, - {file = "pycares-5.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d1713e602ab09882c3e65499b2cc763bff0371117327cad704cf524268c2604"}, - {file = "pycares-5.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:954a379055d6c66b2e878b52235b382168d1a3230793ff44454019394aecac5e"}, - {file = "pycares-5.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:145d8a20f7fd1d58a2e49b7ef4309ec9bdcab479ac65c2e49480e20d3f890c23"}, - {file = "pycares-5.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ebc9daba03c7ff3f62616c84c6cb37517445d15df00e1754852d6006039eb4a4"}, - {file = "pycares-5.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e0a86eff6bf9e91d5dd8876b1b82ee45704f46b1104c24291d3dea2c1fc8ebcb"}, - {file = "pycares-5.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89fbb801bd7328d38025ab3576eee697cf9eca1f45774a0353b6a68a867e5516"}, - {file = "pycares-5.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f760ed82ad8b7311ada58f9f68673e135ece3b1beb06d3ec8723a4f3d5dd824e"}, - {file = "pycares-5.0.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94cb140b78bde232f6eb64c95cdac08dac9ae1829bfee1c436932eea10aabd39"}, - {file = "pycares-5.0.1-cp39-cp39-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83da4b2e30bb80a424337376af0bce1216d787821b71c74d2f2bf3d40ea0bcf9"}, - {file = "pycares-5.0.1-cp39-cp39-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07260c6c0eff8aa809d6cd64010303098c7d0fe79176aba207d747c9ffc7a95a"}, - {file = "pycares-5.0.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e1630844c695fc41e760d653d775d03c61bf8c5ac259e90784f7f270e8c440c"}, - {file = "pycares-5.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8dc84c0bce595c572971c1a9c7a3b417465572382968faac9bfddebd60e946b4"}, - {file = "pycares-5.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:83115177cc0f1c8e6fbeb4e483d676f91d0ce90aad2933d5f0c87feccdc05688"}, - {file = "pycares-5.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:eb93ea76094c46fd4a1294eb49affcf849d36d9b939322009d2bee7d507fcb20"}, - {file = "pycares-5.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:534dd25083e7ba4c65fedbc94126bada53fe8de4466d9ca29b7aa2ab4eec36b4"}, - {file = "pycares-5.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:52901b7a15a3b99631021a90fa3d1451d42b47b977208928012bf8238f70ba13"}, - {file = "pycares-5.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:153239d8c51f9e051d37867287ee1b283a201076e4cd9f4624ead30c86dfd5c9"}, - {file = "pycares-5.0.1.tar.gz", hash = "sha256:5a3c249c830432631439815f9a818463416f2a8cbdb1e988e78757de9ae75081"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0b1", markers = "python_version >= \"3.14\""} - -[package.extras] -idna = ["idna (>=2.1)"] - -[[package]] -name = "pycognito" -version = "2024.5.1" -description = "Python class to integrate Boto3's Cognito client so it is easy to login users. With SRP support." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea"}, - {file = "pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4"}, -] - -[package.dependencies] -boto3 = ">=1.10.49" -envs = ">=1.3" -pyjwt = {version = ">=2.8.0", extras = ["crypto"]} -requests = ">=2.22.0" - -[[package]] -name = "pycparser" -version = "2.23" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -markers = "implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, -] - -[[package]] -name = "pydantic" -version = "2.12.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae"}, - {file = "pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.4" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.4" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.10.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, -] - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pylint" -version = "4.0.5" -description = "python code static checker" -optional = false -python-versions = ">=3.10.0" -groups = ["dev"] -files = [ - {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"}, - {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"}, -] - -[package.dependencies] -astroid = ">=4.0.2,<=4.1.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=5,<5.13 || >5.13,<9" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2" -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pylint-per-file-ignores" -version = "1.4.0" -description = "A pylint plugin to ignore error codes per file." -optional = false -python-versions = "<4.0.0,>=3.8.1" -groups = ["dev"] -files = [ - {file = "pylint_per_file_ignores-1.4.0-py3-none-any.whl", hash = "sha256:0cd82d22551738b4e63a0aa1dab2a1fc4016e8f27f1429159616483711e122fd"}, - {file = "pylint_per_file_ignores-1.4.0.tar.gz", hash = "sha256:c0de7b3d0169571aefaa1ac3a82a265641b8825b54a0b6f5ef27c3b76b988609"}, -] - -[[package]] -name = "pyobjc-core" -version = "11.1" -description = "Python<->ObjC Interoperability Module" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -markers = "platform_system == \"Darwin\"" -files = [ - {file = "pyobjc_core-11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4c7536f3e94de0a3eae6bb382d75f1219280aa867cdf37beef39d9e7d580173c"}, - {file = "pyobjc_core-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ec36680b5c14e2f73d432b03ba7c1457dc6ca70fa59fd7daea1073f2b4157d33"}, - {file = "pyobjc_core-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:765b97dea6b87ec4612b3212258024d8496ea23517c95a1c5f0735f96b7fd529"}, - {file = "pyobjc_core-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c"}, - {file = "pyobjc_core-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2"}, - {file = "pyobjc_core-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731"}, - {file = "pyobjc_core-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96"}, - {file = "pyobjc_core-11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4a99e6558b48b8e47c092051e7b3be05df1c8d0617b62f6fa6a316c01902d157"}, - {file = "pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe"}, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "11.1" -description = "Wrappers for the Cocoa frameworks on macOS" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Darwin\"" -files = [ - {file = "pyobjc_framework_cocoa-11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b27a5bdb3ab6cdeb998443ff3fce194ffae5f518c6a079b832dbafc4426937f9"}, - {file = "pyobjc_framework_cocoa-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b9a9b8ba07f5bf84866399e3de2aa311ed1c34d5d2788a995bdbe82cc36cfa0"}, - {file = "pyobjc_framework_cocoa-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806de56f06dfba8f301a244cce289d54877c36b4b19818e3b53150eb7c2424d0"}, - {file = "pyobjc_framework_cocoa-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da"}, - {file = "pyobjc_framework_cocoa-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350"}, - {file = "pyobjc_framework_cocoa-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0"}, - {file = "pyobjc_framework_cocoa-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71"}, - {file = "pyobjc_framework_cocoa-11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbee71eeb93b1b31ffbac8560b59a0524a8a4b90846a260d2c4f2188f3d4c721"}, - {file = "pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038"}, -] - -[package.dependencies] -pyobjc-core = ">=11.1" - -[[package]] -name = "pyobjc-framework-corebluetooth" -version = "11.1" -description = "Wrappers for the framework CoreBluetooth on macOS" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Darwin\"" -files = [ - {file = "pyobjc_framework_corebluetooth-11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ab509994503a5f0ec0f446a7ccc9f9a672d5a427d40dba4563dd00e8e17dfb06"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:433b8593eb1ea8b6262b243ec903e1de4434b768ce103ebe15aac249b890cc2a"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:36bef95a822c68b72f505cf909913affd61a15b56eeaeafea7302d35a82f4f05"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:992404b03033ecf637e9174caed70cb22fd1be2a98c16faa699217678e62a5c7"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ebb8648f5e33d98446eb1d6c4654ba4fcc15d62bfcb47fa3bbd5596f6ecdb37c"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:e84cbf52006a93d937b90421ada0bc4a146d6d348eb40ae10d5bd2256cc92206"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:4da1106265d7efd3f726bacdf13ba9528cc380fb534b5af38b22a397e6908291"}, - {file = "pyobjc_framework_corebluetooth-11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e9fa3781fea20a31b3bb809deaeeab3bdc7b86602a1fd829f0e86db11d7aa577"}, - {file = "pyobjc_framework_corebluetooth-11.1.tar.gz", hash = "sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190"}, -] - -[package.dependencies] -pyobjc-core = ">=11.1" -pyobjc-framework-Cocoa = ">=11.1" - -[[package]] -name = "pyobjc-framework-libdispatch" -version = "11.1" -description = "Wrappers for libdispatch on macOS" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Darwin\"" -files = [ - {file = "pyobjc_framework_libdispatch-11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9c598c073a541b5956b5457b94bd33b9ce19ef8d867235439a0fad22d6beab49"}, - {file = "pyobjc_framework_libdispatch-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ddca472c2cbc6bb192e05b8b501d528ce49333abe7ef0eef28df3133a8e18b7"}, - {file = "pyobjc_framework_libdispatch-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc9a7b8c2e8a63789b7cf69563bb7247bde15353208ef1353fff0af61b281684"}, - {file = "pyobjc_framework_libdispatch-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c4e219849f5426745eb429f3aee58342a59f81e3144b37aa20e81dacc6177de1"}, - {file = "pyobjc_framework_libdispatch-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a9357736cb47b4a789f59f8fab9b0d10b0a9c84f9876367c398718d3de085888"}, - {file = "pyobjc_framework_libdispatch-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:cd08f32ea7724906ef504a0fd40a32e2a0be4d64b9239530a31767ca9ccfc921"}, - {file = "pyobjc_framework_libdispatch-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:5d9985b0e050cae72bf2c6a1cc8180ff4fa3a812cd63b2dc59e09c6f7f6263a1"}, - {file = "pyobjc_framework_libdispatch-11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfe515f4c3ea66c13fce4a527230027517b8b779b40bbcb220ff7cdf3ad20bc4"}, - {file = "pyobjc_framework_libdispatch-11.1.tar.gz", hash = "sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87"}, -] - -[package.dependencies] -pyobjc-core = ">=11.1" -pyobjc-framework-Cocoa = ">=11.1" - -[[package]] -name = "pyopenssl" -version = "25.3.0" -description = "Python wrapper module around the OpenSSL library" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6"}, - {file = "pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329"}, -] - -[package.dependencies] -cryptography = ">=45.0.7,<47" - -[package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] -test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] - -[[package]] -name = "pyrfc3339" -version = "2.1.0" -description = "Generate and parse RFC 3339 timestamps" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3"}, - {file = "pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f"}, -] - -[[package]] -name = "pyric" -version = "0.1.6.3" -description = "Python Wireless Library" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9"}, -] - -[[package]] -name = "pyright" -version = "1.1.405" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a"}, - {file = "pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" -typing-extensions = ">=4.1" - -[package.extras] -all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] -nodejs = ["nodejs-wheel-binaries"] - -[[package]] -name = "pytest" -version = "9.0.0" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96"}, - {file = "pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-aiohttp" -version = "1.1.0" -description = "Pytest plugin for aiohttp support" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d"}, - {file = "pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc"}, -] - -[package.dependencies] -aiohttp = ">=3.11.0b0" -pytest = ">=6.1.0" -pytest-asyncio = ">=0.17.2" - -[package.extras] -testing = ["coverage (==6.2)", "mypy (==1.12.1)"] - -[[package]] -name = "pytest-asyncio" -version = "1.3.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, - {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, -] - -[package.dependencies] -pytest = ">=8.2,<10" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-freezer" -version = "0.4.9" -description = "Pytest plugin providing a fixture interface for spulec/freezegun" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59"}, - {file = "pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a"}, -] - -[package.dependencies] -freezegun = ">=1.1" -pytest = ">=3.6" - -[[package]] -name = "pytest-github-actions-annotate-failures" -version = "0.3.0" -description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf"}, - {file = "pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7"}, -] - -[package.dependencies] -pytest = ">=6.0.0" - -[[package]] -name = "pytest-homeassistant-custom-component" -version = "0.13.315" -description = "Experimental package to automatically extract test plugins for Home Assistant custom components" -optional = false -python-versions = ">=3.13" -groups = ["dev"] -files = [ - {file = "pytest_homeassistant_custom_component-0.13.315-py3-none-any.whl", hash = "sha256:1ac286b2e5c76c2dacc31ace8afdd30456da601d048ef84ee39a1739c08ab013"}, - {file = "pytest_homeassistant_custom_component-0.13.315.tar.gz", hash = "sha256:c52fc8596e43d6a4a43f4514094dabf0a7254089a22071473aa33d6558bb0880"}, -] - -[package.dependencies] -coverage = "7.10.6" -freezegun = "1.5.2" -homeassistant = "2026.2.2" -license-expression = "30.4.3" -mock-open = "1.4.0" -numpy = "2.3.2" -paho-mqtt = "2.1.0" -pipdeptree = "2.26.1" -pydantic = "2.12.2" -pylint-per-file-ignores = "1.4.0" -pytest = "9.0.0" -pytest-aiohttp = "1.1.0" -pytest-asyncio = "1.3.0" -pytest-cov = "7.0.0" -pytest-freezer = "0.4.9" -pytest-github-actions-annotate-failures = "0.3.0" -pytest-picked = "0.5.1" -pytest-socket = "0.7.0" -pytest-sugar = "1.0.0" -pytest-timeout = "2.4.0" -pytest-unordered = "0.7.0" -pytest-xdist = "3.8.0" -requests-mock = "1.12.1" -respx = "0.22.0" -sqlalchemy = "2.0.41" -syrupy = "5.0.0" -tqdm = "4.67.1" - -[[package]] -name = "pytest-picked" -version = "0.5.1" -description = "Run the tests related to the changed files" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_picked-0.5.1-py3-none-any.whl", hash = "sha256:af65c4763b51dc095ae4bc5073a962406902422ad9629c26d8b01122b677d998"}, - {file = "pytest_picked-0.5.1.tar.gz", hash = "sha256:6634c4356a560a5dc3dba35471865e6eb06bbd356b56b69c540593e9d5620ded"}, -] - -[package.dependencies] -pytest = ">=3.7.0" - -[[package]] -name = "pytest-socket" -version = "0.7.0" -description = "Pytest Plugin to disable socket calls during tests" -optional = false -python-versions = ">=3.8,<4.0" -groups = ["dev"] -files = [ - {file = "pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45"}, - {file = "pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[[package]] -name = "pytest-sugar" -version = "1.0.0" -description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, - {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, -] - -[package.dependencies] -packaging = ">=21.3" -pytest = ">=6.2.0" -termcolor = ">=2.1.0" - -[package.extras] -dev = ["black", "flake8", "pre-commit"] - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -description = "pytest plugin to abort hanging tests" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, - {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[[package]] -name = "pytest-unordered" -version = "0.7.0" -description = "Test equality of unordered collections in pytest" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584"}, - {file = "pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[[package]] -name = "pytest-xdist" -version = "3.8.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, - {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-direnv" -version = "0.2.2" -description = "Loads environment variables from a direnv .envrc file." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "python_direnv-0.2.2-py3-none-any.whl", hash = "sha256:a617d14f093f13dd9a858e88c2914bdb16edee992b5148efd8c23c10ca1b50d9"}, - {file = "python_direnv-0.2.2.tar.gz", hash = "sha256:0fe2fb834c901d675edcacc688689cfcf55cf06d9cf27dc7d3768a6c38c35f00"}, -] - -[[package]] -name = "python-slugify" -version = "8.0.4" -description = "A Python slugify application that also handles Unicode" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, - {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, -] - -[package.dependencies] -text-unidecode = ">=1.3" - -[package.extras] -unidecode = ["Unidecode (>=1.1.1)"] - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "radon" -version = "6.0.1" -description = "Code Metrics in Python" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859"}, - {file = "radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5"}, -] - -[package.dependencies] -colorama = {version = ">=0.4.1", markers = "python_version > \"3.4\""} -mando = ">=0.6,<0.8" - -[package.extras] -toml = ["tomli (>=2.0.1)"] - -[[package]] -name = "regex" -version = "2024.11.6" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, - {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, - {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, - {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, - {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, - {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, - {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, - {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, - {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, - {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, - {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, - {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, - {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, - {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-mock" -version = "1.12.1" -description = "Mock out responses from the requests package" -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, - {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, -] - -[package.dependencies] -requests = ">=2.22,<3" - -[package.extras] -fixture = ["fixtures"] - -[[package]] -name = "respx" -version = "0.22.0" -description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0"}, - {file = "respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91"}, -] - -[package.dependencies] -httpx = ">=0.25.0" - -[[package]] -name = "rich" -version = "14.1.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, - {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruff" -version = "0.15.1" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a"}, - {file = "ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602"}, - {file = "ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2"}, - {file = "ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61"}, - {file = "ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f"}, - {file = "ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098"}, - {file = "ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336"}, - {file = "ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416"}, - {file = "ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f"}, -] - -[[package]] -name = "s3transfer" -version = "0.14.0" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, - {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] - -[[package]] -name = "securetar" -version = "2025.2.1" -description = "Python module to handle tarfile backups." -optional = false -python-versions = ">=3.10.0" -groups = ["main", "dev"] -files = [ - {file = "securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4"}, - {file = "securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1"}, -] - -[package.dependencies] -cryptography = "*" - -[[package]] -name = "sentence-stream" -version = "1.2.0" -description = "A small sentence splitter for text streams" -optional = false -python-versions = ">=3.9.0" -groups = ["main", "dev"] -files = [ - {file = "sentence_stream-1.2.0-py3-none-any.whl", hash = "sha256:01874a7e70efc578f891bafd3bbfa84c074fcbbfe29e1f940df969ce59e160a3"}, - {file = "sentence_stream-1.2.0.tar.gz", hash = "sha256:92c7b6aa515d1d2a44693b719c77e3144dd6bbccd405261eee7a065d01191f71"}, -] - -[package.dependencies] -regex = "2024.11.6" - -[package.extras] -dev = ["black (==24.8.0)", "build (==1.2.2)", "flake8 (==7.2.0)", "mypy (==1.14.0)", "pylint (==3.2.7)", "pytest (==8.3.5)", "pytest-asyncio (==1.1.0)", "tox (==4.26.0)"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snitun" -version = "0.45.1" -description = "SNI proxy with TCP multiplexer" -optional = false -python-versions = ">=3.12" -groups = ["main", "dev"] -files = [ - {file = "snitun-0.45.1-py3-none-any.whl", hash = "sha256:c1fa4536320ec3126926ade775c429e20664db1bc61d8fec0e181dc393d36ab4"}, - {file = "snitun-0.45.1.tar.gz", hash = "sha256:d76d48cf4190ea59e8f63892da9c18499bfc6ca796220a463c6f3b32099d661c"}, -] - -[package.dependencies] -aiohttp = ">=3.9.3" -cryptography = ">=2.5" - -[package.extras] -lint = ["ruff (==0.12.11)"] -test = ["covdefaults (==2.3.0)", "pytest (==8.4.1)", "pytest-aiohttp (==1.1.0)", "pytest-codspeed (==4.0.0)", "pytest-cov (==6.2.1)", "pytest-timeout (==2.4.0)"] - -[[package]] -name = "span-panel-api" -version = "2.3.2" -description = "A client library for SPAN Panel API" -optional = false -python-versions = ">=3.10,<4.0" -groups = ["main"] -files = [] -develop = true - -[package.dependencies] -httpx = ">=0.28.1,<0.29.0" -paho-mqtt = ">=2.0.0,<3.0.0" -pyyaml = ">=6.0.0" - -[package.source] -type = "directory" -url = "../span-panel-api" - -[[package]] -name = "sqlalchemy" -version = "2.0.41" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "SQLAlchemy-2.0.41-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-win32.whl", hash = "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2"}, - {file = "SQLAlchemy-2.0.41-cp37-cp37m-win_amd64.whl", hash = "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, - {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, - {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, - {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, - {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-win32.whl", hash = "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45"}, - {file = "sqlalchemy-2.0.41-cp38-cp38-win_amd64.whl", hash = "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440"}, - {file = "sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71"}, - {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, - {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] -aioodbc = ["aioodbc", "greenlet (>=1)"] -aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (>=1)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "standard-aifc" -version = "3.13.0" -description = "Standard library aifc redistribution. \"dead battery\"." -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"}, - {file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"}, -] - -[package.dependencies] -audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} -standard-chunk = {version = "*", markers = "python_version >= \"3.13\""} - -[[package]] -name = "standard-chunk" -version = "3.13.0" -description = "Standard library chunk redistribution. \"dead battery\"." -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"}, - {file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"}, -] - -[[package]] -name = "standard-telnetlib" -version = "3.13.0" -description = "Standard library telnetlib redistribution. \"dead battery\"." -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691"}, - {file = "standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173"}, -] - -[[package]] -name = "stevedore" -version = "5.5.0" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"}, - {file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"}, -] - -[[package]] -name = "syrupy" -version = "5.0.0" -description = "Pytest Snapshot Test Utility" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "syrupy-5.0.0-py3-none-any.whl", hash = "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546"}, - {file = "syrupy-5.0.0.tar.gz", hash = "sha256:3282fe963fa5d4d3e47231b16d1d4d0f4523705e8199eeb99a22a1bc9f5942f2"}, -] - -[package.dependencies] -pytest = ">=8.0.0" - -[[package]] -name = "termcolor" -version = "3.1.0" -description = "ANSI color formatting for output in terminal" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, - {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -optional = false -python-versions = "*" -groups = ["main", "dev"] -files = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250915" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, - {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20250913" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1"}, - {file = "types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main", "dev"] -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "uart-devices" -version = "0.1.1" -description = "UART Devices for Linux" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["main", "dev"] -files = [ - {file = "uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123"}, - {file = "uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34"}, -] - -[[package]] -name = "ulid-transform" -version = "1.5.2" -description = "Create and transform ULIDs" -optional = false -python-versions = ">=3.11" -groups = ["main", "dev"] -files = [ - {file = "ulid_transform-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:591f669a29cac276863fea87569a02932b6cc1f53f622862adaefc3f27d45fec"}, - {file = "ulid_transform-1.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7dcda634b26ed9f084c1c16b891c8026b5dac5d0cb7a3bd34837c3031d1cf4eb"}, - {file = "ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e4c07ccbe3152ff2d3c2c3981005e5942ba855cadab1f40904c5f68b9b02c9"}, - {file = "ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1de8fdf788583b9c785645be8bd7af4c4cc5c2f90121fdd1e08ee95cb83d5e99"}, - {file = "ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee54c4ff4e7e81648d9ccb09f6723e021585ac5479c64623eb80f55788ef6a64"}, - {file = "ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a24f546d4a6ac59049165c1764d5731371f829b47ad0f879b780070a360c31"}, - {file = "ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ae1e2a9c299ff98498470a23cd4a8ea7491225697565189aca5f4bba81573a9a"}, - {file = "ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8b3d1525a2e3ac785c120c3858aee2ca17d90b6903936c7ff6429e2c9ec2b090"}, - {file = "ulid_transform-1.5.2-cp311-cp311-win32.whl", hash = "sha256:a517e7e66b95dcdd3bfcfbf69875401b60586271013a1f2c0280249eb444465c"}, - {file = "ulid_transform-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:ebe99f5ad0026556a4c00400349ba6150c3e83908e1d54222720ecab88ce84a6"}, - {file = "ulid_transform-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2fbadf7fc2b72b1ab0ae176d42492e2b8f69817db32e653fe8d2817ca5b1a714"}, - {file = "ulid_transform-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5766373871bdf874e3468c1e1e9c291e628223fbc085ca10a961f26f720fe9d"}, - {file = "ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5074c362d7bcf53b4a5acaeb0416542ba3838ebabe54d60c3b6ac920aa612d6c"}, - {file = "ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e986b5079764350a586006b6bfda04f9ae87170f03efe377a3a6ec3e0ac086e6"}, - {file = "ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba5f9ed304f956e83c29642704a0f814c05c23c68cc028a27bd234acc2f04782"}, - {file = "ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2303395992e6a3d95f589173a78952a8690bf3b8bd97d248eee580ad9c1898eb"}, - {file = "ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:07cd1b250293d731e3fcf1ad3a4eb260e78f03c51676bf96d4e126c77eca48c4"}, - {file = "ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:547a824a7db7436bc68afd39ccdec0ce5f7e0b235897cc7f01ab148257db9ab9"}, - {file = "ulid_transform-1.5.2-cp312-cp312-win32.whl", hash = "sha256:8b0650b56ee6bde9de7c7f247584c2be931834c8a49b306ed905d2c8a6653eaa"}, - {file = "ulid_transform-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:301aa542de87a792cc4e82dc7678cc9e3be61e60c16bbdf24f12bc4883e9e8e8"}, - {file = "ulid_transform-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:74ba0045e2ab94be1fa6a7901f9958cef6d35cda58546cdfbadc7129ebcdc88b"}, - {file = "ulid_transform-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6354467dab6aa922cdd7e4a8a2da31222d07609616df167e656dac7244d0f658"}, - {file = "ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4faec817e9e5a031d4887c69e1254c428683c62e6f26ae9fd2f0a330a7c4c85"}, - {file = "ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40897a0189cef7cce7c0b26bcff9daafa9df2ce68249e7e6095090a6372ed6ac"}, - {file = "ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:397a2d6c2030a3c3d572dfa35c27c647911219daba93056a80091f76888f597e"}, - {file = "ulid_transform-1.5.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6794612dbc085abdac5ee3c4e3ec141ab8eb0e7b31f94f56111cacbe36137339"}, - {file = "ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497e2dbca8b53c0d072da2c662ac8779faf698ccd52827932a8dcbc5d15960d4"}, - {file = "ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a51bab261fc5a50e1eb81f022129ffc98e64b917a69b29ca8411a00464c09d47"}, - {file = "ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52f728769c822a52c05310598d949541d1e929dd1a481b2e73c971beea089a2a"}, - {file = "ulid_transform-1.5.2-cp313-cp313-win32.whl", hash = "sha256:0b90b0b7ed937f8cf245f8c71adbd73cf93fcc4915cac9150255595a016b53d0"}, - {file = "ulid_transform-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:3c1354706ac87ecf3b941c836c04b96f83e5d50ba7b2c3e2f746da838405bc09"}, - {file = "ulid_transform-1.5.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bae1b0f6041bd8c7d49019c14fb94e42ccab2b59083e7b0cb9f4d13483d7435a"}, - {file = "ulid_transform-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c4a61fd13ced6b0b96f5983ef4e57ad8adefed4361b6d0f55a2bbfbb18b17d8"}, - {file = "ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8065ddfd43827b1299a64da4437161a4f3fa1f03c05d838a3a8c82bc1d014518"}, - {file = "ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b60965067d13942b0eaaaf986da5eff4ba8f1261b379ca1dac78afe47d940f1a"}, - {file = "ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e97a11b56b9e5537ef4521a97fc095b49d849c0ac0ec8d30a2974bd69e5948d"}, - {file = "ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4915062eee740eefa937459ef468f7f1e35bd2ad5bffdf4245051d656df2c4"}, - {file = "ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7d4cf4bb26fe102dfd1bd10c5b18712fe7640433839c8d9dd20e2d8ccefa972d"}, - {file = "ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad4953675e6dec400de633087f61cbb38d0ad978d57b60cc3539f7b821d9559"}, - {file = "ulid_transform-1.5.2-cp314-cp314-win32.whl", hash = "sha256:d6793d4c477b30d95ed84123cc73d515ba4dac58cd01e7584637421b377349d3"}, - {file = "ulid_transform-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:dbbe98fd8b46431e3a15268e0dceeb80291ebfa7741d1ee692006928c0900d0c"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:08286ccc6bac0107e1bd5415a28e730d88089293ba5ce51dc5883175eccc31e2"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed0b533c936cb120312cd98ca1c8ec1f8af66bac6bc08426c030b48291d5505e"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58617bae6fc21507f5151328faf7b77c6ba6a615b42efd18f494564354a3ce68"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e24e68971a64a04af2d0b3df98bfe0087c85d35a1b02fa0bbf43a3a0a99dccf6"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb5da66ec5e7d97f695dd16637d5a8816bb9661df43ff1f2de0d46071d96a7a8"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:397646cf156aa46456cd8504075d117d2983ebf2cff01955c3add6280d0fb3c8"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:dc5ac2ffa704a21df2a36cea47ac1022fb6f8ab03abe31a5f7b81312f972e2c2"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6f29b8004fba0da7061b5eecf6101c6283377b6cd04f3626089cc67d9342c8fd"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-win32.whl", hash = "sha256:c09f58aff7a4974f560dd5fb19dd5144e8964371fcb1971bffa817c9abcb2232"}, - {file = "ulid_transform-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:445e14170301229a486e8815c2f9cec4a10b3e3cd4c9aa509689443d05e4f020"}, - {file = "ulid_transform-1.5.2.tar.gz", hash = "sha256:9a5caf279ec21789ddc2f36b9008ce33a3197d7d66fdd7628fbebec9ba778829"}, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "usb-devices" -version = "0.4.5" -description = "Tools for mapping, describing, and resetting USB devices" -optional = false -python-versions = ">=3.9,<4.0" -groups = ["main", "dev"] -files = [ - {file = "usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf"}, - {file = "usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d"}, -] - -[[package]] -name = "uv" -version = "0.9.26" -description = "An extremely fast Python package and project manager, written in Rust." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "uv-0.9.26-py3-none-linux_armv6l.whl", hash = "sha256:7dba609e32b7bd13ef81788d580970c6ff3a8874d942755b442cffa8f25dba57"}, - {file = "uv-0.9.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b815e3b26eeed00e00f831343daba7a9d99c1506883c189453bb4d215f54faac"}, - {file = "uv-0.9.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b012e6c4dfe767f818cbb6f47d02c207c9b0c82fee69a5de6d26ffb26a3ef3c"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ea296b700d7c4c27acdfd23ffaef2b0ecdd0aa1b58d942c62ee87df3b30f06ac"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1ba860d2988efc27e9c19f8537a2f9fa499a8b7ebe4afbe2d3d323d72f9aee61"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8610bdfc282a681a0a40b90495a478599aa3484c12503ef79ef42cd271fd80fe"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4bf700bd071bd595084b9ee0a8d77c6a0a10ca3773d3771346a2599f306bd9c"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:89a7beea1c692f76a6f8da13beff3cbb43f7123609e48e03517cc0db5c5de87c"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:182f5c086c7d03ad447e522b70fa29a0302a70bcfefad4b8cd08496828a0e179"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d8c62a501f13425b4b0ce1dd4c6b82f3ce5a5179e2549c55f4bb27cc0eb8ef8"}, - {file = "uv-0.9.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e89798bd3df7dcc4b2b4ac4e2fc11d6b3ff4fe7d764aa3012d664c635e2922"}, - {file = "uv-0.9.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:60a66f1783ec4efc87b7e1f9bd66e8fd2de3e3b30d122b31cb1487f63a3ea8b7"}, - {file = "uv-0.9.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63c6a1f1187facba1fb45a2fa45396980631a3427ac11b0e3d9aa3ebcf2c73cf"}, - {file = "uv-0.9.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c6d8650fbc980ccb348b168266143a9bd4deebc86437537caaf8ff2a39b6ea50"}, - {file = "uv-0.9.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:25278f9298aa4dade38241a93d036739b0c87278dcfad1ec1f57e803536bfc49"}, - {file = "uv-0.9.26-py3-none-win32.whl", hash = "sha256:10d075e0193e3a0e6c54f830731c4cb965d6f4e11956e84a7bed7ed61d42aa27"}, - {file = "uv-0.9.26-py3-none-win_amd64.whl", hash = "sha256:0315fc321f5644b12118f9928086513363ed9b29d74d99f1539fda1b6b5478ab"}, - {file = "uv-0.9.26-py3-none-win_arm64.whl", hash = "sha256:344ff38749b6cd7b7dfdfb382536f168cafe917ae3a5aa78b7a63746ba2a905b"}, - {file = "uv-0.9.26.tar.gz", hash = "sha256:8b7017a01cc48847a7ae26733383a2456dd060fc50d21d58de5ee14f6b6984d7"}, -] - -[[package]] -name = "virtualenv" -version = "20.34.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, - {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - -[[package]] -name = "voluptuous" -version = "0.15.2" -description = "Python data validation library" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566"}, - {file = "voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa"}, -] - -[[package]] -name = "voluptuous-openapi" -version = "0.2.0" -description = "Convert voluptuous schemas to OpenAPI Schema object" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "voluptuous_openapi-0.2.0-py3-none-any.whl", hash = "sha256:d51f07be8af44b11570b7366785d90daa716b7fd11ea2845803763ae551f35cf"}, - {file = "voluptuous_openapi-0.2.0.tar.gz", hash = "sha256:2366be934c37bb5fd8ed6bd5a2a46b1079b57dfbdf8c6c02e88f4ca13e975073"}, -] - -[package.dependencies] -voluptuous = "*" - -[[package]] -name = "voluptuous-serialize" -version = "2.7.0" -description = "Convert voluptuous schemas to dictionaries" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "voluptuous_serialize-2.7.0-py3-none-any.whl", hash = "sha256:ee3ebecace6136f38d0bf8c20ee97155db2486c6b2d0795563fafd04a519e76f"}, - {file = "voluptuous_serialize-2.7.0.tar.gz", hash = "sha256:d0da959f2fd93c8f1eb779c5d116231940493b51020c2c1026bab76eb56cd09e"}, -] - -[package.dependencies] -voluptuous = "*" - -[[package]] -name = "voluptuous-stubs" -version = "0.1.1" -description = "voluptuous stubs" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "voluptuous-stubs-0.1.1.tar.gz", hash = "sha256:70fb1c088242f20e11023252b5648cd77f831f692cd910c8f9713cc135cf8cc8"}, - {file = "voluptuous_stubs-0.1.1-py3-none-any.whl", hash = "sha256:f216c427ed7e190b8413e26cf4f67e1bda692ea8225ed0d875f7724d10b7cb10"}, -] - -[package.dependencies] -mypy = ">=0.720" -typing-extensions = ">=3.7.4" - -[[package]] -name = "webrtc-models" -version = "0.3.0" -description = "Python WebRTC models" -optional = false -python-versions = ">=3.12.0" -groups = ["main", "dev"] -files = [ - {file = "webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea"}, - {file = "webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526"}, -] - -[package.dependencies] -mashumaro = ">=3.13,<4.0" -orjson = ">=3.10.7" - -[[package]] -name = "winrt-runtime" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_runtime-3.2.1-cp310-cp310-win32.whl", hash = "sha256:25a2d1e2b45423742319f7e10fa8ca2e7063f01284b6e85e99d805c4b50bbfb3"}, - {file = "winrt_runtime-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:dc81d5fb736bf1ddecf743928622253dce4d0aac9a57faad776d7a3834e13257"}, - {file = "winrt_runtime-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:363f584b1e9fcb601e3e178636d8877e6f0537ac3c96ce4a96f06066f8ff0eae"}, - {file = "winrt_runtime-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9e9b64f1ba631cc4b9fe60b8ff16fef3f32c7ce2fcc84735a63129ff8b15c022"}, - {file = "winrt_runtime-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0a9046ae416808420a358c51705af8ae100acd40bc578be57ddfdd51cbb0f9c"}, - {file = "winrt_runtime-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:e94f3cb40ea2d723c44c82c16d715c03c6b3bd977d135b49535fdd5415fd9130"}, - {file = "winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1"}, - {file = "winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576"}, - {file = "winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195"}, - {file = "winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1"}, - {file = "winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d"}, - {file = "winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159"}, - {file = "winrt_runtime-3.2.1-cp39-cp39-win32.whl", hash = "sha256:07c0cb4a53a4448c2cb7597b62ae8c94343c289eeebd8f83f946eb2c817bde01"}, - {file = "winrt_runtime-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1856325ca3354b45e0789cf279be9a882134085d34214946db76110d98391efa"}, - {file = "winrt_runtime-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:cf237858de1d62e4c9b132c66b52028a7a3e8534e8ab90b0e29a68f24f7be39d"}, - {file = "winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea"}, -] - -[package.dependencies] -typing_extensions = ">=4.12.2" - -[[package]] -name = "winrt-windows-devices-bluetooth" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win32.whl", hash = "sha256:49489351037094a088a08fbdf0f99c94e3299b574edb211f717c4c727770af78"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:20f6a21029034c18ea6a6b6df399671813b071102a0d6d8355bb78cf4f547cdb"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:69c523814eab795bc1bf913292309cb1025ef0a67d5fc33863a98788995e551d"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win32.whl", hash = "sha256:f4082a00b834c1e34b961e0612f3e581356bdb38c5798bd6842f88ec02e5152b"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:44277a3f2cc5ac32ce9b4b2d96c5c5f601d394ac5f02cc71bcd551f738660e2d"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:0803a417403a7d225316b9b0c4fe3f8446579d6a22f2f729a2c21f4befc74a80"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win32.whl", hash = "sha256:32fc355bfdc5d6b3b1875df16eaf12f9b9fc0445e01177833c27d9a4fc0d50b6"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b886ef1fc0ed49163ae6c2422dd5cb8dd4709da7972af26c8627e211872818d0"}, - {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:8643afa53f9fb8fe3b05967227f86f0c8e1d7b822289e60a848c6368acc977d2"}, - {file = "winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Enumeration[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Radios[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Networking[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-devices-bluetooth-advertisement" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win32.whl", hash = "sha256:a758c5f81a98cc38347fdfb024ce62720969480e8c5b98e402b89d2b09b32866"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:f982ef72e729ddd60cdb975293866e84bb838798828933012a57ee4bf12b0ea1"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:e88a72e1e09c7ccc899a9e6d2ab3fc0f43b5dd4509bcc49ec4abf65b55ab015f"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win32.whl", hash = "sha256:fe17c2cf63284646622e8b2742b064bf7970bbf53cfab02062136c67fa6b06c9"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:78e99dd48b4d89b71b7778c5085fdba64e754dd3ebc54fd09c200fe5222c6e09"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6d5d2295474deab444fc4311580c725a2ca8a814b0f3344d0779828891d75401"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win32.whl", hash = "sha256:6c4747d2e5b0e2ef24e9b84a848cf8fc50fb5b268a2086b5ee8680206d1e0197"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:18d4c5d8b80ee2d29cc13c2fc1353fdb3c0f620c8083701c9b9ecf5e6c503c8d"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:75dd856611d847299078d56aee60e319df52975b931c992cd1d32ad5143fe772"}, - {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Devices.Bluetooth[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-devices-bluetooth-genericattributeprofile" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win32.whl", hash = "sha256:af4914d7b30b49232092cd3b934e3ed6f5d3b1715ba47238541408ee595b7f46"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0e557dd52fc80392b8bd7c237e1153a50a164b3983838b4ac674551072efc9ed"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:64cff62baa6b7aadd6c206e61d149113fdcda17360feb6e9d05bc8bbda4b9fde"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win32.whl", hash = "sha256:832cf65d035a11e6dbfef4fd66abdcc46be7e911ec96e2e72e98e12d8d5b9d3c"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8179638a6c721b0bbf04ba251ef98d5e02d9a17f0cce377398e42c4fbb441415"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:70b7edfca3190b89ae38bf60972b11978311b6d933d3142ae45560c955dbf5c7"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win32.whl", hash = "sha256:963339a0161f9970b577a6193924be783978d11693da48b41a025f61b3c5562a"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d43615c5dfa939dd30fe80dc0649434a13cc7cf0294ad0d7283d5a9f48c6ce86"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:8e70fa970997e2e67a8a4172bc00b0b2a79b5ff5bb2668f79cf10b3fd63d3974"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Devices.Bluetooth[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Enumeration[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-devices-enumeration" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win32.whl", hash = "sha256:40dac777d8f45b41449f3ff1ae70f0d457f1ede53f53962a6e2521b651533db5"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:a101ec3e0ad0a0783032fdcd5dc48e7cd68ee034cbde4f903a8c7b391532c71a"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:3296a3863ac086928ff3f3dc872b2a2fb971dab728817424264f3ca547504e9e"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9f29465a6c6b0456e4330d4ad09eccdd53a17e1e97695c2e57db0d4666cc0011"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2a725d04b4cb43aa0e2af035f73a60d16a6c0ff165fcb6b763383e4e33a975fd"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6365ef5978d4add26678827286034acf474b6b133aa4054e76567d12194e6817"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win32.whl", hash = "sha256:986e8d651b769a0e60d2834834bdd3f6959f6a88caa0c9acb917797e6b43a588"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10da7d403ac4afd385fe13bd5808c9a5dd616a8ef31ca5c64cea3f87673661c1"}, - {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:679e471d21ac22cb50de1bf4dfc4c0c3f5da9f3e3fbc7f08dcacfe9de9d6dd58"}, - {file = "winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.ApplicationModel.Background[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Security.Credentials[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.UI.Popups[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.UI[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-foundation" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win32.whl", hash = "sha256:677e98165dcbbf7a2367f905bc61090ef2c568b6e465f87cf7276df4734f3b0b"}, - {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8f27b4f0fdb73ccc4a3e24bc8010a6607b2bdd722fa799eafce7daa87d19d39"}, - {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:d900c6165fab4ea589811efa2feed27b532e1b6f505f63bf63e2052b8cb6bdc4"}, - {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win32.whl", hash = "sha256:d1b5970241ccd61428f7330d099be75f4f52f25e510d82c84dbbdaadd625e437"}, - {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:f3762be2f6e0f2aedf83a0742fd727290b397ffe3463d963d29211e4ebb53a7e"}, - {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:806c77818217b3476e6c617293b3d5b0ff8a9901549dc3417586f6799938d671"}, - {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b"}, - {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9"}, - {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37"}, - {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46"}, - {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479"}, - {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4"}, - {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win32.whl", hash = "sha256:14d5191725301498e4feb744d91f5b46ce317bf3d28370efda407d5c87f4423b"}, - {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:de5e4f61d253a91ba05019dbf4338c43f962bdad935721ced5e7997933994af5"}, - {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:ebbf6e8168398c9ed0c72c8bdde95a406b9fbb9a23e3705d4f0fe28e5a209705"}, - {file = "winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-foundation-collections" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win32.whl", hash = "sha256:46948484addfc4db981dab35688d4457533ceb54d4954922af41503fddaa8389"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:899eaa3a93c35bfb1857d649e8dd60c38b978dda7cedd9725fcdbcebba156fd6"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:c36eb49ad1eba1b32134df768bb47af13cabb9b59f974a3cea37843e2d80e0e6"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9b272d9936e7db4840881c5dcf921eb26789ae4ef23fb6ec15e13e19a16254e7"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c646a5d442dd6540ade50890081ca118b41f073356e19032d0a5d7d0d38fbc89"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:2c4630027c93cdd518b0cf4cc726b8fbdbc3388e36d02aa1de190a0fc18ca523"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win32.whl", hash = "sha256:20610f098b84c87765018cbc71471092197881f3b92e5d06158fad3bfcea2563"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9739775320ac4c0238e1775d94a54e886d621f9995977e65d4feb8b3778c111"}, - {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:e4c6bddb1359d5014ceb45fe2ecd838d4afeb1184f2ea202c2d21037af0d08a3"}, - {file = "winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "winrt-windows-storage-streams" -version = "3.2.1" -description = "Python projection of Windows Runtime (WinRT) APIs" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win32.whl", hash = "sha256:89bb2d667ebed6861af36ed2710757456e12921ee56347946540320dacf6c003"}, - {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:48a78e5dc7d3488eb77e449c278bc6d6ac28abcdda7df298462c4112d7635d00"}, - {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:da71231d4a554f9f15f1249b4990c6431176f6dfb0e3385c7caa7896f4ca24d6"}, - {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win32.whl", hash = "sha256:7dace2f9e364422255d0e2f335f741bfe7abb1f4d4f6003622b2450b87c91e69"}, - {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:b02fa251a7eef6081eca1a5f64ecf349cfd1ac0ac0c5a5a30be52897d060bed5"}, - {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:efdf250140340a75647e8e8ad002782d91308e9fdd1e19470a5b9cc969ae4780"}, - {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78"}, - {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc"}, - {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307"}, - {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163"}, - {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915"}, - {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d"}, - {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win32.whl", hash = "sha256:1c630cfdece58fcf82e4ed86c826326123529836d6d4d855ae8e9ceeff67b627"}, - {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7ff22434a4829d616a04b068a191ac79e008f6c27541bb178c1f6f1fe7a1657"}, - {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:fa90244191108f85f6f7afb43a11d365aca4e0722fe8adc62fb4d2c678d0993d"}, - {file = "winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f"}, -] - -[package.dependencies] -winrt-runtime = ">=3.2.1.0,<3.3.0.0" - -[package.extras] -all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.System[all] (>=3.2.1.0,<3.3.0.0)"] - -[[package]] -name = "yarl" -version = "1.22.0" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, - {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, - {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, - {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, - {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, - {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, - {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, - {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, - {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, - {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, - {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, - {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, - {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, - {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, - {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, - {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, - {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, - {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, - {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, - {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, - {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, - {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, - {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, - {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, - {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, - {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, - {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[[package]] -name = "zeroconf" -version = "0.148.0" -description = "A pure python implementation of multicast DNS service discovery" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "zeroconf-0.148.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9146731bb82bc7b42f009aa69619b17a4b6ddecc75eee9a59249c12c804d0637"}, - {file = "zeroconf-0.148.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db24dc2e5367dc61bacbf302b7c85cc10ee1a9de8f1710380027992afd1ddcb4"}, - {file = "zeroconf-0.148.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2202ac7dc2777249561292c9151919d70fbe25a31983b7e127b43878ea67483c"}, - {file = "zeroconf-0.148.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:876e9e61a7065d201d39c466449e01fa9e19c3c7b2c5ee57bc628f15e21653fb"}, - {file = "zeroconf-0.148.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:171ff9d59283737946d79c6a290a597a3d10d0d24d6a3a87de67ce3064157afc"}, - {file = "zeroconf-0.148.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:76d53985fa40cefb3a82c1d5d761217392bbc811964715e1bf73e74084012062"}, - {file = "zeroconf-0.148.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:429e8ed8428737f2586992aaf11a21302184cd4e1c641fbd7abe8946d9ff7089"}, - {file = "zeroconf-0.148.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa0cdcb91f231789d8f6ba7ed702d05a36975e7b06fd663aff25205ddca2b659"}, - {file = "zeroconf-0.148.0-cp310-cp310-win32.whl", hash = "sha256:3c1ec76c031712c6289cc94acee43e7bf7a6cb52b45675278348926eacffc668"}, - {file = "zeroconf-0.148.0-cp310-cp310-win_amd64.whl", hash = "sha256:144fa2e0246292ea9c62792327d230f1b996c65cec16024b10689ee597b05ab2"}, - {file = "zeroconf-0.148.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b923e26369e302863aa5370eff4d4d72a0b90ba85d3b9f608c62cbab78f14dc2"}, - {file = "zeroconf-0.148.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbffd751877b74cd64c529061e5a524ebfa59af16930330548033e307701fee"}, - {file = "zeroconf-0.148.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34275f60a5ab01d2d0a190662d16603b75f9225cee4ab58d388ff86d8011352a"}, - {file = "zeroconf-0.148.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:556ff0b9dfc0189f33c6e6110aa23d9f7564a7475f4cdc624a0584c1133ae44b"}, - {file = "zeroconf-0.148.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8aa15461e35169b4ec25cc45ec82750023e6c2e96ebc099a014caaf544316f7"}, - {file = "zeroconf-0.148.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:25b8b72177bbe0792f5873c16330d759811540edb24ed9ead305874183eaefd5"}, - {file = "zeroconf-0.148.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:10ce75cdb524f955773114697633d73644aad6c35daef5315fa478bff9bee24d"}, - {file = "zeroconf-0.148.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49512e6d59be66be769f36e1f7f025a2841783da1be708d4b4a92a7b63135b68"}, - {file = "zeroconf-0.148.0-cp311-cp311-win32.whl", hash = "sha256:8ff905f8ff9083a853eb4e65eb31b09fa9d7a6633de92ac1e2018819eee52d30"}, - {file = "zeroconf-0.148.0-cp311-cp311-win_amd64.whl", hash = "sha256:7339a485403c75aa4f3c38ddcb68eb14f01fd5e1dc1ef75b068b185e703ea7ea"}, - {file = "zeroconf-0.148.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef8699ea47cd47c9219e3f110a35ad50c13c34c7c6db992f3c9f75feec6ef8f"}, - {file = "zeroconf-0.148.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9097e7010b9f9a64e5f2084493e9973d446bd85c7a7cbef5032b2b0a2ecc5a12"}, - {file = "zeroconf-0.148.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdc566c387260fb7bf89f91d00460d0c9b9373dfddcf1fcc980ab3f7270154f9"}, - {file = "zeroconf-0.148.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10cbd4134cacc22c3b3b169d7f782472a1dd36895e1421afa4f681caf181c07b"}, - {file = "zeroconf-0.148.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde01541e6a45c4d1b6e6d97b532ea241abc32c183745a74021b134d867388d8"}, - {file = "zeroconf-0.148.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8ceab8f10ab6fc0847a2de74377663793a974fdba77e7e6ba1ff47679f4bb845"}, - {file = "zeroconf-0.148.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0a8c36c37d8835420fc337be4aaa03c3a34272028919de575124c10d31a7e304"}, - {file = "zeroconf-0.148.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:848d57df1bb3b48279ba9b66e6c1f727570e2c8e7e0c4518c2daffaf23419d03"}, - {file = "zeroconf-0.148.0-cp312-cp312-win32.whl", hash = "sha256:ba6eaa6b769924391c213dc391f36bd1c7e3ebe45fa3fa0cd97451b4f9ccef5c"}, - {file = "zeroconf-0.148.0-cp312-cp312-win_amd64.whl", hash = "sha256:cec84ae7028db4a3addcc18628d12456cf39a9e973abee4a41e3b94d0db7df4c"}, - {file = "zeroconf-0.148.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ad889929bdc3953530546a4a2486d8c07f5a18d4ef494a98446bf17414897a7"}, - {file = "zeroconf-0.148.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29fb10be743650eb40863f1a1ee868df1869357a0c2ab75140ee3d7079540c1e"}, - {file = "zeroconf-0.148.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2995e74969c577461060539164c47e1ba674470585cb0f954ebeb77f032f3c2"}, - {file = "zeroconf-0.148.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5be50346efdc20823f9d68d8757612767d11ceb8da7637d46080977b87912551"}, - {file = "zeroconf-0.148.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc88fd01b5552ffb4d5bc551d027ac28a1852c03ceab754d02bd0d5f04c54e85"}, - {file = "zeroconf-0.148.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5af260c74187751c0df6a40f38d6fd17cb8658a734b0e1148a86084b71c1977c"}, - {file = "zeroconf-0.148.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6078c73a76d49ba969ca2bb7067e4d58ebd2b79a5f956e45c4c989b11d36e03"}, - {file = "zeroconf-0.148.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3e686bf741158f4253d5e0aa6a8f9d34b3140bf5826c0aca9b906273b9c77a5f"}, - {file = "zeroconf-0.148.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52d6ac06efe05a1e46089cfde066985782824f64b64c6982e8678e70b4b49453"}, - {file = "zeroconf-0.148.0-cp313-cp313-win32.whl", hash = "sha256:b9ba58e2bbb0cff020b54330916eaeb8ee8f4b0dde852e84f670f4ca3a0dd059"}, - {file = "zeroconf-0.148.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee3fcc2edcc04635cf673c400abac2f0c22c9786490fbfb971e0a860a872bf26"}, - {file = "zeroconf-0.148.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:2158d8bfefcdb90237937df65b2235870ccef04644497e4e29d3ab5a4b3199b6"}, - {file = "zeroconf-0.148.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:695f6663bf8df30fe1826a2c4d5acd8213d9cbd9111f59d375bf1ad635790e98"}, - {file = "zeroconf-0.148.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa65a24ec055be0a1cba2b986ac3e1c5d97a40abe164991aabc6a6416cc9df02"}, - {file = "zeroconf-0.148.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79890df4ff696a5cdc4a59152957be568bea1423ed13632fc09e2a196c6721d5"}, - {file = "zeroconf-0.148.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c0ca6e8e063eb5a385469bb8d8dec12381368031cb3a82c446225511863ede3"}, - {file = "zeroconf-0.148.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ece6f030cc7a771199760963c11ce4e77ed95011eedffb1ca5186247abfec24a"}, - {file = "zeroconf-0.148.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c3f860ad0003a8999736fa2ae4c2051dd3c2e5df1bc1eaea2f872f5fcbd1f1c1"}, - {file = "zeroconf-0.148.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab8e687255cf54ebeae7ede6a8be0566aec752c570e16dbea84b3f9b149ba829"}, - {file = "zeroconf-0.148.0-cp314-cp314-win32.whl", hash = "sha256:6b1a6ddba3328d741798c895cecff21481863eb945c3e5d30a679461f4435684"}, - {file = "zeroconf-0.148.0-cp314-cp314-win_amd64.whl", hash = "sha256:2588f1ca889f57cdc09b3da0e51175f1b6153ce0f060bf5eb2a8804c5953b135"}, - {file = "zeroconf-0.148.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:40fe100381365c983a89e4b219a7ececcc2a789ac179cd26d4a6bbe00ae3e8fe"}, - {file = "zeroconf-0.148.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b9c7bcae8af8e27593bad76ee0f0c21d43c6a2324cd1e34d06e6e08cb3fd922"}, - {file = "zeroconf-0.148.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8ba75dacd58558769afb5da24d83da4fdc2a5c43a52f619aaa107fa55d3fdc"}, - {file = "zeroconf-0.148.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75f9a8212c541a4447c064433862fd4b23d75d47413912a28204d2f9c4929a59"}, - {file = "zeroconf-0.148.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be64c0eb48efa1972c13f7f17a7ac0ed7932ebb9672e57f55b17536412146206"}, - {file = "zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac1d4ee1d5bac71c27aea6d1dc1e1485423a1631a81be1ea65fb45ac280ade96"}, - {file = "zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8da9bdb39ead9d5971136046146cd5e11413cb979c011e19f717b098788b5c37"}, - {file = "zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6e3dd22732df47a126aefb5ca4b267e828b47098a945d4468d38c72843dd6df"}, - {file = "zeroconf-0.148.0-cp314-cp314t-win32.whl", hash = "sha256:cdc8083f0b5efa908ab6c8e41687bcb75fd3d23f49ee0f34cbc58422437a456f"}, - {file = "zeroconf-0.148.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f72c1f77a89638e87f243a63979f0fd921ce391f83e18e17ec88f9f453717701"}, - {file = "zeroconf-0.148.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a6e61c5b3905efed2137a07d84953ba4419795646fd18eccbd17018da2e965d"}, - {file = "zeroconf-0.148.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae41805df91ff657dd70179089df1d03e7ab756feb13dbcbc8a412cd8c50623e"}, - {file = "zeroconf-0.148.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff53a8d01c3b9a1e50606446ed07d534db5def55046ffdbbacac7888d9c699ae"}, - {file = "zeroconf-0.148.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:45a51e1f507dfc3f621ecc23168aaa56783b33d4f5d676088f69f913f0b56073"}, - {file = "zeroconf-0.148.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b41d1004e0356720ac81cddd7e4bd622c73be951b92c6b89ccaf6429996563ac"}, - {file = "zeroconf-0.148.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a53293291d683fc690c1cee0352f2c6dfc0f717f643e676a3c6f0df37a7f1b17"}, - {file = "zeroconf-0.148.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:04607192ef33f4c9280bbd1b716564f821a7935661b8a35be34ee1e0acc0657d"}, - {file = "zeroconf-0.148.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2545561551a9ba684897785e6678f3c67fee8e13b09f9f88d0f69e570b540d5b"}, - {file = "zeroconf-0.148.0-cp39-cp39-win32.whl", hash = "sha256:d78e200a3830074c79c0a014595ace49a24afa6a8a2d903326f44751107afbfd"}, - {file = "zeroconf-0.148.0-cp39-cp39-win_amd64.whl", hash = "sha256:0800443953f9b490ded275a84008631f441879e9164635a62a4f1c6e71f28bd0"}, - {file = "zeroconf-0.148.0.tar.gz", hash = "sha256:03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f681702"}, -] - -[package.dependencies] -ifaddr = ">=0.1.7" - -[metadata] -lock-version = "2.1" -python-versions = ">=3.14.2,<3.15" -content-hash = "2db133614283a9dad55d9bcf7a8425f77ed42f2f22bfc80db87cf8b490e88243" diff --git a/prek.toml b/prek.toml index d0618b19..85cf1071 100644 --- a/prek.toml +++ b/prek.toml @@ -22,7 +22,7 @@ hooks = [ { id = "pylint-import-check", name = "pylint import-outside-toplevel check", - entry = "poetry run pylint", + entry = "uv run pylint", language = "system", args = ["--disable=all", "--enable=import-outside-toplevel", "--score=no", "custom_components/span_panel/"], pass_filenames = false, @@ -103,15 +103,6 @@ hooks = [ }, ] -# Poetry check for pyproject.toml validation -[[repos]] -repo = "https://github.com/python-poetry/poetry" -rev = "2.1.3" -hooks = [ - { id = "poetry-check" }, - { id = "poetry-lock" }, -] - # Radon for code metrics and maintainability (local) [[repos]] repo = "local" @@ -119,7 +110,7 @@ hooks = [ { id = "radon-complexity", name = "radon complexity check", - entry = "poetry run radon", + entry = "uv run radon", language = "system", args = ["cc", "--min=B", "--show-complexity", "custom_components/span_panel/"], pass_filenames = false, @@ -128,7 +119,7 @@ hooks = [ { id = "radon-maintainability", name = "radon maintainability index", - entry = "poetry run radon", + entry = "uv run radon", language = "system", args = ["mi", "--min=B", "--show", "custom_components/span_panel/"], pass_filenames = false, @@ -145,7 +136,7 @@ hooks = [ name = "coverage summary", entry = "bash", language = "system", - args = ["-c", 'echo "Running tests with coverage..."; poetry run pytest tests/ --cov=custom_components/span_panel --cov-config=pyproject.toml --cov-report=term-missing:skip-covered -v; exit_code=$?; echo; if [ $exit_code -eq 0 ]; then echo "Tests passed with coverage report above"; else echo "Tests failed"; fi; exit $exit_code'], + args = ["-c", 'echo "Running tests with coverage..."; uv run pytest tests/ --cov=custom_components/span_panel --cov-config=pyproject.toml --cov-report=term-missing:skip-covered -v; exit_code=$?; echo; if [ $exit_code -eq 0 ]; then echo "Tests passed with coverage report above"; else echo "Tests failed"; fi; exit $exit_code'], pass_filenames = false, always_run = true, verbose = true, diff --git a/pyproject.toml b/pyproject.toml index d230bd5e..f3854c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,41 +1,41 @@ -[tool.poetry] +[project] name = "span" -# integration version is managed in the manifest.json for HA -# version = "0.0.0" +version = "0.0.0" description = "Span Panel Custom Integration for Home Assistant" -authors = ["SpanPanel"] -license = "MIT" +authors = [{name = "SpanPanel"}] +license = {text = "MIT"} readme = "README.md" -package-mode = false - -[tool.poetry.dependencies] -python = ">=3.14.2,<3.15" -homeassistant = "2026.2.2" # Pin to exact version for custom component compatibility -span-panel-api = {path = "../span-panel-api", develop = true} - -[tool.poetry.group.dev.dependencies] -# Type stubs and dev-only tools that don't conflict with HA runtime -homeassistant-stubs = "2026.2.2" -types-requests = "*" -types-PyYAML = "*" -mypy = "==1.19.1" -pyright = "==1.1.405" -ruff = "==0.15.1" -bandit = "==1.8.6" -prek = ">=0.3.6" -voluptuous-stubs = "*" -python-direnv = "*" -prettier = "*" -radon = "==6.0.1" -pylint = "==4.0.5" # For import-outside-toplevel checks -# Testing dependencies -pytest = "^9.0.0" # Compatible with Python 3.14 -pytest-homeassistant-custom-component = "^0.13.315" # Latest version compatible with HA 2026.2.x -isort = "*" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires-python = ">=3.14.2,<3.15" +dependencies = [ + "homeassistant==2026.2.2", + "span-panel-api==2.3.2", +] + +[dependency-groups] +dev = [ + "homeassistant-stubs==2026.2.2", + "types-requests", + "types-PyYAML", + "mypy==1.19.1", + "pyright==1.1.405", + "ruff==0.15.1", + "bandit[toml]==1.8.6", + "prek>=0.3.6", + "voluptuous-stubs", + "python-direnv", + "prettier", + "radon==6.0.1", + "pylint==4.0.5", + "pytest>=9.0.0", + "pytest-homeassistant-custom-component>=0.13.315", + "isort", +] + +[tool.uv] +package = false + +[tool.uv.sources] +span-panel-api = { path = "../span-panel-api", editable = true } [tool.jscpd] path = ["custom_components/span_panel", "./*.{html,md}"] diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh index f068f8cc..c326201a 100755 --- a/scripts/run-in-env.sh +++ b/scripts/run-in-env.sh @@ -1,7 +1,7 @@ #!/bin/bash # Ensures the script runs in the correct Python environment -# Handles pyenv/virtualenv/poetry activation if needed +# Handles pyenv/virtualenv/uv activation if needed # Find the project root directory (where .git is) PROJECT_ROOT=$(git rev-parse --show-toplevel) @@ -19,7 +19,6 @@ VENV_PATHS=( "venv" ".env" "env" - "$(poetry env info --path 2>/dev/null)" # Try to get Poetry's venv path ) for venv_path in "${VENV_PATHS[@]}"; do @@ -31,12 +30,11 @@ for venv_path in "${VENV_PATHS[@]}"; do fi done -# If poetry is available, ensure dependencies -if command -v poetry &> /dev/null && [ -f "pyproject.toml" ]; then - # Check if pylint is missing +# If uv is available, ensure dependencies +if command -v uv &> /dev/null && [ -f "pyproject.toml" ]; then if ! command -v pylint &> /dev/null; then - echo "pylint not found, installing dependencies with poetry..." - poetry install --only dev + echo "pylint not found, installing dependencies with uv..." + uv sync fi fi diff --git a/scripts/run_mypy.py b/scripts/run_mypy.py index 500fdc75..96efecba 100644 --- a/scripts/run_mypy.py +++ b/scripts/run_mypy.py @@ -9,7 +9,7 @@ def main() -> None: """Run mypy with Home Assistant core path configuration.""" # Run mypy with the provided arguments - result = subprocess.check_call(["poetry", "run", "mypy"] + sys.argv[1:]) # nosec B603 + result = subprocess.check_call(["uv", "run", "mypy"] + sys.argv[1:]) # nosec B603 sys.exit(result) diff --git a/scripts/sync-dependencies.py b/scripts/sync-dependencies.py index e84424f1..f318e568 100755 --- a/scripts/sync-dependencies.py +++ b/scripts/sync-dependencies.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Synchronize dependency versions from manifest.json to CI workflow. +"""Synchronize dependency versions from manifest.json to pyproject.toml. This script reads the dependency versions from custom_components/span_panel/manifest.json -and updates the corresponding sed commands in .github/workflows/ci.yml to match. +and updates the corresponding dependencies in pyproject.toml to match. -Used as a pre-commit hook to ensure CI workflow stays in sync with manifest versions. +Used as a pre-commit hook to ensure pyproject.toml stays in sync with manifest versions. """ import json @@ -45,40 +45,39 @@ def get_manifest_versions(): return None -def update_ci_workflow(versions): - """Update the CI workflow with the specified versions.""" - ci_path = Path(".github/workflows/ci.yml") +def update_pyproject_dependencies(versions): + """Update pyproject.toml dependencies with manifest versions.""" + pyproject_path = Path("pyproject.toml") - if not ci_path.exists(): + if not pyproject_path.exists(): return False try: - with open(ci_path) as f: + with open(pyproject_path) as f: content = f.read() original_content = content - # Update span-panel-api version — preserve exact specifier from manifest + # Update span-panel-api version in [project] dependencies if "span-panel-api" in versions: span_spec = versions["span-panel-api"] content = re.sub( - r'span-panel-api = "[>=~^!]+[0-9.]+"', - f'span-panel-api = "{span_spec}"', + r'"span-panel-api[><=~!]+[0-9.]+"', + f'"span-panel-api{span_spec}"', content, ) - # Update ha-synthetic-sensors version — preserve exact specifier from manifest + # Update ha-synthetic-sensors version in [project] dependencies if "ha-synthetic-sensors" in versions: ha_spec = versions["ha-synthetic-sensors"] content = re.sub( - r'ha-synthetic-sensors = "[>=~^!]+[0-9.]+"', - f'ha-synthetic-sensors = "{ha_spec}"', + r'"ha-synthetic-sensors[><=~!]+[0-9.]+"', + f'"ha-synthetic-sensors{ha_spec}"', content, ) - # Check if changes were made if content != original_content: - with open(ci_path, "w") as f: + with open(pyproject_path, "w") as f: f.write(content) return True @@ -90,15 +89,11 @@ def update_ci_workflow(versions): def main(): """Main function.""" - - # Get versions from manifest versions = get_manifest_versions() if not versions: sys.exit(1) - - # Update CI workflow - changes_made = update_ci_workflow(versions) + changes_made = update_pyproject_dependencies(versions) if changes_made: sys.exit(1) # Exit with error to fail pre-commit diff --git a/scripts/sync-ha-deps.py b/scripts/sync-ha-deps.py index b258522e..cad1bd84 100644 --- a/scripts/sync-ha-deps.py +++ b/scripts/sync-ha-deps.py @@ -12,10 +12,10 @@ def get_ha_dependencies(): - """Get HomeAssistant's dependency pins from poetry show.""" + """Get HomeAssistant's dependency pins from uv pip show.""" try: result = subprocess.run( - ["poetry", "show", "homeassistant", "--format", "json"], + ["uv", "pip", "show", "homeassistant", "--format", "json"], capture_output=True, text=True, check=True, @@ -50,25 +50,26 @@ def update_pyproject_constraints(ha_deps): "pyyaml", } - # Update constraints for deps we care about - deps = ( - pyproject.setdefault("tool", {}).setdefault("poetry", {}).setdefault("dependencies", {}) - ) + deps = pyproject.get("project", {}).get("dependencies", []) updated = [] - for dep_name, version in ha_deps.items(): - if dep_name in exact_match_deps and dep_name in deps: - old_constraint = deps[dep_name] - new_constraint = f"^{version}" # Allow patch updates - if old_constraint != new_constraint: - deps[dep_name] = new_constraint - updated.append(f"{dep_name}: {old_constraint} -> {new_constraint}") + new_deps = [] + for dep_str in deps: + dep_name = dep_str.split("==")[0].split(">=")[0].split("<=")[0].split("~=")[0].split("!=")[0].strip() + if dep_name.lower() in exact_match_deps and dep_name.lower() in {d.lower() for d in ha_deps}: + version = ha_deps.get(dep_name) or ha_deps.get(dep_name.lower()) + if version: + new_constraint = f"{dep_name}>={version}" + if dep_str != new_constraint: + updated.append(f"{dep_name}: {dep_str} -> {new_constraint}") + new_deps.append(new_constraint) + continue + new_deps.append(dep_str) if updated: + pyproject["project"]["dependencies"] = new_deps with open(pyproject_path, "w") as f: toml.dump(pyproject, f) - for _update in updated: - pass return True else: return False diff --git a/setup-hooks.sh b/setup-hooks.sh index 01e64cc9..89982725 100755 --- a/setup-hooks.sh +++ b/setup-hooks.sh @@ -4,7 +4,7 @@ if [[ ! -f ".deps-installed" ]] || [[ "pyproject.toml" -nt ".deps-installed" ]]; then echo "Installing/updating dependencies..." - poetry install --with dev + uv sync if [[ $? -ne 0 ]]; then echo "Failed to install dependencies. Please check the output above." @@ -15,6 +15,6 @@ fi # Install pre-commit hooks (only if not already installed) if [[ ! -f ".git/hooks/pre-commit" ]]; then - poetry run pre-commit install + prek install echo "Git hooks installed successfully!" fi diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..9ecf177e --- /dev/null +++ b/uv.lock @@ -0,0 +1,2910 @@ +version = 1 +revision = 3 +requires-python = ">=3.14.2, <3.15" + +[[package]] +name = "acme" +version = "5.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "josepy" }, + { name = "pyopenssl" }, + { name = "pyrfc3339" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/55/767394a0fdd70ab69f14368109c8db50a1ed937615ab02458120f5356e37/acme-5.2.2.tar.gz", hash = "sha256:7702d5b99149d5cd9cd48a9270c04693e925730c023ca3e1b853ab43746a9d01", size = 90013, upload-time = "2025-12-10T18:17:17.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b3/bef9cc4e3dc4ccc18386b5f4f1f594fa48c738fc085d994b7948fd247849/acme-5.2.2-py3-none-any.whl", hash = "sha256:354ef66cf226b2bef02006311778e97123237207b4febe8829ded9860784ee64", size = 94222, upload-time = "2025-12-10T18:16:56.74Z" }, +] + +[[package]] +name = "aiodns" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycares" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/da/97235e953109936bfeda62c1f9f1a7c5652d4dc49f2b5911f9ae1043afa9/aiodns-4.0.0.tar.gz", hash = "sha256:17be26a936ba788c849ba5fd20e0ba69d8c46e6273e846eb5430eae2630ce5b1", size = 26204, upload-time = "2026-01-10T22:33:27.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/60/14ac40c03e8a26216e4f2642497b776e52f9e3214e4fd537628829bbb082/aiodns-4.0.0-py3-none-any.whl", hash = "sha256:a188a75fb8b2b7862ac8f84811a231402fb74f5b4e6f10766dc8a4544b0cf989", size = 11334, upload-time = "2026-01-10T22:33:25.65Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "mashumaro" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e0/f8865efa28ce22e44e3526f18654c7a69a6f0d0e8523e2aaf743f2798fd8/aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8", size = 44696, upload-time = "2025-10-01T14:55:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/97/b811d22148e7227e6f02a1f0f13f60d959bb163c806feab853544da07c3e/aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f", size = 40486, upload-time = "2025-10-01T14:55:56.52Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiohttp-asyncmdnsresolver" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns" }, + { name = "aiohttp" }, + { name = "zeroconf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/83/09fb97705e7308f94197a09b486669696ea20f28074c14b5811a38bdedc3/aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81", size = 36129, upload-time = "2025-02-14T14:46:44.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d1/4f61508a43de82bb5c60cede3bb89cc57c5e8af7978d93ca03ad60b99368/aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e", size = 13582, upload-time = "2025-02-14T14:46:41.985Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/a6/982f3a013b42e914a2420631afcaecb729c49525cc6cc58e15d27ee4cb4b/aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28", size = 8770, upload-time = "2025-06-07T12:41:49.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/11/ea9ecbcd6cf68c5de690fd39b66341405ab091aa0c3598277e687aa65901/aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e", size = 8615, upload-time = "2025-06-07T12:41:47.454Z" }, +] + +[[package]] +name = "aiooui" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b7/ad0f86010bbabc4e556e98dd2921a923677188223cc524432695966f14fa/aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8", size = 369276, upload-time = "2025-01-19T00:12:44.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/fa/b1310457adbea7adb84d2c144159f3b41341c40c80df3c10ce6b266874b3/aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5", size = 367404, upload-time = "2025-01-19T00:12:42.57Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiozoneinfo" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/00/e437a179ab78ed24780ded10bbb5d7e10832c07f62eab1d44ee2f335c95c/aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3", size = 8381, upload-time = "2025-02-04T19:32:06.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "annotatedyaml" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "propcache" }, + { name = "pyyaml" }, + { name = "voluptuous" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/4b/973067092ee348e331d125acd60c45245f11663373c219650814b43d0025/annotatedyaml-1.0.2.tar.gz", hash = "sha256:f9a49952994ef1952ca17d27bb6478342eb1189d2c28e4c0ddbbb32065471fb0", size = 15366, upload-time = "2025-10-04T14:36:26.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/91/0acf5b74926c6964812d9ed752af77531ab4daa06fba1cb668d9006e9e1f/annotatedyaml-1.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c42f385c3f04f425d5948c16afbb94a876da867be276dbf2c2e7436b9a80792d", size = 58962, upload-time = "2025-10-04T14:42:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/71/f6/5dac1ce125984db4cb99d883f234e6a8c0e49358a9136047a490bc2ba51a/annotatedyaml-1.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5d9d24ba907fd2e905eac69c88e651310c480980a17aa57faf0599ff21f586f", size = 60252, upload-time = "2025-10-04T14:42:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/81dea3e4272927518abb9c96ab299b8c4346c40267740bfb8d6b0cdb317f/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2572b7c3c630dae1dd163d6c6ba847493a7f987437941b32d0ad8354615f358a", size = 71219, upload-time = "2025-10-04T14:42:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/5aa3d7767cb92e8ba34cba582e5b088f42746e6d075f7d387fcdc4e5dd62/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b28fe13eb0014a0dd06c9a292466feed0cd298ab10525ef9a37089df01c7d333", size = 64459, upload-time = "2025-10-04T14:42:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/a0/eb/b29b84eec6d3a1fc3278ff2959388f347e7853a3f82fc5275c591a523835/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987f73a13f121c775bcdb082214c17f114447fee7dad37db2f86b035893ad58d", size = 71175, upload-time = "2025-10-04T14:42:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/4f4bf854f4b62cade7485a9572773d4440ee535e905f166b441b2d3f19a7/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cb4ee79c08da2b8f4f24b1775732ca6c497682f3c9b3fd65dee4ea084fc925c", size = 71824, upload-time = "2025-10-04T14:42:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/68/ac/1f903eeccde636723fcf664b372a6ab253b7f13c3de446ff5bce6852d696/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:da065a8c29556219fce1aa81b406e84f73bc2181067658e57428a8b2e662fc1b", size = 65343, upload-time = "2025-10-04T14:42:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/43e83b50a42ad5c51abf1a335cfc249e182f66542d7c7306ee07397b1956/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ba9937418c1b189b267540b47fa0dc24c148292739d06a6ca31c2ca8482f16", size = 72328, upload-time = "2025-10-04T14:42:08.656Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6a/f5d9c29633c499973f10330af31a8b135a564a4a2e056c32a6ff2c901559/annotatedyaml-1.0.2-cp314-cp314-win32.whl", hash = "sha256:003e16e91b40176dd8fe77d56c6c936106b408b62953e88ce3506e8ba10bf4e1", size = 57286, upload-time = "2025-10-04T14:42:09.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/d8c7464676094658f1caaee6762536ab43867d7153f7c637207c63fc4c97/annotatedyaml-1.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:17e64a7dde47a678db8aa4e934c3ed8da9a52ab1bc6946d12be86f323e6bd8c7", size = 61363, upload-time = "2025-10-04T14:42:10.968Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5d/7e384f4115a7bc113162f7b6eb5d561031e303f840f304b68e3f1b0541a1/annotatedyaml-1.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8698bbbd1d38f8c9ba95a107d7597f5af3f2ba295d1d14227f85b62377998ffc", size = 104776, upload-time = "2025-10-04T14:42:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/77bebdd30118c1e85f11d5a83a3bb5955409bba74d81cfb0f7b551273513/annotatedyaml-1.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cbc661dbc7c5f7ddf69fbf879da6a96745b8cd39ae1338dab3a0aa8eb208367", size = 107716, upload-time = "2025-10-04T14:42:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/bfc798abb154e398d5210304ba3beff9ad9c7b6ec4574ffb705493b8e2d5/annotatedyaml-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db1c3ca021bbd390354037ede5c255657afb2a7544b7cfa0e091b62b888aa462", size = 130361, upload-time = "2025-10-04T14:42:15.494Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "astral" +version = "2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/c3/76dfe55a68c48a1a6f3d2eeab2793ebffa9db8adfba82774a7e0f5f43980/astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe", size = 578223, upload-time = "2020-05-20T14:23:17.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/60/7cc241b9c3710ebadddcb323e77dd422c693183aec92449a1cf1fb59e1ba/astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a", size = 30775, upload-time = "2020-05-20T14:23:14.866Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "async-interrupt" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/79/732a581e3ceb09f938d33ad8ab3419856181d95bb621aa2441a10f281e10/async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7", size = 8484, upload-time = "2025-02-22T17:15:04.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/77/060b972fa7819fa9eea9a70acf8c7c0c58341a1e300ee5ccb063e757a4a7/async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c", size = 8907, upload-time = "2025-02-22T17:15:01.971Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "atomicwrites-homeassistant" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/10ff0fd9aa04f78a0b31bb617c8d29796a12bea33f1e48aa54687d635e44/atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f", size = 12223, upload-time = "2022-07-08T20:56:46.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1b/872dd3b11939edb4c0a27d2569a9b7e77d3b88995a45a331f376e13528c0/atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d", size = 7128, upload-time = "2022-07-08T20:56:44.186Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252, upload-time = "2024-08-04T21:13:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183, upload-time = "2024-08-04T21:13:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726, upload-time = "2024-08-04T21:14:00.846Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718, upload-time = "2024-08-04T21:14:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326, upload-time = "2024-08-04T21:14:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539, upload-time = "2024-08-04T21:14:04.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577, upload-time = "2024-08-04T21:14:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074, upload-time = "2024-08-04T21:14:09.99Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210, upload-time = "2024-08-04T21:14:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664, upload-time = "2024-08-04T21:14:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255, upload-time = "2024-08-04T21:14:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760, upload-time = "2024-08-04T21:14:14.74Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992, upload-time = "2024-08-04T21:14:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload-time = "2024-08-04T21:14:20.438Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload-time = "2024-08-04T21:14:21.342Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload-time = "2024-08-04T21:14:22.193Z" }, +] + +[[package]] +name = "awesomeversion" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/3a/c97ef69b8209aa9d7209b143345fe49c1e20126f62a775038ab6dcd78fd5/awesomeversion-25.8.0.tar.gz", hash = "sha256:e6cd08c90292a11f30b8de401863dcde7bc66a671d8173f9066ebd15d9310453", size = 70873, upload-time = "2025-08-03T08:54:07.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/b3/c6be343010721bfdd3058b708eb4868fa1a207534a3b6c80de74d35fb568/awesomeversion-25.8.0-py3-none-any.whl", hash = "sha256:1c314683abfcc3e26c62af9e609b585bbcbf2ec19568df2f60ff1034fb1dae28", size = 15919, upload-time = "2025-08-03T08:54:06.265Z" }, +] + +[[package]] +name = "bandit" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/b5/7eb834e213d6f73aace21938e5e90425c92e5f42abafaf8a6d5d21beed51/bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b", size = 4240271, upload-time = "2025-07-06T03:10:50.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/ba5f909b40ea12ec542d5d7bdd13ee31c4d65f3beed20211ef81c18fa1f3/bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", size = 133808, upload-time = "2025-07-06T03:10:49.134Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "bleak" +version = "2.1.1" +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/45/8a/5acbd4da6a5a301fab56ff6d6e9e6b6945e6e4a2d1d213898c21b1d3a19b/bleak-2.1.1.tar.gz", hash = "sha256:4600cc5852f2392ce886547e127623f188e689489c5946d422172adf80635cf9", size = 120634, upload-time = "2025-12-31T20:43:28.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fe/22aec895f040c1e457d6e6fcc79286fbb17d54602600ab2a58837bec7be1/bleak-2.1.1-py3-none-any.whl", hash = "sha256:61ac1925073b580c896a92a8c404088c5e5ec9dc3c5bd6fc17554a15779d83de", size = 141258, upload-time = "2025-12-31T20:43:27.302Z" }, +] + +[[package]] +name = "bleak-retry-connector" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleak" }, + { name = "bluetooth-adapters", marker = "sys_platform == 'linux'" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/02/494f6a642b2aed04123a07071944681a0776259d656212133c10d0fb4b27/bleak_retry_connector-4.6.0.tar.gz", hash = "sha256:0645ca814fe9e0f2e0716ffdae5e54de25de75de6197145a1784f20f58e76844", size = 18732, upload-time = "2026-03-07T03:06:36.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/38/091973e8b930a551b5454f9a4d6fd3fed55a73d28769e75b1d7761b46572/bleak_retry_connector-4.6.0-py3-none-any.whl", hash = "sha256:6b5ecab9dee8a67b1e64cccec47ffa8c55737b86550c366e02d11ce003d57ebd", size = 18731, upload-time = "2026-03-07T03:06:35.472Z" }, +] + +[[package]] +name = "bluetooth-adapters" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiooui" }, + { name = "bleak" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "uart-devices" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/b37a52f5243cf8bd30cc7e25c1128750d129db15a7b2c7ef107ddb7429f9/bluetooth_adapters-2.1.1.tar.gz", hash = "sha256:f289e0f08814f74252a28862f488283680584744430d7eac45820f9c20ba041a", size = 17234, upload-time = "2025-09-12T17:18:48.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/11/8f344d5379df2d31eea73052128136702630f28b5fb55a8d30250c8112e2/bluetooth_adapters-2.1.1-py3-none-any.whl", hash = "sha256:1f93026e530dcb2f4515a92955fa6f85934f928b009a181ee57edc8b4affd25c", size = 20276, upload-time = "2025-09-12T17:18:47.763Z" }, +] + +[[package]] +name = "bluetooth-auto-recovery" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bluetooth-adapters" }, + { name = "btsocket" }, + { name = "pyric" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/8b/1d6f338ced9b47965382c8f1325bf3d22be65e6f838b4e465227c52d333c/bluetooth_auto_recovery-1.5.3.tar.gz", hash = "sha256:0b36aa6be84474fff81c1ce328f016a6553272ac47050b1fa60f03e36a8db46d", size = 12798, upload-time = "2025-09-13T17:17:09.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/ab/518f14a3c3e43c34c638485cd29bfa80bd35da5a151a434f7ac3c86e1e83/bluetooth_auto_recovery-1.5.3-py3-none-any.whl", hash = "sha256:5d66b859a54ef20fdf1bd3cf6762f153e86651babe716836770da9d9c47b01c4", size = 11750, upload-time = "2025-09-13T17:17:07.681Z" }, +] + +[[package]] +name = "bluetooth-data-tools" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/90/46dfa84798ca4e5c2f66d9a756bb207ed21d89a32b8ef8d3ea89e079455f/bluetooth_data_tools-1.28.4.tar.gz", hash = "sha256:0617a879c30e0410c3506e263ee9e9bd51b06d64db13b4ad0bfd765f794b756f", size = 16488, upload-time = "2025-10-28T15:23:05.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/93/03ba322c36376532f3133c7d56bc80dd2859df9c78aa52de19ed7627b9fb/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:81c6c2b7c844d30a0fd1527e38e47cdb0f350c0297fb11516bfa255b37241fbf", size = 383677, upload-time = "2025-10-28T15:36:33.905Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2e/74e7b4857ba10a524cd00177fbd78764c50810fb523020b7d5cbf0fdbac8/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:99896987f48d762694cdea7a8a7091031cdf40dc65e8e934a7422746264865ba", size = 385890, upload-time = "2025-10-28T15:36:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/35bc257ed1935e55ac7bfb56172a290f094f8b982f65f68aadb0f03ceab5/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebac9d60786bd7c403f472fcda871cb74d0aef0d4e713715af2e5e095d15a625", size = 412966, upload-time = "2025-10-28T15:36:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/2ed3dff30e85029e631a211d93e11aab7dc4a899d9c96a15eca18541e66e/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:06a2750e49fed2310ddd7b51388b891cbd4457ee7392f3a17c387591cbb74ace", size = 129887, upload-time = "2025-10-28T15:36:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/824f3b34b0fab4e57efd457ea8b9bdf41d279a44eb19cfde5ede159d90b3/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5dccfe237237463c3d74fa425aaf8a9d78b26a5177e6777b10039699313a335", size = 412909, upload-time = "2025-10-28T15:36:39.552Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/f2217b88c32b470e5f9dc9fbce38f24b9548c0776be7c5e0db1249c42ae9/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4a071d7af2614af9a00f65063adaacda94f4357cc2dfedda7057c005f437dacd", size = 413005, upload-time = "2025-10-28T15:36:41.572Z" }, + { url = "https://files.pythonhosted.org/packages/6d/da/cde7557972e50cbb8a92291cc34e5de07f0e2bbc28a388151e738e9efe84/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:bd84c4f2d24103ff43044ccd3cf8c0e05ee285bd6f9eddc9772b2069cfb6c271", size = 131426, upload-time = "2025-10-28T15:36:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/925fd28e2695ba810b1f7f02f2d5ab8635a11d6e415ac4039446145f9e48/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e3895dbbdad2a39de5a7b36a4ddb5e2f8ad38029628e3eddfde31a5c56d81b5", size = 414955, upload-time = "2025-10-28T15:36:43.775Z" }, + { url = "https://files.pythonhosted.org/packages/03/b1/cbf3a2c8404862605e487200d45aefb130c0c0ce3df219230155eeb95199/bluetooth_data_tools-1.28.4-cp314-cp314-win32.whl", hash = "sha256:1d9b22827144329e3ca1348b8473fe6b48127707a81539848232847c4cb08e1d", size = 286157, upload-time = "2025-10-28T15:36:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/68/eb168b986eebc0c98fb0a6a521719a33d218bafc46c48c5279322d15e9b2/bluetooth_data_tools-1.28.4-cp314-cp314-win_amd64.whl", hash = "sha256:04c91b6f2dfaa419652356488fa50dfb0f54cb20b1f90f9e5e1d6911430d9688", size = 286151, upload-time = "2025-10-28T15:36:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/f2ce46cf82b32d6a62171753a2d6550d633af5b27f0ad2c2ff5fef1980a4/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a44c48bf163606a2915d12ffb3ac1b022548e566c062907f98266e8a19c6173c", size = 488264, upload-time = "2025-10-28T15:36:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/ba/32/c3bbee5b7c66190f0729e71fefe44adb49e7bb94407b110d972d817561a2/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b76a6c8c6d610844c8712cecf207c16373cad3361fb29e6dbcdcb12f2700bcb9", size = 492846, upload-time = "2025-10-28T15:36:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/71/5c/751028e7fab907c0c2fc7749f088d19bf2b938e5cdd7d0e68ddbcacb7b79/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61b827616075ecee12c374b04b14d81575403849435bf915c9a3812138f046b7", size = 548041, upload-time = "2025-10-28T15:36:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/77/02/4d8f4a9cb2a2beaaedda71fb3017f6bb5eb3de08656adfb9a8a773ec7912/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:525646baaf5f741ea071aa4babd8313e4e9bae75b46757c4b0f6aeadfa71b52a", size = 517778, upload-time = "2025-10-28T15:36:51.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/9b/90d65fed47b531b0f0f4c8be012d35c97950c97fb7b74501bfe938c7f7ca/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c06b66ef406c68a95052a87640fa34d402d31120a8b0b62f99080169621697a", size = 546643, upload-time = "2025-10-28T15:36:52.971Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6b/c15363ccfc208a34cd6d627610350c72633e2a6764d37d04a1340fb13844/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:152232c157f2f6d8265c0141e56423bbedd9e84044fb815e69d786a73fb195c7", size = 548872, upload-time = "2025-10-28T15:36:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/b649eeea14e6330da34f42dc1407424cd929af3ae1298b5651459d0c4bb8/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:243163028565955e73f19c0c462b619fd0f56e31875c30f5f3af2a48b43adb67", size = 524783, upload-time = "2025-10-28T15:36:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6e/96c762f8a49f65348748d72c515c5a79c9179c685d3e02694c380bdafa72/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a1608bca00e24b6ca3b98ed7d797a03988a44285d74286e045446c8161a62ea", size = 551318, upload-time = "2025-10-28T15:36:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7d/796cbb679d19425ff381ebbe7a5238217b3f3e5c65b9a46e7be57ba105fc/bluetooth_data_tools-1.28.4-cp314-cp314t-win32.whl", hash = "sha256:25918d7ece36f29ebde21aaf70f3c1e1c63501206dd1c7713bbd8911d43d0dce", size = 286158, upload-time = "2025-10-28T15:36:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/639329ba05947018ba928162042dfb162a31b85757e27591bb6aa96c1f42/bluetooth_data_tools-1.28.4-cp314-cp314t-win_amd64.whl", hash = "sha256:276528d7ea2419ccab14ddf044ee7f65a5b6bc35c49264625560ad0c184dc67a", size = 286163, upload-time = "2025-10-28T15:36:59.861Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.73" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/8b/d00575be514744ca4839e7d85bf4a8a3c7b6b4574433291e58d14c68ae09/boto3-1.42.73.tar.gz", hash = "sha256:d37b58d6cd452ca808dd6823ae19ca65b6244096c5125ef9052988b337298bae", size = 112775, upload-time = "2026-03-20T19:39:52.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/05/1fcf03d90abaa3d0b42a6bfd10231dd709493ecbacf794aa2eea5eae6841/boto3-1.42.73-py3-none-any.whl", hash = "sha256:1f81b79b873f130eeab14bb556417a7c66d38f3396b7f2fe3b958b3f9094f455", size = 140556, upload-time = "2026-03-20T19:39:50.298Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.73" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/23/0c88ca116ef63b1ae77c901cd5d2095d22a8dbde9e80df74545db4a061b4/botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c", size = 15008008, upload-time = "2026-03-20T19:39:40.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/65/971f3d55015f4d133a6ff3ad74cd39f4b8dd8f53f7775a3c2ad378ea5145/botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe", size = 14681861, upload-time = "2026-03-20T19:39:35.341Z" }, +] + +[[package]] +name = "btsocket" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/b1/0ae262ecf936f5d2472ff7387087ca674e3b88d8c76b3e0e55fbc0c6e956/btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d", size = 19563, upload-time = "2024-06-10T07:05:27.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/2b/9bf3481131a24cb29350d69469448349362f6102bed9ae4a0a5bb228d731/btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c", size = 14807, upload-time = "2024-06-10T07:05:26.381Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +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/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/075724aea06c98626109bfd670c27c248c87b9ba33e637f069bf46e8c4c3/ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae", size = 31909, upload-time = "2025-08-20T16:31:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3a/54ad0ae2257870076b4990545a8f16221470fecea0aa7a4e1f39506db8c5/ciso8601-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82db4047d74d8b1d129e7a8da578518729912c3bd19cb71541b147e41f426381", size = 16115, upload-time = "2025-08-20T16:30:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9fe767d44520691e2b706769466852fbdeb44a82dc294c2766bce1049d22/ciso8601-2.3.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a553f3fc03a2ed5ca6f5716de0b314fa166461df01b45d8b36043ccac3a5e79f", size = 24214, upload-time = "2025-08-20T16:30:56.359Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ac/984fd3948f372c46c436a2b48da43f4fb7bc6f156a6f4bc858adaab79d42/ciso8601-2.3.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:ff59c26083b7bef6df4f0d96e4b649b484806d3d7bcc2de14ad43147c3aafb04", size = 15929, upload-time = "2025-08-20T16:30:58.352Z" }, + { url = "https://files.pythonhosted.org/packages/de/3a/5572917d4e0bec2c1ef0eda8652f9dc8d1850d29d3eef9e5e82ffe5d6791/ciso8601-2.3.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99a1fa5a730790431d0bfcd1f3a6387f60cddc6853d8dcc5c2e140cd4d67a928", size = 41578, upload-time = "2025-08-20T16:30:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cf/07321ce5cf099b98de0c02cd4bab4818610da69743003e94c8fb6e8a59cb/ciso8601-2.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c35265c1b0bd2ac30ed29b49818dd38b0d1dfda43086af605d8b91722727dec0", size = 42085, upload-time = "2025-08-20T16:31:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c7/3c521d6779ee433d9596eb3fcded79549bbe371843f25e62006c04f74dc9/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa9df2f84ab25454f14df92b2dd4f9aae03dbfa581565a716b3e89b8e2110c03", size = 41313, upload-time = "2025-08-20T16:31:01.313Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/efd40db0d6b512be1cbe4e7e750882c2e88f580e17f35b3e9cc9c23004b5/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32e06a35eb251cfc4bbe01a858c598da0a160e4ad7f42ff52477157ceaf48061", size = 41443, upload-time = "2025-08-20T16:31:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/515f9404faa39af8df5e2b899cafbca5dbe7cd2ffe5cc124ef393ffdaf1c/ciso8601-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:7657ba9730dc1340d73b9e61eca14f341c41dd308128c808b8b084d2b85bc03e", size = 17977, upload-time = "2025-08-20T16:31:03.429Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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" }, +] + +[[package]] +name = "coverage" +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +] + +[[package]] +name = "cronsim" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +] + +[[package]] +name = "dbus-fast" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f7/36515d10e85ab6d6193edbabbcae974c25d6fbabb8ead84cfd2b4ee8eaf6/dbus_fast-4.0.0.tar.gz", hash = "sha256:e1d3ee49a4a81524d7caaa2d5a31fc71075a1c977b661df958cee24bef86b8fe", size = 75082, upload-time = "2026-02-01T20:56:27.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/c5/5fee1e5d59b2856db9da8372c67ed7699b262108a4540d5858f34a67699f/dbus_fast-4.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e53d7e19d2433f2ca1d811856e4b80a3b3126f361703e5caf6e7f086a03b994", size = 804142, upload-time = "2026-02-01T21:05:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/91a9339278ccee8be93df337c69703dd9d3f5b8fc97dadb2f8a3ff06f6c0/dbus_fast-4.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b430760c925e0b695b6f1a3f21f6e57954807cab4704a3bc4bc5f311261016b", size = 846011, upload-time = "2026-02-01T21:05:34.875Z" }, + { url = "https://files.pythonhosted.org/packages/34/bf/bab415e523fc67a3b1d246a677dcac1198b5cf4d89ae594b2b25b71c02c7/dbus_fast-4.0.0-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:2818d76da8291202779fe8cb23edc62488786eee791f332c2c40350552288d8b", size = 844116, upload-time = "2026-02-01T20:56:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cc517508d102242656c06acb3980decd243e56470f9cb51dc736a9197ef/dbus_fast-4.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0b2aaf80991734e2bbff60b0f57b70322668acccb8bb15a0380ca80b8f8c5d72", size = 810621, upload-time = "2026-02-01T21:05:36.208Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/686bd523c9966bbd9c0705984782fcb33d3a2aae75a2ebbb34b37aca1f3b/dbus_fast-4.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93a864c9e39ab03988c95e2cd9368a4b6560887d53a197037dfc73e7d966b690", size = 853111, upload-time = "2026-02-01T21:05:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/26a2a2120c32bf6a61b81a19d7d20cd440c79f1c4679b04af85af93bc0e4/dbus_fast-4.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c71b369f8fd743c0d03e5fd566ff5d886cb5ad7f3d187f36185a372096a2a096", size = 1534384, upload-time = "2026-02-01T21:05:41.636Z" }, + { url = "https://files.pythonhosted.org/packages/d0/53/916c2bbb6601108f694b7c37c71c650ef8d06c2ed282a704b5c8cca67edf/dbus_fast-4.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffc16ee344e68a907a40327074bca736086897f2e783541086eedb5e6855f3f0", size = 1610347, upload-time = "2026-02-01T21:05:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f6/05eeb374a02f63b0e29b1ee2073569e8cf42f655970a651f938bcdbe7eae/dbus_fast-4.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f8f4b0f8af730c39bbb83de1e299e706fbd7f7f3955764471213b013fa59516", size = 1549395, upload-time = "2026-02-01T21:05:45.159Z" }, + { url = "https://files.pythonhosted.org/packages/a4/87/d03a718e7bfdbbebaa4b6a66ba5bb069bc00a84e5ad176d8198cc785cd42/dbus_fast-4.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6af190d8306f1bd506740c39701f5c211aa31ac660a3fcb401ebb97d33166c7", size = 1627620, upload-time = "2026-02-01T21:05:46.878Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "envs" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/2098df91ff1499860935b4276ea0c27d3234170b03f803a8b9c97e42f0e9/envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398", size = 9230, upload-time = "2021-12-09T22:16:52.616Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fnvhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/10f4e1b4bbfe7cf162d20bb4d54bd62935d652e2ea107ddb0b5a6c4e8b75/fnv_hash_fast-1.6.0.tar.gz", hash = "sha256:a09feefad2c827192dc4306826df3ffb7c6288f25ab7976d4588fdae9cbb7661", size = 5675, upload-time = "2025-10-04T19:35:00.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/17/9c724ac795f53578dd6be61d6a0466c4cd51550485b301764ddfc6ed5ad1/fnv_hash_fast-1.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:07bb79eaa44f91db2aab3b641194f68dc4ddd15701756f687c1a7a294bfa9c06", size = 13296, upload-time = "2025-10-04T19:45:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/0c/11/a2eb0a7fbfb5d5cb5d27df7f6d4e395ce2f328da16d32702909af00ffe82/fnv_hash_fast-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4176315430f9fcf5346a0339b0f55982e1715452345d70c2887755bfd5aa2b64", size = 13879, upload-time = "2025-10-04T19:45:32.063Z" }, + { url = "https://files.pythonhosted.org/packages/0e/85/3a297faae2416916f7a5cb858b08b500296bbc7d7136faf2cfbadde61e33/fnv_hash_fast-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31db9d944c91d286475870855b9203f4fb4794cb0674de5458e9d1231e07f37", size = 15222, upload-time = "2025-10-04T19:45:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/9c81426e4a22d15dc9c1a73536c6a7e2aeb8a71ac0b398d841ebd287e8e5/fnv_hash_fast-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fc1bbec5871060c6efa6a444a554496f372f1f4a7e83b99989be5ea6b97435f", size = 16379, upload-time = "2025-10-04T19:45:34.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/7f1454ebc9dee224d6ee5360111e3855802ce79f48f1808117998771ffaa/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:91ed6df63ab2082b5b48a6b8f5d7eb7b51d39c2eeffd64821301bf6d9662ff11", size = 16252, upload-time = "2025-10-04T19:45:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/cedd70c2e09ba09f5834c7e50f8fed4a37bba38c0c2471849bb4dac91148/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d34541e15bbc3877da7541f059fb1eadf53031abe7fc4318b28421e02eff383", size = 15570, upload-time = "2025-10-04T19:45:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/9c68ad33e254af9809bbd504b9895a93cb67472fc39bcd656f02c2703637/fnv_hash_fast-1.6.0-cp314-cp314-win32.whl", hash = "sha256:74320b9033c13e851174edf959c167619907eb985176e795d17d7fbe29cf3a45", size = 15484, upload-time = "2025-10-04T19:45:37.392Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/8ead2c631323c8a755c8437641e832ba2eaf27bf2577535cf40d57b62def/fnv_hash_fast-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:540670ff837824939d2af90dd89cddbd02d238d778999a403cdb4a4de8c65a73", size = 16345, upload-time = "2025-10-04T19:45:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/b5bd2b9a06269098af059e79e05ceff320a405b1c49b9f3d29708324179b/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:83aa2d791193e3b3f4132741c4dc09eed4f7df8000d76ad77fb9d24db8e59a88", size = 21338, upload-time = "2025-10-04T19:45:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/21/07/1688d543a7688529857cd43bcff3ac324c69fd2923a9b40a1adc120cef20/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b8d33f002bb336f9f0949a32d7da07cc9d340a9d07e4f16cc9ece982842eb4e0", size = 22455, upload-time = "2025-10-04T19:45:40.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/588f43d8dd122fc884c3556f993a3e3db953afecc62fa812d439f69ec067/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0042af2a1cb7ffae412ec3cb6ae8c581a73610fd523f7e17ed58a5359505ffec", size = 25053, upload-time = "2025-10-04T19:45:41.74Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/6209457f59e0ff43b066ca8cbfeb800bc0af478e221e74beadaf0b58effa/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73308e11c0e5a2dba433fc5645672de4756a52b323de1dab20e45d4fe5e83994", size = 27875, upload-time = "2025-10-04T19:45:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/c4796f6b1ee6cb778620663d00eadb970a8271fb537ce75774d5acfeecdb/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96282ecb75bec190af0111e82ddd38afc98e9cb867a1689e873ab6802af951b7", size = 27443, upload-time = "2025-10-04T19:45:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/846b8f977dda4f0e7f1ec4ffff6707b9e666dabb9eb203c4c2bfc4b0b6fe/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cae16753c1d85ed358df13824bd8a474bfa9da34daddc1a90c72b25ff4177f51", size = 25765, upload-time = "2025-10-04T19:45:45.565Z" }, + { url = "https://files.pythonhosted.org/packages/2f/98/1371f0a765a3160a4c864de1b6d5ea696ba3ca822e3cea74357e15aca85d/fnv_hash_fast-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:e2efb5953475a5a0529ca9757d6782c5174a3b8a3fbdc4e1c1273ac1d293316b", size = 26343, upload-time = "2025-10-04T19:45:46.566Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/99586ce163eeead7373db6dc3aa01998c42211ad11bbd7f6d21824fc5c80/fnv_hash_fast-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6eb03cd17c134d412fed9f05dc6f9ff9a8aa3b8e69c0135603a521e77720c93", size = 28057, upload-time = "2025-10-04T19:45:48.033Z" }, +] + +[[package]] +name = "fnvhash" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/43/30d2dd2b14621b2004f658ba5335e5a6f5a9c1338ed37678d7fd247b7a9c/fnvhash-0.2.1.tar.gz", hash = "sha256:0c7e885f44c8f06de07f442befebc590ee9ca0cc88846681f608496284ce9cd5", size = 19057, upload-time = "2025-05-05T16:59:10.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/92/7c8abc21a1de7159013c0b0bd2ecf06530959bb14fd5c3bf0045e788c6d9/fnvhash-0.2.1-py3-none-any.whl", hash = "sha256:00fab14bec841e4cb29b4fd2ed9358f8bf9f4600d9d8149cde27a191193a33e8", size = 18115, upload-time = "2025-05-05T16:59:09.269Z" }, +] + +[[package]] +name = "freezegun" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/75/0455fa5029507a2150da59db4f165fbc458ff8bb1c4f4d7e8037a14ad421/freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181", size = 34855, upload-time = "2025-05-24T12:38:47.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/b2/68d4c9b6431121b6b6aa5e04a153cac41dcacc79600ed6e2e7c3382156f5/freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b", size = 18715, upload-time = "2025-05-24T12:38:45.274Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "habluetooth" +version = "5.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-interrupt" }, + { name = "bleak" }, + { name = "bleak-retry-connector" }, + { name = "bluetooth-adapters" }, + { name = "bluetooth-auto-recovery" }, + { name = "bluetooth-data-tools" }, + { name = "btsocket" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/10/c06b3610410931bec2bbc8ab8db0be77b8ef770a15feff683cc765756786/habluetooth-5.10.2.tar.gz", hash = "sha256:1ae243e27e1a9faf552a844431e617bf1a93ff10ee3212ec5e601d2e8d21097a", size = 49840, upload-time = "2026-03-15T09:08:36.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d7/a044a60ebd7c6cb9a5d6961503a6b40e3d812ace12a9705dc1b1222a468c/habluetooth-5.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f8555995a0cd2fc36ab211642caa3ce2f803e530b3dea1b3af2dba2b2dd432f", size = 572538, upload-time = "2026-03-15T09:40:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/90/47/3f5372c31579c27e93a1d441c9748414fc46a5fef60b98dcd44b3dd91d31/habluetooth-5.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:616285f3705050e97278f26597896257bccc02c1f30f809bc805c22defc37132", size = 673476, upload-time = "2026-03-15T09:40:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fc/fa1e2166fa8ede368bedcab6df8cb486d76763e043633699e2cac8653a4c/habluetooth-5.10.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bc0f86d1b46969e286c027be3c051ac80b0471259bc3033cde5ba814204a523", size = 640804, upload-time = "2026-03-15T09:40:34.353Z" }, + { url = "https://files.pythonhosted.org/packages/72/b0/30fd29f45f1049278e5df60c4b65e63316fd702cbb256b97278869b840b4/habluetooth-5.10.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19a46f5e65d72307ce3034ea5ebccb5e20eb4ec323320ca99a7b6df6cccd0614", size = 711562, upload-time = "2026-03-15T09:40:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7a/930622e7c119887d0ca46b72248cd22cf5f88ea20b8835a9d95b10591223/habluetooth-5.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8414de442f14277b3542647242fea6ec7bcd5726b8531a1746af7a08a7542cf0", size = 682424, upload-time = "2026-03-15T09:40:37.85Z" }, + { url = "https://files.pythonhosted.org/packages/d0/52/d2e9be00ba1d546fc1159f2474b740e3cf75a5307d164a63f0fad6e21ee7/habluetooth-5.10.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:de0dcb3092c772636abfd57f31c63c73e350ef4b92955ce89a4de6fed6423b2c", size = 646608, upload-time = "2026-03-15T09:40:39.585Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/1fd3045b709b8357746aac45c2811920e2b3435f291fd0f456563963d89e/habluetooth-5.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:01e71d64c0fa3d2a3b49c43a871933ce12b5e7b31a6edd4c0c5561901c967744", size = 717325, upload-time = "2026-03-15T09:40:41.432Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/9c5dda5ec5ede6f3f25e9b6952b5b6d4780f1c43bf09156220e7f5e85c2a/habluetooth-5.10.2-cp314-cp314-win32.whl", hash = "sha256:3f7945606e2f900e1f008d506cdfa943b64b2b1cdda18ff546c6219c9b2c424d", size = 468639, upload-time = "2026-03-15T09:40:43.336Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e2/7b01b70319af48ba520ffe23aab481d77e16babef6d11a3e19861202d4b4/habluetooth-5.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:f4ff9fc1c8253eaf82be457e5e35ff7abb21a5fe3655512089d0e99df73cb369", size = 542122, upload-time = "2026-03-15T09:40:45.178Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/3bfc433d1fa04537cc4446afe157708104742665fd6021edb1cf2fa54ece/habluetooth-5.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f174e10fab62c1d35814ab5288f443234d069fbc9fa063d8eda54ed443610154", size = 1136533, upload-time = "2026-03-15T09:40:46.719Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b9/9872ae1cd98d70ed047e33dc71efee8452536cd149f99eaee49272685388/habluetooth-5.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea14411d2c7d0a48eb4bb84b4989fbeb35ab0306ccc78bfd458ad6a6d19d6783", size = 1289834, upload-time = "2026-03-15T09:40:48.348Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5c/d4d704a324994c4e6713fd2cae5f85b9e516dcc6f211ef380c125457786c/habluetooth-5.10.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fc6eed8ebecc4d4987a3ed5a62b7a5181bffb49847e47bbb3b088288b764949e", size = 1205873, upload-time = "2026-03-15T09:40:50.054Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/8fb82d316e695b2347eca9b231985b5f2c8900ebd5fd78b7f57788c907b0/habluetooth-5.10.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e6c03523b1561d0ba4a3cad08fde2163832fd6b35a15a96212bd4968bddeab4", size = 1350748, upload-time = "2026-03-15T09:40:51.652Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1e/5b3f6236e73833eb756ffb7b65467d19751e754e603f84c79db03983e582/habluetooth-5.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50dd2facc74f58766c8319ace068bc79fb27f73ace1bbaeae17f23c1ecae9d10", size = 1309696, upload-time = "2026-03-15T09:40:53.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f7/04f14a0fe8de0aa4f014e0033f6de413b8a3bf21ee9d09e1cd53bb0569b5/habluetooth-5.10.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0bb27c24ee095cdbb4078668f7e6f4d83cb4f6f15cae1741c360b3691c3b39fa", size = 1230790, upload-time = "2026-03-15T09:40:55.231Z" }, + { url = "https://files.pythonhosted.org/packages/92/c5/cef8d156f8d8860e89a7d2906edb130452fdcac4cdc7c8e4948774c8df15/habluetooth-5.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fde6ca79a5754e83f1463e6c4f1cd08bfd73d21e8b7b4af23e2d3f58e811542", size = 1365273, upload-time = "2026-03-15T09:40:56.903Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/ca48e9bf508fdda497e11390d81289ee87422d84c873b7d748832511de7c/habluetooth-5.10.2-cp314-cp314t-win32.whl", hash = "sha256:3aa6e047d864e207616384c6bc11eaaf38c83f778b0c1494a00ec92dcde9ef79", size = 969881, upload-time = "2026-03-15T09:40:58.598Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/326f51d82e4da23962ecbaa3c5b7665691975034e74a9b2d6758e0349951/habluetooth-5.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7cbd53e76abbb65e2ef15f383dd3f08504cb19885325090e71a1d00e7325aaa7", size = 1142045, upload-time = "2026-03-15T09:41:00.643Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "acme" }, + { name = "aiohttp" }, + { name = "async-timeout" }, + { name = "atomicwrites-homeassistant" }, + { name = "attrs" }, + { name = "ciso8601" }, + { name = "cryptography" }, + { name = "grpcio" }, + { name = "icmplib" }, + { name = "josepy" }, + { name = "pycognito" }, + { name = "pyjwt" }, + { name = "sentence-stream" }, + { name = "snitun" }, + { name = "voluptuous" }, + { name = "webrtc-models" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/9d/60316d7866c5818b8002e3d570b9d8042d5dd923a659d542b245f89c955b/hass_nabucasa-1.12.0.tar.gz", hash = "sha256:06bc4ebe89ffd08b744aa6540a2ebc44a82f60e2e74645e3b7498385c88d722c", size = 114395, upload-time = "2026-01-28T12:42:34.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/71/1691d35d7bd103584f43f3d41ea6a0e8aae451ac5d50fa47b53e085c4f53/hass_nabucasa-1.12.0-py3-none-any.whl", hash = "sha256:90debd3efa2bdf6bca03e20f1a61e15441b260661ed17106dca6141b005ef788", size = 89373, upload-time = "2026-01-28T12:42:33.091Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "habluetooth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/0e/c05ee603cab1adb847a305bc8f1034cbdbc0a5d15169fcf68c0d6d21e33f/home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e", size = 7607, upload-time = "2025-02-04T16:11:15.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/9b/9904cec885cc32c45e8c22cd7e19d9c342e30074fdb7c58f3d5b33ea1adb/home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7", size = 7915, upload-time = "2025-02-04T16:11:13.163Z" }, +] + +[[package]] +name = "homeassistant" +version = "2026.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns" }, + { name = "aiohasupervisor" }, + { name = "aiohttp" }, + { name = "aiohttp-asyncmdnsresolver" }, + { name = "aiohttp-cors" }, + { name = "aiohttp-fast-zlib" }, + { name = "aiozoneinfo" }, + { name = "annotatedyaml" }, + { name = "astral" }, + { name = "async-interrupt" }, + { name = "atomicwrites-homeassistant" }, + { name = "attrs" }, + { name = "audioop-lts" }, + { name = "awesomeversion" }, + { name = "bcrypt" }, + { name = "certifi" }, + { name = "ciso8601" }, + { name = "cronsim" }, + { name = "cryptography" }, + { name = "fnv-hash-fast" }, + { name = "hass-nabucasa" }, + { name = "home-assistant-bluetooth" }, + { name = "httpx" }, + { name = "ifaddr" }, + { name = "jinja2" }, + { name = "lru-dict" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "propcache" }, + { name = "psutil-home-assistant" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "securetar" }, + { name = "sqlalchemy" }, + { name = "standard-aifc" }, + { name = "standard-telnetlib" }, + { name = "typing-extensions" }, + { name = "ulid-transform" }, + { name = "urllib3" }, + { name = "uv" }, + { name = "voluptuous" }, + { name = "voluptuous-openapi" }, + { name = "voluptuous-serialize" }, + { name = "webrtc-models" }, + { name = "yarl" }, + { name = "zeroconf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/4e/b1355e2d5225afa2bfd90a2d7336421145166c88464757e43046471c3799/homeassistant-2026.2.2.tar.gz", hash = "sha256:418a5f375bda07d9136ef256a7b1a8fc7c3b891f00e63da59c829cafba7d32ce", size = 30868639, upload-time = "2026-02-13T20:17:53.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0d/4fe54d4bd2eef8ad13805518f2702417aa9f1f6070e3599175955ece71e5/homeassistant-2026.2.2-py3-none-any.whl", hash = "sha256:ef3e6a4e1cf96f4ad36062163b24b08488c3fe32f6115c2e02a0cc30ba65b30a", size = 51184195, upload-time = "2026-02-13T20:17:49.137Z" }, +] + +[[package]] +name = "homeassistant-stubs" +version = "2026.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "homeassistant" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/bb/abe391c97ad68b7edae7a3c089645b3f1063be7abfc5f39dccba527cfa8f/homeassistant_stubs-2026.2.2.tar.gz", hash = "sha256:c660c84f8b81038f77dfb644e098d0d86901d19945364d927a4640414110ca1a", size = 1241003, upload-time = "2026-02-14T03:11:44.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/62/0f0e2bf8e2e8d69295c8e5ff18a06f472a12c97cebcd95431bad3e022c01/homeassistant_stubs-2026.2.2-py3-none-any.whl", hash = "sha256:5cb40c3f07c1d102edbf2e8452f6ef279524482fc6efd6cbd716d01c47c83113", size = 3486560, upload-time = "2026-02-14T03:11:41.872Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "icmplib" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/78/ca07444be85ec718d4a7617f43fdb5b4eaae40bc15a04a5c888b64f3e35f/icmplib-3.0.4.tar.gz", hash = "sha256:57868f2cdb011418c0e1d5586b16d1fabd206569fe9652654c27b6b2d6a316de", size = 26744, upload-time = "2023-10-10T17:05:12.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ab/a47a2fdcf930e986914c642242ce2823753d7b08fda485f52323132f1240/icmplib-3.0.4-py3-none-any.whl", hash = "sha256:336b75c6c23c5ce99ddec33f718fab09661f6ad698e35b6f1fc7cc0ecf809398", size = 30561, upload-time = "2023-10-10T17:05:10.092Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "josepy" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500, upload-time = "2025-10-14T14:54:42.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211, upload-time = "2025-10-14T14:54:41.144Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "license-expression" +version = "30.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/79/efb4637d56dcd265cb9329ab502be0e01f4daed80caffdc5065b4b7956df/license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2", size = 183031, upload-time = "2025-06-25T13:02:25.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ba/f6f6573bb21e51b838f1e7b0e8ef831d50db6d0530a5afaba700a34d9e12/license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280", size = 117085, upload-time = "2025-06-25T13:02:24.503Z" }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } + +[[package]] +name = "mando" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mashumaro" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/3d/0f1bf475109a816c2a31a8b424750911343f0bce304827a5255df167547e/mashumaro-3.20.tar.gz", hash = "sha256:af4573f14ae61be3fbc3a473158ddfc1420f345410385809fd782e0d79e9215c", size = 191643, upload-time = "2026-02-09T21:53:55.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/5a/4fed77781061647d3be98e2f235ef1869289dd839ca0451a8d50a30fcd5c/mashumaro-3.20-py3-none-any.whl", hash = "sha256:648bc326f64c55447988eab67d6bfe3b7958c0961c83590709b1f950f88f4a3c", size = 94942, upload-time = "2026-02-09T21:53:53.343Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mock-open" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/02/cef85a80ff6d3092a458448c46816656d1c532afd45aeeeb8f50a84aed35/mock-open-1.4.0.tar.gz", hash = "sha256:c3ecb6b8c32a5899a4f5bf4495083b598b520c698bba00e1ce2ace6e9c239100", size = 12127, upload-time = "2020-04-15T15:26:51.234Z" } + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "pip" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, +] + +[[package]] +name = "pipdeptree" +version = "2.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ef/9158ee3b28274667986d39191760c988a2de22c6321be1262e21c8a19ccf/pipdeptree-2.26.1.tar.gz", hash = "sha256:92a8f37ab79235dacb46af107e691a1309ca4a429315ba2a1df97d1cd56e27ac", size = 41024, upload-time = "2025-04-20T03:27:42.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/a5/f9f143b420e53a296869636d1c3bdc144be498ca3136a113f52b53ea2b02/pipdeptree-2.26.1-py3-none-any.whl", hash = "sha256:3849d62a2ed641256afac3058c4f9b85ac4a47e9d8c991ee17a8f3d230c5cffb", size = 32802, upload-time = "2025-04-20T03:27:40.413Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prek" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e4/983840179c652feb9793c95b88abfe4b1f1d1aed7a791b45db97241be1a0/prek-0.3.6.tar.gz", hash = "sha256:bdf5c1e13ba0c04c2f488c5f90b1fd97a72aa740dc373b17fbbfc51898fa0377", size = 378106, upload-time = "2026-03-16T08:31:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/05/157631f14fef32361a36956368a1e6559d857443d7585bc4c9225f4a4a18/prek-0.3.6-py3-none-linux_armv6l.whl", hash = "sha256:1713119cf0c390486786f4c84450ea584bcdf43979cc28e1350ec62e5d9a41ed", size = 5126301, upload-time = "2026-03-16T08:31:31.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/0918501708994d165c4bfc64c5749a263d04a08ae1196f3ad3b2e0d93b12/prek-0.3.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b68ef211fa60c53ec8866dcf38bacd8cb86b14f0e2b5491dd7a42370bee32e3e", size = 5527520, upload-time = "2026-03-16T08:31:41.948Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/0d8ed2eaea58d8a7c5a3b0129914b7a73cd1a1fc7513a1d6b1efa0ec4ce4/prek-0.3.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:327b9030c3424c9fbcdf962992288295e89afe54fa94a7e0928e2691d1d2b53d", size = 5120490, upload-time = "2026-03-16T08:31:29.808Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/63e21d19687816082df5bfd234f451b17858b37f500e2a8845cda1a031db/prek-0.3.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:61de3f019f5a082688654139fd9a3e03f74dbd4a09533667714d28833359114d", size = 5355957, upload-time = "2026-03-16T08:31:37.408Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0e/bb52a352e5d7dc92eaebb69aeef4e5b7cddc47c646e24fe9d6a61956b45d/prek-0.3.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bbba688c5283c8e8c907fb00f7c79fce630129f27f77cbee67e356fcfdedea8", size = 5055675, upload-time = "2026-03-16T08:31:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/34/8b/7c2a49314eb4909d50ee1c2171e00d524f9e080a5be598effbe36158d35c/prek-0.3.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dfe26bc2675114734fa626e7dc635f76e53a28fed7470ba6f32caf2f29cc21f", size = 5459285, upload-time = "2026-03-16T08:31:32.764Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/86cbf205b111f93d45b5c04a61ea2cdcf12970b11277fa6a8eef1b8aaa0d/prek-0.3.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f8121060b4610411a936570ebb03b0f78c1b637c25d4914885b3bba127cb554", size = 6391127, upload-time = "2026-03-16T08:31:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d3/bae4a351b9b095e317ad294817d3dff980d73a907a0449b49a9549894a80/prek-0.3.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a38d8061caae4ffd757316b9ef65409d808ae92482386385413365bad033c26", size = 5734755, upload-time = "2026-03-16T08:31:34.387Z" }, + { url = "https://files.pythonhosted.org/packages/ea/48/5b1d6d91407e14f86daf580a93f073d00b70f4dca8ff441d40971652a38e/prek-0.3.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3d9e3b5031608657bec5d572fa45a41b6c7ddbe98f925f8240addbf57af55ea7", size = 5362190, upload-time = "2026-03-16T08:31:49.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/18/38d6ea85770bb522d3dad18e8bbe435365e1e3e88f67716c2d8c2e57a36a/prek-0.3.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a581d2903be460a236748fb3cfcb5b7dbe5b4af2409f06c0427b637676d4b78a", size = 5181858, upload-time = "2026-03-16T08:31:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/3b/61/7179e9faffa3722a96fee8d9cebdb3982390410b85fc2aaeacfe49c361b5/prek-0.3.6-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:d663f1c467dccbd414ab0caa323230f33aa27797c575d98af1013866e1f83a12", size = 5023469, upload-time = "2026-03-16T08:31:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/ad/69/8a496892f8c9c898dea8cfe4917bbd58808367975132457b5ab5ac095269/prek-0.3.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cbc7f0b344432630e990a6c6dd512773fbb7253c8df3c3f78eedd80b115ed3c9", size = 5322570, upload-time = "2026-03-16T08:31:51.034Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/f174bcfd73e8337a4290cb7eaf70b37aaec228e4f5d5ec6e61e0546ee896/prek-0.3.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6ef02ce9d2389daae85f099fd4f34aa5537e3670b5e2a3174c9110ce69958c10", size = 5848197, upload-time = "2026-03-16T08:31:44.975Z" }, + { url = "https://files.pythonhosted.org/packages/65/6b/06371fa895a4ee7b7160685e4d3e5f8d3c21826f27fff8ed00334f646b46/prek-0.3.6-py3-none-win32.whl", hash = "sha256:341763a9264133a34570da53de86bbb785d7caf050bf4b077b4f2b098b48e322", size = 4852902, upload-time = "2026-03-16T08:31:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/63b25796e8cdaea1d62d4a82f4852cb4f52dcbad0cae465e9eabbe6acda8/prek-0.3.6-py3-none-win_amd64.whl", hash = "sha256:32803160223ecb1eefffd941804fc1175dc9376b24d10a0f03fef63dc7e10e7c", size = 5253284, upload-time = "2026-03-16T08:31:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/c031f2c6a30c921d6d3656750676c3436d9b8ada771193d36f26cd998066/prek-0.3.6-py3-none-win_arm64.whl", hash = "sha256:5003c183594e15a2d1e6a744c0ee7b1f7e28d7c2f05a1ea533e31e216b14f062", size = 5101874, upload-time = "2026-03-16T08:31:46.325Z" }, +] + +[[package]] +name = "prettier" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/0e/a3375ca8f83f03662f22cbfee801badc4e4da19e4c06ade5ccfab4297678/prettier-0.0.7.tar.gz", hash = "sha256:6c34b8cd09fd9c8956c05d6395ea3f575e0122dce494ba57685c07065abed427", size = 16124, upload-time = "2022-04-27T09:06:51.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/cf/c401d2af75c2c271375bbe3654e408af2c296650f88b8b3d3942e3c8b63e/prettier-0.0.7-py3-none-any.whl", hash = "sha256:20e76791de41cafe481328dd49552303f29ca192151cee1b120c26f66cae9bfc", size = 16260, upload-time = "2022-04-27T09:06:50.104Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psutil-home-assistant" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/4f/32a51f53d645044740d0513a6a029d782b35bdc51a55ea171ce85034f5b7/psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8", size = 6045, upload-time = "2022-08-25T14:28:39.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, +] + +[[package]] +name = "pycares" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/a0/9c823651872e6a0face3f0311de2a40c8bbcb9c8dcb15680bd019ac56ac7/pycares-5.0.1.tar.gz", hash = "sha256:5a3c249c830432631439815f9a818463416f2a8cbdb1e988e78757de9ae75081", size = 652222, upload-time = "2026-01-01T12:37:00.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b2/4af99ff17acb81377c971831520540d1859bf401dc85712eb4abc2e6751f/pycares-5.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e330e3561be259ad7a1b7b0ce282c872938625f76587fae7ac8d6bc5af1d0c3d", size = 136635, upload-time = "2026-01-01T12:35:53.365Z" }, + { url = "https://files.pythonhosted.org/packages/42/da/e2e1683811c427492ee0e86e8fae8d55eb5cca032220438599991fdad866/pycares-5.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82bd37fec2a3fa62add30d4a3854720f7b051386e2f18e6e8f4ee94b89b5a7b0", size = 131093, upload-time = "2026-01-01T12:35:54.28Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2a/9cf2120cafc19e5c589d5252a9ddd3108cc87e9db09938d16317807de03b/pycares-5.0.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:258c38aaa82ad1d565b4591cdb93d2c191be8e0a2c70926999c8e0b717a01f2a", size = 221096, upload-time = "2026-01-01T12:35:57.096Z" }, + { url = "https://files.pythonhosted.org/packages/2c/cc/c5fbf6377e2d6b1f1618f147ad898e5d8ae1585fc726d6301f07aeda6cac/pycares-5.0.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ccc1b2df8a09ca20eefbe20b9f7a484d376525c0fb173cfadd692320013c6bc5", size = 252330, upload-time = "2026-01-01T12:35:58.182Z" }, + { url = "https://files.pythonhosted.org/packages/3b/df/17a7c518c45bb994f76d9064d2519674e2a3950f895abbe6af123ead04ac/pycares-5.0.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c4dfc80cc8b43dc79e02a15486c58eead5cae0a40906d6be64e2522285b5b39", size = 239799, upload-time = "2026-01-01T12:36:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/d79c94809742b56b9180a9a9ec2937607db0b8eb34b8ca75d86d3114d6dd/pycares-5.0.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f498a6606247bfe896c2a4d837db711eb7b0ba23e409e16e4b23def4bada4b9d", size = 223501, upload-time = "2026-01-01T12:36:02.695Z" }, + { url = "https://files.pythonhosted.org/packages/69/08/83084b67cbce08f44fd803b88816fc80d2fe2fb3d483d5432925df44371b/pycares-5.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a7d197835cdb4b202a3b12562b32799e27bb132262d4aa1ac3ee9d440e8ec22c", size = 223708, upload-time = "2026-01-01T12:36:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/63a6e9ef356c5149b8ec72a694e02207fd8ae643895aeb78a9f0c07f1502/pycares-5.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f78ab823732b050d658eb735d553726663c9bccdeeee0653247533a23eb2e255", size = 251816, upload-time = "2026-01-01T12:36:05.618Z" }, + { url = "https://files.pythonhosted.org/packages/43/1c/1c85c6355cf7bc3ae86a1024d60f9cabdc12af63306a5f59370ac8718a41/pycares-5.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f444ab7f318e9b2c209b45496fb07bff5e7ada606e15d5253a162964aa078527", size = 238259, upload-time = "2026-01-01T12:36:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7f/bd5ff5a460e50433f993560e4e5d229559a8bf271dbdf6be832faf1973b5/pycares-5.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de80997de7538619b7dd28ec4371e5172e3f9480e4fc648726d3d5ba661ca05", size = 223732, upload-time = "2026-01-01T12:36:09.893Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/e77738366e00dc0918bbeb0c8fc63579e5d9cec748a2b838e207e548b5d9/pycares-5.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:206ce9f3cb9d51f5065c81b23c22996230fbc2cf58ae22834c623631b2b473aa", size = 120847, upload-time = "2026-01-01T12:36:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/81/17/758e9af7ee8589ac6deddf7ea56d75b982f155bc2052ef61c45d5f371389/pycares-5.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:45fb3b07231120e8cb5b75be7f15f16115003e9251991dc37a3e5c63733d63b5", size = 112595, upload-time = "2026-01-01T12:36:12.973Z" }, + { url = "https://files.pythonhosted.org/packages/56/12/4f1d418fed957fc96089c69d9ec82314b3b91c48c7f9463385842acad9c4/pycares-5.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:602f3eac4b880a2527d21f52b2319cb10fde9225d103d338c4d0b2b07f136849", size = 137061, upload-time = "2026-01-01T12:36:15.027Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/559cea98a8a5d0f38b50b4b812a07fdbcdb1a961bed9e2e9d5d343e53c6f/pycares-5.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1c3736deef003f0c57bc4e7f94d54270d0824350a8f5ceaba3a20b2ce8fb427", size = 131551, upload-time = "2026-01-01T12:36:16.74Z" }, + { url = "https://files.pythonhosted.org/packages/34/cd/aee5d8070888d7be509d4f32a348e2821309ec67980498e5a974cd9e4990/pycares-5.0.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e63328df86d37150ce697fb5d9313d1d468dd4dddee1d09342cb2ed241ce6ad9", size = 230409, upload-time = "2026-01-01T12:36:18.909Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/15d5cf7d8e7af4b4ce3e19ea117dfe565c08d60d82f043ad23843703a135/pycares-5.0.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57f6fd696213329d9a69b9664a68b1ff2a71ccbdc1fc928a42c9a92858c1ec5d", size = 261297, upload-time = "2026-01-01T12:36:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/af/46/24f6ddc7a37ec6eaa1c38f617f39624211d8e7cdca49b644bfc5f467f275/pycares-5.0.1-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d0878edabfbecb48a29e8769284003d8dbc05936122fe361849cd5fa52722e0", size = 248071, upload-time = "2026-01-01T12:36:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/7eb7fe44f0db55b9083725ab7a084874c2dc02806d9613e07e719838c2ab/pycares-5.0.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50e21f27a91be122e066ddd78c2d0d2769e547561481d8342a9d652a345b89f7", size = 232073, upload-time = "2026-01-01T12:36:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cd/993b17e0c049a56b5af4df3fd053acc57b37e17e0dcd709b2d337c22d57d/pycares-5.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:97ceda969f5a5d5c6b15558b658c29e4301b3a2c4615523797b5f9d4ac74772e", size = 232815, upload-time = "2026-01-01T12:36:27.798Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ff/170177bcc5dff31e735f209f5de63362f513ac18846c83d50e4e68f57866/pycares-5.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d1713e602ab09882c3e65499b2cc763bff0371117327cad704cf524268c2604", size = 261111, upload-time = "2026-01-01T12:36:29.94Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4a/4c6497b8ca9279b4038ee8c7e2c49504008d594d06a044e00678b30c10fe/pycares-5.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:954a379055d6c66b2e878b52235b382168d1a3230793ff44454019394aecac5e", size = 246311, upload-time = "2026-01-01T12:36:31.352Z" }, + { url = "https://files.pythonhosted.org/packages/06/19/1603f51f0d73bf34017a9e6967540c2bc138f9541aa7cc1ef38990b3ce9d/pycares-5.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:145d8a20f7fd1d58a2e49b7ef4309ec9bdcab479ac65c2e49480e20d3f890c23", size = 232027, upload-time = "2026-01-01T12:36:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/7a/de/c000a682757b84688722ac232a24a86b6f195f1f4732432ecf35d0a768a5/pycares-5.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ebc9daba03c7ff3f62616c84c6cb37517445d15df00e1754852d6006039eb4a4", size = 121267, upload-time = "2026-01-01T12:36:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c4/8bfffecd08b9b198113fcff5f0ab84bbe696f07dec46dd1ccae0e7b28c23/pycares-5.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e0a86eff6bf9e91d5dd8876b1b82ee45704f46b1104c24291d3dea2c1fc8ebcb", size = 113043, upload-time = "2026-01-01T12:36:37.895Z" }, +] + +[[package]] +name = "pycognito" +version = "2024.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "envs" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/67/3975cf257fcc04903686ef87d39be386d894a0d8182f43d37e9cbfc9609f/pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4", size = 31182, upload-time = "2024-05-16T10:02:28.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/7a/f38dd351f47596b22ddbde1b8906e7f43d14be391dcdbd0c2daba886f26c/pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea", size = 26607, upload-time = "2024-05-16T10:02:27.3Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + +[[package]] +name = "pylint-per-file-ignores" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/3d/21bec2f2f432519616c34a64ba0766ef972fdfb6234a86bb1b8baf4b0c7c/pylint_per_file_ignores-1.4.0.tar.gz", hash = "sha256:c0de7b3d0169571aefaa1ac3a82a265641b8825b54a0b6f5ef27c3b76b988609", size = 4419, upload-time = "2025-01-17T21:35:02.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/0e/bf3473d86648a17e6dd6ee9e6abce526b077169031177f4f2031368f864a/pylint_per_file_ignores-1.4.0-py3-none-any.whl", hash = "sha256:0cd82d22551738b4e63a0aa1dab2a1fc4016e8f27f1429159616483711e122fd", size = 4888, upload-time = "2025-01-17T21:35:00.371Z" }, +] + +[[package]] +name = "pyobjc-core" +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/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" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/86/99/8ed2f0ca02b9abe204966142bd8c4501cf6da94234cc320c4c0562c467e8/pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1", size = 13408, upload-time = "2025-11-14T09:44:16.558Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/99/32/15e08a0c4bb536303e1568e2ba5cae1ce39a2e026a03aea46173af4c7a2d/pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7", size = 15976, upload-time = "2025-11-14T09:53:07.936Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "pyrfc3339" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589, upload-time = "2025-08-23T16:40:31.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, +] + +[[package]] +name = "pyric" +version = "0.1.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" } + +[[package]] +name = "pyright" +version = "1.1.405" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, +] + +[[package]] +name = "pytest-aiohttp" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/4b/d326890c153f2c4ce1bf45d07683c08c10a1766058a22934620bc6ac6592/pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc", size = 12842, upload-time = "2025-01-23T12:44:04.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/0f/e6af71c02e0f1098eaf7d2dbf3ffdf0a69fc1e0ef174f96af05cef161f1b/pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d", size = 8932, upload-time = "2025-01-23T12:44:03.27Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-freezer" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freezegun" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/f0/98dcbc5324064360b19850b14c84cea9ca50785d921741dbfc442346e925/pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a", size = 3177, upload-time = "2024-12-12T08:53:08.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e9/30252bc05bcf67200a17f4f0b4cc7598f0a68df4fa9fa356193aa899f145/pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59", size = 3192, upload-time = "2024-12-12T08:53:07.641Z" }, +] + +[[package]] +name = "pytest-github-actions-annotate-failures" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d4/c54ee6a871eee4a7468e3a8c0dead28e634c0bc2110c694309dcb7563a66/pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7", size = 11248, upload-time = "2025-01-17T22:39:32.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/73/7b0b15cb8605ee967b34aa1d949737ab664f94e6b0f1534e8339d9e64ab2/pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf", size = 6030, upload-time = "2025-01-17T22:39:31.701Z" }, +] + +[[package]] +name = "pytest-homeassistant-custom-component" +version = "0.13.315" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "freezegun" }, + { name = "homeassistant" }, + { name = "license-expression" }, + { name = "mock-open" }, + { name = "numpy" }, + { name = "paho-mqtt" }, + { name = "pipdeptree" }, + { name = "pydantic" }, + { name = "pylint-per-file-ignores" }, + { name = "pytest" }, + { name = "pytest-aiohttp" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-freezer" }, + { name = "pytest-github-actions-annotate-failures" }, + { name = "pytest-picked" }, + { name = "pytest-socket" }, + { name = "pytest-sugar" }, + { name = "pytest-timeout" }, + { name = "pytest-unordered" }, + { name = "pytest-xdist" }, + { name = "requests-mock" }, + { name = "respx" }, + { name = "sqlalchemy" }, + { name = "syrupy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/2ee59e89d3a101a2909e8132990946267255cddafa3936295adad48902b0/pytest_homeassistant_custom_component-0.13.315.tar.gz", hash = "sha256:c52fc8596e43d6a4a43f4514094dabf0a7254089a22071473aa33d6558bb0880", size = 65358, upload-time = "2026-02-14T05:21:50.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/e018099a6eeb31da9a492ca8b23258d4baaa64e71ed80f0df8420cbf5ade/pytest_homeassistant_custom_component-0.13.315-py3-none-any.whl", hash = "sha256:1ac286b2e5c76c2dacc31ace8afdd30456da601d048ef84ee39a1739c08ab013", size = 71089, upload-time = "2026-02-14T05:21:48.779Z" }, +] + +[[package]] +name = "pytest-picked" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/e4/51a54dd6638fd4a7c45bb20a737235fd92cbb4d24b5ff681d64ace5d02e9/pytest_picked-0.5.1.tar.gz", hash = "sha256:6634c4356a560a5dc3dba35471865e6eb06bbd356b56b69c540593e9d5620ded", size = 8401, upload-time = "2024-11-06T23:19:52.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/81/450c017746caab376c4b6700439de9f1cc7d8e1f22dec3c1eb235cd9ad3e/pytest_picked-0.5.1-py3-none-any.whl", hash = "sha256:af65c4763b51dc095ae4bc5073a962406902422ad9629c26d8b01122b677d998", size = 6608, upload-time = "2024-11-06T23:19:51.284Z" }, +] + +[[package]] +name = "pytest-socket" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389, upload-time = "2024-01-28T20:17:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754, upload-time = "2024-01-28T20:17:22.105Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-unordered" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/3e/6ec9ec74551804c9e005d5b3cbe1fd663f03ed3bd4bdb1ce764c3d334d8e/pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f", size = 7930, upload-time = "2025-06-03T12:56:04.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/95/ae2875e19472797e9672b65412858ab6639d8e55defd9859241e5ff80d02/pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584", size = 6210, upload-time = "2025-06-03T12:36:06.66Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-direnv" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/64/7903608ed54e13bddcf40944f71fbedf6ccb422975713ba37077a424e855/python_direnv-0.2.2.tar.gz", hash = "sha256:0fe2fb834c901d675edcacc688689cfcf55cf06d9cf27dc7d3768a6c38c35f00", size = 6960, upload-time = "2024-09-06T01:21:49.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ce/09d7491019a54f3614af00dadd3a0caf0514f417b57390195059dcb7c35c/python_direnv-0.2.2-py3-none-any.whl", hash = "sha256:a617d14f093f13dd9a858e88c2914bdb16edee992b5148efd8c23c10ca1b50d9", size = 7339, upload-time = "2024-09-06T01:21:48.468Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "radon" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "mando" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "securetar" +version = "2025.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/5b/da5f56ad39cbb1ca49bd0d4cccde7e97ea7d01fa724fa953746fa2b32ee6/securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1", size = 16124, upload-time = "2025-02-25T14:17:51.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e0/b93a18e9bb7f7d2573a9c6819d42d996851edde0b0406d017067d7d23a0a/securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4", size = 11545, upload-time = "2025-02-25T14:17:50.832Z" }, +] + +[[package]] +name = "sentence-stream" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/69/f3d048692aac843f41102507f6257138392ec841c16718f0618d27051caf/sentence_stream-1.3.0.tar.gz", hash = "sha256:b06261d35729de97df9002a1cc708f9a888f662b80d5d6d008ee69c51f36041b", size = 10049, upload-time = "2026-01-08T16:25:06.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b6/48339c109bab6f54ff608800773b9425464c6cbf7fd3f2ba01294d78be3d/sentence_stream-1.3.0-py3-none-any.whl", hash = "sha256:7448d131315b85eefdf238e5edd9caa62899acf609145d5e0e10c09812eb8a1d", size = 8707, upload-time = "2026-01-08T16:25:05.918Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snitun" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e2/b5bbf04971d1c3e07a3e16a706ea3c1a4b711c6d8c9566e8012772d3351a/snitun-0.45.1.tar.gz", hash = "sha256:d76d48cf4190ea59e8f63892da9c18499bfc6ca796220a463c6f3b32099d661c", size = 43335, upload-time = "2025-09-25T05:24:07.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1b/83ff83003994bc8b56483c75a710de588896c167c7c42d66d059a2eb48dc/snitun-0.45.1-py3-none-any.whl", hash = "sha256:c1fa4536320ec3126926ade775c429e20664db1bc61d8fec0e181dc393d36ab4", size = 51236, upload-time = "2025-09-25T05:24:06.412Z" }, +] + +[[package]] +name = "span" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "homeassistant" }, + { name = "span-panel-api" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "homeassistant-stubs" }, + { name = "isort" }, + { name = "mypy" }, + { name = "prek" }, + { name = "prettier" }, + { name = "pylint" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-homeassistant-custom-component" }, + { name = "python-direnv" }, + { name = "radon" }, + { name = "ruff" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, + { name = "voluptuous-stubs" }, +] + +[package.metadata] +requires-dist = [ + { name = "homeassistant", specifier = "==2026.2.2" }, + { name = "span-panel-api", editable = "../span-panel-api" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", extras = ["toml"], specifier = "==1.8.6" }, + { name = "homeassistant-stubs", specifier = "==2026.2.2" }, + { name = "isort" }, + { name = "mypy", specifier = "==1.19.1" }, + { name = "prek", specifier = ">=0.3.6" }, + { name = "prettier" }, + { name = "pylint", specifier = "==4.0.5" }, + { name = "pyright", specifier = "==1.1.405" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-homeassistant-custom-component", specifier = ">=0.13.315" }, + { name = "python-direnv" }, + { name = "radon", specifier = "==6.0.1" }, + { name = "ruff", specifier = "==0.15.1" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, + { name = "voluptuous-stubs" }, +] + +[[package]] +name = "span-panel-api" +version = "2.3.2" +source = { editable = "../span-panel-api" } +dependencies = [ + { name = "httpx" }, + { name = "paho-mqtt" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1,<0.29.0" }, + { name = "paho-mqtt", specifier = ">=2.0.0,<3.0.0" }, + { name = "pyyaml", specifier = ">=6.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.9.4" }, + { name = "black" }, + { name = "coverage" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov" }, + { name = "radon" }, + { name = "ruff", specifier = ">=0.15.5" }, + { name = "twine" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, + { name = "vulture", specifier = ">=2.14" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts" }, + { name = "standard-chunk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-telnetlib" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, +] + +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "syrupy" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/90/1a442d21527009d4b40f37fe50b606ebb68a6407142c2b5cc508c34b696b/syrupy-5.0.0.tar.gz", hash = "sha256:3282fe963fa5d4d3e47231b16d1d4d0f4523705e8199eeb99a22a1bc9f5942f2", size = 48881, upload-time = "2025-09-28T21:15:12.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/9a/6c68aad2ccfce6e2eeebbf5bb709d0240592eb51ff142ec4c8fbf3c2460a/syrupy-5.0.0-py3-none-any.whl", hash = "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546", size = 49087, upload-time = "2025-09-28T21:15:11.639Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uart-devices" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/08/a8fd6b3dd2cb92344fb4239d4e81ee121767430d7ce71f3f41282f7334e0/uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34", size = 5167, upload-time = "2025-02-22T16:47:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/64/edf33c2d7fba7d6bf057c9dc4235bfc699517ea4c996240a1a9c2bf51c29/uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123", size = 4827, upload-time = "2025-02-22T16:47:04.286Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/44/2ef5e7218ad021fda3fedcb6c1347dd3bf972e9cbdea94644aaa7e4884bb/ulid_transform-1.5.2.tar.gz", hash = "sha256:9a5caf279ec21789ddc2f36b9008ce33a3197d7d66fdd7628fbebec9ba778829", size = 14247, upload-time = "2025-10-04T20:57:22.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c5/8eb0aa7bcd5cfb772ae8535b63d8d5fe7503c3d0adda931e7ee7e5e9af39/ulid_transform-1.5.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bae1b0f6041bd8c7d49019c14fb94e42ccab2b59083e7b0cb9f4d13483d7435a", size = 41085, upload-time = "2025-10-04T21:04:13.468Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ff/45cfb8ceaa67eea28c7a1a90242c1ade01b2a1b714feec7af47c086c6945/ulid_transform-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c4a61fd13ced6b0b96f5983ef4e57ad8adefed4361b6d0f55a2bbfbb18b17d8", size = 41575, upload-time = "2025-10-04T21:04:14.379Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/ab80688863e7228732736ec39764910891b0978ae0d1953395ce2d505cdc/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8065ddfd43827b1299a64da4437161a4f3fa1f03c05d838a3a8c82bc1d014518", size = 48948, upload-time = "2025-10-04T21:04:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/9bd352aac0fddf167a70dcab386cc4d8d099676531a89afa5019c6f1dbe7/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b60965067d13942b0eaaaf986da5eff4ba8f1261b379ca1dac78afe47d940f1a", size = 45789, upload-time = "2025-10-04T21:04:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/cb/90/0b4b4e0ac6061ea90cbdc526e17a75aad0fefafadbe43c56bfd7a77b8a85/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e97a11b56b9e5537ef4521a97fc095b49d849c0ac0ec8d30a2974bd69e5948d", size = 49351, upload-time = "2025-10-04T21:04:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7b/57da5afcd538306c44c093ef314bef4b04768f04b3c509144ed201b7d374/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4915062eee740eefa937459ef468f7f1e35bd2ad5bffdf4245051d656df2c4", size = 1028528, upload-time = "2025-10-04T21:04:19.278Z" }, + { url = "https://files.pythonhosted.org/packages/a1/96/5d3c3464bb64b4fd6b6605787b3a4fef982ba207ba8a8ecc432543fe954e/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7d4cf4bb26fe102dfd1bd10c5b18712fe7640433839c8d9dd20e2d8ccefa972d", size = 894080, upload-time = "2025-10-04T21:04:20.548Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/05dc2a2b2f981617a3ba1cdd8277b86504c4293feefc3a3ba342bac7cbec/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad4953675e6dec400de633087f61cbb38d0ad978d57b60cc3539f7b821d9559", size = 1080292, upload-time = "2025-10-04T21:04:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/f3/78/06032df3d6cc211a4d3edad92ed9433fa84e654c42936c1e268d53feab31/ulid_transform-1.5.2-cp314-cp314-win32.whl", hash = "sha256:d6793d4c477b30d95ed84123cc73d515ba4dac58cd01e7584637421b377349d3", size = 39903, upload-time = "2025-10-04T21:04:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/c0b0f757e9d2b5c565624227336fcc353c5e3160667d451ac361d342b11d/ulid_transform-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:dbbe98fd8b46431e3a15268e0dceeb80291ebfa7741d1ee692006928c0900d0c", size = 41987, upload-time = "2025-10-04T21:04:24.576Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/4f74de4fe96bb85fcc1e7fd572527aafd0999d48de3f6d1a41c66cdebc41/ulid_transform-1.5.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:08286ccc6bac0107e1bd5415a28e730d88089293ba5ce51dc5883175eccc31e2", size = 68442, upload-time = "2025-10-04T21:04:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/854e4bd4539be70e5714002198e0565f897cf5b65203d9c230b362cc41df/ulid_transform-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed0b533c936cb120312cd98ca1c8ec1f8af66bac6bc08426c030b48291d5505e", size = 69113, upload-time = "2025-10-04T21:04:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/73d6aa7c63e1e472bbe8e54a0892cdec78dc8438798e9ea518f1c371f640/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58617bae6fc21507f5151328faf7b77c6ba6a615b42efd18f494564354a3ce68", size = 85341, upload-time = "2025-10-04T21:04:28.046Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/263774b2249d683addd0de5746d4c4debbb33966277d4d33390150944ba3/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e24e68971a64a04af2d0b3df98bfe0087c85d35a1b02fa0bbf43a3a0a99dccf6", size = 78616, upload-time = "2025-10-04T21:04:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/2eda3645a002a9fd141c19fd7416de87adaa12b25f8916b42b78644dbddd/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb5da66ec5e7d97f695dd16637d5a8816bb9661df43ff1f2de0d46071d96a7a8", size = 85582, upload-time = "2025-10-04T21:04:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/e08e975e4ed61fb2ae7014591fcc7e5f1a62966c7ed53fc1c95c3da78923/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:397646cf156aa46456cd8504075d117d2983ebf2cff01955c3add6280d0fb3c8", size = 1066091, upload-time = "2025-10-04T21:04:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e7/43474bf4ec56eadf2509939585894ef094dc364143596f62639bebd6d42e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:dc5ac2ffa704a21df2a36cea47ac1022fb6f8ab03abe31a5f7b81312f972e2c2", size = 926160, upload-time = "2025-10-04T21:04:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/36df80548d96ffdaa3ae124854bbd5a0a0b07da22a8d22b301e5ec17de6e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6f29b8004fba0da7061b5eecf6101c6283377b6cd04f3626089cc67d9342c8fd", size = 1116228, upload-time = "2025-10-04T21:04:35.325Z" }, + { url = "https://files.pythonhosted.org/packages/9f/85/9cd506a4e0fe06946c3fd75b89c7d1e9a175a5ac11dfd8e4cc56658ff389/ulid_transform-1.5.2-cp314-cp314t-win32.whl", hash = "sha256:c09f58aff7a4974f560dd5fb19dd5144e8964371fcb1971bffa817c9abcb2232", size = 67476, upload-time = "2025-10-04T21:04:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/41/a9/161ab974510c78a28155de33091a0063ed84b110e6d4fb866d5110112ce9/ulid_transform-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:445e14170301229a486e8815c2f9cec4a10b3e3cd4c9aa509689443d05e4f020", size = 72206, upload-time = "2025-10-04T21:04:38.286Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "usb-devices" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/48/dbe6c4c559950ebebd413e8c40a8a60bfd47ddd79cb61b598a5987e03aad/usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d", size = 5421, upload-time = "2023-12-16T19:59:53.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c9/26171ae5b78d72dd006bbc51ca9baa2cbb889ae8e91608910207482108fd/usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf", size = 5349, upload-time = "2023-12-16T19:59:51.604Z" }, +] + +[[package]] +name = "uv" +version = "0.9.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/6a/ef4ea19097ecdfd7df6e608f93874536af045c68fd70aa628c667815c458/uv-0.9.26.tar.gz", hash = "sha256:8b7017a01cc48847a7ae26733383a2456dd060fc50d21d58de5ee14f6b6984d7", size = 3790483, upload-time = "2026-01-15T20:51:33.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/e1/5c0b17833d5e3b51a897957348ff8d937a3cdfc5eea5c4a7075d8d7b9870/uv-0.9.26-py3-none-linux_armv6l.whl", hash = "sha256:7dba609e32b7bd13ef81788d580970c6ff3a8874d942755b442cffa8f25dba57", size = 22638031, upload-time = "2026-01-15T20:51:44.187Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8b/68ac5825a615a8697e324f52ac0b92feb47a0ec36a63759c5f2931f0c3a0/uv-0.9.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b815e3b26eeed00e00f831343daba7a9d99c1506883c189453bb4d215f54faac", size = 21507805, upload-time = "2026-01-15T20:50:42.574Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a2/664a338aefe009f6e38e47455ee2f64a21da7ad431dbcaf8b45d8b1a2b7a/uv-0.9.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b012e6c4dfe767f818cbb6f47d02c207c9b0c82fee69a5de6d26ffb26a3ef3c", size = 20249791, upload-time = "2026-01-15T20:50:49.835Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3d/b8186a7dec1346ca4630c674b760517d28bffa813a01965f4b57596bacf3/uv-0.9.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ea296b700d7c4c27acdfd23ffaef2b0ecdd0aa1b58d942c62ee87df3b30f06ac", size = 22039108, upload-time = "2026-01-15T20:51:00.675Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a9/687fd587e7a3c2c826afe72214fb24b7f07b0d8b0b0300e6a53b554180ea/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1ba860d2988efc27e9c19f8537a2f9fa499a8b7ebe4afbe2d3d323d72f9aee61", size = 22174763, upload-time = "2026-01-15T20:50:46.471Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/7fa03ee7d59e562fca1426436f15a8c107447d41b34e0899e25ee69abfad/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8610bdfc282a681a0a40b90495a478599aa3484c12503ef79ef42cd271fd80fe", size = 22189861, upload-time = "2026-01-15T20:51:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/4be446a2ec09f3c428632b00a138750af47c76b0b9f987e9a5b52fef0405/uv-0.9.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4bf700bd071bd595084b9ee0a8d77c6a0a10ca3773d3771346a2599f306bd9c", size = 23005589, upload-time = "2026-01-15T20:50:57.185Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/860990b812136695a63a8da9fb5f819c3cf18ea37dcf5852e0e1b795ca0d/uv-0.9.26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:89a7beea1c692f76a6f8da13beff3cbb43f7123609e48e03517cc0db5c5de87c", size = 24713505, upload-time = "2026-01-15T20:51:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/01/43/5d7f360d551e62d8f8bf6624b8fca9895cea49ebe5fce8891232d7ed2321/uv-0.9.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:182f5c086c7d03ad447e522b70fa29a0302a70bcfefad4b8cd08496828a0e179", size = 24342500, upload-time = "2026-01-15T20:51:47.863Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9c/2bae010a189e7d8e5dc555edcfd053b11ce96fad2301b919ba0d9dd23659/uv-0.9.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d8c62a501f13425b4b0ce1dd4c6b82f3ce5a5179e2549c55f4bb27cc0eb8ef8", size = 23222578, upload-time = "2026-01-15T20:51:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/38/16/a07593a040fe6403c36f3b0a99b309f295cbfe19a1074dbadb671d5d4ef7/uv-0.9.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e89798bd3df7dcc4b2b4ac4e2fc11d6b3ff4fe7d764aa3012d664c635e2922", size = 23250201, upload-time = "2026-01-15T20:51:19.117Z" }, + { url = "https://files.pythonhosted.org/packages/23/a0/45893e15ad3ab842db27c1eb3b8605b9b4023baa5d414e67cfa559a0bff0/uv-0.9.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:60a66f1783ec4efc87b7e1f9bd66e8fd2de3e3b30d122b31cb1487f63a3ea8b7", size = 22229160, upload-time = "2026-01-15T20:51:22.931Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c0/20a597a5c253702a223b5e745cf8c16cd5dd053080f896bb10717b3bedec/uv-0.9.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63c6a1f1187facba1fb45a2fa45396980631a3427ac11b0e3d9aa3ebcf2c73cf", size = 23090730, upload-time = "2026-01-15T20:51:26.611Z" }, + { url = "https://files.pythonhosted.org/packages/40/c9/744537867d9ab593fea108638b57cca1165a0889cfd989981c942b6de9a5/uv-0.9.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c6d8650fbc980ccb348b168266143a9bd4deebc86437537caaf8ff2a39b6ea50", size = 22436632, upload-time = "2026-01-15T20:51:12.045Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e2/be683e30262f2cf02dcb41b6c32910a6939517d50ec45f502614d239feb7/uv-0.9.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:25278f9298aa4dade38241a93d036739b0c87278dcfad1ec1f57e803536bfc49", size = 23480064, upload-time = "2026-01-15T20:50:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/50/3e/4a7e6bc5db2beac9c4966f212805f1903d37d233f2e160737f0b24780ada/uv-0.9.26-py3-none-win32.whl", hash = "sha256:10d075e0193e3a0e6c54f830731c4cb965d6f4e11956e84a7bed7ed61d42aa27", size = 21000052, upload-time = "2026-01-15T20:51:40.753Z" }, + { url = "https://files.pythonhosted.org/packages/07/5d/eb80c6eff2a9f7d5cf35ec84fda323b74aa0054145db28baf72d35a7a301/uv-0.9.26-py3-none-win_amd64.whl", hash = "sha256:0315fc321f5644b12118f9928086513363ed9b29d74d99f1539fda1b6b5478ab", size = 23684930, upload-time = "2026-01-15T20:51:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9d/3b2631931649b1783f5024796ca8ad2b42a01a829b9ce1202d973cc7bce5/uv-0.9.26-py3-none-win_arm64.whl", hash = "sha256:344ff38749b6cd7b7dfdfb382536f168cafe917ae3a5aa78b7a63746ba2a905b", size = 22158123, upload-time = "2026-01-15T20:51:30.939Z" }, +] + +[[package]] +name = "voluptuous" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "voluptuous" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/15/ac7a98afd478e9afc804354fe9d9715e0e560a590fdd425b22b65a152bb3/voluptuous_openapi-0.2.0.tar.gz", hash = "sha256:2366be934c37bb5fd8ed6bd5a2a46b1079b57dfbdf8c6c02e88f4ca13e975073", size = 15789, upload-time = "2025-08-21T04:49:16.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/eb/2ae58431a078318f03267f196137282ea6f01ea7f7e0fcba2b25a30b0bf2/voluptuous_openapi-0.2.0-py3-none-any.whl", hash = "sha256:d51f07be8af44b11570b7366785d90daa716b7fd11ea2845803763ae551f35cf", size = 10180, upload-time = "2025-08-21T04:49:15.885Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "voluptuous" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/70/03a9b61324e1bb8b16682455b8b953bccd1001a28e43478c86f539e26285/voluptuous_serialize-2.7.0.tar.gz", hash = "sha256:d0da959f2fd93c8f1eb779c5d116231940493b51020c2c1026bab76eb56cd09e", size = 9202, upload-time = "2025-08-17T10:43:04.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/41/d536d9cf39821c35cc13aff403728e60e32b2fd711c240b6b9980af1c03f/voluptuous_serialize-2.7.0-py3-none-any.whl", hash = "sha256:ee3ebecace6136f38d0bf8c20ee97155db2486c6b2d0795563fafd04a519e76f", size = 7850, upload-time = "2025-08-17T10:43:03.498Z" }, +] + +[[package]] +name = "voluptuous-stubs" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/18/9a4c1be6336a4f403ba3237c594fc2c68eb3cbf3774f0c79d8c84968ce9f/voluptuous-stubs-0.1.1.tar.gz", hash = "sha256:70fb1c088242f20e11023252b5648cd77f831f692cd910c8f9713cc135cf8cc8", size = 3654, upload-time = "2020-04-24T04:16:30.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/fc/aaf18e1cc066277df80aff1d988284216ca3881f1e6601f2362189b683c5/voluptuous_stubs-0.1.1-py3-none-any.whl", hash = "sha256:f216c427ed7e190b8413e26cf4f67e1bda692ea8225ed0d875f7724d10b7cb10", size = 4971, upload-time = "2020-04-24T04:16:28.667Z" }, +] + +[[package]] +name = "webrtc-models" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mashumaro" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/e8/050ffe3b71ff44d3885eee2bed763ca937e2a30bc950d866f22ba657776b/webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526", size = 9411, upload-time = "2024-11-18T17:43:45.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/e7/62f29980c9e8d75af93b642a0c37aa8e201fd5268ba3a7179c172549bac3/webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea", size = 7476, upload-time = "2024-11-18T17:43:44.165Z" }, +] + +[[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/c8/87/88bd98419a9da77a68e030593fee41702925a7ad8a8aec366945258cbb31/winrt_runtime-3.2.1-cp314-cp314-win32.whl", hash = "sha256:9b6298375468ac2f6815d0c008a059fc16508c8f587e824c7936ed9216480dad", size = 210257, upload-time = "2025-09-20T07:06:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/e5c2a10d287edd9d3ee8dc24bf7d7f335636b92bf47119768b7dd2fd1669/winrt_runtime-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:e36e587ab5fd681ee472cd9a5995743f75107a1a84d749c64f7e490bc86bc814", size = 241873, upload-time = "2025-09-20T07:06:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/eb9e78397132175f70dd51dfa4f93e489c17d6b313ae9dce60369b8d84a7/winrt_runtime-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:35d6241a2ebd5598e4788e69768b8890ee1eee401a819865767a1fbdd3e9a650", size = 416222, upload-time = "2025-09-20T07:06:43.376Z" }, +] + +[[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/b7/95/91cfdf941a1ba791708ab3477fc4e46793c8fe9117fc3e0a8c5ac5d7a09c/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win32.whl", hash = "sha256:de36ded53ca3ba12fc6dd4deb14b779acc391447726543815df4800348aad63a", size = 109015, upload-time = "2025-09-20T07:09:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/7460655628d0f340a93524f5236bb9f8514eb0e1d334b38cba8a89f6c1a6/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3295d932cc93259d5ccb23a41e3a3af4c78ce5d6a6223b2b7638985f604fa34c", size = 115931, upload-time = "2025-09-20T07:09:51.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/70/e1248dea2ab881eb76b61ff1ad6cb9c07ac005faf99349e4af0b29bc3f1b/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1f61c178766a1bbce0669f44790c6161ff4669404c477b4aedaa576348f9e102", size = 109561, upload-time = "2025-09-20T07:09:52.733Z" }, +] + +[[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/27/3d/421d04a20037370baf13de929bc1dc5438b306a76fe17275ec5d893aae6c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win32.whl", hash = "sha256:2985565c265b3f9eab625361b0e40e88c94b03d89f5171f36146f2e88b3ee214", size = 92264, upload-time = "2025-09-20T07:09:53.563Z" }, + { url = "https://files.pythonhosted.org/packages/07/c7/43601ab82fe42bcff430b8466d84d92b31be06cc45c7fd64e9aac40f7851/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d102f3fac64fde32332e370969dfbc6f37b405d8cc055d9da30d14d07449a3c2", size = 97517, upload-time = "2025-09-20T07:09:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/e3303f6a25a2d98e424b06580fc85bbfd068f383424c67fa47cb1b357a46/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:ffeb5e946cd42c32c6999a62e240d6730c653cdfb7b49c7839afba375e20a62a", size = 94122, upload-time = "2025-09-20T07:09:55.187Z" }, +] + +[[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/b7/32/cb447ca7730a1e05730272309b074da6a04af29a8c0f5121014db8a2fc02/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win32.whl", hash = "sha256:d5f83739ca370f0baf52b0400aebd6240ab80150081fbfba60fd6e7b2e7b4c5f", size = 185249, upload-time = "2025-09-20T07:09:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fa/f465d5d44dda166bf7ec64b7a950f57eca61f165bfe18345e9a5ea542def/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:13786a5853a933de140d456cd818696e1121c7c296ae7b7af262fc5d2cffb851", size = 193739, upload-time = "2025-09-20T07:09:59.893Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/51c53ac3c704cd92da5ed7e7b9b57159052f6e46744e4f7e447ed708aa22/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:5140682da2860f6a55eb6faf9e980724dc457c2e4b4b35a10e1cebd8fc97d892", size = 194836, upload-time = "2025-09-20T07:10:00.87Z" }, +] + +[[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/41/31/5785cd1ec54dc0f0e6f3e6a466d07a62b8014a6e2b782e80444ef87e83ab/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win32.whl", hash = "sha256:e087364273ed7c717cd0191fed4be9def6fdf229fe9b536a4b8d0228f7814106", size = 134252, upload-time = "2025-09-20T07:10:12.935Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/68d91068048410f49794c0b19c45759c63ca559607068cfe5affba2f211b/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:0da1ddb8285d97a6775c36265d7157acf1bbcb88bcc9a7ce9a4549906c822472", size = 145509, upload-time = "2025-09-20T07:10:13.797Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a4/898951d5bfc474aa9c7d133fe30870f0f2184f4ba3027eafb779d30eb7bc/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:09bf07e74e897e97a49a9275d0a647819254ddb74142806bbbcf4777ed240a22", size = 141334, upload-time = "2025-09-20T07:10:14.637Z" }, +] + +[[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/76/79/4627afae6b389ddd1e5f1d691663c6b14d6c8f98959082aed1217cc57ef9/winrt_windows_devices_radios-3.2.1-cp314-cp314-win32.whl", hash = "sha256:21452e1cae50e44cd1d5e78159e1b9986ac3389b66458ad89caa196ce5eca2d6", size = 39521, upload-time = "2025-09-20T07:11:17.992Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/c6aea91908ee7279ed51d12157bc8aeecb8850af2441073c3c91b261ad31/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:6a8413e586fe597c6849607885cca7e0549da33ae5699165d11f7911534c6eaf", size = 41121, upload-time = "2025-09-20T07:11:18.747Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/652f14e3c501452ad8e0723518d9bbd729219b47f4a4dbe2966c2f82dca8/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:39129fd9d09103adb003575f59881c1a5a70a43310547850150b46c6f4020312", size = 38114, upload-time = "2025-09-20T07:11:19.599Z" }, +] + +[[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/e3/0a/d77346e39fe0c81f718cde49f83fe77c368c0e14c6418f72dfa1e7ef22d0/winrt_windows_foundation-3.2.1-cp314-cp314-win32.whl", hash = "sha256:35e973ab3c77c2a943e139302256c040e017fd6ff1a75911c102964603bba1da", size = 114590, upload-time = "2025-09-20T07:11:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/4d2b545bea0f34f68df6d4d4ca22950ff8a935497811dccdc0ca58737a05/winrt_windows_foundation-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22a7ebcec0d262e60119cff728f32962a02df60471ded8b2735a655eccc0ef5", size = 122148, upload-time = "2025-09-20T07:11:50.826Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ed/b9d3a11cac73444c0a3703200161cd7267dab5ab85fd00e1f965526e74a8/winrt_windows_foundation-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3be7fbae829b98a6a946db4fbaf356b11db1fbcbb5d4f37e7a73ac6b25de8b87", size = 114360, upload-time = "2025-09-20T07:11:51.626Z" }, +] + +[[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/e1/47/b3301d964422d4611c181348149a7c5956a2a76e6339de451a000d4ae8e7/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win32.whl", hash = "sha256:33188ed2d63e844c8adfbb82d1d3d461d64aaf78d225ce9c5930421b413c45ab", size = 62211, upload-time = "2025-09-20T07:11:52.411Z" }, + { url = "https://files.pythonhosted.org/packages/20/59/5f2c940ff606297129e93ebd6030c813e6a43a786de7fc33ccb268e0b06b/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d4cfece7e9c0ead2941e55a1da82f20d2b9c8003bb7a8853bb7f999b539f80a4", size = 70399, upload-time = "2025-09-20T07:11:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/2c8eb89062c71d4be73d618457ed68e7e2ba29a660ac26349d44fc121cbf/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3884146fea13727510458f6a14040b7632d5d90127028b9bfd503c6c655d0c01", size = 61392, upload-time = "2025-09-20T07:11:53.993Z" }, +] + +[[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/55/70/2869ea2112c565caace73c9301afd1d7afcc49bdd37fac058f0178ba95d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win32.whl", hash = "sha256:5cd0dbad86fcc860366f6515fce97177b7eaa7069da261057be4813819ba37ee", size = 131701, upload-time = "2025-09-20T07:17:16.849Z" }, + { 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 = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zeroconf" +version = "0.148.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ifaddr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/46/10db987799629d01930176ae523f70879b63577060d63e05ebf9214aba4b/zeroconf-0.148.0.tar.gz", hash = "sha256:03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f681702", size = 164447, upload-time = "2025-10-05T00:21:19.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/46/ac86e3a3ff355058cd0818b01a3a97ca3f2abc0a034f1edb8eea27cea65c/zeroconf-0.148.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:2158d8bfefcdb90237937df65b2235870ccef04644497e4e29d3ab5a4b3199b6", size = 1714870, upload-time = "2025-10-05T01:08:47.624Z" }, + { url = "https://files.pythonhosted.org/packages/de/02/c5e8cd8dfda0ca16c7309c8d12c09a3114e5b50054bce3c93da65db8b8e4/zeroconf-0.148.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:695f6663bf8df30fe1826a2c4d5acd8213d9cbd9111f59d375bf1ad635790e98", size = 1697756, upload-time = "2025-10-05T01:08:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/04/a66c1011d05d7bb8ae6a847d41ac818271a942390f3d8c83c776389ca094/zeroconf-0.148.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa65a24ec055be0a1cba2b986ac3e1c5d97a40abe164991aabc6a6416cc9df02", size = 2146784, upload-time = "2025-10-05T01:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/2239d87c3f60f886bd2dd299e9c63b811efd58b8b6fc659d8fd0900db3bc/zeroconf-0.148.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79890df4ff696a5cdc4a59152957be568bea1423ed13632fc09e2a196c6721d5", size = 1899394, upload-time = "2025-10-05T01:08:53.457Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/534a4b576a8f9f5edff648ac9a5417323bef3086a77397f2f2058125a3c8/zeroconf-0.148.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c0ca6e8e063eb5a385469bb8d8dec12381368031cb3a82c446225511863ede3", size = 2221319, upload-time = "2025-10-05T01:08:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8c/1c8e9b7d604910830243ceb533d796dae98ed0c72902624a642487edfd61/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ece6f030cc7a771199760963c11ce4e77ed95011eedffb1ca5186247abfec24a", size = 2178586, upload-time = "2025-10-05T01:08:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/16/55/178c4b95840dc687d45e413a74d2236a25395ab036f4813628271306ab9d/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c3f860ad0003a8999736fa2ae4c2051dd3c2e5df1bc1eaea2f872f5fcbd1f1c1", size = 1972371, upload-time = "2025-10-05T01:08:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/b599421fe634d9f3a2799f69e6e7db9f13f77d326331fa2bb5982e936665/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab8e687255cf54ebeae7ede6a8be0566aec752c570e16dbea84b3f9b149ba829", size = 2244286, upload-time = "2025-10-05T01:09:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cb/a30c42057be5da6bb4cbe1ab53bc3a7d9a29cd59caae097d3072a9375c14/zeroconf-0.148.0-cp314-cp314-win32.whl", hash = "sha256:6b1a6ddba3328d741798c895cecff21481863eb945c3e5d30a679461f4435684", size = 1321693, upload-time = "2025-10-05T01:09:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/2c/38/06873cdf769130af463ef5acadbaf4a50826a7274374bc3b9a4ec5d32678/zeroconf-0.148.0-cp314-cp314-win_amd64.whl", hash = "sha256:2588f1ca889f57cdc09b3da0e51175f1b6153ce0f060bf5eb2a8804c5953b135", size = 1563980, upload-time = "2025-10-05T01:09:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/36/fb/53d749793689279bc9657d818615176577233ad556d62f76f719e86ead1d/zeroconf-0.148.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:40fe100381365c983a89e4b219a7ececcc2a789ac179cd26d4a6bbe00ae3e8fe", size = 3418152, upload-time = "2025-10-05T01:09:06.71Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/5eb647f7277378cbfdb6943dc8e60c3b17cdd1556f5082ccfdd6813e1ce8/zeroconf-0.148.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b9c7bcae8af8e27593bad76ee0f0c21d43c6a2324cd1e34d06e6e08cb3fd922", size = 3389671, upload-time = "2025-10-05T01:09:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/3134aa54d30a9ae2e2473212eab586fe1779f845bf241e68729eca63d2ab/zeroconf-0.148.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8ba75dacd58558769afb5da24d83da4fdc2a5c43a52f619aaa107fa55d3fdc", size = 4123125, upload-time = "2025-10-05T01:09:11.064Z" }, + { url = "https://files.pythonhosted.org/packages/12/23/4a0284254ebce373ff1aee7240932a0599ecf47e3c711f93242a861aa382/zeroconf-0.148.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75f9a8212c541a4447c064433862fd4b23d75d47413912a28204d2f9c4929a59", size = 3651426, upload-time = "2025-10-05T01:09:13.725Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/7b79ef986b5467bb8f17b9a9e6eea887b0b56ecafc00515c81d118e681b4/zeroconf-0.148.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be64c0eb48efa1972c13f7f17a7ac0ed7932ebb9672e57f55b17536412146206", size = 4263151, upload-time = "2025-10-05T01:09:15.732Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0a/caa6d05548ca7cf28a0b8aa20a9dbb0f8176172f28799e53ea11f78692a3/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac1d4ee1d5bac71c27aea6d1dc1e1485423a1631a81be1ea65fb45ac280ade96", size = 4191717, upload-time = "2025-10-05T01:09:18.071Z" }, + { url = "https://files.pythonhosted.org/packages/46/f6/dbafa3b0f2d7a09315ed3ad588d36de79776ce49e00ec945c6195cad3f18/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8da9bdb39ead9d5971136046146cd5e11413cb979c011e19f717b098788b5c37", size = 3793490, upload-time = "2025-10-05T01:09:20.045Z" }, + { url = "https://files.pythonhosted.org/packages/c4/05/f8b88937659075116c122355bdd9ce52376cc46e2269d91d7d4f10c9a658/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6e3dd22732df47a126aefb5ca4b267e828b47098a945d4468d38c72843dd6df", size = 4311455, upload-time = "2025-10-05T01:09:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/58/c0/359bdb3b435d9c573aec1f877f8a63d5e81145deb6c160de89647b237363/zeroconf-0.148.0-cp314-cp314t-win32.whl", hash = "sha256:cdc8083f0b5efa908ab6c8e41687bcb75fd3d23f49ee0f34cbc58422437a456f", size = 2755961, upload-time = "2025-10-05T01:09:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ab/7b487afd5d1fd053c5a018565be734ac6d5e554bce938c7cc126154adcfc/zeroconf-0.148.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f72c1f77a89638e87f243a63979f0fd921ce391f83e18e17ec88f9f453717701", size = 3309977, upload-time = "2025-10-05T01:09:26.039Z" }, +] From 65fa4dec1b1b543d944352771a9ccb9eaa4e92fd Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:28:36 -0700 Subject: [PATCH 33/36] Simplify setup-hooks.sh to always run uv sync Remove the .deps-installed timestamp sentinel since uv sync is fast and idempotent, and uv.lock can change independently of pyproject.toml (e.g. pulled from remote). --- setup-hooks.sh | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/setup-hooks.sh b/setup-hooks.sh index 89982725..589ee2fb 100755 --- a/setup-hooks.sh +++ b/setup-hooks.sh @@ -1,16 +1,10 @@ #!/bin/bash -# Ensure dependencies are installed first -if [[ ! -f ".deps-installed" ]] || [[ "pyproject.toml" -nt ".deps-installed" ]]; then - echo "Installing/updating dependencies..." - - uv sync - - if [[ $? -ne 0 ]]; then - echo "Failed to install dependencies. Please check the output above." - exit 1 - fi - touch .deps-installed +# Ensure dependencies are up to date (uv sync is fast and idempotent) +uv sync +if [[ $? -ne 0 ]]; then + echo "Failed to install dependencies. Please check the output above." + exit 1 fi # Install pre-commit hooks (only if not already installed) From c027d9d21abd13e3d8f898c1ef589a61c90c8f90 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:43:01 -0700 Subject: [PATCH 34/36] Reference SPAN Panel Simulator add-on and fix mypy pre-commit hook --- CHANGELOG.md | 4 +++- README.md | 4 +++- .../span_panel/schema_validation.py | 2 +- prek.toml | 21 +++++++------------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a445c891..c2c4c747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,9 @@ All notable changes to this project will be documented in this file. ### Changed - **Simulation moved to dedicated add-on** — Panel cloning and simulation are no longer part of the integration's options flow. A new `export_circuit_manifest` - service provides panel parameters to the standalone SPAN Panel Simulator add-on. + service provides panel parameters to the standalone [SPAN Panel Simulator](https://github.com/SpanPanel/simulator) add-on, which now supports upgrade + modelling (evaluate firmware or integration upgrades in a sandbox before applying them to your real panel) and panel clone (replicate your panel's circuit + layout for testing). ### Fixed diff --git a/README.md b/README.md index affd1dc5..10c77134 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ API directly: > Warranty. See the SPAN API Scope & Responsibility Model in the [SPAN API documentation](https://github.com/spanio/SPAN-API-Client-Docs). This integration provides sensors and controls for understanding an installation's power consumption, energy usage, and controlling user-manageable panel -circuits. You can optionally use the [span-card](https://github.com/SpanPanel/span-card) Lovelace card for visualization and switch control. +circuits. You can optionally use the [span-card](https://github.com/SpanPanel/span-card) Lovelace card for visualization and switch control. The +[SPAN Panel Simulator](https://github.com/SpanPanel/simulator) add-on lets you clone your panel's circuit layout for testing, or model an upgrade to evaluate +firmware or integration changes in a sandbox before applying them to your real panel. This integration communicates with the SPAN Panel over your local network using SPAN's official [Electrification Bus (eBus)](https://github.com/spanio/SPAN-API-Client-Docs) framework — an open, multi-vendor integration standard for home energy diff --git a/custom_components/span_panel/schema_validation.py b/custom_components/span_panel/schema_validation.py index 4f39724e..41771de9 100644 --- a/custom_components/span_panel/schema_validation.py +++ b/custom_components/span_panel/schema_validation.py @@ -10,7 +10,7 @@ All output is log-only. No entity creation or sensor behavior changes. -Phase 1 of the schema-driven changes plan (see docs/Dev/schema_driven_changes.md). +Phase 1 of the schema-driven changes plan (see docs/dev/schema_driven_changes.md). Usage: Called from the coordinator after the first successful data refresh. diff --git a/prek.toml b/prek.toml index 85cf1071..391559e8 100644 --- a/prek.toml +++ b/prek.toml @@ -40,23 +40,18 @@ hooks = [ { id = "ruff-check", args = ["--fix"], exclude = '^tests/.*|scripts/.*|\.\S*_cache/.*|dist/.*|venv/.*' }, ] -# MyPy for type checking +# MyPy for type checking (local hook — needs project venv for span-panel-api) [[repos]] -repo = "https://github.com/pre-commit/mirrors-mypy" -rev = "v1.16.0" +repo = "local" hooks = [ { id = "mypy", - additional_dependencies = [ - "httpx", - "pydantic", - "typing-extensions", - "pytest", - "homeassistant-stubs", - "types-PyYAML", - "types-aiofiles", - ], - args = ["--config-file=pyproject.toml"], + name = "mypy", + entry = "uv run mypy", + language = "system", + args = ["--config-file=pyproject.toml", "custom_components/span_panel/"], + pass_filenames = false, + files = '\.py$', exclude = '^tests/.*|^scripts/.*|docs/.*|\.\S*_cache/.*|dist/.*|venv/.*', }, ] From e24849090783e384a28a76c88c2d986127a5791e Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:51:57 -0700 Subject: [PATCH 35/36] Fix simulation migration retry, add service name, remove untranslated strings --- custom_components/span_panel/__init__.py | 8 ++++++-- custom_components/span_panel/services.yaml | 1 + .../span_panel/translations/es.json | 17 ----------------- .../span_panel/translations/fr.json | 17 ----------------- .../span_panel/translations/ja.json | 17 ----------------- .../span_panel/translations/pt.json | 17 ----------------- tests/test_v2_config_flow.py | 12 +++++++++--- 7 files changed, 16 insertions(+), 73 deletions(-) diff --git a/custom_components/span_panel/__init__.py b/custom_components/span_panel/__init__.py index 3872eaa5..1da20b4b 100644 --- a/custom_components/span_panel/__init__.py +++ b/custom_components/span_panel/__init__.py @@ -262,10 +262,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> notification_id=f"span_simulation_removed_{config_entry.entry_id}", ) _LOGGER.warning( - "Config entry %s is a simulation entry — rejecting migration", + "Config entry %s is a simulation entry — setup will be skipped", config_entry.entry_id, ) - return False hass.config_entries.async_update_entry( config_entry, @@ -280,6 +279,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) -> """Set up Span Panel from a config entry.""" _LOGGER.debug("Setting up entry %s (version %s)", entry.entry_id, entry.version) + # Simulation entries were removed in v6 — skip setup, notification was + # already created during migration. + if entry.data.get(CONF_API_VERSION) == "simulation" or entry.data.get("simulation_mode", False): + return False + # Register WebSocket commands once per HA instance domain_data: dict[str, bool] = hass.data.setdefault(DOMAIN, {}) if not domain_data.get("websocket_registered"): diff --git a/custom_components/span_panel/services.yaml b/custom_components/span_panel/services.yaml index 783a238c..ed15a12c 100644 --- a/custom_components/span_panel/services.yaml +++ b/custom_components/span_panel/services.yaml @@ -1,4 +1,5 @@ export_circuit_manifest: + name: Export circuit manifest description: >- Export an authoritative circuit-to-entity manifest for all configured SPAN panels. Returns panel serial numbers, circuit power sensor entity diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json index c8692178..a2aedeac 100644 --- a/custom_components/span_panel/translations/es.json +++ b/custom_components/span_panel/translations/es.json @@ -65,23 +65,6 @@ "data_description": { "hop_passphrase": "La contraseña utilizada para autenticar con la API v2 del SPAN Panel" } - }, - "register_fqdn": { - "title": "Registering Domain Name" - }, - "fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_register_fqdn": { - "title": "Registering Domain Name" - }, - "reconfigure_fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_fqdn_done": { - "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json index 0124e7c5..663b073e 100644 --- a/custom_components/span_panel/translations/fr.json +++ b/custom_components/span_panel/translations/fr.json @@ -65,23 +65,6 @@ "data_description": { "hop_passphrase": "Le mot de passe utilisé pour s'authentifier avec l'API v2 du SPAN Panel" } - }, - "register_fqdn": { - "title": "Registering Domain Name" - }, - "fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_register_fqdn": { - "title": "Registering Domain Name" - }, - "reconfigure_fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_fqdn_done": { - "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json index e56dc67c..daaa5716 100644 --- a/custom_components/span_panel/translations/ja.json +++ b/custom_components/span_panel/translations/ja.json @@ -65,23 +65,6 @@ "data_description": { "hop_passphrase": "SPAN Panel v2 APIでの認証に使用するパスフレーズ" } - }, - "register_fqdn": { - "title": "Registering Domain Name" - }, - "fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_register_fqdn": { - "title": "Registering Domain Name" - }, - "reconfigure_fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_fqdn_done": { - "title": "Reconfiguration Complete" } } }, diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json index c243a3c5..f357c46a 100644 --- a/custom_components/span_panel/translations/pt.json +++ b/custom_components/span_panel/translations/pt.json @@ -65,23 +65,6 @@ "data_description": { "hop_passphrase": "A frase-passe utilizada para autenticar com a API v2 do SPAN Panel" } - }, - "register_fqdn": { - "title": "Registering Domain Name" - }, - "fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_register_fqdn": { - "title": "Registering Domain Name" - }, - "reconfigure_fqdn_failed": { - "title": "Domain Registration", - "description": "The domain name could not be registered with the panel, or the TLS certificate was not updated in time. You may continue, but the MQTT connection may fail if the panel's certificate does not include your domain name." - }, - "reconfigure_fqdn_done": { - "title": "Reconfiguration Complete" } } }, diff --git a/tests/test_v2_config_flow.py b/tests/test_v2_config_flow.py index 84ffb19d..c89a51ec 100644 --- a/tests/test_v2_config_flow.py +++ b/tests/test_v2_config_flow.py @@ -422,7 +422,7 @@ async def test_migration_blocked_when_panel_unreachable(hass: HomeAssistant) -> @pytest.mark.asyncio async def test_migration_v5_to_v6_rejects_simulation_entry(hass: HomeAssistant) -> None: - """Simulation entries at v5 should be rejected by v5→v6 migration.""" + """Simulation entries at v5 should migrate to v6 but setup should be skipped.""" entry = MockConfigEntry( version=5, minor_version=1, @@ -440,11 +440,17 @@ async def test_migration_v5_to_v6_rejects_simulation_entry(hass: HomeAssistant) ) entry.add_to_hass(hass) - from custom_components.span_panel import async_migrate_entry + from custom_components.span_panel import async_migrate_entry, async_setup_entry result = await async_migrate_entry(hass, entry) - assert result is False + # Migration succeeds (no retry on every restart) + assert result is True + assert entry.version == 6 + + # Setup is skipped for simulation entries + setup_result = await async_setup_entry(hass, entry) + assert setup_result is False # ---------- zeroconf v2 discovery ---------- From 0bdb3d9120e824da18a49c4bdc9840a204561e12 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:01:34 -0700 Subject: [PATCH 36/36] Move docs to root and images/, remove docs/ directory --- CHANGELOG.md | 7 +- README.md | 6 +- bess-grid-management.md | 8 +- .../span_panel/schema_validation.py | 2 +- docs/developer.md => developer.md | 0 docs/dev/HA_core/ha_core_work_plan.md | 287 ---------- docs/dev/architecture.md | 330 ----------- ...per_attribute_entity_renaming_migration.md | 318 ----------- docs/dev/dynamic_enum_options.md | 245 -------- docs/dev/energy_dip_compensation.md | 104 ---- docs/dev/evse_span_drive_support.md | 347 ------------ docs/dev/mqtt-sensor-topic.md | 491 ----------------- docs/dev/schema_driven_changes.md | 228 -------- docs/dev/statistics_spike_cleanup_proposal.md | 476 ---------------- docs/images/.gitignore | 2 - .../2026-03-20-poetry-to-uv-migration.md | 521 ------------------ .../bess-topology-integrated.drawio | 0 .../bess-topology-integrated.svg | 0 .../bess-topology-non-integrated.drawio | 0 .../bess-topology-non-integrated.svg | 0 docs/v1-legacy.md => v1-legacy.md | 0 docs/websocket-api.md => websocket-api.md | 0 22 files changed, 11 insertions(+), 3361 deletions(-) rename docs/developer.md => developer.md (100%) delete mode 100644 docs/dev/HA_core/ha_core_work_plan.md delete mode 100644 docs/dev/architecture.md delete mode 100644 docs/dev/developer_attribute_entity_renaming_migration.md delete mode 100644 docs/dev/dynamic_enum_options.md delete mode 100644 docs/dev/energy_dip_compensation.md delete mode 100644 docs/dev/evse_span_drive_support.md delete mode 100644 docs/dev/mqtt-sensor-topic.md delete mode 100644 docs/dev/schema_driven_changes.md delete mode 100644 docs/dev/statistics_spike_cleanup_proposal.md delete mode 100644 docs/images/.gitignore delete mode 100644 docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md rename {docs/images => images}/bess-topology-integrated.drawio (100%) rename {docs/images => images}/bess-topology-integrated.svg (100%) rename {docs/images => images}/bess-topology-non-integrated.drawio (100%) rename {docs/images => images}/bess-topology-non-integrated.svg (100%) rename docs/v1-legacy.md => v1-legacy.md (100%) rename docs/websocket-api.md => websocket-api.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c4c747..0a690038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,8 +124,8 @@ upgrade migrates to the SPAN official eBus API. Make a backup first.** ⚠️ ### Developer / Card Support - **WebSocket Topology API**: New `span_panel/panel_topology` WebSocket command that returns the full physical layout of a panel in a single call — circuits - with breaker slot positions, entity IDs grouped by role, and sub-devices (BESS, EVSE) with their entities. See - [WebSocket API Reference](docs/websocket-api.md) for schema and examples + with breaker slot positions, entity IDs grouped by role, and sub-devices (BESS, EVSE) with their entities. See [WebSocket API Reference](websocket-api.md) for + schema and examples ### Improvements @@ -175,8 +175,7 @@ upgrade migrates to the SPAN official eBus API. Make a backup first.** ⚠️ ### 📝 Notes -- A future release may implement local energy calculation from power values to eliminate both the freezing issue and negative spikes. See the - [energy calculation proposal](docs/dev/energy_calculation_proposal.md) for details. +- A future release may implement local energy calculation from power values to eliminate both the freezing issue and negative spikes. ## [1.2.8] - 2025-12-10 diff --git a/README.md b/README.md index 10c77134..ad728344 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Users MUST upgrade by the end of 2026 to avoid disruption. Upgrade to the latest - Requires firmware `spanos2/r202603/05` or later — panels on older firmware will not work - `Cellular` binary sensor removed — replaced by `Vendor Cloud` sensor -> Running older firmware? See [v1 Legacy Documentation](docs/v1-legacy.md). +> Running older firmware? See [v1 Legacy Documentation](v1-legacy.md). See [CHANGELOG.md](CHANGELOG.md) for all additions or value changes. @@ -462,7 +462,7 @@ belong to the same circuit by parsing naming patterns. That correlation is fragi device than the panel) and requires multiple round-trips. The topology command provides all of these relationships explicitly, keyed by circuit UUID, so the card can render the panel layout without guessing. -See [WebSocket API Reference](docs/websocket-api.md) for the full schema, response format, and usage examples. +See [WebSocket API Reference](websocket-api.md) for the full schema, response format, and usage examples. ## Troubleshooting @@ -508,7 +508,7 @@ Setting the interval to 0 disables debouncing entirely and rebuilds on every MQT ## Development -See [Developer Documentation](docs/developer.md) for setup instructions, prerequisites, and tooling. +See [Developer Documentation](developer.md) for setup instructions, prerequisites, and tooling. ## License diff --git a/bess-grid-management.md b/bess-grid-management.md index f2810b8c..154ee853 100644 --- a/bess-grid-management.md +++ b/bess-grid-management.md @@ -39,9 +39,9 @@ islanded during an outage. GFE reports GRID, power flows show battery as grid, a The diagrams below show the key components and where sensors can observe power. The critical insight is that the panel's sensors are on the **home side** of the MID — they cannot see what is happening on the utility side when the MID is open. -![Integrated BESS Topology](docs/images/bess-topology-integrated.svg) +![Integrated BESS Topology](images/bess-topology-integrated.svg) -Editable source: [docs/images/bess-topology-integrated.drawio](docs/images/bess-topology-integrated.drawio) +Editable source: [images/bess-topology-integrated.drawio](images/bess-topology-integrated.drawio) **Key observations:** @@ -57,9 +57,9 @@ When the battery system is not on the between the BESS and the panel. The panel cannot distinguish battery power from grid power and has no awareness that it is islanded during an outage. No automatic load shedding is available. -![Non-Integrated BESS Topology](docs/images/bess-topology-non-integrated.svg) +![Non-Integrated BESS Topology](images/bess-topology-non-integrated.svg) -Editable source: [docs/images/bess-topology-non-integrated.drawio](docs/images/bess-topology-non-integrated.drawio) +Editable source: [images/bess-topology-non-integrated.drawio](images/bess-topology-non-integrated.drawio) **Key differences from the integrated topology:** diff --git a/custom_components/span_panel/schema_validation.py b/custom_components/span_panel/schema_validation.py index 41771de9..34febe52 100644 --- a/custom_components/span_panel/schema_validation.py +++ b/custom_components/span_panel/schema_validation.py @@ -10,7 +10,7 @@ All output is log-only. No entity creation or sensor behavior changes. -Phase 1 of the schema-driven changes plan (see docs/dev/schema_driven_changes.md). +Phase 1 of the schema-driven changes plan. Usage: Called from the coordinator after the first successful data refresh. diff --git a/docs/developer.md b/developer.md similarity index 100% rename from docs/developer.md rename to developer.md diff --git a/docs/dev/HA_core/ha_core_work_plan.md b/docs/dev/HA_core/ha_core_work_plan.md deleted file mode 100644 index 1e3d2303..00000000 --- a/docs/dev/HA_core/ha_core_work_plan.md +++ /dev/null @@ -1,287 +0,0 @@ -# Home Assistant Core Submission Plan - -## Overview - -This document outlines the work required to submit the SPAN Panel integration to Home Assistant core. The analysis compares the current custom integration -against HA's [Integration Quality Scale][quality-scale] and [developer documentation][dev-docs]. - -[quality-scale]: https://developers.home-assistant.io/docs/core/integration-quality-scale/ -[dev-docs]: https://developers.home-assistant.io/docs/development_index/ - -## Current Alignment - -The integration already satisfies these core requirements: - -- Config flow with zeroconf discovery, reauth, and reconfigure -- Coordinator pattern in `coordinator.py` -- Consistent entity unique IDs -- `has_entity_name = True` on all entities (via `SpanPanelEntity` base) -- Push streaming (`local_push` IoT class) -- Device info construction (consolidated in `entity.py`) -- Translations in `strings.json` -- Strict typing (mypy strict, pyright) -- Config version migration (v1 through v5) -- Good test coverage (41 test files, 267 tests) -- Typed `runtime_data` (`SpanPanelRuntimeData` / `SpanPanelConfigEntry`) -- `PARALLEL_UPDATES` on all platforms -- `entity.py` shared base class (`common-modules`) -- No service actions registered (`action-setup`) -- Flat directory structure (no subdirectory packages) -- Appropriate polling (`appropriate-polling`): 60s fallback, push is primary -- Entity event setup (`entity-event-setup`): coordinator manages streaming lifecycle -- Test before configure (`test-before-configure`): host validated before entry creation -- Test before setup (`test-before-setup`): `ConfigEntryNotReady` / `ConfigEntryAuthFailed` / `ConfigEntryError` raised -- Unique config entry (`unique-config-entry`): serial-based unique ID with abort on duplicate -- Reconfiguration flow (`reconfiguration-flow`): host update with serial number mismatch guard -- Config entry unloading (`config-entry-unloading`): MQTT disconnect, streaming cleanup, coordinator shutdown -- Reauthentication flow (`reauthentication-flow`): v2 passphrase and proximity reauth tested -- Device classes applied (`entity-device-class`): power, energy, battery, current, connectivity, tamper, plug, charging -- Entity translations (`entity-translations`): all panel/status sensor names via `translation_key` in `strings.json` -- Icon translations (`icon-translations`): `icons.json` with state-based icons, no hardcoded Python icons -- Entity categories (`entity-category`): `DIAGNOSTIC` on status/monitoring sensors, `CONFIG` on circuit priority -- Diagnostics (`diagnostics`): `diagnostics.py` with `async_redact_data()` for credentials -- Exception translations (`exception-translations`): `strings.json` `exceptions` section for error messages -- Stale devices (`stale-devices`): `async_remove_config_entry_device` for manual sub-device removal - ---- - -## Phase 1: Strip Non-Core Features - -Core integrations communicate with real devices only. Features that exist purely for development convenience, direct database manipulation, or custom UX -patterns not supported by core must be removed from the integration destined for core submission. - -> **Note on simulation:** Simulation will be reimplemented as an eBus-level simulator external to the integration. From the integration's perspective it will -> always be talking to a real device (or something indistinguishable from one). No simulation awareness needs to exist in the integration code. - -### 1.1 Remove Simulation Mode - -Remove all simulation-related code paths and files: - -- `simulation_factory.py` -- `simulation_generator.py` -- `simulation_utils.py` -- `simulation_configs/` directory -- `config_flow_utils/simulation.py` -- Simulation branches in `config_flow.py` (`simulator_config` step, simulation serial generation, simulation start time handling) -- Simulation branches in `coordinator.py` (polling path for `DynamicSimulationEngine`, offline simulation minutes) -- Simulation-related constants (`CONF_SIMULATION_CONFIG`, `CONF_SIMULATION_START_TIME`, `CONF_SIMULATION_OFFLINE_MINUTES`) -- Simulation-related options in `options.py` - -### 1.2 Inline Config Flow Utils - -After simulation removal (§1.1), only `options.py` (171 lines) and `validation.py` (125 lines) remain (~296 lines total). Inline into `config_flow.py`. - -### 1.3 Manifest Adjustments - -| Change | Reason | -| ----------------------------- | --------------------------------------- | -| Remove `version` | Not used in core integrations | -| Remove `issue_tracker` | Not used in core integrations | -| Add `quality_scale: "bronze"` | Initial submission target | -| Add `loggers` array | List logger names from `span-panel-api` | - ---- - -## Phase 2: Remaining Bronze / External Items - -### 2.1 Ensure Dependency Transparency - -**Rule:** `dependency-transparency` - -The `span-panel-api` library must satisfy all four requirements: - -1. Source code under an OSI-approved license -2. Published on PyPI -3. Built from a public CI pipeline (GitHub Actions) -4. PyPI versions correspond to tagged releases - -Additionally for Platinum (`strict-typing`): the library must include a `py.typed` marker file (PEP 561). - -### 2.2 External Submissions - -| Rule | Action | -| -------------------------------- | ----------------------------------------------------- | -| `brands` | Submit branding to `home-assistant/brands` repository | -| `docs-actions` | Document remaining service actions | -| `docs-high-level-description` | Write integration overview for HA docs site | -| `docs-installation-instructions` | Write setup guide | -| `docs-removal-instructions` | Write uninstall steps | - ---- - -## Phase 3: Silver Requirements - -Silver adds 10 rules on top of Bronze. These improve reliability and maintainability. - -### 3.1 Entity Unavailability - -**Rule:** `entity-unavailable` - -Verify coordinator-based entities correctly report unavailable when data fetch fails. The existing offline handling logic should cover this but needs an audit -post-simplification. - -### 3.2 Unavailability Logging - -**Rule:** `log-when-unavailable` - -Log exactly once at `info` level when the panel becomes unreachable, and exactly once when it comes back online. If the coordinator's built-in `UpdateFailed` -handling is used, this is automatic. - -### 3.3 Test Coverage - -**Rule:** `test-coverage` - -Achieve >95% test coverage across all integration modules. Measure with: - -```bash -pytest tests/ --cov=custom_components.span_panel --cov-report term-missing -``` - -### 3.4 Documentation - -**Rules:** `docs-configuration-parameters`, `docs-installation-parameters`, `integration-owner` - -- Document all configuration options and installation parameters -- Designate a responsible maintainer in `codeowners` - ---- - -## Phase 4: Gold Requirements - -Gold adds 24 rules. These represent a polished, production-quality integration. - -### 4.1 Entity Disabled by Default - -**Rule:** `entity-disabled-by-default` - -Set `_attr_entity_registry_enabled_default = False` on noisy or supplementary entities: - -- Per-circuit produced/consumed/net energy (high cardinality) -- Phase voltage sensors -- Unmapped circuit backing data sensors -- Tab attribute sensors - -### 4.2 Additional Gold Rules - -| Rule | Action | -| -------------------------- | ----------------------------------------------------- | -| `discovery-update-info` | Update device network info from discovery data | -| `dynamic-devices` | Auto-add entities for circuits appearing after setup | -| `repair-issues` | Use `ir.async_create_issue()` for actionable problems | -| `docs-data-update` | Document push streaming data refresh model | -| `docs-examples` | Provide automation examples | -| `docs-known-limitations` | Document constraints (v1 vs v2 differences) | -| `docs-supported-devices` | List compatible SPAN panel models/firmware | -| `docs-supported-functions` | Detail all available functionality | -| `docs-troubleshooting` | Diagnostic guidance | -| `docs-use-cases` | Practical use case illustrations | - ---- - -## Phase 5: Platinum Requirements - -Platinum adds 3 final rules for the highest quality tier. - -### 5.1 Async Dependency - -**Rule:** `async-dependency` - -The `span-panel-api` library must use `asyncio` natively. It currently uses async MQTT (aiomqtt) so this should already be satisfied. Verify no blocking I/O -calls exist in the library. - -### 5.2 Inject Web Session - -**Rule:** `inject-websession` - -If the library makes HTTP requests (e.g., for v1 REST or v2 auth), it should accept an injected `aiohttp.ClientSession` from HA: - -```python -from homeassistant.helpers.aiohttp_client import async_get_clientsession -session = async_get_clientsession(hass) -client = SpanPanelClient(host, session=session) -``` - -### 5.3 Strict Typing - -**Rule:** `strict-typing` - -- Add integration to HA core's `.strict-typing` file -- Full mypy compliance (already enforced locally) -- Library must include `py.typed` marker (PEP 561) -- Use typed config entry alias consistently -- Avoid `Any` and `# type: ignore` - ---- - -## Phase 6: Submission Preparation - -### 6.1 Code Style Alignment - -- Match HA core's ruff configuration (line length, rule set) -- PEP 257 docstring conventions -- Alphabetically ordered constants, dict items, list contents -- `%` formatting in logging (not f-strings) -- Comments as full sentences ending with periods - -### 6.2 Branding - -Submit to `home-assistant/brands` repository: - -- `icon.png` (256x256, transparent background) -- `icon@2x.png` (512x512) -- `logo.png` (horizontal logo) - -### 6.3 Documentation - -Write integration documentation for the HA docs site covering all required `docs-*` rules from Bronze through Gold. - -### 6.4 Validation - -Run HA's validation tools: - -```bash -python3 -m script.hassfest -python3 -m script.translations develop -``` - -### 6.5 Pull Request - -Open PR to `home-assistant/core` following the [integration PR template][pr-template]. - -[pr-template]: https://github.com/home-assistant/core/blob/dev/.github/PULL_REQUEST_TEMPLATE.md - ---- - -## Files to Remove (Phase 1 Summary) - -```text -custom_components/span_panel/ -├── simulation_factory.py # Simulation (§1.1) -├── simulation_generator.py # Simulation (§1.1) -├── simulation_utils.py # Simulation (§1.1) -├── simulation_configs/ # Simulation (§1.1) -├── entity_summary.py # Naming support -├── migration.py # Naming migration -├── migration_utils.py # Naming migration -├── config_flow_utils/ # Inline into config_flow.py (§1.2) -│ ├── __init__.py -│ ├── simulation.py # Simulation (§1.1) -│ ├── options.py # Inline into config_flow.py -│ └── validation.py # Inline into config_flow.py -└── translations/ # Non-English (HA handles translations) - ├── es.json - ├── fr.json - ├── ja.json - └── pt.json -``` - -## Estimated Scope - -| Phase | Effort | Description | -| ------- | ------ | ------------------------------------------------------ | -| Phase 1 | Large | Strip ~4000 lines of simulation code | -| Phase 2 | Small | Dependency transparency, external submissions | -| Phase 3 | Small | Audit unavailability, logging, coverage gaps | -| Phase 4 | Small | Entity disabled-by-default, discovery, dynamic-devices | -| Phase 5 | Small | Library compliance, strict typing | -| Phase 6 | Medium | Docs, branding, validation, PR | diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md deleted file mode 100644 index 770c0808..00000000 --- a/docs/dev/architecture.md +++ /dev/null @@ -1,330 +0,0 @@ -# SPAN Panel Integration Architecture - -This document describes the high-level architecture of the SPAN Panel Home Assistant integration and the `span-panel-api` library it depends on. It covers -component responsibilities, data flow, and the key design decisions that shape runtime behavior. Implementation details and code-level documentation live in the -other docs in this directory. - -## System Overview - -The system is split across two repositories: - -| Repository | Role | -| ------------------ | -------------------------------------------------------------------------------------- | -| **span-panel-api** | Transport library — MQTT connection, Homie parsing, snapshot building, circuit control | -| **span** | HA integration — coordinator, entity platforms, config flow, services, migrations | - -The library knows nothing about Home Assistant. The integration knows nothing about MQTT framing or Homie parsing. All communication between them flows through -a small set of protocols and a single snapshot model. - -## span-panel-api - -### Protocols - -The library defines three runtime protocols that any transport must satisfy: - -| Protocol | Responsibility | -| -------------------------- | ------------------------------------------------------------------- | -| `SpanPanelClientProtocol` | Connect, close, ping, `get_snapshot()`, expose `capabilities` flags | -| `StreamingCapableProtocol` | Register snapshot callbacks, start/stop streaming | -| `CircuitControlProtocol` | Set circuit relay state, set circuit shed priority | - -Integration code programs against these protocols, never against concrete transport classes. This lets the simulation engine and the MQTT client be used -interchangeably. - -### Capability Flags - -`PanelCapability` is a set of runtime feature flags advertised by the client after connection: - -| Flag | Meaning | -| ----------------- | ----------------------------------------------- | -| `PUSH_STREAMING` | Client supports push callbacks | -| `EBUS_MQTT` | v2 MQTT transport is available | -| `CIRCUIT_CONTROL` | Client can change relay state and shed priority | -| `BATTERY_SOE` | Battery state-of-energy data is present | - -The integration reads these flags at setup time to decide which entity platforms to load and which sensors to create. - -### Snapshot Model - -`SpanPanelSnapshot` is the single point-in-time view of all panel state. It is transport-agnostic — the same dataclass is returned whether the data came from -MQTT, REST (legacy), or simulation. - -Key sections of the snapshot: - -| Section | Contents | -| ----------- | ------------------------------------------------------------------------------------------------------------- | -| Identity | Serial number, firmware version | -| Grid state | `dsm_grid_state`, `current_run_config`, `dominant_power_source`, `main_relay_state` | -| Main meter | Instantaneous grid power, consumed/produced energy counters, L1/L2 voltage and current | -| Feedthrough | Feedthrough power and energy counters, downstream lug currents | -| Power flows | Aggregate PV, battery, grid, and site power (instantaneous only, no energy counters) | -| Circuits | `dict[circuit_id, SpanCircuitSnapshot]` — per-circuit power, energy, relay state, tabs, device type, metadata | -| Battery | `SpanBatterySnapshot` — state-of-energy percentage, kWh, vendor metadata | -| PV | `SpanPVSnapshot` — vendor metadata, nameplate capacity | -| Hardware | Door state, ethernet/WiFi/cellular link, panel size, WiFi SSID | - -`SpanCircuitSnapshot` carries the circuit's identity (UUID, name, tabs), real-time measurements (power, energy counters, current), relay/priority state, and -metadata (device type, breaker rating, always-on flag). The `device_type` field distinguishes load circuits from PV and EVSE circuits, which affects sign -conventions and entity naming. - -### MQTT Transport - -The MQTT transport is composed of three layers: - -| Layer | Class | Responsibility | -| ------ | --------------------- | ------------------------------------------------------------------------------------------------- | -| Socket | `AsyncMqttBridge` | Event-loop-driven paho-mqtt wrapper, TLS, reconnection | -| Parser | `HomieDeviceConsumer` | Homie v5 message routing, property accumulation, snapshot assembly | -| Client | `SpanMqttClient` | Composition root — wires bridge to parser, implements all three protocols, manages debounce timer | - -#### Connection Flow - -1. The bridge downloads the panel's CA certificate over REST and configures TLS -2. paho-mqtt connects to the panel's MQTT broker (TCP or WebSocket) using credentials from the v2 auth registration -3. The client subscribes to the panel's Homie topic tree (`ebus/5/{serial}/#`) -4. The parser waits for the Homie `$state=ready` signal, then polls until circuit names are populated -5. Once names arrive the connection is considered fully established - -#### Property Accumulation - -Every MQTT message updates a single entry in the parser's in-memory property dictionary — a cheap dict write. No snapshot is built at this stage. The dictionary -is the authoritative store of all Homie property values and their arrival timestamps. - -#### Snapshot Building - -`build_snapshot()` iterates the accumulated properties and assembles a `SpanPanelSnapshot`. This is the expensive operation — it walks all node types, extracts -typed values, derives grid state and run config, correlates PV/EVSE metadata to circuits via the `feed` property, and synthesizes unmapped tab entries for -breaker positions with no physical circuit. - -Snapshot building is triggered in one of two ways: - -- **On demand** — `get_snapshot()` calls `build_snapshot()` synchronously from the property store. No network call. -- **On push** — when streaming is active, the debounce timer fires and builds a snapshot, then dispatches it to all registered callbacks. - -#### Debounce Timer - -The SPAN panel publishes roughly 100 MQTT messages per second. Without rate-limiting, each message would trigger a full snapshot rebuild and entity update -cycle. The debounce timer prevents this: - -1. An MQTT message arrives and updates the property store -2. If no timer is running and streaming is active, a timer is scheduled for `snapshot_interval` seconds (default 1.0, configurable 0–15) -3. Further messages during the window are absorbed — they update properties but do not reset or extend the timer -4. When the timer fires it builds one snapshot and dispatches it to all callbacks -5. Setting the interval to 0 disables debouncing — every message triggers immediate dispatch - -The interval is user-configurable via the integration's options flow and can be adjusted at runtime without reconnecting. - -#### Circuit Control - -Relay and priority commands are published to the circuit's Homie `/set` topic at QoS 1. The panel applies the change and publishes the updated property value -back through the normal Homie message flow, which the parser picks up on the next snapshot cycle. - -#### Reconnection - -The bridge runs an exponential-backoff reconnection loop (1s → 60s) that activates after the initial connection succeeds. Reconnection is transparent to the -client and parser — the property store retains its last known state during the gap, and streaming resumes automatically once the broker connection is -re-established. - -### Auth and Detection - -| Function | Purpose | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `register_v2()` | POST to the panel's v2 auth endpoint with the user's passphrase, returns MQTT broker credentials and serial number | -| `detect_api_version()` | GET the panel's unauthenticated v2 status endpoint to determine firmware generation | -| `download_ca_cert()` | Fetch the panel's CA certificate PEM for TLS | - -### Simulation Engine - -`DynamicSimulationEngine` implements `SpanPanelClientProtocol` (but not `StreamingCapableProtocol`) and generates snapshots from YAML configuration files. It is -used for integration testing and development without hardware. The coordinator treats it identically to a real client — it just polls instead of streaming. - -## span (HA Integration) - -### Setup and Initialization - -The integration's `async_setup_entry()` determines the transport mode from the config entry's `api_version` field: - -| Mode | Client | Coordinator | Update Path | -| ----------- | -------------------------------------- | ------------------------- | --------------------------- | -| v1 (legacy) | Blocked — raises `ConfigEntryNotReady` | — | Users must upgrade firmware | -| v2 (MQTT) | `SpanMqttClient` | Streaming + fallback poll | Push via MQTT, 60s fallback | -| Simulation | `DynamicSimulationEngine` | Poll only | Configurable interval | - -For v2, the setup sequence is: - -1. Build `MqttClientConfig` from stored broker credentials -2. Create `SpanMqttClient` and call `connect()` -3. Create `SpanPanelCoordinator` with `is_streaming=True` -4. Call `async_config_entry_first_refresh()` — runs one poll cycle, handles any pending migrations -5. Call `async_setup_streaming()` — registers the push callback and starts MQTT streaming -6. Register the device in the device registry -7. Forward setup to entity platforms (sensor, binary_sensor, switch, select) -8. Register services (energy spike cleanup, main meter monitoring) - -### Coordinator - -`SpanPanelCoordinator` extends HA's `DataUpdateCoordinator` and is the central hub between the transport client and all entity platforms. - -#### Two Update Paths - -The coordinator has two paths for receiving data, and both feed into the same entity update machinery: - -**Push path** (streaming mode) — `_on_snapshot_push()` is called by the MQTT client's debounce timer with a freshly built snapshot. It marks the panel as -online, checks for hardware capability changes, calls `async_set_updated_data()` to dispatch to entities, then runs post-update maintenance tasks. - -**Poll path** — `_async_update_data()` is called by the coordinator's built-in timer. It calls `get_snapshot()` on the client, performs the same capability -check, runs post-update tasks, and returns the snapshot. - -#### Fallback Poll Behavior - -HA's `DataUpdateCoordinator` resets its poll timer every time `async_set_updated_data()` is called. During active MQTT streaming, pushes arrive faster than the -60-second fallback interval, so the timer perpetually restarts and the poll path effectively never fires. This is correct for its intended purpose — the -fallback exists to detect MQTT silence, not to run periodically alongside push updates. If pushes stop (panel disconnect, broker crash), the last timer fires 60 -seconds later. - -Because the poll path rarely fires during streaming, all maintenance logic that needs to run on every update lives in a shared `_run_post_update_tasks()` method -called from both paths. This includes reload request handling, pending migration checks, and solar entity migration. - -#### Post-Update Tasks - -After every snapshot update (push or poll), the coordinator runs: - -1. **Reload request check** — if any entity or capability detector set the `_reload_requested` flag, schedule an async reload of the config entry -2. **Legacy migration** — if `pending_legacy_migration` flag is set in options, migrate entity IDs from v1 naming to v2 device-prefixed naming -3. **Naming pattern migration** — if `pending_naming_migration` flag is set, migrate entity IDs between friendly names and circuit numbers -4. **Solar migration** — if `solar_migration_pending` flag is set in config data, rewrite v1 virtual solar entity unique IDs to v2 PV circuit unique IDs - -All migration flags are one-shot — they are cleared after execution to prevent loops. They typically run during `async_config_entry_first_refresh()` before -streaming starts, but the shared task runner ensures they also execute if set later (e.g., via options flow while streaming is active). - -#### Capability Detection - -The coordinator tracks which optional hardware features are present in the snapshot (BESS, PV, power-flows). When a new capability appears — for example, a -battery is commissioned after the integration is already running — the coordinator requests a reload. On reload, the sensor factory re-evaluates the snapshot -and creates the appropriate new entities. - -#### Reload Mechanism - -`request_reload()` sets a flag rather than reloading immediately. The actual reload is scheduled as an async task after the current update cycle completes. This -avoids reloading mid-update and allows multiple reload requests within a single cycle to coalesce into one reload. - -#### Offline Handling - -When a data fetch fails (connection error, timeout, server error), the coordinator sets `_panel_offline = True` and returns the last known snapshot. This keeps -entities updating with stale-but-valid data, which is critical for the energy sensor grace period logic. On the next successful update (push or poll), the -offline flag is cleared. - -### Entity Platforms - -#### Sensor Factory - -The sensor factory inspects the snapshot and config options to decide which entities to create: - -| Factory Function | Entities Created | -| ----------------------------------- | ------------------------------------------------------------------------------------------------ | -| `create_panel_sensors()` | Status sensors, power sensors, energy sensors, battery level, software version | -| `create_circuit_sensors()` | Per-circuit power and energy sensors for all named circuits | -| `create_unmapped_circuit_sensors()` | Invisible backing sensors for unmapped breaker positions (used by synthetic sensor calculations) | -| `create_battery_sensors()` | Battery power sensor (conditional — only when BESS commissioned) | -| `create_power_flow_sensors()` | PV power and site power sensors (conditional — only when PV or power-flows data present) | - -Conditional sensors (battery, PV, site) are gated on snapshot data, not configuration flags. If the hardware is not present, the sensors are not created. If -hardware appears later, capability detection triggers a reload and the factory creates them on the next setup cycle. - -#### Sensor Base Classes - -All sensors inherit from `SpanSensorBase`, which provides: - -- **Unique ID generation** — delegated to subclasses, ensuring stable IDs across renames and migrations -- **Name generation** — two strategies: flag-based friendly name (initial install) and panel-driven name (existing entity, for name sync) -- **Name sync** — detects circuit name changes from the panel and requests a reload to update entity names -- **Offline handling** — power sensors report 0.0 when offline, energy sensors report unknown, string sensors report UNKNOWN -- **Availability** — entities remain available during offline so grace period state is visible - -`SpanEnergySensorBase` extends the base with grace period tracking: it persists the last valid energy value and timestamp across HA restarts using -`RestoreSensor`, and continues reporting that value for a configurable window after the panel goes offline. This prevents statistics spikes from brief -disconnects. - -#### Name Sync - -When a user renames a circuit in the SPAN mobile app, the new name arrives via MQTT and appears in the next snapshot. The name sync mechanism detects this -change and updates entity names in HA: - -1. On entity init, the current circuit name is stored for comparison -2. On each coordinator update, the current name is compared to the stored name -3. If the user has customized the entity name in HA's entity registry, sync is skipped — user overrides take precedence -4. If the name changed, a reload is requested -5. On reload, entities are recreated with the new panel name - -Name sync operates identically across sensors, switches, and selects — each entity type implements the same comparison logic in its -`_handle_coordinator_update()` method. - -#### Circuit Sensors - -Circuit sensors bind to a specific `circuit_id` and read their data from `snapshot.circuits[circuit_id]`. The power sensor carries extra state attributes -(amperage, tabs, voltage, breaker rating, device type, relay state, shed priority). PV circuit power sensors also expose inverter vendor metadata. - -Sign conventions are applied at the entity layer: PV circuit power is negated so positive values represent production, and PV net energy uses -`produced - consumed` while load circuits use `consumed - produced`. - -#### Panel Sensors - -Panel sensors read directly from the top-level snapshot fields. They include grid/feedthrough power and energy, battery and PV power (from power-flows), status -strings (grid state, run config, relay state), and hardware info (firmware version, battery level). - -#### Switch and Select - -Switch entities control circuit relay state (open/closed) via the client's `set_circuit_relay()` method. Select entities control circuit shed priority -(never/soc_threshold/off_grid) via `set_circuit_priority()`. Both use the same naming and name sync patterns as circuit sensors. - -#### Binary Sensors - -Binary sensors report panel hardware status: door state (tamper class), ethernet link, WiFi link, and overall panel connectivity. The door sensor reports -unavailable when the panel returns UNKNOWN (a known firmware quirk that resolves when the door is physically operated). - -### Config Flow - -The config flow handles initial setup and runtime reconfiguration: - -**Setup flow:** - -1. User enters panel IP (or discovered via Zeroconf) -2. Integration detects API version via unauthenticated status endpoint -3. User enters passphrase for v2 auth registration -4. Broker credentials are stored in config entry data -5. User selects entity naming pattern (friendly names or circuit numbers) - -**Options flow:** - -- Entity naming pattern (with migration between patterns) -- Snapshot update interval (debounce timer, 0–15s) -- Energy reporting grace period -- Net energy sensor toggles (panel-level and circuit-level) -- Display precision - -Naming pattern changes trigger an entity ID migration: the old and new flags are stored in options, a `pending_naming_migration` flag is set, and the -coordinator picks it up on the next update cycle. - -### Entity ID Migration - -`EntityIdMigrationManager` handles three migration scenarios: - -1. **Legacy → device prefix** — v1 entities without a device name prefix are renamed to include one -2. **Friendly names ↔ circuit numbers** — entity IDs are rewritten to match the selected naming pattern -3. **Combined** — both prefix and pattern changes in one operation - -Migrations rewrite entity IDs in the entity registry while preserving unique IDs, which keeps statistics and history intact. A reload follows each migration to -apply the new IDs. - -### Services - -| Service | Purpose | -| ----------------------- | ------------------------------------------------------------------------------------------- | -| `cleanup_energy_spikes` | Detect and correct erroneous negative energy deltas in statistics caused by firmware resets | -| `undo_stats_adjustment` | Reverse a previous cleanup operation using saved adjustment data | -| `main_meter_monitoring` | Watch main meter consumed energy for firmware-reset-induced drops and notify the user | - -### Zeroconf Discovery - -The integration registers three Zeroconf service types in `manifest.json`: `_span._tcp.local.`, `_ebus._tcp.local.`, and `_secure-mqtt._tcp.local.`. When HA -discovers a device advertising any of these services, it triggers the config flow with the panel's host address pre-filled. diff --git a/docs/dev/developer_attribute_entity_renaming_migration.md b/docs/dev/developer_attribute_entity_renaming_migration.md deleted file mode 100644 index c2d8aa2b..00000000 --- a/docs/dev/developer_attribute_entity_renaming_migration.md +++ /dev/null @@ -1,318 +0,0 @@ -# Synthetic Sensor Entity Renaming and Migration - -## Background - -Only a single renaming is supported: migrating legacy installations (pre-1.0.4 without a device prefix) to device-prefixed entity IDs while preserving the -existing entity_id tails. Post-install flipping between naming patterns (Friendly Names ↔ Circuit Numbers) is not supported in the UI. The goal is to preserve -entity history and user customizations without deleting/recreating entities. - -## Goals - -- Preserve Home Assistant entity history for synthetic sensors when renaming occurs. -- Retain user customizations (such as friendly names, device class, etc.) if the user has not manually changed the entity ID. -- Update entity IDs in YAML definitions only if the entity ID was not customized by the user. -- Safely migrate legacy installations (no device prefix) to device-prefixed naming while preserving existing tails, handling edge cases where user modifications - exist. -- Exclude Panel sensors from migration except in the legacy device prefix migration case. Panel level circuits can be identifiedi through filter logic. -- Exclude User Customized Entity ID's from migration. The process for identifying a user customized entity_id is detailed below. - -The options flow triggers a one-time legacy-to-prefix migration and then saves the updated options. Once entity_ids are migrated, a common routine is used to -modify the SensorSet storage via the synthetic package. - -## Naming Policy (Post-Install Behavior) - -- After initial install, the entity naming style is fixed. The options UI does not offer flipping between Friendly Names and Circuit Numbers. -- Supported migration path is limited to legacy installations (pre-1.0.4) that lacked a device prefix. These can migrate once to device-prefixed naming while - preserving the existing entity_id tails (friendly/circuit text remains unchanged). -- This policy avoids repeated renames, preserves history, and removes the need for dual-calculation under different flag sets. - -## Panel Level vs Named Circuits Filtering - -Panel level circuits are those like the grid power or the mains current_power which apply to the entire panel. Since panel level circuits do not control an -individual numbered circuit, which is a combination of one or more tabs in the panel and they do not have a circuit ID in their sensor key/unique_id. A circuit -ID is a UUID generated by the panel and permanently assigned to a circuit so that adding circuits does not affect the other circuit ID's. Named individual -circuits returned by the panel API are circuits like those used to control individual circuit like a refrigerator. - -For a device with device identifier span_sp3-simulation-001 sensor keys take this form: - -- Panel circuits take the form without the circuit_id/UUID: `span_sp3-simulation-001_current_power` -- Named circuits will have a Sensor Key that contains this circuit_id/UUID: `span_sp3-simulation-001_12ce227695cd44338864b0ef2ec4168b_power` - -## Information Extraction from YAML Export - -From the `SensorSet.export()` YAML dump, extract: - -**1. Suffix from entity_id:** - -- Extract last part of entity_id (e.g., `sensor.span_panel_dining_room_wine_fridge_power` → `power`) -- Reverse map to API description key (`power` → `instantPowerW`) - -**2. Circuit information from variables:** - -- For solar sensors: parse variables (unmapped) to find leg1/leg2 circuit references -- For single-circuit sensors: find circuit backing entity to determine circuit_data -- For multi-circuit sensors: identify which circuits are being combined - -**3. Friendly name from name field:** - -- Use for solar sensors when calling `construct_solar_synthetic_entity_id()` - -### Suffix to API Description Key Mapping - -| Entity ID Suffix | API Description Key | Description | -| -------------------- | ------------------- | -------------------------------- | -| `power` | `instantPowerW` | Current power consumption | -| `energy_produced` | `producedEnergyWh` | Total energy produced | -| `energy_consumed` | `consumedEnergyWh` | Total energy consumed | -| `current_power` | `instantGridPowerW` | Grid power (panel level) | -| `feed_through_power` | `feedthroughPowerW` | Feed through power (panel level) | - -### Developer Helper References (for tooling only) - -The supported migration is prefix-only. The following helper-based generation approach is for development tooling and diagnostics; it is not used by the -supported user migration. - -Use the composite helper function `construct_multi_tab_entity_id_from_key()` which automatically: - -- Determines the appropriate sensor type (panel, multi-tab/240V, circuit-based) -- Extracts required parameters from sensor key and configuration -- Calls the correct specific helper function internally -- Handles all suffix mapping and parameter extraction - -**Usage (dev tooling only):** - -```python -new_entity_id = construct_multi_tab_entity_id_from_key( - coordinator=coordinator, - span_panel=span_panel, - platform="sensor", - sensor_key=sensor_key, - sensor_config=sensor_config, - unique_id=None, # Skip registry during migration -) -``` - -**Internal Parameter Extraction (dev tooling only):** - -The composite helper internally handles: - -- **Suffix extraction**: Uses `get_suffix_from_sensor_key()` to extract suffix from sensor key -- **Sensor type detection**: Identifies panel, multi-tab/240V, or circuit sensors automatically -- **Multi-tab/240V information**: Extracts tab numbers and friendly names from sensor configuration -- **Circuit data**: Determines circuit numbers and friendly names as needed -- **API key mapping**: Handles any required suffix-to-API-key conversions - -## Migration Steps - -### A) Legacy → Device Prefix (Prefix-Only) Migration [Supported] - -1. Export current YAML definitions for all synthetic sensors. -2. Determine the device prefix (slugified `config_entry.data["device_name"]` or `config_entry.title`). -3. For every sensor (panel, solar, circuit): - - If its `entity_id` is not already prefixed, rewrite it as: `sensor.{device_prefix}_{old_tail}` where `{old_tail}` is the existing suffix portion after the - platform (`sensor.`). Do not recompute the tail. - - Build old→new mapping as you rewrite. -4. Update cross-references across the YAML using the mapping (find/replace in strings). -5. Use the synthetic package's `modify()` to persist changes. -6. Save new options (enable device prefix), then reload the integration. - -Notes: - -- Panel sensors are included in this legacy migration (they adopt the device prefix). -- User-customized entity_ids are preserved in their tail; only the device prefix is added if missing. - -### B) Non-Legacy Pattern Flips (Friendly ↔ Circuit Numbers) [Not supported post-install] - -- Post-install flipping is not supported in the options UI. If required for testing, install with the desired pattern instead. -- If a non-legacy migration must be computed (e.g., in development tooling), use `construct_multi_tab_entity_id_from_key()` under explicit flag overrides to - compute the target entity_id, perform cross-reference updates, and persist via `modify()`. - -## Technical Implementation Details - -### Understanding Sensor Keys vs Entity IDs - -**Sensor Keys:** - -- Serve as unique identifiers in the synthetic package with format: - - Panel sensors: `{device_id}_{suffix}` (e.g., `span_sp3-simulation-001_current_power`) - - Circuit sensors: `{device_id}_{circuit_uuid}_{suffix}` (e.g., `span_sp3-simulation-001_12ce227695cd44338864b0ef2ec4168b_power`) - - Solar sensors: `{device_id}_solar_{suffix}` (e.g., `span_sp3-simulation-001_solar_power`) -- Used primarily for: - - Filtering logic to determine panel circuit vs normal named circuit (presence of UUID in sensor key) - - Sensor type identification (solar detection using `solar` in sensor_key.lower()) - - As unique identifiers for synthetic package operations -- Sensor keys themselves are mostly irrelevant for parameter extraction - -**Entity IDs:** - -- Found in the `entity_id` field of sensor configs with format determined by configuration flags: - - Friendly names: `sensor.span_panel_{friendly_name}_{suffix}` (e.g., `sensor.span_panel_dining_room_wine_fridge_power`) - - Circuit numbers: `sensor.span_panel_circuit_{number}_{suffix}` (e.g., `sensor.span_panel_circuit_14_power`) - - Solar sensors (friendly names): `sensor.span_panel_solar_inverter_{suffix}` (e.g., `sensor.span_panel_solar_inverter_power`) - - Solar sensors (circuit numbers): `sensor.span_panel_circuit_{leg1}_{leg2}_{suffix}` (e.g., `sensor.span_panel_circuit_30_32_power`) -- Show the current naming pattern, not what the old or new patterns should be -- Used for extracting the suffix needed by helper functions -- **Helper functions automatically ensure Home Assistant entity ID compliance**: - - The integration's helper functions use `slugify()` to convert circuit names and device names - - Ensures valid entity IDs with only lowercase letters, numbers, and underscores - -### Helper Function Requirements - -- Helper functions expect API description keys, not user-friendly suffixes -- All entity IDs must be generated by helper functions -- Helper functions take parameters like `instantPowerW`, `producedEnergyWh`, `instantGridPowerW` (not user-friendly suffixes like `power`, `energy_produced`, - `grid_power`) -- A reverse mapping from user-friendly suffix back to API description key is required (see table above) - -### Helper Function Reference Table - -| Function Type | Helper Function | Usage Context | Output/Format | -| -------------------------- | ------------------------------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------- | -| **Entity ID Construction** | | | | -| **Panel Sensors** | `construct_panel_entity_id()` | Panel-level sensors (grid, mains, etc.) | `sensor.span_panel_{description}` | -| **Multi-tab/240V Sensors** | `construct_240v_synthetic_entity_id()` | Multi-tab/240V synthetic sensors | `sensor.span_panel_circuit_{tab1}_{tab2}_{suffix}` | -| **Single Circuit** | `construct_single_circuit_entity_id()` | Individual circuit sensors | `sensor.span_panel_{friendly_name}_{suffix}` or `sensor.span_panel_circuit_{number}_{suffix}` | -| **Multi Circuit** | `construct_multi_circuit_entity_id()` | Combined circuit sensors | `sensor.span_panel_{friendly_name}_{suffix}` or `sensor.span_panel_circuit_{numbers}_{suffix}` | -| **Backing/Raw** | `construct_backing_entity_id()` | Underlying SPAN API entities | `sensor.span_panel_{circuit_name}_{suffix}` | -| **Unmapped** | `construct_unmapped_entity_id()` | Internal unmapped circuits (tabs) | `sensor.span_panel_unmapped_tab_{number}_{suffix}` | -| **General Purpose** | `construct_entity_id()` | Legacy/general entity construction | Variable format based on parameters | -| **From Sensor Key** | `construct_multi_tab_entity_id_from_key()` | Convert sensor key to entity ID | Variable format based on sensor type | -| **Suffix Utilities** | | | | -| **Reverse Mapping** | `get_api_description_key_from_suffix()` | Convert entity ID suffix to API key | `"power"` → `"instantPowerW"` | -| **Forward Mapping** | `get_user_friendly_suffix()` | Convert API key to user-friendly suffix | `"instantPowerW"` → `"power"` | -| **Panel Suffix** | `get_panel_entity_suffix()` | Convert panel API key to entity suffix | `"instantGridPowerW"` → `"current_power"` | -| **Extract Suffix** | `get_suffix_from_sensor_key()` | Extract suffix from sensor key | `"span_device_uuid_power"` → `"power"` | - -### Helper Function Selection Guide - -**For Migration Logic:** - -1. **Use the composite helper**: `construct_multi_tab_entity_id_from_key()` handles all sensor types automatically -2. **Sensor type detection**: The helper internally identifies panel, multi-tab/240V, and circuit sensors from sensor keys -3. **Parameter extraction**: All required parameters are extracted automatically from sensor key and configuration - -**Migration Implementation Pattern:** - -```python -# Generate new entity ID with specified flags -new_entity_id = construct_multi_tab_entity_id_from_key( - coordinator=coordinator, - span_panel=span_panel, - platform="sensor", - sensor_key=sensor_key, - sensor_config=sensor_config, - unique_id=None, # Skip registry during migration -) -``` - -**Internal Helper Selection:** - -The composite helper automatically delegates to: - -- **Panel sensors** → `construct_panel_entity_id()` -- **Multi-tab/240V sensors** → `construct_240v_synthetic_entity_id()` -- **Circuit sensors** → `construct_multi_circuit_entity_id()` (with extracted parameters) - -**Key Utility Functions Available:** - -- `get_suffix_from_sensor_key()` - Extract suffix from sensor key (used internally) -- `is_panel_level_sensor_key()` - Determine if sensor is panel-level (used for filtering) -- `is_solar_sensor_key()` - Identify solar sensors (used internally) -- `extract_solar_info_from_sensor_key()` - Extract solar parameters (used internally) - -### Entity ID Validation Requirements - -**Home Assistant Entity ID Rules:** - -- Must contain only lowercase letters, numbers, and underscores: `[a-z0-9_]+` -- Cannot contain dashes, slashes, spaces, or special characters -- The integration uses `homeassistant.util.slugify()` to sanitize circuit names and device names - -### Multi-tab/240V Sensor Examples - -**Multi-tab/240V sensors for tabs 30/32 configuration:** - -**Friendly Names Pattern (USE_DEVICE_PREFIX=True, USE_CIRCUIT_NUMBERS=False):** - -```yaml -span_sp3-simulation-001_240v_power: - name: 240V Current Power - entity_id: sensor.span_panel_240v_power - variables: - tab1_power: sensor.span_panel_unmapped_tab_30_power - tab2_power: sensor.span_panel_unmapped_tab_32_power -``` - -**Circuit Numbers Pattern (USE_DEVICE_PREFIX=True, USE_CIRCUIT_NUMBERS=True):** - -```yaml -span_sp3-simulation-001_240v_power: - name: 240V Current Power - entity_id: sensor.span_panel_circuit_30_32_power - variables: - tab1_power: sensor.span_panel_unmapped_tab_30_power - tab2_power: sensor.span_panel_unmapped_tab_32_power -``` - -Note: The sensor key remains the same (`span_sp3-simulation-001_240v_power`) regardless of naming pattern, only the `entity_id` changes. - -## Common YAML Update Process - -### 1. Update Entity IDs Only - -- Only modify the `entity_id` field in sensor configs -- Leave everything else unchanged: `name`, `formula`, `variables`, `metadata`, `attributes`, etc. -- This preserves all user customizations except entity_id changes - -### 2. Update Cross-References - -- Scan entire YAML document for references to changed entity_ids -- Find/replace all occurrences of old entity_id with new entity_id throughout the document -- This handles cases where users have custom attribute formulas referencing other synthetic sensors, variables in one sensor referencing another synthetic - sensor's entity_id, or any other cross-references in the configuration - -### 3. Apply Changes - -- Do not remove and recreate entities in Home Assistant -- Use the synthetic package's `modify()` routine to write the updated YAML definitions back to storage - -### 4. Reload Integration - -- Trigger a reload of the integration so that Home Assistant picks up the updated YAML and applies the new entity IDs - -## Summary Table - -| Case | Action Taken | -| ---------------------------------------------------------------- | ------------------------------------------------ | -| Legacy (no prefix) migrating all sensors to prefix/friendly only | Update to device prefix friendly | -| Panel-level sensor (see filter, non-legacy) | Excluded from migration | -| User-customized entity ID (differs from expected, non-legacy) | Excluded from migration | -| Non-legacy pattern flip (friendly ↔ circuit) post-install | Not supported in UI (install with desired style) | - -## Benefits - -- Preserves entity history by renaming rather than deleting/recreating entities. -- Respects user customizations by only updating entity IDs that have not been changed by the user. -- Maintains integrity of cross-references between synthetic sensors. -- Ensures smooth migration between naming patterns, including legacy to modern transitions. - -## Implementation Analysis - -### Current Implementation vs Documentation - -The migration implementation in `entity_id_naming_patterns.py` largely follows the documented approach with some key differences: - -1. **Solar Information Extraction**: Verify that `extract_solar_info_from_sensor_key()` correctly identifies leg1/leg2 from sensor configuration variables - -2. **Multi-Circuit Handling**: Ensure multi-circuit sensors are handled correctly with proper circuit number extraction from sensor configurations - -3. **Legacy Migration Coverage**: Confirm that legacy migration includes all sensor types (panel, solar, circuit) as intended - -### Validation Recommendations - -To ensure full migration functionality: - -1. **Test Solar Sensor Migration**: Verify leg1/leg2 extraction works correctly with real solar sensor configurations -2. **Validate Multi-Circuit Scenarios**: Test migration of sensors that combine multiple circuits -3. **Test Legacy Migration**: Verify comprehensive legacy migration handles all sensor types correctly -4. **Cross-Reference Validation**: Ensure entity ID references in formulas and variables are updated properly diff --git a/docs/dev/dynamic_enum_options.md b/docs/dev/dynamic_enum_options.md deleted file mode 100644 index c90f1a6f..00000000 --- a/docs/dev/dynamic_enum_options.md +++ /dev/null @@ -1,245 +0,0 @@ -# Dynamic Enum Options - -This document describes how the SPAN Panel integration handles enumerated state values, where options are derived dynamically at runtime, and where hardcoded -values remain necessary. - -## Background - -SPAN Panel firmware publishes device state over MQTT using the Homie convention. Many properties are typed as `enum` in the Homie schema (available at -`GET /api/v2/homie/schema`), but firmware releases may publish values not yet declared in the schema. The integration must accept any value the panel sends -without raising errors. - -### The Problem - -Home Assistant's `SensorEntity` with `device_class=ENUM` requires an `options` list. If the entity reports a state value not present in `options`, HA raises a -`ValueError` and the sensor becomes unavailable. Hardcoding the options list creates a tight coupling to a specific firmware version and breaks when new values -appear. - -### The Solution - -Enum sensor options are built dynamically from observed MQTT values. Each enum sensor starts with a seed list of `["unknown"]` and appends new values as they -arrive, before setting the native value. This prevents the `ValueError` and makes the integration forward-compatible with any firmware changes. - -## Entity Classification - -### Fully Dynamic (no hardcoded state values) - -These entities receive values directly from Homie MQTT properties. The integration passes them through at face value in uppercase, matching the Homie schema -convention and preserving backward compatibility with v1 automations. - -| Entity | Snapshot field | Homie property | Homie enum format | -| ------------------------- | ----------------------- | ---------------------------- | ---------------------------------------- | -| EVSE Charger Status | `evse.status` | `evse/status` | `UNKNOWN,AVAILABLE,PREPARING,...` | -| EVSE Lock State | `evse.lock_state` | `evse/lock-state` | `UNKNOWN,LOCKED,UNLOCKED` | -| Main Relay State | `main_relay_state` | `core/relay` | `UNKNOWN,OPEN,CLOSED` | -| Grid Forming Entity (DPS) | `dominant_power_source` | `core/dominant-power-source` | `GRID,BATTERY,PV,GENERATOR,NONE,UNKNOWN` | -| Vendor Cloud | `vendor_cloud` | `core/vendor-cloud` | `UNKNOWN,UNCONNECTED,CONNECTED` | - -All of the above use `device_class=SensorDeviceClass.ENUM` with `options=["UNKNOWN"]` and uppercase pass-through value functions. The dynamic options mechanism -in the base sensor class extends the options list at runtime. - -### Case Convention - -The integration preserves uppercase values as received from the Homie schema (e.g., `CLOSED`, `GRID`, `CHARGING`). While HA core internally uses lowercase for -some built-in states (`STATE_UNKNOWN = "unknown"`), the SPAN Panel v1 integration established uppercase as its convention. All user automations, dashboards, and -scripts reference these uppercase values. Changing to lowercase would break existing installations with no functional benefit. - -Translation keys in `translations/*.json` use uppercase to match (e.g., `"CLOSED": "Closed"`, `"CHARGING": "Charging"`). - -### Binary Sensors (boolean interpretation of Homie enums) - -These map Homie enum values to True/False. The boolean logic must be defined in code, but the set of recognized enum values does not need to be exhaustive -- -unrecognized values simply resolve to the else/default branch. - -| Entity | Homie property | True condition | False condition | -| ----------------- | --------------- | ----------------------- | --------------- | -| Door State | `core/door` | `!= CLOSED` | `== CLOSED` | -| EVSE Charging | `evse/status` | status in charging set | otherwise | -| EVSE EV Connected | `evse/status` | status in connected set | otherwise | -| Ethernet Link | `core/ethernet` | boolean | boolean | -| Wi-Fi Link | `core/wifi` | boolean | boolean | - -No options list is involved -- binary sensors are not subject to the `ValueError` problem. - -### Derived Values (computed in span-panel-api, not direct Homie properties) - -These are synthesized from multiple Homie signals by `HomieDeviceConsumer` in the library. Their possible values are defined by our derivation logic, not by the -panel firmware. - -| Entity | Derivation logic | Possible values | -| ------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| DSM State | `_derive_dsm_state()` — priority: bess/grid-state, then DPS + grid power heuristic | `DSM_ON_GRID`, `DSM_OFF_GRID`, `UNKNOWN` | -| DSM Grid State | Deprecated alias of DSM State | same as above | -| Current Run Config | `_derive_run_config()` — from dsm_state + islandable + DPS | `PANEL_ON_GRID`, `PANEL_OFF_GRID`, `PANEL_BACKUP`, `UNKNOWN` | - -These values are controlled by our code and change only when we change the derivation logic. They could be converted to ENUM sensors with a static options list -since we define the domain, but because the values are ours, hardcoding is acceptable and there is no firmware drift risk. - -### Select Entities (control, not observation) - -Select entities present a list of options the user can choose from and write back to the panel. The options must be known in advance because they define valid -commands. - -| Entity | Options source | Hardcoded | -| ---------------- | ------------------------------------------------------------ | -------------------------------------------------------- | -| Circuit Priority | `CircuitPriority` enum: `NEVER`, `SOC_THRESHOLD`, `OFF_GRID` | Yes -- these are the only valid values the panel accepts | - -The `UNKNOWN` member is excluded from the UI options because it is a read-only state, not a valid command. - -### Switch Entities (relay control) - -The circuit relay switch sends `CLOSED` or `OPEN` to the panel. The relay state read-back (`circuit.relay_state`) comes from Homie as an enum -(`UNKNOWN,OPEN,CLOSED`), but the switch maps it to a boolean (`is_on`). No options list is involved. - -### Numeric Sensors (no enum concern) - -All power (W), energy (Wh), current (A), voltage (V), and percentage (%) sensors report float values. They have no options list and are unaffected by this -pattern. - -| Examples | Device class | -| --------------------------------------------- | ------------ | -| Circuit Power, Grid Power, Feed Through Power | `POWER` | -| Consumed/Produced Energy, Net Energy | `ENERGY` | -| EVSE Advertised Current, Lug Currents | `CURRENT` | -| Battery Level | `BATTERY` | - -## Translations - -HA uses translation files (`translations/*.json`) to display friendly labels for enum sensor states. The integration provides translations for all values -declared in the Homie schema across 5 languages (en, es, fr, pt, ja). - -### Known values (schema-declared) - -These have full translations. For example, EVSE status `CHARGING` displays as "Charging" (en), "Cargando" (es), "En charge" (fr), etc. Translations are provided -for all dynamic enum sensors: - -- `evse_status` — 10 OCPP-derived charger states -- `evse_lock_state` — 3 lock states -- `main_relay_state` — 3 relay states (open, closed, unknown) -- `grid_forming_entity` — 6 dominant power source values -- `vendor_cloud` — 3 cloud connection states - -### Unknown values (dynamically discovered) - -When firmware publishes a value not in the schema (e.g., `UNPLUGGED`), the dynamic options mechanism accepts it and the sensor works correctly. However, no -translation exists for that value, so HA displays the raw uppercase key string (e.g., "UNPLUGGED") in the UI. - -This is acceptable behavior: - -- The sensor remains functional -- no errors or unavailability -- The raw string is typically readable in English (Homie enum values are descriptive by convention) -- Non-English users see the untranslated English-like key until a translation is added in a subsequent release -- Adding a translation for a newly discovered value is a one-line change per language file, with no code changes required - -### Translation file structure - -Enum state translations live under `entity.sensor..state` in each translation file: - -```json -{ - "entity": { - "sensor": { - "evse_status": { - "state": { - "UNKNOWN": "Unknown", - "AVAILABLE": "Available", - "CHARGING": "Charging" - } - } - } - } -} -``` - -## Implementation Details - -### How dynamic options work - -In `sensors/base.py :: SpanSensorBase._process_raw_value()`: - -```python -if self._attr_device_class is SensorDeviceClass.ENUM: - str_value = str_value.upper() - if not hasattr(self, "_attr_options") or self._attr_options is None: - self._attr_options = ["UNKNOWN"] - if str_value not in self._attr_options: - self._attr_options.append(str_value) -``` - -This runs before `self._attr_native_value` is set, so HA never sees a state value that isn't in the options list. The `.upper()` normalization ensures -consistency even if firmware sends unexpected casing. - -### The `/api/v2/homie/schema` endpoint - -The panel exposes its full Homie schema at `GET /api/v2/homie/schema` (no auth required). This returns every node type and property with datatype and format -metadata. For enum properties, the `format` field is a comma-separated list of declared values. - -The schema is useful for documentation and validation, but it is not the source of truth for runtime options because: - -1. Firmware may publish values not yet declared in the schema (observed: `UNPLUGGED` on `evse/status` is not in the schema) -2. The schema represents a point-in-time declaration, not a guarantee - -The integration does not currently fetch the schema at runtime. If future needs arise (e.g., pre-populating options for better initial UI), the endpoint is -available and unauthenticated. - -## Schema Validation Utility - -The integration intentionally avoids runtime warnings for unrecognized enum values — the dynamic options mechanism handles them silently. However, developers -need a way to detect when the Homie schema declares values that the integration's translation files don't cover (or vice versa). - -A standalone CLI utility should be created (e.g., `scripts/validate_enum_schema.py`) that runs outside of Home Assistant and performs the following checks: - -### Inputs - -1. **Homie schema** — fetched live from a panel at `GET http:///api/v2/homie/schema`, or loaded from a saved JSON file for offline use. -2. **Translation files** — the `translations/*.json` files in the integration. -3. **Sensor definitions** — the enum sensor descriptions in `sensor_definitions.py` (to map Homie property paths to translation keys). - -### Checks - -| Check | Description | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Missing translations** | Schema declares an enum value (e.g., `UNPLUGGED`) that has no translation key in any language file. These values will display as raw uppercase strings in the UI. | -| **Extra translations** | A translation key exists for a value not declared in the schema. May indicate a stale translation or a firmware regression. | -| **Cross-language gaps** | A value has a translation in some languages but not all. | -| **Case mismatches** | Schema value casing doesn't match translation key casing (should both be uppercase). | - -### Proposed usage - -```bash -# Live panel scan -python scripts/validate_enum_schema.py --host 192.168.65.70 - -# Offline from saved schema -python scripts/validate_enum_schema.py --schema-file tests/fixtures/homie_schema.json - -# CI integration (exit code 1 on any missing translations) -python scripts/validate_enum_schema.py --schema-file tests/fixtures/homie_schema.json --strict -``` - -### Mapping Homie properties to translation keys - -The utility needs a mapping from Homie property paths to sensor `translation_key` values, since the schema uses paths like `core/relay` while translations use -keys like `main_relay_state`. This mapping can be derived from the sensor definitions or maintained as a small lookup table in the utility: - -| Homie property | Translation key | -| ---------------------------- | --------------------- | -| `core/relay` | `main_relay_state` | -| `core/dominant-power-source` | `grid_forming_entity` | -| `core/vendor-cloud` | `vendor_cloud` | -| `evse/status` | `evse_status` | -| `evse/lock-state` | `evse_lock_state` | - -This utility is not yet implemented — this section serves as the design spec. - -## Summary Table - -| Category | Options source | Hardcoded | Dynamic | Status | -| --------------------- | --------------------- | ------------------------ | ------- | ---------------- | -| EVSE enum sensors | MQTT observed values | No | Yes | Done | -| Panel enum sensors | MQTT observed values | No | Yes | Done | -| Derived state sensors | Our derivation logic | Acceptable | N/A | No change needed | -| Binary sensors | Boolean logic | Yes (True/False mapping) | N/A | No change needed | -| Select entities | Valid panel commands | Yes | N/A | No change needed | -| Switch entities | Boolean (OPEN/CLOSED) | Yes | N/A | No change needed | -| Numeric sensors | Float values | N/A | N/A | No change needed | diff --git a/docs/dev/energy_dip_compensation.md b/docs/dev/energy_dip_compensation.md deleted file mode 100644 index f2a3090f..00000000 --- a/docs/dev/energy_dip_compensation.md +++ /dev/null @@ -1,104 +0,0 @@ -# Energy Dip Compensation - -## Rationale - -SPAN panels occasionally report lower energy readings for `TOTAL_INCREASING` sensors (consumed/produced energy at panel and circuit level). Home Assistant's -statistics engine interprets any decrease as a counter reset, creating negative spikes in the energy dashboard. - -A manual `cleanup_energy_spikes` service already exists to fix historical data. This feature adds **proactive** compensation: the integration maintains a -cumulative offset per sensor so HA never sees a decrease. - -## Design Principles - -- **Lightweight** -- no Storage files, no I/O in the MQTT event hot path. The offset lives in memory and is persisted via HA's `ExtraStoredData` mechanism (same - as grace period state). -- **Opt-in for existing installs** -- defaults to OFF on upgrade (no schema migration needed; `options.get(key, False)` handles it). -- **On by default for new installs** -- the config flow checkbox defaults to `True`. -- **Covering an upstream defect** -- this feature may be short-lived if SPAN fixes the firmware bug; disabling clears all accumulated offsets cleanly. - -## Architecture - -### Data Flow - -```text -Panel reading (raw) - → SpanEnergySensorBase._process_raw_value() - → dip detected? (raw < last_panel_reading by ≥ 1.0 Wh) - YES → offset += dip; report to coordinator - NO → passthrough - → super()._process_raw_value(raw + offset) - → HA sees monotonically increasing value -``` - -### Key Fields (SpanEnergySensorBase) - -| Field | Type | Description | -| --------------------------- | ------- | ------------------------------------------- | --------------------------------------------- | -| `_energy_offset` | `float` | Cumulative Wh added to compensate dips | -| `_last_panel_reading` | `float | None` | Last raw panel value (before offset) | -| `_last_dip_delta` | `float | None` | Size of the most recent dip (for diagnostics) | -| `_is_total_increasing` | `bool` | Whether this sensor's state_class qualifies | -| `_dip_compensation_enabled` | `bool` | Current option value (re-read each cycle) | - -### Persistence (SpanEnergyExtraStoredData) - -Three new optional fields: `energy_offset`, `last_panel_reading`, `last_dip_delta`. Old stored data missing these keys deserializes with `None` -- no migration -needed. - -Restoration only happens when compensation is **enabled**. Disabling and reloading leaves the init defaults (0.0/None), implementing "disabling clears offsets." - -## Configuration - -### Fresh Install - -The config flow (`config_flow.py`) shows a checkbox: "Auto-Compensate Energy Dips" -- defaults to **True**. - -### Options Flow - -General Options includes a toggle: "Auto-Compensate Energy Dips" -- defaults to **False** for existing installs. - -The description notes: "Disabling clears all accumulated offsets." - -## Affected Sensors - -Only `TOTAL_INCREASING` energy sensors get compensation: - -| Sensor | Class | Level | -| --------------------------- | ------------------------- | ------- | -| `mainMeterEnergyProducedWh` | `SpanPanelEnergySensor` | Panel | -| `mainMeterEnergyConsumedWh` | `SpanPanelEnergySensor` | Panel | -| `circuit_energy_produced` | `SpanCircuitEnergySensor` | Circuit | -| `circuit_energy_consumed` | `SpanCircuitEnergySensor` | Circuit | - -`TOTAL` sensors (net energy, feedthrough) and `MEASUREMENT` sensors pass through unchanged. - -## Sensor Attributes - -When compensation is enabled and the sensor is `TOTAL_INCREASING`: - -| Attribute | Shown When | Description | -| ---------------- | ---------------- | ---------------------------------- | -| `energy_offset` | offset > 0 | Cumulative Wh compensation applied | -| `last_dip_delta` | dip has occurred | Size of the most recent dip in Wh | - -## Notification Behavior - -When one or more sensors detect a dip during an update cycle: - -1. Each sensor calls `coordinator.report_energy_dip()` (sync, no I/O). -2. After all entities update, `_run_post_update_tasks()` drains the list. -3. A single persistent notification is created listing all affected sensors. -4. Uses stable `notification_id=span_energy_dip_{entry_id}` so repeated events update rather than stack. -5. The notification mentions the `cleanup_energy_spikes` service for historical data. - -## Edge Cases - -| Case | Behavior | -| ------------------------------ | ---------------------------------------------------- | -| First reading (no baseline) | Sets baseline, no compensation | -| HA restart | Offset + last_panel_reading restored from storage | -| Disable then re-enable | Disable skips restoration (clears offsets), fresh | -| Float precision noise | 1.0 Wh minimum threshold prevents false triggers | -| Multiple sensors dip at once | All detected independently, batched notification | -| Panel firmware reset | Primary use case -- offset compensates automatically | -| Dip below threshold (< 1.0 Wh) | Ignored, treated as normal fluctuation | diff --git a/docs/dev/evse_span_drive_support.md b/docs/dev/evse_span_drive_support.md deleted file mode 100644 index 061978bf..00000000 --- a/docs/dev/evse_span_drive_support.md +++ /dev/null @@ -1,347 +0,0 @@ -# EVSE (SPAN Drive) Entity Support - -## Context - -Issue [#153](https://github.com/SpanPanel/span/issues/153) requests SPAN Drive EV charger status in the integration. Previously blocked because the v1 REST API -exposed no EVSE data. The v2 eBus/MQTT API now exposes a full `energy.ebus.device.evse` Homie node with 9 properties. All are read-only. - -### eBus EVSE Schema - -```yaml -energy.ebus.device.evse: - vendor-name: string - product-name: string - part-number: string - serial-number: string - software-version: string - feed: enum # Circuit ID the EVSE is connected to - lock-state: enum # UNKNOWN | LOCKED | UNLOCKED - status: enum # OCPP-based charger status (10 values) - advertised-current: float (A) # Current being offered to the EV -``` - -**Status enum values** (OCPP-derived): `UNKNOWN`, `AVAILABLE`, `PREPARING`, `CHARGING`, `SUSPENDED_EV`, `SUSPENDED_EVSE`, `FINISHING`, `RESERVED`, `FAULTED`, -`UNAVAILABLE` - -**None of these properties have `settable: true`** — we cannot control charge rate or lock state via eBus. The only controllable properties are on the circuit -node itself (relay, shed-priority), which we already expose. - -### Current State - -The library (`span-panel-api`) extracts only `feed` and `relative-position` from EVSE nodes to annotate circuits with `device_type="evse"`. All other EVSE -properties are parsed by MQTT but discarded during snapshot building. The integration creates breaker switches and shed priority selects for EVSE circuits; -standard circuit sensors (power, energy) already exist. - ---- - -## Design: Beyond span-hass - -The [span-hass](https://github.com/electrification-bus/span-hass) reference implementation creates 9 sensor entities per EVSE (status, lock-state, -advertised-current, plus 5 diagnostic string sensors for vendor/product/serial/version/part-number, plus feed circuit). All are plain string sensors under a -generic "EV Charger" sub-device. - -This design improves on that approach: - -| Feature | span-hass | This design | -| ---------------------- | --------------------------- | ------------------------------------------------------------------ | -| EVSE metadata | 5 diagnostic string sensors | HA DeviceInfo attributes (manufacturer, model, serial, sw_version) | -| Charger status | Plain string sensor | `SensorDeviceClass.ENUM` with translated state names | -| Lock state | Plain string sensor | Enum sensor with translations | -| Derived binary sensors | None | Charging (`BATTERY_CHARGING`), EV Connected (`PLUG`) | -| Translations | English only | 5 languages (en, es, fr, ja, pt) | -| EVSE circuit naming | Generic | "EV Charger" fallback name for unnamed circuits | -| Simulation support | None | EVSE entities in simulator mode | -| Multiple EVSE support | Single node | `dict[str, SpanEvseSnapshot]` keyed by node_id | - -### Architecture: Dual-Node Design - -EVSE has two representations in the Homie description: - -1. **Metadata node** (`energy.ebus.device.evse`): Device info, charger status, lock state, advertised current. References a circuit via `feed`. -2. **Circuit node** (physical breaker): Power, energy, relay state, shed priority. - -Power and energy flow through the circuit node. The EVSE metadata node adds charger-specific state. This means EVSE entities live on a **sub-device** (the -charger), while circuit power/energy entities remain on the panel device as regular circuit sensors. - -### Sub-Device with Rich DeviceInfo - -Instead of creating 5 diagnostic entities, we map EVSE metadata into HA's DeviceInfo: - -```python -DeviceInfo( - identifiers={(DOMAIN, f"{panel_id}_evse_{evse_node_id}")}, - name=f"{panel_name} {base_name} ({display_suffix})", - manufacturer=vendor_name or "SPAN", - model=product_name or "SPAN Drive", - serial_number=serial_number, - sw_version=software_version, - via_device=(DOMAIN, panel_identifier), # links to parent panel -) -``` - -The HA device page shows manufacturer, model, serial, and firmware natively. `via_device` creates the parent-child hierarchy in the device registry. -`part_number` goes into `extra_state_attributes` on the status sensor since DeviceInfo has no part_number field. - -#### Device Name Architecture - -HA does not incorporate the parent device name (`via_device`) into child device entity IDs — entity IDs are derived solely from the child device's own name. To -avoid entity ID collisions across multi-panel installations and to support HA's bulk device rename feature, the EVSE device name includes: - -1. **Panel device name prefix** — e.g., "Main House", "Museum Garage" -2. **Base name** — `product_name` or "EV Charger" fallback -3. **Display suffix** (optional) — differentiates multiple chargers on the same panel - -The display suffix is resolved by `resolve_evse_display_suffix()` in `helpers.py`: - -- **Friendly names** (`USE_CIRCUIT_NUMBERS=False`): uses the fed circuit's panel name (e.g., "Garage") -- **Circuit numbers** (`USE_CIRCUIT_NUMBERS=True`): uses the EVSE serial number (e.g., "SN-EVSE-001") -- Returns `None` when no meaningful suffix is available (no empty parentheses) - -Example device names for a two-panel installation: - -| Panel | Circuit | Device Name | -| ------------- | -------- | ---------------------------------- | -| Main House | Garage | `Main House SPAN Drive (Garage)` | -| Main House | Driveway | `Main House SPAN Drive (Driveway)` | -| Museum Garage | Bay 1 | `Museum Garage SPAN Drive (Bay 1)` | -| Museum Garage | Bay 2 | `Museum Garage SPAN Drive (Bay 2)` | - ---- - -## Entities Per EVSE Device - -| Entity | Platform | Device Class | State Class | Icon | -| ------------------ | ------------- | ------------------ | ------------- | ---------------- | -| Charger Status | sensor | `ENUM` | — | `mdi:ev-station` | -| Advertised Current | sensor | `CURRENT` | `MEASUREMENT` | — | -| Lock State | sensor | `ENUM` | — | `mdi:lock` | -| Charging | binary_sensor | `BATTERY_CHARGING` | — | — | -| EV Connected | binary_sensor | `PLUG` | — | — | - -**5 entities** per EVSE, compared to span-hass's 9. - -### Derived Binary Sensors - -These provide high-value automation triggers that span-hass does not offer: - -**Charging** — ON when `status == "CHARGING"`. Device class `BATTERY_CHARGING` gives HA native "Charging" / "Not charging" display. Useful for automations like -"notify when car finishes charging" or "shift loads while EV charges." - -**EV Connected** — ON when `status` is in `{PREPARING, CHARGING, SUSPENDED_EV, SUSPENDED_EVSE, FINISHING}`. Device class `PLUG` gives HA native "Plugged in" / -"Unplugged" display. Useful for "remind me to plug in the car" automations. - -### Charger Status Extra Attributes - -The status sensor includes additional context as extra_state_attributes: - -- `advertised_current_a` — current being offered (amps) -- `lock_state` — connector lock state -- `part_number` — EVSE part number (no DeviceInfo field for this) -- `feed_circuit_id` — which circuit the EVSE is connected to - -This gives users a single entity to inspect for full charger state. - ---- - -## Data Flow - -```text -MQTT: ebus/5/{serial}/evse/{property} - | - v -HomieDeviceConsumer._handle_property() - | - v -_build_evse_devices() # NEW — iterates all TYPE_EVSE nodes - | # extracts all 9 properties per node - v -dict[str, SpanEvseSnapshot] # keyed by Homie node_id - | - v -SpanPanelSnapshot.evse # NEW field on the snapshot model - | - v -SpanPanelCoordinator # auto-detect "evse" capability - | # trigger reload when EVSE first appears - v -sensor platform # SpanEvseSensor entities (status, current, lock) -binary_sensor platform # SpanEvseBinarySensor entities (charging, connected) -``` - -The existing `_build_feed_metadata()` continues to annotate circuits with `device_type="evse"` — this is unchanged. The new `_build_evse_devices()` runs -alongside it, capturing the metadata that was previously discarded. - ---- - -## Implementation: Library (`span-panel-api`) - -### models.py - -New `SpanEvseSnapshot` dataclass after `SpanPVSnapshot`: - -```python -@dataclass(frozen=True, slots=True) -class SpanEvseSnapshot: - """EV Charger (EVSE) state — populated when EVSE node is commissioned.""" - - node_id: str # Homie node ID - feed_circuit_id: str # Normalized circuit ID - status: str = "UNKNOWN" # OCPP charger status - lock_state: str = "UNKNOWN" # LOCKED | UNLOCKED | UNKNOWN - advertised_current_a: float | None = None # Amps offered to EV - vendor_name: str | None = None - product_name: str | None = None - part_number: str | None = None - serial_number: str | None = None - software_version: str | None = None -``` - -Add to `SpanPanelSnapshot`: - -```python -evse: dict[str, SpanEvseSnapshot] = field(default_factory=dict) -``` - -### mqtt/homie.py - -New method `_build_evse_devices()` — iterates all `TYPE_EVSE` nodes, extracts all 9 properties, returns `dict[str, SpanEvseSnapshot]`. Called from -`build_snapshot()` alongside `_build_battery()` and `_build_pv()`. - -### simulation.py - -Generate EVSE snapshot entries for bidirectional circuits (existing `device_type == "evse"` detection). Simulated EVSE shows `CHARGING` when power > 100W, -`AVAILABLE` otherwise. Provides realistic EVSE data for development/testing without hardware. - -### **init**.py - -Export `SpanEvseSnapshot` from the library's public API. - ---- - -## Implementation: Integration (`span`) - -### util.py — `evse_device_info()` - -New helper creating `DeviceInfo` with `via_device` linking to the parent panel. Accepts `panel_name` (prepended for entity ID namespacing) and optional -`display_suffix` (from `resolve_evse_display_suffix()` in `helpers.py`) to differentiate multiple chargers. Maps vendor/product/serial/version from EVSE -metadata into native DeviceInfo fields. - -### helpers.py — EVSE helpers - -- `build_evse_unique_id(serial, evse_id, description_key)` — pure function returning `span_{serial}_evse_{evse_id}_{key}` -- `build_evse_unique_id_for_entry(coordinator, snapshot, evse_id, description_key, device_name)` — `_for_entry` wrapper handling simulators -- `resolve_evse_display_suffix(evse, snapshot, use_circuit_numbers)` — resolves display suffix from naming option - -### sensor_definitions.py - -New `SpanEvseSensorEntityDescription` dataclass with `value_fn: Callable[[SpanEvseSnapshot], ...]`. Three sensor definitions: - -1. **evse_status** — `SensorDeviceClass.ENUM`, 10 options -2. **evse_advertised_current** — `SensorDeviceClass.CURRENT`, `SensorStateClass.MEASUREMENT` -3. **evse_lock_state** — `SensorDeviceClass.ENUM`, 3 options - -### sensors/evse.py (new file) - -`SpanEvseSensor` extends `SpanSensorBase[SpanEvseSensorEntityDescription, SpanEvseSnapshot]`. Overrides `_attr_device_info` to use EVSE sub-device. Each sensor -instance holds an `_evse_id` referencing a specific EVSE in the snapshot dict. - -Unique ID pattern: `span_{serial}_evse_{node_id}_{key}` - -### binary_sensor.py - -New `SpanEvseBinarySensorEntityDescription` and `SpanEvseBinarySensor` class. Two descriptions: - -1. **evse_charging** — `BinarySensorDeviceClass.BATTERY_CHARGING` -2. **evse_ev_connected** — `BinarySensorDeviceClass.PLUG` - -Created conditionally in `async_setup_entry()` when `snapshot.evse` is non-empty. - -### sensors/factory.py - -- `has_evse(snapshot)` — `len(snapshot.evse) > 0` -- `create_evse_sensors(coordinator, snapshot)` — iterates all EVSE devices and descriptions -- Update `detect_capabilities()` to include `"evse"` -- Update `create_native_sensors()` to call `create_evse_sensors()` - -### coordinator.py - -Add EVSE detection to `_detect_capabilities()`: - -```python -if any(c.device_type == "evse" for c in snapshot.circuits.values()) or len(snapshot.evse) > 0: - caps.add("evse") -``` - -### sensors/circuit.py - -Add "EV Charger" as fallback name for unnamed EVSE circuits, matching the existing "Solar" pattern for PV circuits. - -### strings.json + translations - -Entity names and enum state translations in all 6 language files. Status states get human-readable translations: - -| Status | English | Spanish | French | Japanese | Portuguese | -| ------------ | -------------------- | ----------------------- | --------------------- | -------------- | --------------------- | -| CHARGING | Charging | Cargando | En charge | 充電中 | Carregando | -| AVAILABLE | Available | Disponible | Disponible | 利用可能 | Disponivel | -| PREPARING | Preparing | Preparando | Preparation | 準備中 | Preparando | -| SUSPENDED_EV | Suspended by Vehicle | Suspendido por Vehiculo | Suspendu par Vehicule | 車両による中断 | Suspenso pelo Veiculo | -| FAULTED | Faulted | En fallo | En panne | 故障 | Com falha | - -(Complete translations for all 10 status values + 3 lock states in all 5 languages.) - ---- - -## Edge Cases - -| Case | Behavior | -| ------------------------ | --------------------------------------------------------- | -| No EVSE commissioned | `snapshot.evse` is empty dict; no entities created | -| EVSE appears after setup | Capability detection sees new "evse" cap, triggers reload | -| Multiple EVSE devices | Each gets its own sub-device + full entity set | -| EVSE removed from panel | Entities become unavailable (standard HA behavior) | -| Missing metadata fields | DeviceInfo uses fallback values ("SPAN", "SPAN Drive") | -| Simulation mode | Bidirectional circuits generate simulated EVSE snapshots | -| EVSE without feed | Skipped — `feed` is required to associate with a circuit | - ---- - -## Files Modified - -### Library (`span-panel-api`) - -| File | Change | -| ---------------------------------- | --------------------------------------------------------- | -| `src/span_panel_api/models.py` | Add `SpanEvseSnapshot`, add `evse` to `SpanPanelSnapshot` | -| `src/span_panel_api/mqtt/homie.py` | Add `_build_evse_devices()`, call from `build_snapshot()` | -| `src/span_panel_api/simulation.py` | Generate EVSE entries for bidirectional circuits | -| `src/span_panel_api/__init__.py` | Export `SpanEvseSnapshot` | -| `tests/test_mqtt_homie.py` | EVSE parsing tests | - -### Integration (`span`) - -| File | Change | -| ---------------------------------------------------- | -------------------------------------------------------------- | -| `custom_components/span_panel/util.py` | Add `evse_device_info()` with `panel_name` + `display_suffix` | -| `custom_components/span_panel/helpers.py` | Add EVSE unique ID helpers + `resolve_evse_display_suffix()` | -| `custom_components/span_panel/sensor_definitions.py` | Add EVSE sensor descriptions | -| `custom_components/span_panel/sensors/evse.py` | **New** — EVSE sensor entity class | -| `custom_components/span_panel/sensors/factory.py` | Add `has_evse()`, `create_evse_sensors()`, update capabilities | -| `custom_components/span_panel/binary_sensor.py` | Add EVSE binary sensor descriptions + class | -| `custom_components/span_panel/coordinator.py` | Add "evse" to `_detect_capabilities()` | -| `custom_components/span_panel/sensors/circuit.py` | "EV Charger" fallback name | -| `custom_components/span_panel/strings.json` | EVSE entity names + enum states | -| `custom_components/span_panel/translations/*.json` | 5 translation files | -| `tests/test_evse_entities.py` | **New** — EVSE entity tests | - ---- - -## Verification - -1. `cd /Users/bflood/projects/HA/span-panel-api && python -m pytest tests/ -q` — library tests pass -2. `cd /Users/bflood/projects/HA/span && python -m pytest tests/ -q` — integration tests pass -3. Simulator mode with bidirectional circuit creates EVSE entities -4. EVSE sub-device in HA device registry shows manufacturer/model/serial/sw_version -5. Charger Status shows translated enum states -6. Binary sensors derive correctly from charger status -7. Capability detection triggers reload when EVSE first appears diff --git a/docs/dev/mqtt-sensor-topic.md b/docs/dev/mqtt-sensor-topic.md deleted file mode 100644 index 90eb2f3d..00000000 --- a/docs/dev/mqtt-sensor-topic.md +++ /dev/null @@ -1,491 +0,0 @@ -# V2 Sensor Alignment - -## Context - -The v2 MQTT migration is functionally complete (181 tests passing, both repos on `ebus_integration` branch). The integration receives all Homie data from the -panel's private eBus MQTT broker, but several sensors still expose v1-derived shim values (DSM_GRID_UP, PANEL_ON_GRID, etc.) instead of the actual v2 MQTT -values. Additionally, the panel's MQTT schema exposes rich topology and metadata (breaker ratings, dipole status, per-phase currents/voltages) that is not -surfaced to users at all. - -**Goal:** Extend the HA entity model with honest v2 state values, new measurements, and enriched attributes — exposing panel topology and metadata through -sensor attributes rather than a separate MQTT bridge. All structural data (tabs, breaker ratings, device types, panel size) is accessible through entity -attributes, allowing custom Lovelace cards to read everything they need from the HA entity model directly. - -## Architecture - -```text -SPAN eBus MQTT Broker (private, TLS, panel-specific creds) - │ - └── SpanMqttClient (library) ── subscribes to ebus/5/{serial}/# - │ - └── HomieDeviceConsumer → SpanPanelSnapshot - │ - └── Coordinator → Entities (sensors, switches, selects, binary_sensors) - │ - ├── Sensors: live measurements, state values, energy tracking - └── Attributes: topology, metadata, per-leg voltages, breaker ratings -``` - -## Part 1: Sensor & Attribute Changes - -### 1A. Replace v1-derived panel status sensors with honest v2 values - -**Remove:** - -| Key | Name | Value source | Problem | -| ----------- | --------- | ---------------------------------------- | ------------------------------------------------------- | -| `dsm_state` | DSM State | `_derive_dsm_state()` → DSM_GRID_UP/DOWN | Lossy: DPS=PV → "GRID_DOWN" even when grid is connected | - -`dsm_state` conflates "what is providing power" with "is the grid connected" — solar-dominant panels report DSM_GRID_DOWN while the grid is still up. Replaced -by `dominant_power_source` which exposes the raw enum without lossy interpretation. - -**Keep (improved derivation):** - -| Key | Name | Value source | Change | -| -------------------- | ------------------ | ----------------------------------- | ----------------------------------------------------- | -| `dsm_grid_state` | DSM Grid State | `_derive_dsm_grid_state()` improved | Multi-signal heuristic instead of BESS-only lookup | -| `current_run_config` | Current Run Config | `_derive_run_config()` improved | Tri-state from dsm_grid_state + grid_islandable + DPS | - -**`dsm_grid_state` — improved derivation:** - -The current `_derive_dsm_grid_state()` only checks `bess/grid-state`, returning UNKNOWN for every non-BESS panel. The improved derivation combines three signals -already on the snapshot (see Part 2D for implementation): - -```text -1. bess/grid-state available? → use it (authoritative) -2. dominant_power_source == GRID? → DSM_ON_GRID (grid is primary source) -3. dominant_power_source != GRID - AND instant_grid_power_w != 0? → DSM_ON_GRID (grid still exchanging power) -4. dominant_power_source != GRID - AND instant_grid_power_w == 0? → DSM_OFF_GRID (nothing flowing + grid not dominant) -``` - -Case 4 avoids the net-zero false positive: if the panel is grid-connected but at net-zero exchange, DPS is still GRID (the grid is the primary source/sink), so -case 2 fires — not case 4. - -**`current_run_config` — improved derivation:** - -The v1 derivation collapsed `dominant_power_source` to PANEL_ON_GRID/PANEL_OFF_GRID, losing the PANEL_BACKUP value entirely. Now that `dsm_grid_state` reliably -answers "is the grid connected?" and we have `grid_islandable` and `dominant_power_source`, we can reconstruct the full tri-state: - -| `grid_islandable` | `dsm_grid_state` | `dominant_power_source` | → `current_run_config` | Reasoning | -| ----------------- | ---------------- | ----------------------- | ---------------------- | ---------------------------------------------------- | -| false | DSM_ON_GRID | \* | PANEL_ON_GRID | Non-islandable panel, grid connected | -| true | DSM_ON_GRID | \* | PANEL_ON_GRID | Islandable panel, grid connected | -| true | DSM_OFF_GRID | BATTERY | PANEL_BACKUP | Islanded, running on battery — grid failure | -| true | DSM_OFF_GRID | PV / GENERATOR | PANEL_OFF_GRID | Islanded, running on local generation — intentional | -| true | DSM_OFF_GRID | NONE / UNKNOWN | UNKNOWN | Islanded, power source unclear | -| false | DSM_OFF_GRID | \* | UNKNOWN | Shouldn't happen — non-islandable panel can't island | - -The key insight: when the panel is off-grid and running on **battery**, that's a backup scenario (grid failed, battery keeping things alive). When off-grid and -running on **PV or generator**, that's intentional off-grid operation. This distinction was available in the v1 REST API but lost in the original v2 derivation. - -**Add:** - -| Key | Name | Value source | MQTT property | -| ----------------------- | --------------------- | ------------------------- | ------------------------------------------------------------------------- | -| `dominant_power_source` | Dominant Power Source | `s.dominant_power_source` | core/dominant-power-source (enum: GRID,BATTERY,PV,GENERATOR,NONE,UNKNOWN) | - -**Kept unchanged:** `main_relay_state` (Main Relay State) — already maps directly to core/relay. - -**Rationale:** `dsm_state` derived grid-up/grid-down from `dominant_power_source`, conflating "what is providing power" with "is the grid connected". Now that -`dominant_power_source` is exposed directly as a sensor, `dsm_state` is redundant. `current_run_config` is retained because it answers a distinct question — -"what operational mode is the panel in?" — by combining `dsm_grid_state`, `grid_islandable`, and `dominant_power_source` into a meaningful tri-state that the -other sensors individually cannot express. `grid_islandable` is a static boolean (panel capability, not state) — exposed as an attribute on the panel power -sensor. - -### 1B. New panel-level sensors - -| Key | Name | Device class | Unit | Value source | MQTT property | -| -------------- | ------------ | ------------ | ---- | ---------------- | ------------------------------------------------------- | -| `vendor_cloud` | Vendor Cloud | — | — | `s.vendor_cloud` | core/vendor-cloud (enum: UNKNOWN,UNCONNECTED,CONNECTED) | - -**Removed from binary_sensor.py:** The `SYSTEM_CELLULAR_LINK` ("Vendor Cloud") entry is removed from `BINARY_SENSORS`. It was coercing a tri-state enum -(UNKNOWN/UNCONNECTED/CONNECTED) to boolean. The new regular sensor exposes the actual value. - -### 1C. New power-flows sensors - -The `energy.ebus.device.power-flows` node provides system-level power flow data. Of the four properties (pv, battery, grid, site), two provide value as sensors: - -- `power_flow_grid` — redundant with `instant_grid_power_w` (upstream lugs active-power), not exposed -- `power_flow_pv` — derivable from PV circuit power sensors (circuits with device_type "pv"), not exposed -- `power_flow_battery` — **genuinely new**: battery charge/discharge rate, no existing sensor -- `power_flow_site` — mathematically grid + PV + battery, but valuable as a direct historical metric - -**Entity ID consistency:** These sensors follow the established `*_power` suffix pattern used by existing panel power sensors (`current_power`, -`feed_through_power`). The sensor definition `key` doubles as the entity suffix since these are v2-native (no legacy camelCase key to map from). Unique ID and -entity ID construction are handled by the existing helper infrastructure (`construct_synthetic_unique_id_for_entry()`, `get_panel_entity_suffix()`) — the same -code paths used by `instantGridPowerW` and `feedthroughPowerW`. - -| Key | Name | Suffix | Device class | Unit | Value source | -| --------------- | ------------- | --------------- | ------------ | ---- | ---------------------- | -| `battery_power` | Battery Power | `battery_power` | `POWER` | W | `s.power_flow_battery` | -| `site_power` | Site Power | `site_power` | `POWER` | W | `s.power_flow_site` | - -**Naming infrastructure updates:** - -- `helpers.py` `PANEL_ENTITY_SUFFIX_MAPPING`: add `"battery_power": "battery_power"`, `"site_power": "site_power"` -- `helpers.py` `PANEL_SUFFIX_MAPPING`: add `"battery_power": "battery_power"`, `"site_power": "site_power"` -- `entity_id_naming_patterns.py` `panel_level_suffixes`: add `"battery_power"`, `"site_power"` - -**Battery:** Positive = discharging (providing power to the home), negative = charging. Enables HA consumption/production/net energy tracking for the battery — -the missing piece for battery owners. - -**Site:** Total site consumption/production regardless of source. For a solar+battery panel this shows the complete picture of that installation over time — a -different value than grid (import/export) or battery (charge/discharge) alone. Useful for HA energy dashboard without requiring a template sensor to sum the -parts. - -The library parses all four power-flow properties (stored on `SpanPanelSnapshot`), but only battery, pv, and site are exposed as sensors. Grid remains available -on the snapshot for internal use (e.g., the `_derive_dsm_grid_state()` heuristic). - -### 1D. PV and BESS metadata as sensor attributes - -The PV and BESS Homie nodes publish commissioning metadata (vendor, product, nameplate capacity) that is useful context for users monitoring their systems. -These are exposed as attributes on the corresponding power sensors: - -**PV Power sensor** (`pvPowerW`) attributes: - -| Attribute | Value source | Notes | -| ---------------------- | --------------------------- | --------------------------------------------- | -| `vendor_name` | `s.pv.vendor_name` | PV inverter vendor (e.g., "Enphase", "Other") | -| `product_name` | `s.pv.product_name` | PV inverter product (e.g., "IQ8+") | -| `nameplate_capacity_w` | `s.pv.nameplate_capacity_w` | Rated inverter capacity in W | - -**Battery Power sensor** (`batteryPowerW`) attributes: - -| Attribute | Value source | Notes | -| ------------------------ | ---------------------------------- | ----------------------------- | -| `vendor_name` | `s.battery.vendor_name` | BESS vendor name | -| `product_name` | `s.battery.product_name` | BESS product name | -| `nameplate_capacity_kwh` | `s.battery.nameplate_capacity_kwh` | Rated battery capacity in kWh | - -**Library models:** - -- `SpanPVSnapshot` (new): `vendor_name`, `product_name`, `nameplate_capacity_w` — populated from first PV metadata node -- `SpanBatterySnapshot` (extended): `vendor_name`, `product_name`, `nameplate_capacity_kwh` — parsed from BESS metadata node - -### 1E. Enriched circuit sensor attributes - -**Circuit power sensor** (`SpanCircuitPowerSensor.extra_state_attributes`) currently exposes: `tabs`, `voltage`, `amperage`. - -The existing `voltage` attribute derives 120/240 from tab count via `construct_voltage_attribute()`. This is equivalent to `circuit.is_240v` (dipole), so -`is_240v` is not added as a separate attribute — the information is already present. The existing helper could be refactored to use `circuit.is_240v` directly -instead of counting tabs, but that's an implementation detail, not a new attribute. - -**Add:** - -| Attribute | Value source | Notes | -| ----------------- | -------------------------- | -------------------------------------------------- | -| `breaker_rating` | `circuit.breaker_rating_a` | Integer, amps. "15A"/"20A" badge on SPAN dashboard | -| `device_type` | `circuit.device_type` | "circuit", "pv", or "evse" | -| `always_on` | `circuit.always_on` | Boolean | -| `relay_state` | `circuit.relay_state` | OPEN/CLOSED/UNKNOWN | -| `relay_requester` | `circuit.relay_requester` | Who requested the relay state | -| `shed_priority` | `circuit.priority` | NEVER/SOC_THRESHOLD/OFF_GRID/UNKNOWN | -| `is_sheddable` | `circuit.is_sheddable` | Boolean | - -These are all already on `SpanCircuitSnapshot` — no library changes needed. - -### 1F. Enriched panel power sensor attributes - -**Panel power sensor** (`SpanPanelPowerSensor.extra_state_attributes`) currently hardcodes `voltage=240`, calculates `amperage=power/240`. - -**Update:** - -- Keep `voltage=240` as the nominal value (this is "it's a 240V split-phase panel", not a measurement) -- Add actual measured per-leg voltages: - - `l1_voltage` — L1 leg voltage (V), from core/l1-voltage (actual, e.g., 121.3) - - `l2_voltage` — L2 leg voltage (V), from core/l2-voltage (actual, e.g., 119.8) - - L1/L2 show leg symmetry at a glance (both should be ~120V); they're not interesting as time-series sensors but are useful as live attributes on the power - measurement they relate to -- Use actual voltage (L1 + L2) for the amperage calculation when available, fall back to nominal 240 -- Add `main_breaker_rating` — the main breaker limits total panel current, which is what this sensor measures. Same pattern as circuit `breaker_rating` on - circuit power sensors. Can change with service upgrade. -- Add `grid_islandable` (boolean — static panel capability, contextual to power) - -**Software version sensor** (`SpanPanelStatus.extra_state_attributes`) — add panel metadata attributes: - -- `panel_size` — total number of breaker spaces (e.g., 32, 40). Static panel hardware spec. -- `wifi_ssid` — informational, current Wi-Fi network - -### 1G. Lugs per-phase current as attributes on panel power sensors - -The lugs nodes have `l1-current` and `l2-current` properties that are not currently parsed. Add these to `SpanPanelSnapshot` and expose as attributes on the -panel power sensors — upstream lugs on Current Power, downstream lugs on Feed Through Power: - -**Current Power (upstream lugs):** - -| Attribute | Value source | Notes | -| ------------- | ------------------------- | ------------------------ | -| `l1_amperage` | `s.upstream_l1_current_a` | Upstream lugs L1 current | -| `l2_amperage` | `s.upstream_l2_current_a` | Upstream lugs L2 current | - -**Feed Through Power (downstream lugs):** - -| Attribute | Value source | Notes | -| ------------- | --------------------------- | -------------------------- | -| `l1_amperage` | `s.downstream_l1_current_a` | Downstream lugs L1 current | -| `l2_amperage` | `s.downstream_l2_current_a` | Downstream lugs L2 current | - -### 1H. Entity registry migration - -Old unique IDs must be migrated to preserve history: - -```text -span_{serial}_dsm_state → (removed, replaced by dominant_power_source) -span_{serial}_dsm_grid_state → (kept, derivation improved — no migration needed) -span_{serial}_current_run_config → (kept, derivation improved — no migration needed) -span_{serial}_wwanLink → (removed, binary sensor platform can't migrate to sensor) -``` - -The vendor-cloud binary sensor entity is simply removed — its boolean history is not meaningful for the new tri-state sensor. `dsm_state` is removed — -`dominant_power_source` exposes the raw enum with higher fidelity. `dsm_grid_state` and `current_run_config` are retained with improved derivations (see 1A). - -## Part 2: Library Changes (span-panel-api) - -### 2A. Add power-flows node parsing - -Currently `TYPE_POWER_FLOWS` is defined in `mqtt/const.py` but never used in `homie.py`. - -**`models.py` — add fields to `SpanPanelSnapshot`:** - -```python -# Power flows (None when node not present) -power_flow_pv: float | None = None -power_flow_battery: float | None = None -power_flow_grid: float | None = None -power_flow_site: float | None = None -``` - -**`mqtt/homie.py` — parse power-flows node in `_build_snapshot()`:** - -```python -pf_node = self._find_node_by_type(TYPE_POWER_FLOWS) -power_flow_pv = _parse_float(self._get_prop(pf_node, "pv")) if pf_node else None -# ... etc for battery, grid, site -``` - -### 2B. Add PV and BESS metadata models - -**`models.py` — new `SpanPVSnapshot`:** - -```python -@dataclass(frozen=True, slots=True) -class SpanPVSnapshot: - vendor_name: str | None = None - product_name: str | None = None - nameplate_capacity_w: float | None = None -``` - -**`models.py` — extend `SpanBatterySnapshot`:** - -```python -# Added fields: -vendor_name: str | None = None -product_name: str | None = None -nameplate_capacity_kwh: float | None = None -``` - -**`models.py` — add `pv` field to `SpanPanelSnapshot`:** - -```python -pv: SpanPVSnapshot = field(default_factory=SpanPVSnapshot) -``` - -**`mqtt/homie.py` — new `_build_pv()` and extended `_build_battery()`:** - -Parses `vendor-name`, `product-name`, `nameplate-capacity` from the first PV and BESS metadata nodes. - -### 2C. Add lugs per-phase current - -**`models.py` — add fields to `SpanPanelSnapshot`:** - -```python -# Upstream lugs per-phase current (None when not available) -upstream_l1_current_a: float | None = None -upstream_l2_current_a: float | None = None -``` - -**`mqtt/homie.py` — parse in `_build_snapshot()`:** - -```python -if upstream_lugs is not None: - l1_i = self._get_prop(upstream_lugs, "l1-current") - upstream_l1_current = _parse_float(l1_i) if l1_i else None - # ... etc -``` - -### 2D. Improve `_derive_dsm_grid_state()` heuristic - -The current implementation only checks `bess/grid-state`, returning UNKNOWN for non-BESS panels. Replace with a multi-signal approach using values already on -the snapshot: - -**`mqtt/homie.py` — updated `_derive_dsm_grid_state()`:** - -```python -def _derive_dsm_grid_state(self, core_node: str | None, grid_power: float) -> str: - """Derive v1-compatible dsm_grid_state from multiple signals. - - Priority: - 1. bess/grid-state — authoritative when BESS is commissioned - 2. dominant-power-source == GRID — grid is the primary source - 3. grid_power != 0 — grid is exchanging power (even if not dominant) - 4. grid_power == 0 AND DPS != GRID — islanded (no flow + grid not dominant) - """ - # 1. BESS grid-state is authoritative when available - bess_node = self._find_node_by_type(TYPE_BESS) - if bess_node is not None: - gs = self._get_prop(bess_node, "grid-state") - if gs == "ON_GRID": - return "DSM_ON_GRID" - if gs == "OFF_GRID": - return "DSM_OFF_GRID" - - # 2. Dominant power source == GRID → on-grid by definition - if core_node is not None: - dps = self._get_prop(core_node, "dominant-power-source") - if dps == "GRID": - return "DSM_ON_GRID" - - # 3. Grid still exchanging power → on-grid (just not dominant) - if dps in ("BATTERY", "PV", "GENERATOR") and grid_power != 0.0: - return "DSM_ON_GRID" - - # 4. Non-grid dominant + zero grid power → off-grid - if dps in ("BATTERY", "PV", "GENERATOR") and grid_power == 0.0: - return "DSM_OFF_GRID" - - return "UNKNOWN" -``` - -The `grid_power` parameter is `instant_grid_power_w` from upstream lugs — already parsed in `_build_snapshot()`. The method signature changes from -`_derive_dsm_grid_state(self)` to `_derive_dsm_grid_state(self, core_node, grid_power)`, and the call site in `_build_snapshot()` passes the already-computed -values. - -Also remove `_derive_dsm_state()` — no longer needed. Remove the `dsm_state` field from `SpanPanelSnapshot` (or keep as deprecated with a fixed "UNKNOWN" value -if backward compatibility is needed during transition). - -**`mqtt/homie.py` — updated `_derive_run_config()`:** - -The improved derivation combines `dsm_grid_state`, `grid_islandable`, and `dominant_power_source` to reconstruct the full v1 tri-state. The method now takes the -already-computed values as parameters instead of re-querying MQTT properties: - -```python -def _derive_run_config( - self, dsm_grid_state: str, grid_islandable: bool | None, dps: str | None -) -> str: - """Derive current_run_config from grid state, islandability, and power source. - - Decision table: - ┌─────────────────┬───────────────┬─────────────────────────┬────────────────────────┐ - │ grid_islandable │ dsm_grid_state│ dominant_power_source │ current_run_config │ - ├─────────────────┼───────────────┼─────────────────────────┼────────────────────────┤ - │ false │ DSM_ON_GRID │ * │ PANEL_ON_GRID │ - │ true │ DSM_ON_GRID │ * │ PANEL_ON_GRID │ - │ true │ DSM_OFF_GRID │ BATTERY │ PANEL_BACKUP │ - │ true │ DSM_OFF_GRID │ PV / GENERATOR │ PANEL_OFF_GRID │ - │ true │ DSM_OFF_GRID │ NONE / UNKNOWN │ UNKNOWN │ - │ false │ DSM_OFF_GRID │ * │ UNKNOWN (shouldn't happen) │ - └─────────────────┴───────────────┴─────────────────────────┴────────────────────────┘ - """ - if dsm_grid_state == "DSM_ON_GRID": - return "PANEL_ON_GRID" - - if dsm_grid_state == "DSM_OFF_GRID": - if not grid_islandable: - return "UNKNOWN" # Non-islandable panel reporting off-grid — unexpected - if dps == "BATTERY": - return "PANEL_BACKUP" - if dps in ("PV", "GENERATOR"): - return "PANEL_OFF_GRID" - return "UNKNOWN" - - return "UNKNOWN" -``` - -The call site in `_build_snapshot()` chains the two derivations: - -```python -dsm_grid_state = self._derive_dsm_grid_state(core_node, grid_power) -current_run_config = self._derive_run_config(dsm_grid_state, grid_islandable, dominant_power_source) -``` - -### 2E. Simulation engine updates - -`DynamicSimulationEngine` should populate the new fields (`power_flow_*`, `upstream_l1_current_a`, etc.) in its generated snapshots so simulation mode continues -to work with the new sensors. - -## Files Modified - -### span-panel-api (`/Users/bflood/projects/HA/span-panel-api`) - -| File | Changes | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/span_panel_api/models.py` | Add SpanPVSnapshot, extend SpanBatterySnapshot with metadata; add power*flow*\*, upstream/downstream l1/l2_current_a, pv fields to SpanPanelSnapshot | -| `src/span_panel_api/mqtt/homie.py` | Parse power-flows node, parse lugs l1/l2-current, parse PV/BESS metadata; improve `_derive_dsm_grid_state()` and `_derive_run_config()` heuristics; remove `_derive_dsm_state()` | -| `src/span_panel_api/simulation.py` | Populate new snapshot fields in simulated data | -| `tests/` | Update snapshot fixtures, add power-flows, lugs current, PV metadata, and BESS metadata tests | - -### span (HA integration) (`/Users/bflood/projects/HA/span`) - -| File | Changes | -| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `custom_components/span_panel/const.py` | Remove DSM_GRID_UP/DOWN, SYSTEM_CELLULAR_LINK | -| `custom_components/span_panel/sensor_definitions.py` | Remove dsm_state; add dominant_power_source, vendor_cloud, battery_power, site_power (keep dsm_grid_state, current_run_config) | -| `custom_components/span_panel/binary_sensor.py` | Remove SYSTEM_CELLULAR_LINK entry from BINARY_SENSORS | -| `custom_components/span_panel/sensors/circuit.py` | Add breaker_rating, device_type, always_on, relay_state, relay_requester, shed_priority, is_sheddable to extra_state_attributes | -| `custom_components/span_panel/sensors/panel.py` | Add l1/l2 voltage, l1/l2 amperage (upstream + downstream lugs), main_breaker_rating, grid_islandable to panel power attributes; add PV/BESS metadata attributes to pvPowerW/batteryPowerW sensors; add panel_size, wifi_ssid to software version attributes | -| `custom_components/span_panel/sensors/factory.py` | Wire new sensors | -| `custom_components/span_panel/helpers.py` | Remove dsmState from suffix mappings; add battery_power, site_power mappings | -| `custom_components/span_panel/migration.py` | Update native_sensor_map for new keys | -| `custom_components/span_panel/entity_id_naming_patterns.py` | Update panel sensor key list | -| `custom_components/span_panel/__init__.py` | Entity registry migration (remove dsm_state entity) | -| `tests/` | Update sensor expectations, add new sensor/attribute tests | - -## Implementation Order - -**Phase A — Library (span-panel-api):** - -1. Add new fields to SpanPanelSnapshot and SpanCircuitSnapshot -2. Add SpanPVSnapshot model, extend SpanBatterySnapshot with metadata fields -3. Parse power-flows node in homie.py -4. Parse lugs l1/l2-current in homie.py -5. Parse PV and BESS metadata in homie.py -6. Improve `_derive_dsm_grid_state()` and `_derive_run_config()` heuristics; remove `_derive_dsm_state()` -7. Update simulation engine -8. Update tests - -**Phase B — Integration sensors & attributes:** - -1. const.py — remove v1-derived constants (DSM_GRID_UP/DOWN); keep DSM_ON_GRID/OFF_GRID, PANEL_ON_GRID/OFF_GRID/BACKUP -2. sensor_definitions.py — remove dsm_state; add dominant_power_source, vendor_cloud, battery_power, site_power sensors (keep dsm_grid_state, - current_run_config) -3. binary_sensor.py — remove vendor-cloud binary sensor -4. helpers.py — update suffix mappings -5. sensors/factory.py — wire new sensors -6. sensors/circuit.py — enrich extra_state_attributes -7. sensors/panel.py — add l1/l2 voltage, l1/l2 amperage (upstream lugs on Current Power, downstream lugs on Feed Through Power), main_breaker_rating, - grid_islandable to panel power attributes; add PV metadata to pvPowerW, BESS metadata to batteryPowerW; add panel_size and wifi_ssid to software version - attributes -8. migration.py — update legacy mapping -9. entity_id_naming_patterns.py — update key list -10. `__init__.py` — entity registry migration (remove dsm_state entity) - -**Phase C — Tests & cleanup:** - -1. Update all affected tests - -## Verification - -1. `cd /Users/bflood/projects/HA/span-panel-api && python -m pytest tests/ -q` — all tests pass -2. `cd /Users/bflood/projects/HA/span && python -m pytest tests/ -q` — all tests pass -3. `grep -r "DSM_GRID_UP" custom_components/` — no hits outside migration/simulation -4. `grep -r "dsm_state" custom_components/span_panel/sensor_definitions.py` — no hits (dsm_grid_state and current_run_config should still be present) -5. `grep -r "SYSTEM_CELLULAR_LINK" custom_components/span_panel/binary_sensor.py` — no hits -6. New sensors appear: dominant_power_source, vendor_cloud, battery_power, site_power; dsm_grid_state and current_run_config retained with improved derivations -7. Circuit power sensor attributes include: breaker_rating, device_type, always_on, relay_state, shed_priority, is_sheddable -8. Panel power sensor (Current Power) attributes include: l1_voltage, l2_voltage, l1_amperage, l2_amperage, main_breaker_rating, grid_islandable 8b. Panel power - sensor (Feed Through Power) attributes include: l1_amperage, l2_amperage -9. PV Power sensor attributes include: vendor_name, product_name, nameplate_capacity_w -10. Battery Power sensor attributes include: vendor_name, product_name, nameplate_capacity_kwh -11. Software version sensor attributes include: panel_size, wifi_ssid diff --git a/docs/dev/schema_driven_changes.md b/docs/dev/schema_driven_changes.md deleted file mode 100644 index 84956fab..00000000 --- a/docs/dev/schema_driven_changes.md +++ /dev/null @@ -1,228 +0,0 @@ -# Schema-Driven Sensor Discovery - -This document describes a phased approach to reducing the manual coupling between the SPAN Homie MQTT schema, the `span-panel-api` library, and the HA -integration's sensor definitions. The goal is not full auto-generation but a practical reduction in the maintenance surface when SPAN firmware adds, corrects, -or extends properties. - -## Motivation - -Today the system has three layers of hardcoded knowledge about SPAN panel properties: - -1. **Homie schema** (`GET /api/v2/homie/schema`) -- declares every node type, property, datatype, unit, format, and settable flag. Self-describing and - firmware-versioned. -2. **span-panel-api** -- hand-coded `HomieDeviceConsumer._build_snapshot()` (653 lines) maps MQTT properties to frozen dataclass fields. Sign conventions, - cross-references, and derived state are embedded here. -3. **span integration** -- 47+ `SensorEntityDescription` instances in `sensor_definitions.py`, each with a `value_fn` lambda reaching into a specific snapshot - field plus HA metadata (`device_class`, `state_class`, `native_unit_of_measurement`). - -Adding a new sensor requires changes to all three layers. Correcting a unit requires changes to layers 2 and 3. The schema itself evolves with firmware -releases. - -### Why Not Go Fully Schema-Driven - -The Homie schema is self-describing but not self-correct. The 202609 changelog documented unit declaration errors (`kW` declared when values were actually `W`) -for `active-power` and PV `nameplate-capacity`. The integration's hardcoded knowledge of the correct units protected users from displaying values 1000x off. A -schema-driven integration would have propagated the error. - -Additional blockers: - -- **No schema versioning** -- the schema is tied to firmware releases (`rYYYYWW`), which conflates "the software running on the panel" with "the data contract - the panel exposes." A firmware update may change dozens of things without touching the schema, or alter one property's unit declaration without changing the - firmware version format. There is no independent schema version, no mechanism to request a specific version, and no backwards-compatibility guarantee. The - Homie API (`/api/v2/`) is currently in beta, which explains the in-place schema mutations; post-beta breaking changes would be expected under a new endpoint - (e.g. `/api/v3/`). The schema hash computed by Phase 1 drift detection is the best available proxy for a schema version -- a content-addressed identifier for - the exact set of node types, properties, units, and datatypes. -- **Irreducible semantic layer** -- sign conventions, derived state machines (`dsm_state`, `current_run_config`), cross-references (EVSE `feed` to circuit), - unmapped tab synthesis, and energy dip compensation are domain logic not representable in the Homie schema. -- **HA-specific metadata** -- `device_class`, `state_class`, `entity_category`, `suggested_display_precision` have no Homie equivalent. -- **User stability** -- HA users build automations and dashboards against stable entity IDs and sensor behaviors. Schema-driven changes that silently alter a - sensor's unit or meaning would break installations. - -The phased approach below progressively surfaces schema metadata for validation and diagnostic purposes first, then optionally for reducing entity definition -boilerplate on reviewed fields, without ever trusting the schema blindly for units or semantics and without ever exposing fields to users without human review. - -## Phase 1: Schema Metadata Exposure (Validation and Diagnostics) — COMPLETE - -**Status**: Implemented and tested across both repositories. - -### Architectural Boundary - -The integration knows nothing about Homie, MQTT, node types, or property IDs. All transport knowledge lives in `span-panel-api`. The integration sees only: - -- **Snapshot field paths** -- `"panel.instant_grid_power_w"`, `"circuit.current_a"`, etc. -- **Field metadata** -- unit and datatype per field, exposed by the library in transport-agnostic terms. -- **Sensor definitions** -- the integration's own HA metadata for each sensor. - -### span-panel-api Implementation - -| Module | Purpose | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| `models.py` | `FieldMetadata(unit, datatype)` frozen dataclass | -| `mqtt/field_metadata.py` | `_PROPERTY_FIELD_MAP` (Homie property → field path), `build_field_metadata()`, `log_schema_drift()` | -| `mqtt/client.py` | Retains schema across connections, builds/caches field metadata during `connect()`, detects schema hash changes and diffs properties | -| `protocol.py` | `field_metadata` property added to `SpanPanelClientProtocol` | - -**Data flow at connect time:** - -1. `SpanMqttClient.connect()` fetches the Homie schema -2. If the schema hash changed since last connection, `log_schema_drift()` diffs the old and new schemas at the property level (new/removed node types, - new/removed properties, unit/datatype/format changes) -- all logged internally, never exposed to the integration -3. `build_field_metadata(schema.types)` iterates `_PROPERTY_FIELD_MAP`, looks up each Homie property in the live schema for its declared unit and datatype, and - produces `dict[str, FieldMetadata]` keyed by snapshot field path -4. The result is cached on the client as the `field_metadata` property - -**Field path convention:** `{snapshot_type}.{field_name}` where snapshot_type is one of `panel`, `circuit`, `battery`, `pv`, `evse`. The library defines this -convention in `_PROPERTY_FIELD_MAP` (~55 entries covering all properties that `_build_snapshot()` reads). - -### span Integration Implementation - -| Module | Purpose | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `schema_expectations.py` | `SENSOR_FIELD_MAP` -- sensor definition key → snapshot field path (the ONLY manually-maintained data) | -| `schema_validation.py` | `validate_field_metadata()` -- unit cross-check, unmapped field reporting. `collect_sensor_definitions()` -- builds the sensor defs dict | -| `coordinator.py` | `_run_schema_validation()` -- one-shot call after first successful refresh | - -**Data flow at first refresh:** - -1. `SpanPanelCoordinator._run_post_update_tasks()` fires `_run_schema_validation()` once -2. Reads `client.field_metadata` and converts `FieldMetadata` objects to plain dicts -3. `collect_sensor_definitions()` gathers all sensor descriptors into a dict keyed by sensor key -4. `validate_field_metadata()` walks `SENSOR_FIELD_MAP`, for each entry: - - Looks up the field path in the library's metadata to get the schema-declared unit - - Looks up the sensor key in the sensor definitions to get the HA `native_unit_of_measurement` - - Compares them and logs mismatches -5. Reports fields in the library's metadata that no sensor references - -**Validation checks:** - -| Check | Severity | Example | -| ---------------- | -------- | ------------------------------------------------------------ | -| Unit mismatch | DEBUG | Field metadata says `kW`, sensor definition has `W` | -| Missing metadata | DEBUG | Sensor reads a field the library has no metadata for | -| Missing unit | DEBUG | Field metadata has no unit but sensor definition expects `V` | -| Unmapped field | DEBUG | Library metadata contains a field no sensor references | - -All output is DEBUG-level only -- invisible to users at default HA log levels. A maintainer enables it by setting -`logger: custom_components.span_panel.schema_validation: debug` in their HA configuration. No entity creation or sensor behavior changes. - -### Tests - -**span-panel-api** (`tests/test_field_metadata.py`, 15 tests): field metadata building for all snapshot types, unit/datatype correctness, enum and boolean -handling, empty schema, field path convention, generic lugs fallback. - -**span integration** (`tests/test_schema_validation.py`, 13 tests): mapping structure validation, sensor keys match definitions, field paths match snapshot -attributes, unit cross-check (match, mismatch, missing), unmapped field detection, no-op when metadata unavailable. - -### What This Achieves - -- Zero risk to users -- no sensor behavior changes, log-only output. -- Clean architectural boundary -- integration never sees Homie/MQTT details. -- Early warning when schema-derived field metadata disagrees with sensor definitions (e.g. the kW/W error). -- Foundation for Phase 2 -- the field metadata and mapping are reusable. - -## Versioning Model - -The `span-panel-api` library is the gating factor for all schema changes reaching the integration. Even before SPAN corrects known unit declaration errors in -the Homie schema, the library applies the correct interpretation -- the snapshot contract defines the truth, not the schema. This isolation has two -consequences: - -1. **Schema corrections (declaration-only)** -- when SPAN fixes a unit declaration (e.g. `kW` → `W`) without changing actual values, neither repo needs code - changes. The library's `build_field_metadata()` automatically reflects the corrected declaration, and Phase 1 validation mismatches resolve themselves. - -2. **Value changes** -- if SPAN changes actual transmitted values (e.g. starts sending kW-scale values to match a `kW` declaration), the library must apply a - conversion in `_build_snapshot()` to maintain the snapshot contract. The integration bumps the library version; no other changes needed. - -In both cases, the library version pins a specific interpretation of firmware data. The version sequence for a breaking firmware change: - -1. SPAN releases firmware with changed property behavior -2. Library releases a new version with the conversion/adaptation -3. Integration bumps its library dependency -4. User updates the integration -- changelog explains what changed - -Each step is a human decision point. No change reaches users without explicit maintainer review. - -### Schema Version vs Firmware Version - -The correct thing to version against is the schema, not the firmware. The `rYYYYWW` firmware identifier conflates panel software with data contract. Ideally -SPAN would provide a declared `schema_version` field -- a monotonically increasing version or a semver -- so the library can say "I understand schema versions -up to X" rather than "I was built against firmware rXXXXYY." - -The current unit corrections and schema changes being made without a version bump are beta-phase behavior -- the Homie API is served at `/api/v2/` and is not -yet stable. Once the API exits beta, breaking changes to the schema would be expected to land under a new endpoint (e.g. `/api/v3/`), not as in-place mutations -to the v2 schema. This distinction matters: the current churn is not representative of the long-term maintenance burden, and the trigger criteria for later -phases should be evaluated against post-beta stability, not beta-phase corrections. - -Until SPAN provides a declared schema version, the library's schema hash (computed during Phase 1 drift detection) serves as the implicit schema version. The -library could maintain a known-schema-hashes table, mapping each validated hash to the set of corrections it applies. When encountering an unknown hash, it logs -a warning (drift detection already does this) and falls back to existing corrections -- safe-by-default behavior. - -### New Fields Require Human Review - -A new property appearing in the Homie schema must not be automatically exposed to users. The kW/W precedent proves that schema declarations cannot be trusted -for correctness on first appearance. If a field were surfaced automatically, users would build automations on it, and a subsequent correction to its unit or -sign convention would break those automations. - -The path for a new field: - -1. Phase 1 drift detection logs the new property -2. A maintainer reviews the property's actual values against its declared unit and datatype -3. The library adds the field to `_build_snapshot()` and `_PROPERTY_FIELD_MAP` -4. The integration adds a `SensorEntityDescription` with verified HA metadata -5. Both repos release new versions - -This is the same human-gated process used for existing fields. The library absorbs transport details; the integration adds HA semantics; nothing reaches users -without review. - -## Phase 2: Override-Table Entity Creation (Future) - -**Prerequisite**: Phase 1 complete. Schema metadata proven stable across multiple firmware releases. Schema unit corrections resolved (no outstanding known -errors). - -Replace the 47+ hardcoded `SensorEntityDescription` instances with: - -1. **A declarative override table** mapping snapshot field paths to HA metadata (`device_class`, `state_class`, sign convention, entity category). The library's - field metadata provides the base unit and datatype; the override table adds HA-specific semantics. -2. **A generic entity factory** that iterates the library's field metadata, applies overrides where present, and creates entities for fields that have an - override entry. Fields without an override entry are not exposed -- they remain invisible until a maintainer explicitly reviews them and adds an override. - -The override table reduces boilerplate for reviewed fields: the maintainer writes only the HA-specific semantics (device class, sign convention, etc.) and the -factory derives the rest from the library's field metadata. But no field is ever exposed without an explicit override entry. The integration never references -Homie node types or property IDs -- it operates entirely in terms of snapshot field paths and the library's field metadata. - -### Trigger Criteria for Phase 2 - -Do not proceed to Phase 2 until: - -- [ ] The Homie schema has had at least two firmware releases with no unit corrections -- [ ] Phase 1 validation logging has run in production and confirmed schema accuracy -- [ ] SPAN introduces schema versioning or a backwards-compatibility guarantee -- [ ] The rate of new properties is high enough that manual sensor additions are a meaningful maintenance burden - -## Phase 3: Build-Time Dataclass Generation (Future) - -**Prerequisite**: Phase 2 complete. Schema stable. Property additions are frequent. - -Auto-generate `span-panel-api` snapshot dataclasses from the schema at build time: - -1. A codegen script reads the schema from a reference panel (or saved fixture). -2. Outputs `models_generated.py` with typed frozen dataclasses matching the schema. -3. Manual `models.py` inherits from generated classes and adds derived fields (`dsm_state`, `current_run_config`, etc.). -4. `mypy` and IDE autocomplete continue working against concrete types. - -This reduces the library-side work for new fields -- the snapshot dataclass picks up new fields automatically from the schema. However, generated fields still -require human review before they are exposed to the integration. The maintainer must verify the field's actual values against its declared unit and datatype, -then add an override entry in the integration's Phase 2 override table. The codegen eliminates the library boilerplate but does not bypass the review gate. - -### Trigger Criteria for Phase 3 - -Do not proceed to Phase 3 until: - -- [ ] Phase 2 override-table model is proven and the generic entity factory is stable -- [ ] SPAN releases firmware updates with new properties frequently enough to justify the codegen infrastructure -- [ ] The manual snapshot dataclass maintenance cost exceeds the codegen maintenance cost - -## Cross-References - -- [Architecture](architecture.md) -- system overview and data flow -- [Dynamic Enum Options](dynamic_enum_options.md) -- runtime enum handling and schema trust limitations (directly relevant to Phase 1 validation) -- [SPAN API Client Docs](https://github.com/spanio/SPAN-API-Client-Docs) -- upstream Homie schema documentation and changelog diff --git a/docs/dev/statistics_spike_cleanup_proposal.md b/docs/dev/statistics_spike_cleanup_proposal.md deleted file mode 100644 index 913fea74..00000000 --- a/docs/dev/statistics_spike_cleanup_proposal.md +++ /dev/null @@ -1,476 +0,0 @@ -# Proposal: Statistics Spike Cleanup Service - -## Status - -**Implemented** - See PR for details - -## Problem Statement - -When the SPAN panel undergoes a firmware update, it sometimes resets or loses energy counter data, causing: - -1. **Negative Spikes in Energy Dashboard** - - Panel value drops from 8,913,289 Wh to 8,551,863 Wh - - HA statistics calculates: `-361,426 Wh` consumption - - Massive negative spike appears in Energy Dashboard charts - -2. **Current User Workaround is Tedious** - - Developer Tools → Statistics - - Find each affected entity - - Locate problematic timestamp - - Delete or adjust entry - - Repeat for 20+ circuits - - **Takes 20+ minutes per firmware update** - -## Proposed Solution - -### Statistics Spike Cleanup Service - -Provide a service that **detects and removes** negative energy spikes from Home Assistant's statistics database. - -**Key Principle:** Simple tool to cleanup spikes. User triggers when needed (e.g., after firmware update). No automation, no history tracking - KISS. - -## Architecture - -### Service: `span_panel.cleanup_energy_spikes` - -```yaml -service: span_panel.cleanup_energy_spikes -data: - # Optional: target specific entities (omit to process all SPAN energy sensors) - entity_id: - - sensor.span_panel_solar_produced_energy - - sensor.span_panel_main_meter_consumed_energy - # Optional: how many days in past to scan - days_back: 1 # Default: 1 day (last 24 hours) - # Optional: dry run mode - defaults to true for safety - dry_run: true # Set false to actually delete entries -``` - -### Detection Algorithm - -The service detects **firmware reset spikes** using the main meter as an indicator, then cleans up all affected sensors. - -**Simple Rule:** - -1. Find timestamps where main meter decreased (firmware reset) -2. Delete ALL SPAN energy sensor entries at those timestamps -3. Firmware reset affects all sensors simultaneously - -**Why Main Meter is the Indicator:** - -- Main meter always affected by firmware reset -- Single sensor to check vs. 32+ circuits -- Timestamp of main meter reset = timestamp for all sensors -- No need to check each sensor individually - -```python -async def cleanup_energy_spikes( - days_back: int = 1, - dry_run: bool = False -) -> dict: - """Detect and remove firmware reset spikes from all SPAN energy sensors. - - Uses main meter to detect reset timestamps, then deletes all - SPAN TOTAL_INCREASING sensor entries at those timestamps. - - Args: - days_back: How many days to scan (default: 1) - dry_run: Preview mode without making changes - - Returns summary of spikes found and removed. - """ - - # 1. Get main meter statistics - # 2. Find timestamps where main meter decreased (delta < 0) - # 3. Get all SPAN energy sensors (TOTAL_INCREASING) - # 4. Delete entries for ALL sensors at reset timestamps - # 5. Return summary report -``` - -### Implementation Steps - -1. **Query Main Meter Statistics** - - ```python - from homeassistant.components.recorder.statistics import ( - get_instance, - statistics_during_period, - ) - - # Get main meter statistics - main_meter_entity = "sensor.span_panel_main_meter_consumed_energy" - stats = await statistics_during_period( - hass, start_time, end_time, {main_meter_entity}, "hour" - ) - ``` - -2. **Detect Reset Timestamps** - - ```python - def find_reset_timestamps(stats: list) -> list[datetime]: - """Find timestamps where main meter decreased. - - These timestamps indicate firmware resets affecting all sensors. - """ - reset_timestamps = [] - - for i in range(1, len(stats)): - delta = stats[i]['sum'] - stats[i-1]['sum'] - - # Any negative delta = firmware reset - if delta < 0: - reset_timestamps.append(stats[i]['start']) - - return reset_timestamps - ``` - -3. **Get All SPAN Energy Sensors** - - ```python - # Get all SPAN energy sensors with TOTAL_INCREASING state class - span_energy_sensors = [ - entity_id - for entity_id in hass.states.async_entity_ids('sensor') - if entity_id.startswith('sensor.span_panel_') - and hass.states.get(entity_id).attributes.get('state_class') == 'total_increasing' - ] - ``` - -4. **Delete Entries at Reset Timestamps** - - ```python - from homeassistant.components.recorder import get_instance - from homeassistant.components.recorder.models import StatisticsShortTerm - - # Delete statistics entries - recorder = get_instance(hass) - with recorder.session_scope() as session: - for spike in spikes: - session.query(StatisticsShortTerm).filter( - StatisticsShortTerm.metadata_id == metadata_id, - StatisticsShortTerm.start == spike['timestamp'] - ).delete() - ``` - -5. **Delete Entries at Reset Timestamps** - - ```python - from homeassistant.components.recorder import get_instance - from homeassistant.components.recorder.models import StatisticsShortTerm - - # Delete statistics entries for ALL sensors at reset timestamps - recorder = get_instance(hass) - with recorder.session_scope() as session: - for timestamp in reset_timestamps: - for entity_id in span_energy_sensors: - # Get metadata_id for entity - metadata_id = get_metadata_id(session, entity_id) - - # Delete entry at this timestamp - session.query(StatisticsShortTerm).filter( - StatisticsShortTerm.metadata_id == metadata_id, - StatisticsShortTerm.start == timestamp - ).delete() - ``` - -6. **Return Summary Report** - - ```python - return { - 'reset_timestamps': reset_timestamps, - 'entities_affected': len(span_energy_sensors), - 'entries_deleted': total_deleted, - 'dry_run': dry_run - } - ``` - -## User Experience - -### Manual Trigger After Firmware Update - -```yaml -service: span_panel.cleanup_energy_spikes -# No parameters = scan all SPAN energy entities for last 24 hours -``` - -### Automated Cleanup via Automation - -```yaml -automation: - - alias: "SPAN: Auto-cleanup after firmware update" - trigger: - - platform: state - entity_id: sensor.span_panel_software_version - action: - - delay: "00:05:00" # Wait for panel to stabilize - - service: span_panel.cleanup_energy_spikes - data: - start_time: "{{ now() - timedelta(hours=1) }}" - - service: persistent_notification.create - data: - title: "SPAN Energy Statistics Cleaned" - message: "Removed firmware update spikes from Energy Dashboard" -``` - -### Dry Run Mode for Safety - -```yaml -# Preview what would be deleted without actually deleting -service: span_panel.cleanup_energy_spikes -data: - dry_run: true -``` - -**Dry run returns detailed results via:** - -1. **Service Response** (visible in Developer Tools → Services): - - ```json - { - "dry_run": true, - "entities_processed": 3, - "spikes_found": 5, - "entries_deleted": 0, - "details": [ - { - "entity_id": "sensor.span_panel_solar_produced_energy", - "spikes": [ - { - "timestamp": "2025-12-09T13:35:00+00:00", - "current_value": 8551863.5, - "previous_value": 8913289.5, - "delta": -361426.0 - } - ] - }, - { - "entity_id": "sensor.span_panel_kitchen_consumed_energy", - "spikes": [ - { - "timestamp": "2025-12-09T13:35:00+00:00", - "current_value": 33137.19, - "previous_value": 37087.67, - "delta": -3950.48 - } - ] - } - ] - } - ``` - - **Why Flagged:** All entries shown have negative delta (counter decreased), which violates TOTAL_INCREASING contract. - -2. **Persistent Notification** (appears in HA notification bell): - - ```text - Title: SPAN Energy Spike Cleanup (Dry Run) - Message: - Found 5 spikes that would be deleted: - - • solar_produced_energy: -361,426 Wh at 13:35 - • kitchen_consumed_energy: -3,950 Wh at 13:35 - • main_meter_consumed_energy: -474,642 Wh at 13:35 - - Run without dry_run to delete these entries. - ``` - -3. **Detailed Logs** (for debugging): - - ```text - INFO: Dry run - would delete spike at 2025-12-09 13:35:00 for sensor.span_panel_solar_produced_energy (delta: -361426.0 Wh) - ``` - -### Targeted Cleanup - -```yaml -# Scan longer period if firmware update was days ago -service: span_panel.cleanup_energy_spikes -data: - days_back: 7 # Scan last week - dry_run: true -``` - -## Service Definition - -```yaml -# services.yaml -cleanup_energy_spikes: - name: Cleanup Energy Spikes - description: - Detect and remove negative energy spikes from all SPAN energy sensors caused by panel firmware updates. Uses main meter to detect reset timestamps. - fields: - days_back: - name: Days to Scan - description: Number of days in the past to scan for spikes. Defaults to 1 (last 24 hours). - required: false - default: 1 - selector: - number: - min: 1 - max: 30 - mode: box - unit_of_measurement: "days" - dry_run: - name: Dry Run - description: Preview spikes without deleting. Returns list of what would be deleted. Defaults to true for safety. - required: false - default: true - selector: - boolean: -``` - -## Implementation Plan - -1. **Statistics query helper** - - Query recorder database for entity statistics - - Calculate deltas between consecutive entries - - Identify negative spikes using 3-sigma rule - -2. **Statistics deletion helper** - - Delete specific statistics entries - - Handle both short-term and long-term statistics - - Transaction safety - -3. **Service implementation** - - Register cleanup service - - Parameter validation - - Call detection and cleanup helpers - - Return simple report - -4. **Dry run mode** - - Preview mode without actual deletion - - Return structured data via service response - - Create persistent notification with summary - - Detailed logging for debugging - -5. **Main meter monitoring** (optional diagnostic) - - Check main meter energy sensor on state change - - Detect negative delta (potential firmware reset) - - Send persistent notification alerting user - - User decides whether to run cleanup service - - Simple, non-intrusive awareness feature - -6. **Result formatting** - - ```python - # Service returns simple structure - result = { - "dry_run": True, - "entities_processed": 3, - "spikes_found": 5, - "entries_deleted": 0, # 0 if dry_run, count if executed - "details": [ - { - "entity_id": "sensor.span_panel_solar_produced_energy", - "spikes": [ - { - "timestamp": "2025-12-09T13:35:00+00:00", - "current_value": 8551863.5, - "previous_value": 8913289.5, - "delta": -361426.0 - } - ] - } - ] - } - - # Create persistent notification - await hass.components.persistent_notification.async_create( - title="SPAN Energy Spike Cleanup" + (" (Dry Run)" if dry_run else ""), - message=format_notification_message(result), - notification_id="span_panel_spike_cleanup" - ) - ``` - -7. **User feedback** - - Persistent notification with summary - - Logging at INFO level - - Simple error messages - -8. **Main meter spike monitoring** (implementation) - - ```python - async def _async_main_meter_state_changed(event): - """Monitor main meter for firmware resets (any decrease in TOTAL_INCREASING).""" - new_state = event.data.get("new_state") - old_state = event.data.get("old_state") - - if not new_state or not old_state: - return - - try: - new_value = float(new_state.state) - old_value = float(old_state.state) - delta = new_value - old_value - - # ANY decrease in TOTAL_INCREASING sensor = firmware reset - if delta < 0: - await hass.components.persistent_notification.async_create( - title="⚠️ SPAN Panel Firmware Reset Detected", - message=( - f"Main meter energy decreased by {abs(delta):,.0f} Wh. " - f"This indicates a panel firmware update.\n\n" - f"To clean up negative spikes in Energy Dashboard:\n" - f"1. Open Developer Tools → Services\n" - f"2. Run 'SPAN Panel: Cleanup Energy Spikes' with dry_run: true\n" - f"3. Review the results\n" - f"4. Run again with dry_run: false to apply cleanup" - ), - notification_id="span_panel_firmware_reset_detected" - ) - except (ValueError, TypeError): - pass - - # Register listener on integration setup - async_track_state_change_event( - hass, - ["sensor.span_panel_main_meter_consumed_energy"], - _async_main_meter_state_changed - ) - ``` - -9. **Documentation** - - Service usage guide in README - - Example service calls - - Troubleshooting tips - - Explanation of main meter monitoring - -## Testing Strategy - -1. **Unit tests** - - Negative delta detection (delta < 0) - - Statistics deletion logic - - Parameter validation - - Main meter monitoring trigger - -2. **Integration tests** - - Query recorder database - - Delete statistics entries - - Dry run mode behavior - - Notification creation - -3. **Manual testing** - - Simulate firmware update spike - - Run cleanup service (dry run first) - - Verify Energy Dashboard correction - - Test main meter decrease detection - -## Implementation Notes - -- **Database access** - Use HA's recorder API, not direct SQL -- **Transaction safety** - Proper session management -- **Input validation** - Sanitize entity IDs and timestamps -- **Query optimization** - Use indexed queries on recorder DB -- **Time range limits** - Default 1 day scan (configurable) - -## Success Criteria - -✅ Service detects all negative deltas in TOTAL_INCREASING sensors ✅ Cleanup removes invalid entries without affecting normal data ✅ Dry run mode accurately -previews changes ✅ Main meter monitoring alerts user immediately on any decrease ✅ Energy Dashboard shows correct data after cleanup ✅ User experience: -seconds to cleanup vs. 20+ minutes manual editing - -## References - -- [GitHub Issue #87: wild numbers after a panel reset](https://github.com/SpanPanel/span/issues/87) -- [HA Recorder Component](https://www.home-assistant.io/integrations/recorder/) -- [HA Statistics Documentation](https://developers.home-assistant.io/docs/core/entity/sensor#long-term-statistics) diff --git a/docs/images/.gitignore b/docs/images/.gitignore deleted file mode 100644 index 753c0f2a..00000000 --- a/docs/images/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.bkp -*.drawio.svg diff --git a/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md b/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md deleted file mode 100644 index 059c3c46..00000000 --- a/docs/superpowers/plans/2026-03-20-poetry-to-uv-migration.md +++ /dev/null @@ -1,521 +0,0 @@ -# Poetry to uv Migration Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan -> task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace Poetry with uv as the Python package manager and task runner across the entire project. - -**Architecture:** Convert pyproject.toml from Poetry-specific format to PEP 621 (`[project]`) + PEP 735 (`[dependency-groups]`) + `[tool.uv]`. All `poetry run` -invocations become `uv run`. CI uses `astral-sh/setup-uv@v5`. Local path dependency uses `[tool.uv.sources]` which CI strips to resolve from PyPI. - -**Tech Stack:** uv, PEP 621/735, astral-sh/setup-uv GitHub Action - ---- - -## File Map - -| Action | File | Responsibility | -| -------- | --------------------------------- | --------------------------------------------------------------------------------------------------------- | -| Rewrite | `pyproject.toml` | Project metadata, dependencies, tool config | -| Delete | `poetry.lock` | Replaced by `uv.lock` | -| Generate | `uv.lock` | New lock file from `uv lock` | -| Modify | `prek.toml` | Replace `poetry run` entries, remove poetry hooks | -| Modify | `.github/workflows/ci.yml` | uv setup, uv sync, uv run | -| Modify | `scripts/run-in-env.sh` | Replace poetry env detection with uv | -| Modify | `scripts/run_mypy.py` | `poetry run mypy` -> `uv run mypy` | -| Modify | `scripts/sync-ha-deps.py` | `poetry show` -> `uv pip show` / parse uv.lock | -| Modify | `scripts/sync-dependencies.py` | Update to sync manifest.json versions into pyproject.toml `[project]` deps instead of ci.yml sed commands | -| Modify | `docs/developer.md` | Update prerequisites and setup instructions | -| Modify | `.github/copilot-instructions.md` | Replace all poetry references | - ---- - -### Task 1: Convert pyproject.toml - -**Files:** - -- Modify: `pyproject.toml` - -Replace the Poetry-specific sections with PEP 621 / PEP 735 / uv equivalents. All `[tool.*]` sections (mypy, ruff, pyright, bandit, etc.) are unchanged. - -- [ ] **Step 1: Replace `[tool.poetry]` + `[tool.poetry.dependencies]` + `[tool.poetry.group.dev.dependencies]` + `[build-system]`** - -Remove: - -```toml -[tool.poetry] -name = "span" -# ... -package-mode = false - -[tool.poetry.dependencies] -python = ">=3.14.2,<3.15" -homeassistant = "2026.2.2" -span-panel-api = {path = "../span-panel-api", develop = true} - -[tool.poetry.group.dev.dependencies] -# all dev deps... - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" -``` - -Replace with: - -```toml -[project] -name = "span" -version = "0.0.0" -description = "Span Panel Custom Integration for Home Assistant" -authors = [{name = "SpanPanel"}] -license = {text = "MIT"} -readme = "README.md" -requires-python = ">=3.14.2,<3.15" -dependencies = [ - "homeassistant==2026.2.2", - "span-panel-api==2.3.2", -] - -[dependency-groups] -dev = [ - "homeassistant-stubs==2026.2.2", - "types-requests", - "types-PyYAML", - "mypy==1.19.1", - "pyright==1.1.405", - "ruff==0.15.1", - "bandit[toml]==1.8.6", - "prek>=0.3.6", - "voluptuous-stubs", - "python-direnv", - "prettier", - "radon==6.0.1", - "pylint==4.0.5", - "pytest>=9.0.0", - "pytest-homeassistant-custom-component>=0.13.315", - "isort", -] - -[tool.uv] -package = false - -[tool.uv.sources] -span-panel-api = { path = "../span-panel-api", editable = true } -``` - -Notes: - -- `package = false` is the uv equivalent of Poetry's `package-mode = false` -- No `[build-system]` needed for virtual (non-package) projects -- `bandit[toml]` includes the TOML extra directly (eliminates the separate `pip install` in CI) -- Poetry `*` becomes unconstrained (just package name) -- Poetry `^X` becomes `>=X` -- `develop = true` becomes `editable = true` in `[tool.uv.sources]` -- `span-panel-api==2.3.2` version must match `manifest.json` - -- [ ] **Step 2: Verify pyproject.toml is valid** - -Run: `cd /Users/bflood/projects/HA/span && uv lock` Expected: Lock file generated without errors - -- [ ] **Step 3: Install dependencies with uv** - -Run: `cd /Users/bflood/projects/HA/span && uv sync` Expected: All dependencies installed, `.venv` created/updated - -- [ ] **Step 4: Verify tools work** - -Run: `cd /Users/bflood/projects/HA/span && uv run ruff --version && uv run mypy --version && uv run pytest --version` Expected: All tools report their versions - ---- - -### Task 2: Delete poetry.lock - -**Files:** - -- Delete: `poetry.lock` - -- [ ] **Step 1: Remove poetry.lock from repo** - -Run: `cd /Users/bflood/projects/HA/span && git rm poetry.lock` Expected: File staged for deletion - ---- - -### Task 3: Update prek.toml - -**Files:** - -- Modify: `prek.toml` - -Three changes: replace `poetry run` in local hooks, remove poetry-check/poetry-lock hooks. - -- [ ] **Step 1: Replace `poetry run pylint` with `uv run pylint`** - -Line 25: `entry = "poetry run pylint"` -> `entry = "uv run pylint"` - -- [ ] **Step 2: Replace `poetry run radon` with `uv run radon`** (two hooks) - -Line 122: `entry = "poetry run radon"` -> `entry = "uv run radon"` Line 131 (radon-maintainability): same replacement - -- [ ] **Step 3: Replace `poetry run pytest` with `uv run pytest`** - -Line 148: the pytest-cov-summary entry argument string: Replace `poetry run pytest tests/ --cov=...` with `uv run pytest tests/ --cov=...` - -- [ ] **Step 4: Remove poetry-check and poetry-lock hooks** - -Delete lines 106-113 (the entire poetry repo block): - -```toml -# Poetry check for pyproject.toml validation -[[repos]] -repo = "https://github.com/python-poetry/poetry" -rev = "2.1.3" -hooks = [ - { id = "poetry-check" }, - { id = "poetry-lock" }, -] -``` - -- [ ] **Step 5: Verify prek hooks run** - -Run: `cd /Users/bflood/projects/HA/span && prek run --all-files` Expected: All hooks pass (poetry-check and poetry-lock no longer appear) - ---- - -### Task 4: Update CI workflow - -**Files:** - -- Modify: `.github/workflows/ci.yml` - -- [ ] **Step 1: Replace Poetry setup with uv setup** - -Replace: - -```yaml -- name: Install Poetry - uses: snok/install-poetry@v1 -``` - -With: - -```yaml -- name: Install uv - uses: astral-sh/setup-uv@v5 -``` - -- [ ] **Step 2: Replace dependency installation step** - -Replace: - -```yaml -- name: Install dependencies - run: | - # Replace path dependencies with PyPI versions for CI - sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = "==2.3.2"/' pyproject.toml - sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml - # Regenerate lock file with the modified dependencies - poetry lock - poetry install --with dev - # Install bandit with TOML support - poetry run pip install 'bandit[toml]' -``` - -With: - -```yaml -- name: Install dependencies - run: | - # Remove local path source overrides so uv resolves from PyPI - sed -i '/^\[tool\.uv\.sources\]/,/^$/d' pyproject.toml - uv lock - uv sync -``` - -Notes: - -- The `sed` removes the `[tool.uv.sources]` block through the next blank line, so uv resolves `span-panel-api==2.3.2` from PyPI -- `uv sync` installs all dependency groups (including dev) by default -- `bandit[toml]` is already in `[dependency-groups]` dev, no separate pip install needed - -- [ ] **Step 3: Replace all `poetry run` with `uv run`** - -```yaml -- name: Format check with ruff - run: uv run ruff format --check custom_components/span_panel - -- name: Lint with ruff - run: uv run ruff check custom_components/span_panel - -- name: Type check with mypy - run: uv run mypy custom_components/span_panel - -- name: Security check with bandit - run: uv run bandit -c pyproject.toml -r custom_components/span_panel -``` - -- [ ] **Step 4: Remove `poetry check` step** - -Delete: - -```yaml -- name: Check poetry configuration - run: poetry check -``` - -- [ ] **Step 5: Update prek SKIP env var** - -Replace: - -```yaml -env: - SKIP: poetry-lock,poetry-check -``` - -With (remove the env block entirely or clear the SKIP list if no other hooks need skipping): - -```yaml -env: - SKIP: "" -``` - -Or remove the `env:` block entirely if all hooks should run. - -- [ ] **Step 6: Replace test runner** - -Replace: - -```yaml -- name: Run tests with coverage - run: poetry run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing -``` - -With: - -```yaml -- name: Run tests with coverage - run: uv run pytest tests/ --cov=custom_components/span_panel --cov-report=xml --cov-report=term-missing -``` - ---- - -### Task 5: Update scripts - -**Files:** - -- Modify: `scripts/run-in-env.sh` -- Modify: `scripts/run_mypy.py` -- Modify: `scripts/sync-ha-deps.py` -- Modify: `scripts/sync-dependencies.py` - -- [ ] **Step 1: Update run-in-env.sh** - -Replace the poetry venv detection and install logic: - -```bash -VENV_PATHS=( - ".venv" - "venv" - ".env" - "env" - "$(poetry env info --path 2>/dev/null)" # Try to get Poetry's venv path -) -``` - -With (remove the poetry line since uv uses `.venv` by default): - -```bash -VENV_PATHS=( - ".venv" - "venv" - ".env" - "env" -) -``` - -Replace the poetry install fallback: - -```bash -# If poetry is available, ensure dependencies -if command -v poetry &> /dev/null && [ -f "pyproject.toml" ]; then - # Check if pylint is missing - if ! command -v pylint &> /dev/null; then - echo "pylint not found, installing dependencies with poetry..." - poetry install --only dev - fi -fi -``` - -With: - -```bash -# If uv is available, ensure dependencies -if command -v uv &> /dev/null && [ -f "pyproject.toml" ]; then - if ! command -v pylint &> /dev/null; then - echo "pylint not found, installing dependencies with uv..." - uv sync - fi -fi -``` - -Update the comment on line 4: `# Handles pyenv/virtualenv/poetry activation if needed` -> `# Handles pyenv/virtualenv/uv activation if needed` - -- [ ] **Step 2: Update run_mypy.py** - -Replace line 12: - -```python -result = subprocess.check_call(["poetry", "run", "mypy"] + sys.argv[1:]) # nosec B603 -``` - -With: - -```python -result = subprocess.check_call(["uv", "run", "mypy"] + sys.argv[1:]) # nosec B603 -``` - -- [ ] **Step 3: Update sync-ha-deps.py** - -Replace the `get_ha_dependencies()` function. Change from `poetry show homeassistant --format json` to `uv pip show homeassistant --format json`: - -```python -def get_ha_dependencies(): - """Get HomeAssistant's dependency pins from uv pip show.""" - try: - result = subprocess.run( - ["uv", "pip", "show", "homeassistant", "--format", "json"], - capture_output=True, - text=True, - check=True, - ) - ha_info = json.loads(result.stdout) - return {dep["name"]: dep["version"] for dep in ha_info.get("dependencies", [])} - except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError): - return {} -``` - -Also update `update_pyproject_constraints()` to work with PEP 621 `[project]` format instead of `[tool.poetry]`: - -Replace: - -```python -deps = ( - pyproject.setdefault("tool", {}).setdefault("poetry", {}).setdefault("dependencies", {}) -) -``` - -With logic that reads/updates the `[project]` dependencies list (which is a list of PEP 508 strings, not a dict). - -- [ ] **Step 4: Update sync-dependencies.py** - -The CI workflow no longer has version-specific sed commands. Instead, the version lives in `[project]` dependencies. Update this script to sync versions from -manifest.json into pyproject.toml `[project]` dependencies instead of ci.yml sed commands. - -Replace `update_ci_workflow()` with `update_pyproject_dependencies()` that reads pyproject.toml, finds the dependency string (e.g., `"span-panel-api==2.3.2"`), -and updates the version to match manifest.json. - ---- - -### Task 6: Update documentation - -**Files:** - -- Modify: `docs/developer.md` -- Modify: `.github/copilot-instructions.md` - -- [ ] **Step 1: Rewrite docs/developer.md** - -Replace full content with: - -```markdown -# Development Notes - -## Developer Prerequisites - -- uv -- prek -- Python 3.14.2+ - -This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Linting and type checking are accomplished using -[prek](https://github.com/j178/prek), a fast Rust-based pre-commit framework. - -## Developer Setup - -1. Install [uv](https://docs.astral.sh/uv/). -2. In the project root run `uv sync` to install dependencies. -3. Run `prek install` to install pre-commit hooks. -4. Optionally use `Tasks: Run Task` from the command palette to run `Run all Pre-commit checks` or `prek run --all-files` from the terminal to manually run - hooks on files locally in your environment as you make changes. - -The linters may make changes to files when you try to commit, for example to sort imports. Files that are changed or fail tests will be unstaged. After -reviewing these changes or making corrections, you can re-stage the changes and recommit or rerun the checks. After the prek hook run succeeds, your commit can -proceed. - -## VS Code - -See the .vscode/settings.json.example file for starter settings -``` - -- [ ] **Step 2: Update .github/copilot-instructions.md** - -Replace all `poetry` references: - -- Line 18: `Poetry (not pip)` -> `uv (not pip)` -- Lines 78-79: `poetry install --with dev` -> `uv sync` -- Line 92: `poetry run pytest` -> `uv run pytest` -- Lines 95-96: `poetry run pytest` -> `uv run pytest` -- Lines 98-99: `poetry run mypy` -> `uv run mypy` -- Lines 101-102: `poetry run ruff check` -> `uv run ruff check` -- Lines 104-105: `poetry run ruff format` -> `uv run ruff format` -- Lines 107-108: `poetry run bandit` -> `uv run bandit` -- Lines 110-111: `poetry run radon` -> `uv run radon` -- Line 121: `poetry add` / `poetry add --group dev` -> `uv add` / `uv add --group dev` -- Line 143: `poetry.lock` managed by Poetry -> `uv.lock` managed by uv, use `uv lock` to update - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "Migrate from Poetry to uv" -``` - ---- - -### Task 7: Verify end-to-end - -- [ ] **Step 1: Clean install from scratch** - -```bash -cd /Users/bflood/projects/HA/span -rm -rf .venv -uv sync -``` - -Expected: Fresh `.venv` created with all deps - -- [ ] **Step 2: Run all tools** - -```bash -uv run ruff format --check custom_components/span_panel -uv run ruff check custom_components/span_panel -uv run mypy custom_components/span_panel -uv run bandit -c pyproject.toml -r custom_components/span_panel -uv run pytest tests/ -q -``` - -Expected: All pass - -- [ ] **Step 3: Run prek hooks** - -```bash -prek run --all-files -``` - -Expected: All hooks pass - -- [ ] **Step 4: Verify no poetry references remain** - -```bash -grep -r "poetry" --include="*.py" --include="*.toml" --include="*.yml" --include="*.yaml" --include="*.md" --include="*.sh" . -``` - -Expected: No matches (except possibly in git history references or this plan file) diff --git a/docs/images/bess-topology-integrated.drawio b/images/bess-topology-integrated.drawio similarity index 100% rename from docs/images/bess-topology-integrated.drawio rename to images/bess-topology-integrated.drawio diff --git a/docs/images/bess-topology-integrated.svg b/images/bess-topology-integrated.svg similarity index 100% rename from docs/images/bess-topology-integrated.svg rename to images/bess-topology-integrated.svg diff --git a/docs/images/bess-topology-non-integrated.drawio b/images/bess-topology-non-integrated.drawio similarity index 100% rename from docs/images/bess-topology-non-integrated.drawio rename to images/bess-topology-non-integrated.drawio diff --git a/docs/images/bess-topology-non-integrated.svg b/images/bess-topology-non-integrated.svg similarity index 100% rename from docs/images/bess-topology-non-integrated.svg rename to images/bess-topology-non-integrated.svg diff --git a/docs/v1-legacy.md b/v1-legacy.md similarity index 100% rename from docs/v1-legacy.md rename to v1-legacy.md diff --git a/docs/websocket-api.md b/websocket-api.md similarity index 100% rename from docs/websocket-api.md rename to websocket-api.md