From 746eaa23c5e8b04e6d4ea063ecb35fbbe1e55bfe Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:01:26 -0700 Subject: [PATCH 1/4] fix: preserve property values on lifecycle reset to prevent false energy dips The 2.5.2 property clear caused _parse_float('') to return 0.0 for energy counters during panel reboots or network interruptions, triggering false dip-compensation offsets that permanently inflated energy sensor values. Remove the three .clear() calls from _handle_description(). Keep the generation counter increment so consumer snapshot caches are invalidated and rebuilt from current accumulator state. Pre-reboot values serve as safe placeholders until the panel re-publishes fresh data. --- src/span_panel_api/mqtt/accumulator.py | 27 +++++----- tests/test_accumulator.py | 68 ++++++++++++++------------ tests/test_mqtt_homie.py | 13 +++-- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/span_panel_api/mqtt/accumulator.py b/src/span_panel_api/mqtt/accumulator.py index 76d38f0..1cfd042 100644 --- a/src/span_panel_api/mqtt/accumulator.py +++ b/src/span_panel_api/mqtt/accumulator.py @@ -56,8 +56,8 @@ def __init__(self, serial_number: str) -> None: # Node type mapping from $description self._node_types: dict[str, str] = {} - # Generation counter — incremented when $description clears property - # values so consumers can invalidate caches built from stale data. + # Generation counter — incremented on lifecycle resets so consumers + # can invalidate snapshot caches built from pre-reboot data. self._generation: int = 0 # Dirty tracking @@ -85,7 +85,7 @@ def ready_since(self) -> float: @property def generation(self) -> int: - """Counter incremented on the initial $description and after lifecycle resets.""" + """Counter incremented on lifecycle resets to invalidate consumer caches.""" return self._generation def is_ready(self) -> bool: @@ -225,16 +225,21 @@ def _handle_description(self, payload: str) -> None: # _handle_state() already reset _received_description to False due to # a state change that starts a new panel lifecycle, including # $state=disconnected/lost and other non-ready states such as init. - # This means the panel rebooted while we were connected. On a pure - # MQTT reconnect (no panel reboot), _received_description is still - # True from the previous session so we skip the clear — the retained - # property messages will carry the correct (unchanged) values. + # Increment the generation counter to invalidate consumer snapshot + # caches, but preserve property values — pre-reboot readings serve + # as safe placeholders until the panel re-publishes. Clearing values + # would emit 0.0 for energy counters via _parse_float(""), triggering + # false dip-compensation offsets in the integration. + # + # On a pure MQTT reconnect (no panel reboot), _received_description + # is still True from the previous session so we skip this block — + # the retained property messages carry the correct (unchanged) values. if not self._received_description: - self._property_values.clear() - self._property_timestamps.clear() - self._target_values.clear() self._generation += 1 - _LOGGER.debug("Cleared stale property values (generation %d)", self._generation) + _LOGGER.debug( + "Panel reboot detected (generation %d); preserving property values as placeholders", + self._generation, + ) self._received_description = True self._node_types.clear() diff --git a/tests/test_accumulator.py b/tests/test_accumulator.py index 21bc76c..0639caa 100644 --- a/tests/test_accumulator.py +++ b/tests/test_accumulator.py @@ -164,17 +164,21 @@ def test_init_state_moves_to_connected(self): # --------------------------------------------------------------------------- -# Panel reboot: $description clears stale property values +# Panel reboot: $description preserves property values as placeholders # --------------------------------------------------------------------------- -class TestDescriptionClearsProperties: - """Verify that a panel reboot (disconnected -> $description) clears stale data. +class TestDescriptionOnReboot: + """Verify that a panel reboot preserves property values as placeholders. - The clear only happens when _received_description is False, which requires - a $state=disconnected (or lost) to have reset the lifecycle first. A - re-delivered retained $description on a pure network reconnect does NOT - clear, because _received_description is still True from the previous session. + On lifecycle reset ($state=disconnected/lost or non-ready states like init), + _received_description is set to False. The subsequent $description increments + the generation counter (invalidating consumer snapshot caches) but does NOT + clear property values — pre-reboot values serve as safe placeholders until + the panel re-publishes fresh data. + + A re-delivered retained $description on a pure network reconnect does NOT + increment generation, because _received_description is still True. """ def _simulate_reboot(self, acc: HomiePropertyAccumulator) -> None: @@ -183,36 +187,37 @@ def _simulate_reboot(self, acc: HomiePropertyAccumulator) -> None: acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) acc.handle_message(f"{PREFIX}/$state", "ready") - def test_description_clears_property_values_on_reboot(self): - """A panel reboot must clear property values from the previous lifecycle.""" + def test_reboot_preserves_property_values(self): + """A panel reboot must preserve property values as placeholders.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") assert acc.get_prop("circuit-1", "exported-energy") == "1000" - # Panel reboots — $state=disconnected resets lifecycle, then $description clears + # Panel reboots — $state=disconnected resets lifecycle, then $description preserves self._simulate_reboot(acc) - assert acc.get_prop("circuit-1", "exported-energy") == "" + assert acc.get_prop("circuit-1", "exported-energy") == "1000" - def test_description_clears_timestamps_on_reboot(self): - """Timestamps must also be cleared so stale timing data doesn't persist.""" + def test_reboot_preserves_timestamps(self): + """Timestamps must be preserved on reboot so callers can detect stale data if needed.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") - assert acc.get_timestamp("circuit-1", "exported-energy") > 0 + ts_before = acc.get_timestamp("circuit-1", "exported-energy") + assert ts_before > 0 self._simulate_reboot(acc) - assert acc.get_timestamp("circuit-1", "exported-energy") == 0 + assert acc.get_timestamp("circuit-1", "exported-energy") == ts_before - def test_description_clears_target_values_on_reboot(self): - """Target values must also be cleared on panel reboot.""" + def test_reboot_preserves_target_values(self): + """Target values must also be preserved on panel reboot.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN") assert acc.get_target("circuit-1", "relay") == "OPEN" self._simulate_reboot(acc) - assert acc.get_target("circuit-1", "relay") is None + assert acc.get_target("circuit-1", "relay") == "OPEN" def test_reboot_increments_generation(self): """Each panel reboot must advance the generation counter.""" @@ -228,8 +233,8 @@ def test_reboot_increments_generation(self): acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) assert acc.generation == 2 - def test_retained_redescription_does_not_clear(self): - """A re-delivered retained $description without a disconnect must NOT clear.""" + def test_retained_redescription_preserves_values_without_generation_bump(self): + """A re-delivered retained $description without a disconnect must NOT bump generation.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") @@ -237,24 +242,24 @@ def test_retained_redescription_does_not_clear(self): # Simulate network reconnect — $description re-delivered without $state=disconnected acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) - # Property values should be preserved + # Property values should be preserved and generation must not increment assert acc.get_prop("circuit-1", "exported-energy") == "1000" assert acc.generation == 1 # still 1 from initial boot, no increment on re-delivery - def test_fresh_properties_available_after_reboot(self): - """Post-reboot properties should be stored normally after clear.""" + def test_fresh_properties_overwrite_preserved_after_reboot(self): + """Post-reboot properties should overwrite the preserved placeholder values.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") self._simulate_reboot(acc) - # Fresh post-reboot value + # Fresh post-reboot value overwrites preserved placeholder acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "50") assert acc.get_prop("circuit-1", "exported-energy") == "50" - def test_fresh_target_values_available_after_reboot(self): - """Post-reboot target values should be stored normally after clear.""" + def test_fresh_target_values_overwrite_preserved_after_reboot(self): + """Post-reboot target values should overwrite the preserved placeholder values.""" acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN") @@ -262,16 +267,17 @@ def test_fresh_target_values_available_after_reboot(self): self._simulate_reboot(acc) - # Fresh post-reboot target value + # Fresh post-reboot target value overwrites preserved placeholder acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "CLOSED") assert acc.get_target("circuit-1", "relay") == "CLOSED" - def test_fast_reboot_without_lwt_still_clears(self): + def test_fast_reboot_without_lwt_preserves_values(self): """Panel reboots so fast that $state=disconnected (LWT) is skipped. The panel goes directly from ready -> init -> description -> ready. $state=init must reset _received_description so the subsequent - $description triggers the property clear. + $description increments the generation counter but does NOT clear + property values — pre-reboot values serve as safe placeholders. """ acc = HomiePropertyAccumulator(SERIAL) _make_ready(acc) @@ -283,8 +289,8 @@ def test_fast_reboot_without_lwt_still_clears(self): acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) acc.handle_message(f"{PREFIX}/$state", "ready") - # Property values should be cleared - assert acc.get_prop("circuit-1", "exported-energy") == "" + # Property values should be preserved + assert acc.get_prop("circuit-1", "exported-energy") == "1000" assert acc.generation == gen_before + 1 diff --git a/tests/test_mqtt_homie.py b/tests/test_mqtt_homie.py index 79a2878..50a52c3 100644 --- a/tests/test_mqtt_homie.py +++ b/tests/test_mqtt_homie.py @@ -906,7 +906,7 @@ def test_description_change_triggers_full_rebuild(self): assert snap2 is not snap1 def test_description_invalidates_cache_via_generation(self): - """Panel reboot ($description) must clear cached snapshot so stale data is not reused.""" + """Panel reboot ($description) must invalidate cached snapshot but preserve values.""" acc, consumer = _build_ready_consumer() node = "aabbccdd-1122-3344-5566-778899001122" @@ -916,16 +916,21 @@ def test_description_invalidates_cache_via_generation(self): circuit_id = "aabbccdd112233445566778899001122" assert snap_pre.circuits[circuit_id].consumed_energy_wh == 1000.0 - # Panel reboots — $state=disconnected resets lifecycle, $description clears values + # Panel reboots — $state=disconnected resets lifecycle, $description preserves values acc.handle_message(f"{PREFIX}/$state", "disconnected") acc.handle_message(f"{PREFIX}/$description", _make_description(_full_description())) acc.handle_message(f"{PREFIX}/$state", "ready") - # Post-reboot: circuit publishes reset energy = 50 + # Before new property messages: snapshot uses preserved pre-reboot value + snap_preserved = consumer.build_snapshot() + assert snap_preserved.circuits[circuit_id].consumed_energy_wh == 1000.0 + assert snap_preserved is not snap_pre # cache was invalidated + + # Post-reboot: circuit publishes new value acc.handle_message(f"{PREFIX}/{node}/exported-energy", "50") snap_post = consumer.build_snapshot() - # Must reflect post-reboot value, not stale pre-reboot cache + # Must reflect post-reboot value assert snap_post.circuits[circuit_id].consumed_energy_wh == 50.0 From 1c1577610e89c908c3d03d2413ad3e229bad8fd1 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:04:05 -0700 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20update=20changelog=20=E2=80=94=20re?= =?UTF-8?q?tire=202.5.2,=20add=202.5.3=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark 2.5.2 as retired due to false energy dip spikes caused by property clearing on lifecycle resets. Carry forward the snapshot cache invalidation entry to 2.5.3 alongside the preservation fix. --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e124d..21e6d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.5.2] - 04/2026 +## [2.5.3] - 04/2026 + +### Fixed + +- **Preserve property values on lifecycle reset** — the 2.5.2 property clear caused `_parse_float('')` to return `0.0` for energy counters during panel reboots or network interruptions, triggering false dip-compensation offsets in the integration that + permanently inflated energy sensor values. Removed the property/timestamp/target clearing from `_handle_description()`. Pre-reboot values now serve as safe placeholders until the panel re-publishes fresh data. +- **Snapshot cache invalidated on reboot** — the generation counter still increments on lifecycle resets, forcing consumers to discard cached snapshots and rebuild from current accumulator state. + +## [2.5.2] - 04/2026 (retired) + +> **Retired:** The property-clearing behavior introduced in this release caused false energy dip spikes. Superseded by 2.5.3. ### Fixed @@ -334,29 +344,30 @@ Package versions prior to 2.0.0 depend on the SPAN v1 REST API. SPAN will sunset ## Version History Summary -| Version | Date | Transport | Summary | -| ---------- | ------- | ---------- | ------------------------------------------------------------------------------------------------- | -| **2.5.2** | 04/2026 | MQTT/Homie | Clear stale property values on panel reboot; fast reboot detection; cache generation invalidation | -| **2.5.1** | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook | -| **2.5.0** | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching | -| **2.4.2** | 03/2026 | MQTT/Homie | SSL context creation moved to executor | -| **2.4.1** | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint | -| **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup | -| **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints | -| **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError | -| **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed | -| **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits | -| **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot | -| **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support | -| **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation | -| **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata | -| **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling | -| **1.1.9** | 9/2025 | REST | Simulation sign corrections | -| **1.1.8** | 2024 | REST | Simulation power sign fix | -| **1.1.6** | 2024 | REST | YAML simulation API, battery simulation | -| **1.1.5** | 2024 | REST | Simulation edge cases | -| **1.1.4** | 2024 | REST | Formatting and linting | -| **1.1.3** | 2024 | REST | Test and lint fixes | -| **1.1.2** | 2024 | REST | Simulation mode added | -| **1.1.1** | 2024 | REST | Dependency updates | -| **1.1.0** | 2024 | REST | Initial release | +| Version | Date | Transport | Summary | +| ---------- | ------- | ---------- | ----------------------------------------------------------------------------------------- | +| **2.5.3** | 04/2026 | MQTT/Homie | Preserve property values on lifecycle reset; fix false energy dip spikes from 2.5.2 clear | +| **2.5.2** | 04/2026 | MQTT/Homie | _(retired)_ Clear stale property values on panel reboot; caused false energy dip spikes | +| **2.5.1** | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook | +| **2.5.0** | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching | +| **2.4.2** | 03/2026 | MQTT/Homie | SSL context creation moved to executor | +| **2.4.1** | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint | +| **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup | +| **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints | +| **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError | +| **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed | +| **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits | +| **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot | +| **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support | +| **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation | +| **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata | +| **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling | +| **1.1.9** | 9/2025 | REST | Simulation sign corrections | +| **1.1.8** | 2024 | REST | Simulation power sign fix | +| **1.1.6** | 2024 | REST | YAML simulation API, battery simulation | +| **1.1.5** | 2024 | REST | Simulation edge cases | +| **1.1.4** | 2024 | REST | Formatting and linting | +| **1.1.3** | 2024 | REST | Test and lint fixes | +| **1.1.2** | 2024 | REST | Simulation mode added | +| **1.1.1** | 2024 | REST | Dependency updates | +| **1.1.0** | 2024 | REST | Initial release | From a39debfe8dfee8c4e9d78d1cb976fc1c306e7979 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:05:30 -0700 Subject: [PATCH 3/4] bump version to 2.5.3 --- pyproject.toml | 2 +- uv.lock | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 385aaf1..5541ec7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "span-panel-api" -version = "2.5.2" +version = "2.5.3" description = "A client library for SPAN Panel API" authors = [ {name = "SpanPanel"} diff --git a/uv.lock b/uv.lock index d25c919..876cce9 100644 --- a/uv.lock +++ b/uv.lock @@ -130,31 +130,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8 wheels = [ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { 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/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" }, @@ -426,27 +438,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, @@ -1292,7 +1310,7 @@ wheels = [ [[package]] name = "span-panel-api" -version = "2.5.2" +version = "2.5.3" source = { editable = "." } dependencies = [ { name = "httpx" }, From 52117ee8dc99485dda846ff8bd7a447c5eb72b9c Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:39:54 -0700 Subject: [PATCH 4/4] fix: clarify generation counter comments to include initial boot case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generation counter increments on the initial $description too (0→1), not just on lifecycle resets. Updated the __init__ comment and generation property docstring to accurately reflect this. --- src/span_panel_api/mqtt/accumulator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/span_panel_api/mqtt/accumulator.py b/src/span_panel_api/mqtt/accumulator.py index 1cfd042..c4fbc14 100644 --- a/src/span_panel_api/mqtt/accumulator.py +++ b/src/span_panel_api/mqtt/accumulator.py @@ -56,8 +56,9 @@ def __init__(self, serial_number: str) -> None: # Node type mapping from $description self._node_types: dict[str, str] = {} - # Generation counter — incremented on lifecycle resets so consumers - # can invalidate snapshot caches built from pre-reboot data. + # Generation counter — incremented whenever a new lifecycle's + # $description is accepted (including initial boot) so consumers + # can invalidate snapshot caches built from prior-lifecycle data. self._generation: int = 0 # Dirty tracking @@ -85,7 +86,7 @@ def ready_since(self) -> float: @property def generation(self) -> int: - """Counter incremented on lifecycle resets to invalidate consumer caches.""" + """Counter incremented on each new lifecycle's ``$description`` to invalidate consumer caches.""" return self._generation def is_ready(self) -> bool: