diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3728aee..4f530d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,31 @@ jobs: print(f'{wheel}: py.typed present') " + # Reference captures are test data. They shipped in both wheels until 3.1.0 + # -- not through any packaging declaration, but because a directory inside a + # package directory ships -- and no runtime path ever read them. Nothing in + # the manifests would object to that happening again, so the built artifact + # is where it has to be asserted. Every wheel, not the two known ones: an + # adapter added under packages/ is covered the day it exists. + - name: Verify no wheel ships reference payloads + run: | + python -c " + import glob, posixpath, sys, zipfile + wheels = glob.glob('dist/*.whl') + if not wheels: + sys.exit('::error::no wheels were built') + for wheel in wheels: + names = zipfile.ZipFile(wheel).namelist() + carried = [ + n for n in names + if 'reference_payloads' in n.split('/') + or posixpath.basename(n) in ('homie_schema.json', 'parent_child_tree.json') + ] + if carried: + sys.exit(f'::error::{wheel} ships test data: {carried}. Reference captures are fixtures under tests/reference_payloads; a directory inside a package directory ships whether or not the manifest names it.') + print(f'{wheel}: no reference payloads') + " + # The configuration entry-point discovery exists to support, and the one # nothing else in CI exercises: the bootstrap wheel installed with no # adapter present. It must import, and it must fail by name rather than diff --git a/.github/workflows/peer-drift.yml b/.github/workflows/peer-drift.yml index 26d14ea..296c2d1 100644 --- a/.github/workflows/peer-drift.yml +++ b/.github/workflows/peer-drift.yml @@ -193,7 +193,7 @@ jobs: print("```bash") print(f"# in a checkout of the emitter at v{latest}, from its own environment") print("uv run python ../span-panel-api/scripts/capture_parent_child_reference.py \\") - print(" ../span-panel-api/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json") + print(" ../span-panel-api/tests/reference_payloads/parent_child_tree.json") print("```\n") print("The script refuses until `peers.ebus-panel-sim.version` and `.commit` in") print("`spec_lock.json` name the release you captured from, which is what keeps the") diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc6c95c..7c8e6b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -87,7 +87,13 @@ repos: # types rather than silently reporting import-not-found. - ebus-sdk>=0.19.0 args: ['--config-file=pyproject.toml'] - exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/.*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*' + # `tests/reference_payloads/` is the one thing under tests/ this hook does + # check. Its five accessors were type-checked as package data until 3.1.0 + # and are read by twenty test modules; moving them out of `src/` should not + # have quietly cost them `--strict`. `pyproject.toml`'s own `exclude` still + # names tests/, which is fine — that flag governs directory discovery, and + # a file passed by name is checked regardless. + exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/(?!reference_payloads/).*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*' # Pylint for code quality - repo: https://github.com/pycqa/pylint diff --git a/CHANGELOG.md b/CHANGELOG.md index 289b998..7cdabb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -142,6 +142,17 @@ install of the adapter distribution does not, and a 1.0.0 adapter against this b would have meant a caller passing both got system trust while believing it had pinned the panel CA — a security control that appears to be on and is off. When both are supplied, a dedicated client is built for the call and closed after it. The cost is named rather than hidden: those calls lose the injected client's connection pool, timeout and header policy. Acceptable because every caller here is bootstrap — registration, detection, schema, FQDN, status — a handful of calls per config entry. +### Removed + +- **BREAKING: `span_panel_api.reference_payloads` is gone, and the wheel no longer carries `homie_schema.json`.** The captured `GET /api/v2/homie/schema` response is a fixture of this repository's test suite now, at + `tests/reference_payloads/homie_schema.json`, read through `homie_schema()` / `homie_schema_types()` there. Anyone importing the module from an installed distribution has to vendor the bytes instead — and should record which release they were taken from, + asserting that against `importlib.metadata.version("span-panel-api")`, so a pin that moves past a stale copy fails loudly rather than checking declarations against a schema no panel runs. That version claim is available to any consumer without a + checkout, which is what makes vendoring safe and is the whole reason this can be removed. + + It shipped in the first place to spare consumers a copy that goes stale in silence, which was a real problem badly solved: no runtime path ever read the file, so every install of both distributions paid for test data it could not use, and the import + surface committed each distribution to a promise it never meant to make. Nothing declared the payloads as package data — a directory inside a package directory ships whether or not a manifest names it, which is exactly why this was easy to miss. + `tests/test_packaging.py` now fails if a capture reappears inside a shipped package, and CI asserts the same against every built wheel. See #162. + ## [3.0.1] ### Fixed diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f6a381c..9f63545 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -78,17 +78,17 @@ now records the simulator commit its output came from, for the same reason `spec Two scripts, one per producer, and neither is run automatically — a capture is a deliberate act. -| Artifact | Producer | Script | -| ----------------------------------------------------------------- | ---------------- | ------------------------------------------- | -| `tests/fixtures/flat_wire.json` | `simulator` | `scripts/capture_flat_reference.py` | -| `packages/schema-1/.../reference_payloads/parent_child_tree.json` | `ebus-panel-sim` | `scripts/capture_parent_child_reference.py` | +| Artifact | Producer | Script | +| ------------------------------------------------- | ---------------- | ------------------------------------------- | +| `tests/fixtures/flat_wire.json` | `simulator` | `scripts/capture_flat_reference.py` | +| `tests/reference_payloads/parent_child_tree.json` | `ebus-panel-sim` | `scripts/capture_parent_child_reference.py` | Both run from the **producer's** environment rather than this one — each producer caps a dependency this repo installs above — and both substitute the transport rather than reassembling the emitter, because a capture taken through different wiring than a real panel uses is a capture of the wiring. Point them at a checkout with `SIMULATOR_DIR` / `PANEL_SIM_DIR`. `capture_parent_child_reference.py` goes one step further than documenting its producer: it reads the release it is a capture of out of `spec_lock.json` (`peers.ebus-panel-sim.version`) and **refuses to write** when the installed package disagrees. The pin -therefore has exactly one home, and re-capturing against a newer emitter is a two-place change made together — that peer block, and the provenance section of `reference_payloads/README.md`. That is what stops the bytes and the claim about them drifting -apart, and the drift is not hypothetical: it is how a producer defect in `$settable` on a locked relay reached about thirty test files across two repositories with no conformance gate objecting. +therefore has exactly one home, and re-capturing against a newer emitter is a two-place change made together — that peer block, and the provenance section of `tests/reference_payloads/README.md`. That is what stops the bytes and the claim about them +drifting apart, and the drift is not hypothetical: it is how a producer defect in `$settable` on a locked relay reached about thirty test files across two repositories with no conformance gate objecting. Its input is committed too, as `scripts/reference_panel.yaml`, pinned as `peers.ebus-panel-sim.manifest` — a capture whose input is not in the tree is the same class of problem as one whose producer is not recorded. That manifest is a synthetic `example-*` panel that mirrors the emitter's own `examples/forty_tab_minimal.yaml` key for key and marks its two deliberate divergences at the head of the file: spec-legal shed priorities in place of a value the emitter degrades to `UNKNOWN` diff --git a/README.md b/README.md index ae1e688..7c02b5c 100644 --- a/README.md +++ b/README.md @@ -527,26 +527,11 @@ The `PanelCapability` flag enum advertises transport features at runtime: ## Reference Payloads -Captures of what a panel actually serves, shipped as package data so a consumer can check its own assumptions against real bytes without vendoring a copy that silently goes stale: +Captures of what a panel actually serves — the `GET /api/v2/homie/schema` document and a full 40-space parent/child retained-topic tree — live in [`tests/reference_payloads/`](tests/reference_payloads/README.md), with their provenance. -```python -from span_panel_api.reference_payloads import homie_schema, homie_schema_types - -document = homie_schema() # the captured GET /api/v2/homie/schema response -types = homie_schema_types() # its `types` map, typed as HomieSchemaTypes -``` - -`homie_schema_types()` returns exactly what `span_panel_api_schema_0.field_metadata.build_field_metadata` accepts, so building real adapter metadata to compare against is two lines and no file handling. - -The parent/child device tree is the schema_1 counterpart and ships from that adapter, with the parser that can interpret it: - -```python -from span_panel_api_schema_1.reference_payloads import devices_from_tree, parent_child_tree - -devices = devices_from_tree(parent_child_tree()) -``` - -Each payload carries the version of the release it shipped in. Pin a version and you read the bytes that version was written against. +**They are fixtures of this repository, not package data.** Until 3.1.0 they sat inside the two source packages and were therefore carried in the wheels, which no runtime path ever read. `span_panel_api.reference_payloads` and +`span_panel_api_schema_1.reference_payloads` no longer exist; a consumer that was importing them should vendor the bytes it needs and record the release it took them from, asserting that against `importlib.metadata.version(...)` so a moved pin that outruns +the copy fails loudly instead of testing against a schema no panel runs. ## Project Structure @@ -567,7 +552,6 @@ src/span_panel_api/ # distribution: span-panel-api (no parser) ├── phase_validation.py # Electrical phase utilities ├── protocol.py # PEP 544 protocols, SchemaAdapter, PanelCapability flags ├── schema_drift.py # Reporting a panel that outruns what we can read -├── reference_payloads/ # Captured GET /api/v2/homie/schema, shipped as package data └── mqtt/ ├── __init__.py ├── async_client.py # NullLock + AsyncMQTTClient (HA core pattern) @@ -585,7 +569,7 @@ packages/schema-1/ # distribution: span-panel-api-schema-1 ├── spec/ # eBus capability catalogs, byte-copied; checked against, never parsed └── src/span_panel_api_schema_1/ # Parent/child parser: ControllerRoutes, snapshot mapper, - # adoption, catalog validator, spec_lock.json, reference payloads + # adoption, catalog validator, spec_lock.json ``` ## Development diff --git a/packages/schema-1/CHANGELOG.md b/packages/schema-1/CHANGELOG.md index 30b2731..27bd1c2 100644 --- a/packages/schema-1/CHANGELOG.md +++ b/packages/schema-1/CHANGELOG.md @@ -84,6 +84,16 @@ Requires `span-panel-api` **3.1.0 or newer**, and the two must be upgraded toget handling them is a contract obligation regardless of what hardware emits — and `test_every_catalogued_priority_is_carried_through_rather_than_repaired` is now parametrised over the catalog's declared format rather than over whatever the capture happens to contain. Contract obligations come from the catalog; representativeness comes from the capture. A value added upstream reaches that test the moment the catalog is re-vendored. +### Removed + +- **BREAKING: `span_panel_api_schema_1.reference_payloads` is gone, and the wheel no longer carries `parent_child_tree.json`.** The 40-space retained-topic capture, and the `parent_child_tree()` / `device_from_topics()` / `devices_from_tree()` replay that + reads it, are fixtures of the repository's test suite now, at `tests/reference_payloads/schema_one.py`. The helpers stay beside the capture wherever it lives, for the reason they were written: a tree is not directly usable, and separating them would put + the same twelve lines of replay in each of the modules that read it. + + It was package data for a good reason — a vendored copy has no version and goes stale in silence — but the reason does not survive inspection: no runtime path here reads the capture, so every install paid 40 KB for test data, and detecting staleness + never needed the file, only the version claim. A consumer that vendors the bytes and asserts its recorded source release against `importlib.metadata.version("span-panel-api-schema-1")` gets the loud failure the package data was protecting, with no + checkout and no dependency on this distribution's test fixtures. `spec_lock.json`'s `peers.ebus-panel-sim.produces.tree` names the new location, and a test resolves it so the record cannot outlive the file. See #162. + ## [1.0.0] First release as a standalone distribution, and the first parser for the parent/child data model. Requires `span-panel-api` 3.0.0 or newer, and `ebus-sdk` `>=0.19,<0.24`. diff --git a/packages/schema-1/README.md b/packages/schema-1/README.md index 10a94b8..2441d48 100644 --- a/packages/schema-1/README.md +++ b/packages/schema-1/README.md @@ -50,15 +50,7 @@ returns `None`, and an entity disappears. ## Reference payloads -A retained-topic capture of a full 40-space parent/child panel ships as package data, with the replay that turns it back into devices: +A retained-topic capture of a full 40-space parent/child panel, and the replay that turns it back into devices, are fixtures of the repository's test suite at `tests/reference_payloads/`. -```python -from span_panel_api_schema_1.reference_payloads import devices_from_tree, parent_child_tree - -devices = devices_from_tree(parent_child_tree()) -``` - -It ships here rather than from the bootstrap because a retained topic tree is only interpretable by the parser that speaks its vocabulary, and the eBus SDK is this distribution's dependency alone. `devices_from_tree` takes the tree rather than reading it, -so a consumer can filter the capture first — dropping the BESS to model a panel that has none — and still build devices the same way. The bootstrap ships the schema document it fetches; see `span_panel_api.reference_payloads`. - -Each payload carries the version of the release it shipped in. Pin a version and you read the bytes that version was written against. +**`span_panel_api_schema_1.reference_payloads` no longer exists.** It was package data until 1.1.0 — carried in this wheel because it sat inside the package directory, though no runtime path read it. A consumer that was importing it should vendor the bytes +it needs and record the release it took them from, asserting that against `importlib.metadata.version("span-panel-api-schema-1")` so a moved pin that outruns the copy fails loudly instead of testing against a tree no panel publishes. diff --git a/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json b/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json index 95416e6..a46d775 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json +++ b/packages/schema-1/src/span_panel_api_schema_1/spec_lock.json @@ -35,7 +35,7 @@ "capture_script": "scripts/capture_parent_child_reference.py", "manifest": "scripts/reference_panel.yaml", "produces": { - "tree": "packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json" + "tree": "tests/reference_payloads/parent_child_tree.json" } } }, diff --git a/scripts/capture_parent_child_reference.py b/scripts/capture_parent_child_reference.py index 82b1773..8d6d7a5 100644 --- a/scripts/capture_parent_child_reference.py +++ b/scripts/capture_parent_child_reference.py @@ -1,16 +1,17 @@ """Capture the parent/child emitter's retained surface, without a broker. -Produces `packages/schema-1/src/span_panel_api_schema_1/reference_payloads/ -parent_child_tree.json`, the schema_1 reference tree that ships as package data -and that fifteen test modules here replay through `devices_from_tree`. +Produces `tests/reference_payloads/parent_child_tree.json`, the schema_1 +reference tree that fifteen test modules here replay through `devices_from_tree`. +A repository fixture, not package data: it sat inside the adapter's package +directory until 3.1.0 and was carried in the wheel for it, and no consumer of +either distribution reads it at runtime. Run it from the **emitter's** environment, not this one — it imports `ebus_panel_sim`, which caps `ebus-sdk` below the version this repo installs: cd ../distribution-enclosure-simulator uv run python ../span-panel-api/scripts/capture_parent_child_reference.py \\ - ../span-panel-api/packages/schema-1/src/span_panel_api_schema_1/\\ -reference_payloads/parent_child_tree.json + ../span-panel-api/tests/reference_payloads/parent_child_tree.json `PANEL_SIM_DIR` overrides where the checkout is looked for; it defaults to a `distribution-enclosure-simulator` directory beside this repo. Passing no output @@ -475,11 +476,11 @@ def main() -> None: if PRODUCER_VERSION != expected: raise SystemExit( f"{SIM} is ebus-panel-sim {PRODUCER_VERSION}, and spec_lock.json records the reference " - f"tree as a capture of {expected}. Capturing anyway would put bytes in the wheel that " - "the lockfile attributes to a release that did not make them. Move the checkout to the " - "pinned release, or take the new capture deliberately: update peers.ebus-panel-sim's " + f"tree as a capture of {expected}. Capturing anyway would put bytes in this repository " + "that the lockfile attributes to a release that did not make them. Move the checkout to " + "the pinned release, or take the new capture deliberately: update peers.ebus-panel-sim's " "version, tag and commit in spec_lock.json, and the provenance section of " - "reference_payloads/README.md, in the same change." + "tests/reference_payloads/README.md, in the same change." ) profile = Profile(MANIFEST) diff --git a/scripts/reference_panel.yaml b/scripts/reference_panel.yaml index 73a8311..06fafe7 100644 --- a/scripts/reference_panel.yaml +++ b/scripts/reference_panel.yaml @@ -2,7 +2,7 @@ # # Read by `scripts/capture_parent_child_reference.py`, which drives the eBus # emitter (`ebus-panel-sim`) with it and records the retained topics as -# `packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json`. +# `tests/reference_payloads/parent_child_tree.json`. # Recorded in `spec_lock.json` as `peers.ebus-panel-sim.manifest`, so the capture's # input is pinned the same way its producer is: a capture whose input is not in the # tree is the same class of problem as one whose producer version is not written down. diff --git a/scripts/verify_reconnect.py b/scripts/verify_reconnect.py index ef9c523..c28128e 100644 --- a/scripts/verify_reconnect.py +++ b/scripts/verify_reconnect.py @@ -39,7 +39,7 @@ --broker-host 127.0.0.1 --broker-port 1883 --no-tls \ --data-model-version 1.0 \ --adapter span_panel_api_schema_1:SchemaOneAdapter \ - --seed packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json + --seed tests/reference_payloads/parent_child_tree.json Exits non-zero if any check fails. """ diff --git a/src/span_panel_api/reference_payloads/README.md b/src/span_panel_api/reference_payloads/README.md deleted file mode 100644 index 80d5fd1..0000000 --- a/src/span_panel_api/reference_payloads/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Reference payloads - -Shipped as package data and read through `span_panel_api.reference_payloads`, never by path — a consumer that installs this distribution gets these bytes, and a consumer that pins a version gets the bytes that version was written against. - -## `homie_schema.json` - -The `GET /api/v2/homie/schema` response, captured from a live SPAN Panel running firmware `spanos2/r202603/05`. Unauthenticated endpoint. Serial numbers are masked (last 4 chars replaced with `XXXX`). - -Schema hash `sha256:d347556a07d98f40` — compare against `typesSchemaHash` in a live response to detect a schema change across firmware versions. `span_panel_api_schema_0.const.SCHEMA_ANCHOR` is pinned to this value, and `tests/test_schema_provenance.py` -fails when the two diverge. - -### Node types present - -| Node Type | Properties | Notes | -| ------------------------------------------------ | ---------- | -------------------------------------------------- | -| `energy.ebus.device.distribution-enclosure.core` | 17 | Panel-wide state, network, hardware | -| `energy.ebus.device.lugs` | 7 | Upstream (main meter) and downstream (feedthrough) | -| `energy.ebus.device.circuit` | 16 | Per-circuit — one node per commissioned circuit | -| `energy.ebus.device.bess` | 12 | Battery — optional, only if commissioned | -| `energy.ebus.device.pv` | 7 | Solar — optional, only if commissioned | -| `energy.ebus.device.evse` | 9 | EV charger — optional, only if commissioned | -| `energy.ebus.device.pcs` | 15 | Power Control System — optional | -| `energy.ebus.device.power-flows` | 4 | Aggregated power flows (W) | diff --git a/src/span_panel_api/reference_payloads/__init__.py b/src/span_panel_api/reference_payloads/__init__.py deleted file mode 100644 index a8596f9..0000000 --- a/src/span_panel_api/reference_payloads/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Reference wire payloads, shipped as package data. - -These are captures of what a panel actually serves, published as part of the -distribution rather than kept in `tests/` — because the consumers that need -them most are *other* repositories. The Home Assistant integration checks the -field paths it declares against what the adapters can actually produce, and it -can only do that against a real schema document. Vendoring a copy of one is the -obvious move and the wrong one: a copy has no version, so it goes stale in -silence and the check starts verifying declarations against a schema no panel -runs. - -Shipping the payload here gives it the version of the release it came with. A -consumer that pins `span-panel-api==X` reads the document that release was -written against, by construction, with no copy to keep in sync. - -`homie_schema.json` belongs to this distribution and not to an adapter one: it -is the response of `span_panel_api.auth.get_homie_schema()`, modelled by -`V2HomieSchema` here, and dispatch reads its `data_model_version` to decide -*which* adapter parses the panel at all. The parent/child device tree is the -other half of that story and lives with the parser that can interpret it, in -`span_panel_api_schema_1.reference_payloads`. -""" - -from __future__ import annotations - -from collections.abc import Mapping -from importlib import resources -import json - -from span_panel_api.models import HomieSchemaTypes - -_PACKAGE = "span_panel_api.reference_payloads" -_HOMIE_SCHEMA = "homie_schema.json" - - -def _load_object(name: str) -> Mapping[str, object]: - """Read one shipped payload and require it to be a JSON object.""" - text = resources.files(_PACKAGE).joinpath(name).read_text(encoding="utf-8") - document: object = json.loads(text) - if not isinstance(document, dict): - raise TypeError(f"{name} is not a JSON object") - return document - - -def homie_schema() -> Mapping[str, object]: - """The captured `GET /api/v2/homie/schema` response, parsed. - - Taken from a live 32-space panel on `spanos2/r202603/05`; serial numbers are - masked. `typesSchemaHash` is `sha256:d347556a07d98f40`, which is the value - `span_panel_api_schema_0.const.SCHEMA_ANCHOR` is pinned to. - """ - return _load_object(_HOMIE_SCHEMA) - - -def homie_schema_types() -> HomieSchemaTypes: - """The captured schema's `types` map. - - Separate from `homie_schema()` because this is the shape a field-metadata - build takes — `span_panel_api_schema_0.field_metadata.build_field_metadata` - accepts exactly this type — so a caller checking an adapter's output against - the schema never has to reach into an untyped document to get it. - """ - types = homie_schema()["types"] - if not isinstance(types, dict): - raise TypeError(f"{_HOMIE_SCHEMA} has no `types` object") - return types diff --git a/tests/fixtures/v2/README.md b/tests/fixtures/v2/README.md index bfeabd1..20a2c41 100644 --- a/tests/fixtures/v2/README.md +++ b/tests/fixtures/v2/README.md @@ -10,5 +10,5 @@ Captured from a live SPAN Panel running firmware `spanos2/r202603/05`. Serial nu ## Moved -`homie_schema.json` is no longer a test fixture. It ships as package data at `src/span_panel_api/reference_payloads/homie_schema.json` and is read through `span_panel_api.reference_payloads.homie_schema()` — by this suite and by consumers alike, so there -is no copy anywhere that can go stale. Its provenance, schema hash and node-type table live in the README next to it. +`homie_schema.json` lives at [`tests/reference_payloads/homie_schema.json`](../../reference_payloads/README.md) and is read through `reference_payloads.bootstrap.homie_schema()`. It was package data under `src/span_panel_api/` between 3.0.0 and 3.1.0; +nothing at runtime read it there, so it is an ordinary fixture again. Its provenance, schema hash and node-type table live in the README next to it. diff --git a/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/README.md b/tests/reference_payloads/README.md similarity index 59% rename from packages/schema-1/src/span_panel_api_schema_1/reference_payloads/README.md rename to tests/reference_payloads/README.md index add042d..33efe0f 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/README.md +++ b/tests/reference_payloads/README.md @@ -1,13 +1,39 @@ # Reference payloads -Shipped as package data and read through `span_panel_api_schema_1.reference_payloads`, never by path — a consumer that installs this distribution gets these bytes, and a consumer that pins a version gets the bytes that version's parser was written against. +Captures of what a panel actually serves, read by this repository's test suite through `reference_payloads.bootstrap` and `reference_payloads.schema_one`. + +**These are repository fixtures, not package data.** Until 3.1.0 they sat inside `src/span_panel_api/` and `packages/schema-1/src/span_panel_api_schema_1/`, so both wheels carried them — not through any packaging declaration, but because a directory inside +a package directory ships. Nothing at runtime read them, and nothing does now. `tests/test_packaging.py` fails if a payload directory reappears inside a shipped package, and CI asserts the same against the built wheels. + +## `homie_schema.json` + +The `GET /api/v2/homie/schema` response, captured from a live SPAN Panel running firmware `spanos2/r202603/05`. Unauthenticated endpoint. Serial numbers are masked (last 4 chars replaced with `XXXX`). + +Schema hash `sha256:d347556a07d98f40` — compare against `typesSchemaHash` in a live response to detect a schema change across firmware versions. `span_panel_api_schema_0.const.SCHEMA_ANCHOR` is pinned to this value, and `tests/test_schema_provenance.py` +fails when the two diverge. + +### Node types present + +| Node Type | Properties | Notes | +| ------------------------------------------------ | ---------- | -------------------------------------------------- | +| `energy.ebus.device.distribution-enclosure.core` | 17 | Panel-wide state, network, hardware | +| `energy.ebus.device.lugs` | 7 | Upstream (main meter) and downstream (feedthrough) | +| `energy.ebus.device.circuit` | 16 | Per-circuit — one node per commissioned circuit | +| `energy.ebus.device.bess` | 12 | Battery — optional, only if commissioned | +| `energy.ebus.device.pv` | 7 | Solar — optional, only if commissioned | +| `energy.ebus.device.evse` | 9 | EV charger — optional, only if commissioned | +| `energy.ebus.device.pcs` | 15 | Power Control System — optional | +| `energy.ebus.device.power-flows` | 4 | Aggregated power flows (W) | ## `parent_child_tree.json` Retained topics captured off the eBus emitter's parent/child tree for a 40-space panel: 13 devices — the panel, both lugs, a BESS with its MID, a PV, an EVSE, and the circuits. Shape is `{device_id: {topic: payload}}`, every value a string, exactly as the broker retains them. `$description` is therefore a **JSON string**, not a nested object; `device_from_topics` replays it the way the transport does. `bess-mid` is typed -`energy.ebus.device.mid`, not `.bess` — a consumer filtering the tree by type marker has to expect the MID to survive a BESS filter. +`energy.ebus.device.mid`, not `.bess` — a reader filtering the tree by type marker has to expect the MID to survive a BESS filter. + +`devices_from_tree` and `device_from_topics` live in `schema_one.py` beside the capture rather than in whichever test first needed them: a tree is not directly usable, every reader of it has to replay the retained topics through `DiscoveredDevice` first, +and splitting the two would put the same twelve lines in each of the modules that read it. ### Provenance @@ -34,7 +60,7 @@ producer is not written down. It mirrors `examples/forty_tab_minimal.yaml` key f production enclosures we hold captures from — 27 circuits — no panel has ever published `UNKNOWN`, so this manifest uses values a real panel publishes. **`UNKNOWN` is still a legal enum member and this parser must handle it**; that obligation comes from `load-shed` 0.3's declared `$format` and is tested from the catalog in `tests/test_schema_one_circuits.py`, not from this capture. Contract obligations come from the catalog; representativeness comes from the capture. 2. **Identity properties.** The BESS's part/serial/firmware, the MID's model, firmware and hardware version, and the PV's firmware are all published by real panels and unset in the example. A capture omitting them understates what a consumer has to parse — - which is what left four library tests injecting those values by hand, reading as coverage while asking nothing about what a panel sends. Every value is synthetic (`example-40t-001`, `EXAMPLE-BESS-40T-001`), because these bytes ship in a wheel. + which is what left four library tests injecting those values by hand, reading as coverage while asking nothing about what a panel sends. Every value is synthetic (`example-40t-001`, `EXAMPLE-BESS-40T-001`). The cost of that choice is real: the capture is no longer reproducible by running an example anyone can find in the emitter. The committed manifest is what buys it back. diff --git a/tests/reference_payloads/__init__.py b/tests/reference_payloads/__init__.py new file mode 100644 index 0000000..2f70784 --- /dev/null +++ b/tests/reference_payloads/__init__.py @@ -0,0 +1,29 @@ +"""Reference wire payloads, as ordinary fixtures of this repository's suite. + +Captures of what a panel actually serves. They lived inside the two source +packages until 3.1.0 and were therefore carried in the wheels — not through any +packaging declaration, but simply because a directory inside `src//` +ships. Nothing at runtime ever read them, so every install paid for test data it +could not use, and a consumer who imported them acquired a dependency on files +the distributions never meant to promise. They are here now, where their only +readers are. + +Two modules rather than one, split the way the distributions are: + +- `bootstrap` — `homie_schema.json`, the document `span_panel_api.auth` fetches + and dispatch reads `data_model_version` out of. No adapter is involved, so + this module imports nothing an adapter-less environment lacks. +- `schema_one` — `parent_child_tree.json`, a retained-topic capture, which is + only interpretable by the parser that speaks its vocabulary. Importing it + reaches the eBus SDK, which is `span-panel-api-schema-1`'s dependency alone. + +Keeping the split means a test that needs the schema document never drags the +SDK in behind it, which is the same reason the two payloads were in different +distributions to begin with. + +Imported as a top-level package — `from reference_payloads.schema_one import +...` — because pytest puts `tests/` on `sys.path`, the same arrangement that +makes `from conftest import ...` work throughout this suite. +""" + +from __future__ import annotations diff --git a/tests/reference_payloads/bootstrap.py b/tests/reference_payloads/bootstrap.py new file mode 100644 index 0000000..c1c3a3c --- /dev/null +++ b/tests/reference_payloads/bootstrap.py @@ -0,0 +1,48 @@ +"""The bootstrap distribution's reference payload: the homie schema document. + +`homie_schema.json` is the response of `span_panel_api.auth.get_homie_schema()`, +modelled by `V2HomieSchema`, and dispatch reads its `data_model_version` to +decide *which* adapter parses the panel at all. The parent/child device tree is +the other half of that story and lives in `schema_one`, beside the replay that +can interpret it. + +Read by path rather than through `importlib.resources`: this is a file in a test +tree now, not package data, and saying so in the loader is part of the point. +""" + +from __future__ import annotations + +from collections.abc import Mapping +import json +from pathlib import Path + +from span_panel_api.models import HomieSchemaTypes + +_HOMIE_SCHEMA = Path(__file__).parent / "homie_schema.json" + + +def homie_schema() -> Mapping[str, object]: + """The captured `GET /api/v2/homie/schema` response, parsed. + + Taken from a live 32-space panel on `spanos2/r202603/05`; serial numbers are + masked. `typesSchemaHash` is `sha256:d347556a07d98f40`, which is the value + `span_panel_api_schema_0.const.SCHEMA_ANCHOR` is pinned to. + """ + document: object = json.loads(_HOMIE_SCHEMA.read_text(encoding="utf-8")) + if not isinstance(document, dict): + raise TypeError(f"{_HOMIE_SCHEMA.name} is not a JSON object") + return document + + +def homie_schema_types() -> HomieSchemaTypes: + """The captured schema's `types` map. + + Separate from `homie_schema()` because this is the shape a field-metadata + build takes — `span_panel_api_schema_0.field_metadata.build_field_metadata` + accepts exactly this type — so a caller checking an adapter's output against + the schema never has to reach into an untyped document to get it. + """ + types = homie_schema()["types"] + if not isinstance(types, dict): + raise TypeError(f"{_HOMIE_SCHEMA.name} has no `types` object") + return types diff --git a/src/span_panel_api/reference_payloads/homie_schema.json b/tests/reference_payloads/homie_schema.json similarity index 100% rename from src/span_panel_api/reference_payloads/homie_schema.json rename to tests/reference_payloads/homie_schema.json diff --git a/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json b/tests/reference_payloads/parent_child_tree.json similarity index 100% rename from packages/schema-1/src/span_panel_api_schema_1/reference_payloads/parent_child_tree.json rename to tests/reference_payloads/parent_child_tree.json diff --git a/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/__init__.py b/tests/reference_payloads/schema_one.py similarity index 56% rename from packages/schema-1/src/span_panel_api_schema_1/reference_payloads/__init__.py rename to tests/reference_payloads/schema_one.py index 05c360a..e3ce37a 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/reference_payloads/__init__.py +++ b/tests/reference_payloads/schema_one.py @@ -1,25 +1,24 @@ -"""Reference wire payloads for the parent/child schema, shipped as package data. - -The counterpart to `span_panel_api.reference_payloads`, and here rather than -there for the reason that decides every placement in this workspace: a retained -topic tree is only interpretable by the parser that speaks its vocabulary, and -the eBus SDK that turns it back into devices is this distribution's dependency -alone. The bootstrap ships the document it fetches; this ships the tree it -cannot read. - -`devices_from_tree` is exported alongside the capture because a tree is not -directly usable — every consumer of it has to replay the retained topics -through `DiscoveredDevice` first, and that replay is the parser's own knowledge -of how the transport feeds it. Shipping the capture without the replay just -moves a copy of this module's logic into every consumer, which is the burden -the package data exists to remove. +"""The parent/child schema's reference payload, and the replay that reads it. + +`parent_child_tree.json` is a retained-topic capture: only the parser that +speaks its vocabulary can interpret it, and the eBus SDK that turns it back into +devices is `span-panel-api-schema-1`'s dependency alone. Importing this module +therefore reaches the SDK; importing `bootstrap` does not. + +`devices_from_tree` stays beside the capture for the reason it was written: a +tree is not directly usable, every consumer of it has to replay the retained +topics through `DiscoveredDevice` first, and separating the two would put the +same twelve lines in each of the test modules that read it. + +Read by path rather than through `importlib.resources`: this is a file in a test +tree now, not package data, and saying so in the loader is part of the point. """ from __future__ import annotations from collections.abc import Mapping -from importlib import resources import json +from pathlib import Path from ebus_sdk.homie import DiscoveredDevice @@ -30,8 +29,7 @@ wire exactly as the panel publishes it, and `update_description` parses it. """ -_PACKAGE = "span_panel_api_schema_1.reference_payloads" -_PARENT_CHILD_TREE = "parent_child_tree.json" +_PARENT_CHILD_TREE = Path(__file__).parent / "parent_child_tree.json" _DEFAULT_STATE = "ready" _DOMAIN = "ebus" @@ -41,13 +39,12 @@ def parent_child_tree() -> RetainedTopicTree: """The captured retained topics of a full 40-space panel. Thirteen devices: the panel, both lugs, a BESS with its MID, a PV, an EVSE - and the circuits — enough that a consumer can check what each device class - does and does not declare, including the absences. + and the circuits — enough that a test can check what each device class does + and does not declare, including the absences. """ - text = resources.files(_PACKAGE).joinpath(_PARENT_CHILD_TREE).read_text(encoding="utf-8") - tree: object = json.loads(text) + tree: object = json.loads(_PARENT_CHILD_TREE.read_text(encoding="utf-8")) if not isinstance(tree, dict): - raise TypeError(f"{_PARENT_CHILD_TREE} is not a JSON object") + raise TypeError(f"{_PARENT_CHILD_TREE.name} is not a JSON object") return tree @@ -74,7 +71,7 @@ def device_from_topics(device_id: str, topics: Mapping[str, str]) -> DiscoveredD def devices_from_tree(tree: RetainedTopicTree) -> list[DiscoveredDevice]: """Rebuild every device in a capture. - Takes the tree rather than reading it, so a consumer can filter the capture + Takes the tree rather than reading it, so a caller can filter the capture first — dropping the BESS to model a panel that has none, say — and still build devices the same way. """ diff --git a/tests/test_adoption.py b/tests/test_adoption.py index e01b6d1..eba7c70 100644 --- a/tests/test_adoption.py +++ b/tests/test_adoption.py @@ -17,9 +17,9 @@ from typing import TYPE_CHECKING import pytest +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api.models import SpanPanelSnapshot from span_panel_api_schema_1.adoption import MODELLED_TYPES -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree from span_panel_api_schema_1.snapshot import build_snapshot if TYPE_CHECKING: diff --git a/tests/test_catalog_divergence.py b/tests/test_catalog_divergence.py index e49577c..e22f72b 100644 --- a/tests/test_catalog_divergence.py +++ b/tests/test_catalog_divergence.py @@ -44,9 +44,9 @@ import json from pathlib import Path -from span_panel_api import reference_payloads as flat_payloads +from reference_payloads import bootstrap as flat_payloads +from reference_payloads import schema_one as tree_payloads from span_panel_api_schema_0.field_metadata import _PROPERTY_FIELD_MAP as _FLAT_FIELD_MAP -from span_panel_api_schema_1 import reference_payloads as tree_payloads from span_panel_api_schema_1.catalog import ( CATALOGUED_CONCRETE_UNITS, UNIT_FAMILIES, diff --git a/tests/test_detection_auth.py b/tests/test_detection_auth.py index 9c20962..8c46b80 100644 --- a/tests/test_detection_auth.py +++ b/tests/test_detection_auth.py @@ -629,7 +629,7 @@ def test_panel_size_bad_format_raises(self): def test_panel_size_from_live_fixture(self): """panel_size works with the real panel schema fixture.""" - from span_panel_api.reference_payloads import homie_schema, homie_schema_types + from reference_payloads.bootstrap import homie_schema, homie_schema_types schema = V2HomieSchema( firmware_version=homie_schema()["firmwareVersion"], diff --git a/tests/test_packaging.py b/tests/test_packaging.py index e0b597a..b2f1e03 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -15,6 +15,23 @@ _REPO_ROOT = Path(__file__).resolve().parents[1] +_CAPTURES = Path(__file__).parent / "reference_payloads" +"""Where the reference captures live, and the source of the names below. + +Read off the fixture directory rather than listed here so a capture added there +is covered the day it exists — the same reason `_wheel_source_packages` reads +the manifests. +""" + + +def _is_reference_capture(path: Path) -> bool: + """Would this file be one of the reference captures, wherever it sits? + + Two questions, because there are two ways to reintroduce the problem: move + the directory back inside a package, or drop one capture in beside a module. + """ + return "reference_payloads" in path.parts or path.name in {capture.name for capture in _CAPTURES.glob("*.json")} + def _wheel_source_packages() -> list[tuple[str, Path]]: """Every importable package each distribution in the workspace ships. @@ -67,3 +84,30 @@ def test_every_shipped_package_carries_a_py_typed_marker(distribution: str, pack """ marker = package_dir / "py.typed" assert marker.is_file(), f"{distribution} ships {package_dir.name} without a py.typed marker" + + +@pytest.mark.parametrize( + ("distribution", "package_dir"), + _wheel_source_packages(), + ids=lambda value: value.name if isinstance(value, Path) else str(value), +) +def test_no_shipped_package_carries_a_reference_capture(distribution: str, package_dir: Path) -> None: + """Test data must not sit inside a package directory. + + Nothing declares what ships: hatchling takes the whole of `packages = [...]`, + so a directory dropped inside one is package data by position alone. That is + how both wheels came to carry a reference capture between 3.0.0 and 3.1.0 — + 56 KB no runtime path reads, in every install, plus an import surface the + distributions never meant to promise and could not remove without a breaking + change. + + The captures are fixtures now, under `tests/reference_payloads`. This is the + cheap half of holding that: CI asserts the same thing against the built + wheels, where it is finally true rather than inferred, but a failure here + names the file before anyone builds one. + """ + payloads = [path for path in package_dir.rglob("*.json") if _is_reference_capture(path)] + assert not payloads, ( + f"{distribution} would ship {[str(p.relative_to(package_dir)) for p in payloads]} — " + "reference captures belong in tests/reference_payloads, not inside a package directory" + ) diff --git a/tests/test_reference_tree_values.py b/tests/test_reference_tree_values.py index 2b09f06..6c6d4d5 100644 --- a/tests/test_reference_tree_values.py +++ b/tests/test_reference_tree_values.py @@ -1,7 +1,6 @@ """What the reference tree leaves unvalued must be what the producer leaves unvalued. -`parent_child_tree.json` is shipped package data and the fixture every schema_1 -test is written against, so what it *publishes* is the whole evidence base for +`parent_child_tree.json` is the fixture every schema_1 test is written against, so what it *publishes* is the whole evidence base for "does this adapter read that property". A property the producer values and this capture does not is therefore invisible in both directions at once: no test can fail for not reading it, and no consumer test can fail for not surfacing it. @@ -56,7 +55,7 @@ import json import pathlib -from span_panel_api_schema_1.reference_payloads import parent_child_tree +from reference_payloads.schema_one import parent_child_tree _PANELBENCH_BASELINE = pathlib.Path(__file__).parent / "fixtures" / "panelbench_unvalued_by_both.json" diff --git a/tests/test_schema_one_adapter.py b/tests/test_schema_one_adapter.py index 3facb13..f67933c 100644 --- a/tests/test_schema_one_adapter.py +++ b/tests/test_schema_one_adapter.py @@ -11,11 +11,11 @@ import pytest +from reference_payloads.schema_one import parent_child_tree from span_panel_api.adapters import _derive_required_members from span_panel_api.models import V2HomieSchema from span_panel_api.protocol import SchemaAdapter from span_panel_api_schema_1 import SchemaOneAdapter -from span_panel_api_schema_1.reference_payloads import parent_child_tree _TREE = parent_child_tree() diff --git a/tests/test_schema_one_against_simulator.py b/tests/test_schema_one_against_simulator.py index ea85ced..607325a 100644 --- a/tests/test_schema_one_against_simulator.py +++ b/tests/test_schema_one_against_simulator.py @@ -1,7 +1,7 @@ """Drive the parser end to end from what the simulator actually publishes. -Every other schema_1 test runs on the tree this distribution ships as package -data, which was captured off the upstream *generic* eBus panel simulator. That fixture is fine +Every other schema_1 test runs on the reference tree in +`tests/reference_payloads`, which was captured off the upstream *generic* eBus panel simulator. That fixture is fine for exercising the mapper, but it is not SPAN: it has never carried the extensions and divergences that are SPAN's own vocabulary, which is precisely the part a generic panel cannot produce. diff --git a/tests/test_schema_one_charge_limit.py b/tests/test_schema_one_charge_limit.py index 7991c0c..93ff7a4 100644 --- a/tests/test_schema_one_charge_limit.py +++ b/tests/test_schema_one_charge_limit.py @@ -24,13 +24,13 @@ from ebus_sdk.homie import DiscoveredDevice +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api.exceptions import SpanPanelServerError from span_panel_api.models import V2HomieSchema from span_panel_api_schema_1 import SchemaOneAdapter from span_panel_api_schema_1.charge_limit import resolve_charge_limit from span_panel_api_schema_1.devices import build_evse from span_panel_api_schema_1.field_metadata import build_field_metadata -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree _TREE = parent_child_tree() diff --git a/tests/test_schema_one_circuits.py b/tests/test_schema_one_circuits.py index 78ccfd9..8eb4484 100644 --- a/tests/test_schema_one_circuits.py +++ b/tests/test_schema_one_circuits.py @@ -1,6 +1,6 @@ """Mapping a v1.0 circuit device onto SpanCircuitSnapshot. -Driven from the tree this distribution ships as package data, captured off the +Driven from the reference tree in `tests/reference_payloads`, captured off the eBus emitter rather than hand-written, so the shapes are a conforming publisher's rather than my idea of them. See `scripts/capture_parent_child_reference.py` and the manifest beside it. @@ -21,7 +21,7 @@ from ebus_sdk.homie import DiscoveredDevice -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api_schema_1.circuits import build_circuit _TREE = parent_child_tree() diff --git a/tests/test_schema_one_conformance.py b/tests/test_schema_one_conformance.py index bc71172..ad866ee 100644 --- a/tests/test_schema_one_conformance.py +++ b/tests/test_schema_one_conformance.py @@ -551,6 +551,25 @@ def test_the_peer_is_pinned_to_the_same_specification_commit() -> None: ) +def test_the_recorded_capture_path_names_a_file_that_is_there() -> None: + """`produces.tree` is the one machine-readable statement of where the capture + lives, and three prose readers point at it — DEVELOPMENT.md, the payload + README and the capture script's own usage. + + A path recorded in a lockfile has no compiler: when the capture moved out of + the package directory in 3.1.0 nothing here objected, because nothing + resolved it. Resolving it is the whole check — a rename that leaves this + behind fails at the rename rather than the next time somebody re-captures. + """ + recorded = _peer(PANEL_SIM)["produces"] + assert isinstance(recorded, dict), "peers.ebus-panel-sim.produces should be an object" + tree = recorded["tree"] + assert isinstance(tree, str) + + capture = Path(__file__).parent.parent / tree + assert capture.is_file(), f"spec_lock.json records the reference tree at {tree}, which is not there" + + def test_the_peer_targets_the_same_firmware() -> None: """The firmware range is the anchor the two sides actually share — the spec says what a device class *may* publish, while a panel publishes one tree.""" @@ -741,7 +760,7 @@ def test_the_captured_tree_names_the_emitter_release_that_made_it() -> None: """The capture script, the lockfile and the checkout agree on one version. Three places could disagree, and the failure mode of each is the same: bytes - in the wheel attributed to a producer that did not make them. The script + in the tree attributed to a producer that did not make them. The script reads its expected version *out of this lockfile* rather than carrying a constant of its own, so there is one pin and this test proves the checkout is on it. diff --git a/tests/test_schema_one_connection_health.py b/tests/test_schema_one_connection_health.py index e2a9d94..8bf24d4 100644 --- a/tests/test_schema_one_connection_health.py +++ b/tests/test_schema_one_connection_health.py @@ -33,6 +33,11 @@ import pytest +from reference_payloads.schema_one import ( + RetainedTopicTree, + device_from_topics, + parent_child_tree, +) from span_panel_api.models import SpanEvseSnapshot, SpanPanelSnapshot from span_panel_api_schema_1.const import NODE_CONNECTION from span_panel_api_schema_1.devices import ( @@ -42,11 +47,6 @@ feed_connection_statuses, ) from span_panel_api_schema_1.field_metadata import build_field_metadata -from span_panel_api_schema_1.reference_payloads import ( - RetainedTopicTree, - device_from_topics, - parent_child_tree, -) from span_panel_api_schema_1.snapshot import build_snapshot PANEL = "example-40t-001" diff --git a/tests/test_schema_one_control_refusal.py b/tests/test_schema_one_control_refusal.py index cf8467a..ec56549 100644 --- a/tests/test_schema_one_control_refusal.py +++ b/tests/test_schema_one_control_refusal.py @@ -30,9 +30,9 @@ import pytest +from reference_payloads.schema_one import RetainedTopicTree, parent_child_tree from span_panel_api.models import V2HomieSchema from span_panel_api_schema_1 import SchemaOneAdapter -from span_panel_api_schema_1.reference_payloads import RetainedTopicTree, parent_child_tree _CATALOGS = Path(__file__).parent.parent / "packages" / "schema-1" / "spec" / "catalogs" @@ -75,8 +75,8 @@ def _copy() -> dict[str, dict[str, str]]: """A writable copy of the capture. Copied rather than mutated in place because `parent_child_tree()` reads the - shipped package data once per process and every other module in the suite is - reading the same object. + capture once per process and every other module in the suite is reading the + same object. """ return {device_id: dict(topics) for device_id, topics in parent_child_tree().items()} diff --git a/tests/test_schema_one_devices.py b/tests/test_schema_one_devices.py index 574e185..8d69ba4 100644 --- a/tests/test_schema_one_devices.py +++ b/tests/test_schema_one_devices.py @@ -9,7 +9,7 @@ from ebus_sdk.homie import DiscoveredDevice -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api_schema_1.devices import ( build_mid, build_battery, diff --git a/tests/test_schema_one_discovery.py b/tests/test_schema_one_discovery.py index 4490527..d753710 100644 --- a/tests/test_schema_one_discovery.py +++ b/tests/test_schema_one_discovery.py @@ -30,6 +30,11 @@ from ebus_sdk.homie import DiscoveredDevice import pytest +from reference_payloads.schema_one import ( + device_from_topics, + devices_from_tree, + parent_child_tree, +) from span_panel_api.models import DiscoveredMetadata, SpanPanelSnapshot, is_discovery_path from span_panel_api_schema_1 import field_metadata as field_metadata_module from span_panel_api_schema_1.const import ( @@ -48,11 +53,6 @@ build_discovery, build_field_metadata, ) -from span_panel_api_schema_1.reference_payloads import ( - device_from_topics, - devices_from_tree, - parent_child_tree, -) from span_panel_api_schema_1.snapshot import build_snapshot PANEL_DEVICE_ID = "example-40t-001" diff --git a/tests/test_schema_one_extension.py b/tests/test_schema_one_extension.py index dac7f2e..969a78e 100644 --- a/tests/test_schema_one_extension.py +++ b/tests/test_schema_one_extension.py @@ -22,6 +22,7 @@ from ebus_sdk.homie import DiscoveredDevice import pytest +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api.models import ( ADOPTION_IDENTITY_NODE, ADOPTION_TOPOLOGY_NODE, @@ -33,7 +34,6 @@ ) from span_panel_api_schema_1.extension import build_extension_properties from span_panel_api_schema_1.field_metadata import addressed_rows, build_discovery -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree from span_panel_api_schema_1.snapshot import build_snapshot PANEL_DEVICE_ID = "example-40t-001" diff --git a/tests/test_schema_one_panel.py b/tests/test_schema_one_panel.py index 5ef643b..fd99493 100644 --- a/tests/test_schema_one_panel.py +++ b/tests/test_schema_one_panel.py @@ -1,6 +1,6 @@ """Panel-level mapping from the v1.0 tree. -Driven from the tree this distribution ships as package data, captured off a +Driven from the reference tree in `tests/reference_payloads`, captured off a real `panel_sim` parent/child tree. """ @@ -12,8 +12,8 @@ from ebus_sdk.homie import DiscoveredDevice +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api_schema_1.const import NODE_GRID, TYPE_BESS, TYPE_PV -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree from span_panel_api_schema_1.panel import ( PanelFields, build_unmapped_tabs, diff --git a/tests/test_schema_one_pcs.py b/tests/test_schema_one_pcs.py index 7dc611c..0a58d0a 100644 --- a/tests/test_schema_one_pcs.py +++ b/tests/test_schema_one_pcs.py @@ -27,14 +27,14 @@ from ebus_sdk.homie import DiscoveredDevice -from span_panel_api.models import FieldMetadata, SpanPanelSnapshot, SpanPcsSnapshot -from span_panel_api_schema_1.const import PCS_LIMIT_SOURCES -from span_panel_api_schema_1.field_metadata import build_field_metadata -from span_panel_api_schema_1.reference_payloads import ( +from reference_payloads.schema_one import ( RetainedTopicTree, device_from_topics, parent_child_tree, ) +from span_panel_api.models import FieldMetadata, SpanPanelSnapshot, SpanPcsSnapshot +from span_panel_api_schema_1.const import PCS_LIMIT_SOURCES +from span_panel_api_schema_1.field_metadata import build_field_metadata from span_panel_api_schema_1.snapshot import build_snapshot PANEL = "example-40t-001" diff --git a/tests/test_schema_one_service_entrance.py b/tests/test_schema_one_service_entrance.py index 042413b..4985ac5 100644 --- a/tests/test_schema_one_service_entrance.py +++ b/tests/test_schema_one_service_entrance.py @@ -27,17 +27,17 @@ import pytest +from reference_payloads.schema_one import ( + RetainedTopicTree, + device_from_topics, + parent_child_tree, +) from span_panel_api.models import SpanPanelSnapshot from span_panel_api_schema_1.const import ( NODE_CONNECTION, PROP_FED_BY_DEVICE_ID, PROP_FED_BY_DEVICE_STATUS, ) -from span_panel_api_schema_1.reference_payloads import ( - RetainedTopicTree, - device_from_topics, - parent_child_tree, -) from span_panel_api_schema_1.snapshot import build_snapshot PANEL = "example-40t-001" diff --git a/tests/test_schema_one_shed_forecast.py b/tests/test_schema_one_shed_forecast.py index 08798b2..e1e8f02 100644 --- a/tests/test_schema_one_shed_forecast.py +++ b/tests/test_schema_one_shed_forecast.py @@ -17,13 +17,13 @@ from ebus_sdk.homie import DiscoveredDevice -from span_panel_api.models import FieldMetadata, SpanPanelSnapshot -from span_panel_api_schema_1.field_metadata import build_field_metadata -from span_panel_api_schema_1.reference_payloads import ( +from reference_payloads.schema_one import ( RetainedTopicTree, device_from_topics, parent_child_tree, ) +from span_panel_api.models import FieldMetadata, SpanPanelSnapshot +from span_panel_api_schema_1.field_metadata import build_field_metadata from span_panel_api_schema_1.snapshot import build_snapshot PANEL = "example-40t-001" diff --git a/tests/test_schema_one_snapshot.py b/tests/test_schema_one_snapshot.py index f346c11..1f34063 100644 --- a/tests/test_schema_one_snapshot.py +++ b/tests/test_schema_one_snapshot.py @@ -6,8 +6,8 @@ from ebus_sdk.homie import DiscoveredDevice +from reference_payloads.schema_one import device_from_topics, parent_child_tree from span_panel_api.models import SpanPanelSnapshot -from span_panel_api_schema_1.reference_payloads import device_from_topics, parent_child_tree from span_panel_api_schema_1.snapshot import TreeRoles, build_snapshot _TREE = parent_child_tree() diff --git a/tests/test_schema_provenance.py b/tests/test_schema_provenance.py index 556a809..66e68c6 100644 --- a/tests/test_schema_provenance.py +++ b/tests/test_schema_provenance.py @@ -22,7 +22,7 @@ import pytest -from span_panel_api.reference_payloads import homie_schema +from reference_payloads.bootstrap import homie_schema from span_panel_api_schema_0 import const from span_panel_api_schema_0.field_metadata import _LUGS_FALLBACK, _PROPERTY_FIELD_MAP, _lookup_property