span-panel-api 2.6.4__tar.gz → 3.0.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- span_panel_api-3.0.0b2/.github/workflows/ci.yml +136 -0
- span_panel_api-3.0.0b2/.github/workflows/release.yml +119 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.pre-commit-config.yaml +9 -2
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/CHANGELOG.md +80 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/DEVELOPMENT.md +22 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/PKG-INFO +9 -2
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/README.md +8 -1
- span_panel_api-3.0.0b2/RELEASE.md +193 -0
- span_panel_api-3.0.0b2/packages/schema-0/CHANGELOG.md +51 -0
- span_panel_api-3.0.0b2/packages/schema-0/README.md +32 -0
- span_panel_api-3.0.0b2/packages/schema-0/pyproject.toml +35 -0
- span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0/__init__.py +11 -0
- {span_panel_api-2.6.4/src/span_panel_api/mqtt → span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0}/accumulator.py +2 -1
- span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0/adapter.py +78 -0
- span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0/const.py +83 -0
- span_panel_api-2.6.4/src/span_panel_api/mqtt/homie.py → span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0/consumer.py +9 -3
- {span_panel_api-2.6.4/src/span_panel_api/mqtt → span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0}/field_metadata.py +4 -57
- span_panel_api-3.0.0b2/packages/schema-1/CHANGELOG.md +60 -0
- span_panel_api-3.0.0b2/packages/schema-1/README.md +8 -0
- span_panel_api-3.0.0b2/packages/schema-1/pyproject.toml +42 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/__init__.py +6 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/adapter.py +260 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/circuits.py +177 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/const.py +115 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/devices.py +143 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/field_metadata.py +169 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/panel.py +291 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/py.typed +0 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +155 -0
- span_panel_api-3.0.0b2/packages/schema-1/src/span_panel_api_schema_1/transport.py +194 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/pyproject.toml +44 -2
- span_panel_api-3.0.0b2/scripts/verify_adapterless_install.py +88 -0
- span_panel_api-3.0.0b2/scripts/verify_reconnect.py +530 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/__init__.py +5 -3
- span_panel_api-3.0.0b2/src/span_panel_api/adapters.py +201 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/auth.py +9 -0
- span_panel_api-3.0.0b2/src/span_panel_api/dispatch.py +74 -0
- span_panel_api-3.0.0b2/src/span_panel_api/exceptions.py +105 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/factory.py +26 -2
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/models.py +8 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/__init__.py +5 -6
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/client.py +164 -65
- span_panel_api-3.0.0b2/src/span_panel_api/mqtt/const.py +30 -0
- span_panel_api-3.0.0b2/src/span_panel_api/protocol.py +155 -0
- span_panel_api-3.0.0b2/src/span_panel_api/py.typed +0 -0
- span_panel_api-3.0.0b2/src/span_panel_api/schema_drift.py +65 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/conftest.py +37 -11
- span_panel_api-3.0.0b2/tests/fixtures/parent_child_tree.json +225 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_accumulator.py +2 -2
- span_panel_api-3.0.0b2/tests/test_adapters_discovery.py +350 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_auth_and_homie_helpers.py +2 -2
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_detection_auth.py +48 -0
- span_panel_api-3.0.0b2/tests/test_factory_dispatch.py +310 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_field_metadata.py +2 -1
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_mqtt_client_connection.py +89 -17
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_mqtt_connect_flow.py +14 -17
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_mqtt_homie.py +24 -26
- span_panel_api-3.0.0b2/tests/test_packaging.py +69 -0
- span_panel_api-3.0.0b2/tests/test_protocol_conformance.py +111 -0
- span_panel_api-3.0.0b2/tests/test_public_api_unchanged.py +87 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_adapter.py +304 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_circuits.py +194 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_devices.py +169 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_panel.py +266 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_snapshot.py +136 -0
- span_panel_api-3.0.0b2/tests/test_schema_one_transport.py +233 -0
- span_panel_api-3.0.0b2/tests/test_schema_provenance.py +177 -0
- span_panel_api-3.0.0b2/tests/test_schema_zero_adapter.py +55 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/uv.lock +67 -24
- span_panel_api-2.6.4/.github/workflows/ci.yml +0 -101
- span_panel_api-2.6.4/.github/workflows/release.yml +0 -42
- span_panel_api-2.6.4/src/span_panel_api/exceptions.py +0 -42
- span_panel_api-2.6.4/src/span_panel_api/mqtt/const.py +0 -71
- span_panel_api-2.6.4/src/span_panel_api/protocol.py +0 -79
- span_panel_api-2.6.4/tests/test_protocol_conformance.py +0 -48
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.codefactor +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.codefactor.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.deps-installed +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.github/dependabot.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-approve.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.github/workflows/dependabot-auto-merge.yml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.gitignore +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.markdownlint-cli2.jsonc +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.markdownlint.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.prettierrc.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.vscode/extensions.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/.vscode/tasks.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/LICENSE +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/SECURITY.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/conftest.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/developer_attribute_readme.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/openapi.json +0 -0
- {span_panel_api-2.6.4/src/span_panel_api → span_panel_api-3.0.0b2/packages/schema-0/src/span_panel_api_schema_0}/py.typed +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/pytest.ini +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/pytest_output.log +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/scripts/__init__.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/scripts/coverage.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/scripts/format.sh +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/scripts/format_markdown.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/scripts/test_live_auth.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/setup-hooks.sh +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/const.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/v2/homie_schema.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_exceptions.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-2.6.4 → span_panel_api-3.0.0b2}/tests/test_protocol_models.py +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
# Push and pull_request both arrive by webhook, so a dropped delivery leaves a
|
|
9
|
+
# commit with no run at all -- which reads the same as a commit that passed.
|
|
10
|
+
# Dispatch re-runs this against any ref on demand.
|
|
11
|
+
#
|
|
12
|
+
# gh workflow run ci.yml --ref develop
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint-and-test:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
strategy:
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.14"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout code
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
|
+
|
|
26
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
27
|
+
uses: actions/setup-python@v7
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@v7
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: uv sync --all-packages
|
|
38
|
+
|
|
39
|
+
- name: Run pre-commit hooks
|
|
40
|
+
run: |
|
|
41
|
+
uv run pre-commit run --all-files
|
|
42
|
+
|
|
43
|
+
- name: Run tests with pytest
|
|
44
|
+
run: |
|
|
45
|
+
uv run pytest tests/ -v \
|
|
46
|
+
--cov=src/span_panel_api \
|
|
47
|
+
--cov=packages/schema-0/src/span_panel_api_schema_0 \
|
|
48
|
+
--cov-report=xml --cov-report=term-missing
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
security-scan:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- name: Checkout code
|
|
56
|
+
uses: actions/checkout@v7
|
|
57
|
+
|
|
58
|
+
- name: Set up Python
|
|
59
|
+
uses: actions/setup-python@v7
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.14"
|
|
62
|
+
|
|
63
|
+
- name: Install uv
|
|
64
|
+
uses: astral-sh/setup-uv@v7
|
|
65
|
+
with:
|
|
66
|
+
enable-cache: true
|
|
67
|
+
|
|
68
|
+
- name: Install dependencies
|
|
69
|
+
run: uv sync --all-packages
|
|
70
|
+
|
|
71
|
+
- name: Run Bandit security scan
|
|
72
|
+
run: |
|
|
73
|
+
uv run bandit -r src/ packages/ -f json -o bandit-report.json || true
|
|
74
|
+
|
|
75
|
+
- name: Upload Bandit scan results
|
|
76
|
+
uses: actions/upload-artifact@v7
|
|
77
|
+
with:
|
|
78
|
+
name: bandit-report
|
|
79
|
+
path: bandit-report.json
|
|
80
|
+
|
|
81
|
+
build-check:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- name: Checkout code
|
|
85
|
+
uses: actions/checkout@v7
|
|
86
|
+
|
|
87
|
+
- name: Set up Python
|
|
88
|
+
uses: actions/setup-python@v7
|
|
89
|
+
with:
|
|
90
|
+
python-version: "3.14"
|
|
91
|
+
|
|
92
|
+
- name: Install uv
|
|
93
|
+
uses: astral-sh/setup-uv@v7
|
|
94
|
+
with:
|
|
95
|
+
enable-cache: true
|
|
96
|
+
|
|
97
|
+
- name: Install dependencies
|
|
98
|
+
run: uv sync --all-packages
|
|
99
|
+
|
|
100
|
+
- name: Build packages
|
|
101
|
+
run: uv build --all-packages
|
|
102
|
+
|
|
103
|
+
- name: Check packages
|
|
104
|
+
run: uv run twine check dist/*
|
|
105
|
+
|
|
106
|
+
# Every distribution here is fully annotated, so every distribution has to
|
|
107
|
+
# carry the marker that lets a consumer's type checker see those annotations.
|
|
108
|
+
# Without it the package resolves to Any downstream and the typing is inert.
|
|
109
|
+
- name: Verify every wheel ships a py.typed marker
|
|
110
|
+
run: |
|
|
111
|
+
python -c "
|
|
112
|
+
import glob, sys, zipfile
|
|
113
|
+
wheels = glob.glob('dist/*.whl')
|
|
114
|
+
if not wheels:
|
|
115
|
+
sys.exit('::error::no wheels were built')
|
|
116
|
+
for wheel in wheels:
|
|
117
|
+
if not any(n.endswith('/py.typed') for n in zipfile.ZipFile(wheel).namelist()):
|
|
118
|
+
sys.exit(f'::error::{wheel} ships no py.typed marker; downstream type checking would resolve it as Any')
|
|
119
|
+
print(f'{wheel}: py.typed present')
|
|
120
|
+
"
|
|
121
|
+
|
|
122
|
+
# The configuration entry-point discovery exists to support, and the one
|
|
123
|
+
# nothing else in CI exercises: the bootstrap wheel installed with no
|
|
124
|
+
# adapter present. It must import, and it must fail by name rather than
|
|
125
|
+
# with ModuleNotFoundError.
|
|
126
|
+
- name: Verify the bootstrap installs without an adapter
|
|
127
|
+
run: |
|
|
128
|
+
uv venv /tmp/bootstrap-only
|
|
129
|
+
VIRTUAL_ENV=/tmp/bootstrap-only uv pip install dist/span_panel_api-*.whl
|
|
130
|
+
VIRTUAL_ENV=/tmp/bootstrap-only uv run --no-project python scripts/verify_adapterless_install.py
|
|
131
|
+
|
|
132
|
+
- name: Upload build artifacts
|
|
133
|
+
uses: actions/upload-artifact@v7
|
|
134
|
+
with:
|
|
135
|
+
name: dist
|
|
136
|
+
path: dist/
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
# A `release: published` event reaches this workflow only through webhook
|
|
7
|
+
# delivery, and a dropped delivery is silent: the release exists, the tag
|
|
8
|
+
# exists, nothing publishes, and the run list looks the same as it did before.
|
|
9
|
+
# Dispatch goes through the API instead, so a release can always be driven to
|
|
10
|
+
# PyPI by hand.
|
|
11
|
+
#
|
|
12
|
+
# gh workflow run release.yml --ref schema-1-v0.1.0b1
|
|
13
|
+
#
|
|
14
|
+
# Dispatch a tag, never a branch. The steps below read the distribution and
|
|
15
|
+
# version out of the tag name, so a branch ref carries neither; it falls
|
|
16
|
+
# through to the error case rather than being guessed at.
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
|
|
19
|
+
# This repo publishes two distributions that version independently: the
|
|
20
|
+
# bootstrap (span-panel-api) and each schema adapter (span-panel-api-schema-N).
|
|
21
|
+
# One release publishes exactly one of them, chosen by the tag prefix:
|
|
22
|
+
#
|
|
23
|
+
# v3.0.0b1 -> span-panel-api (the historical convention)
|
|
24
|
+
# schema-0-v1.0.0b1 -> span-panel-api-schema-0
|
|
25
|
+
#
|
|
26
|
+
# The version lives in the distribution's own pyproject.toml and this workflow
|
|
27
|
+
# only verifies the tag agrees. It deliberately does not rewrite the version at
|
|
28
|
+
# release time: the adapter declares a floor on the bootstrap
|
|
29
|
+
# (span-panel-api>=X), so the committed versions are load-bearing for resolution
|
|
30
|
+
# and cannot be treated as placeholders that a release stamps over.
|
|
31
|
+
#
|
|
32
|
+
# Publishing uses PyPI trusted publishing, which is configured per project. A
|
|
33
|
+
# distribution released here for the first time needs a pending publisher
|
|
34
|
+
# created on PyPI beforehand (project name, this repo, workflow `release.yml`,
|
|
35
|
+
# environment `release`); without it the publish step fails on an otherwise
|
|
36
|
+
# correct build.
|
|
37
|
+
jobs:
|
|
38
|
+
deploy:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: release
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # For trusted publishing to PyPI
|
|
43
|
+
|
|
44
|
+
steps:
|
|
45
|
+
- name: Checkout code
|
|
46
|
+
uses: actions/checkout@v7
|
|
47
|
+
|
|
48
|
+
- name: Set up Python
|
|
49
|
+
uses: actions/setup-python@v7
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.14"
|
|
52
|
+
|
|
53
|
+
- name: Install uv
|
|
54
|
+
uses: astral-sh/setup-uv@v7
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: true
|
|
57
|
+
|
|
58
|
+
- name: Resolve the distribution from the tag
|
|
59
|
+
id: target
|
|
60
|
+
run: |
|
|
61
|
+
TAG=${GITHUB_REF#refs/tags/}
|
|
62
|
+
case "$TAG" in
|
|
63
|
+
schema-*-v*)
|
|
64
|
+
SCHEMA=${TAG#schema-}
|
|
65
|
+
SCHEMA=${SCHEMA%%-v*}
|
|
66
|
+
PACKAGE="span-panel-api-schema-$SCHEMA"
|
|
67
|
+
MANIFEST="packages/schema-$SCHEMA/pyproject.toml"
|
|
68
|
+
VERSION=${TAG#schema-$SCHEMA-v}
|
|
69
|
+
;;
|
|
70
|
+
v*)
|
|
71
|
+
PACKAGE="span-panel-api"
|
|
72
|
+
MANIFEST="pyproject.toml"
|
|
73
|
+
VERSION=${TAG#v}
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
echo "::error::Tag '$TAG' names no distribution. Use 'vX.Y.Z' for the bootstrap or 'schema-N-vX.Y.Z' for an adapter."
|
|
77
|
+
exit 1
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
81
|
+
echo "::error::Tag '$TAG' resolves to '$MANIFEST', which does not exist."
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
echo "Tag '$TAG' releases $PACKAGE $VERSION from $MANIFEST"
|
|
85
|
+
echo "package=$PACKAGE" >> "$GITHUB_OUTPUT"
|
|
86
|
+
echo "manifest=$MANIFEST" >> "$GITHUB_OUTPUT"
|
|
87
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
88
|
+
|
|
89
|
+
- name: Verify the tag matches the committed version
|
|
90
|
+
run: |
|
|
91
|
+
DECLARED=$(python -c "import sys, tomllib; print(tomllib.load(open(sys.argv[1], 'rb'))['project']['version'])" "${{ steps.target.outputs.manifest }}")
|
|
92
|
+
if [ "$DECLARED" != "${{ steps.target.outputs.version }}" ]; then
|
|
93
|
+
echo "::error::${{ steps.target.outputs.manifest }} declares version '$DECLARED' but the tag says '${{ steps.target.outputs.version }}'. Commit the version bump before tagging."
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "Version $DECLARED confirmed."
|
|
97
|
+
|
|
98
|
+
# Only the tagged distribution is built, so dist/ holds exactly what this
|
|
99
|
+
# release publishes and the publish step cannot pick up a sibling package.
|
|
100
|
+
- name: Build package
|
|
101
|
+
run: uv build --package "${{ steps.target.outputs.package }}"
|
|
102
|
+
|
|
103
|
+
- name: Verify the wheel ships a py.typed marker
|
|
104
|
+
run: |
|
|
105
|
+
python -c "
|
|
106
|
+
import glob, sys, zipfile
|
|
107
|
+
wheels = glob.glob('dist/*.whl')
|
|
108
|
+
if not wheels:
|
|
109
|
+
sys.exit('::error::no wheel was built')
|
|
110
|
+
for wheel in wheels:
|
|
111
|
+
if not any(n.endswith('/py.typed') for n in zipfile.ZipFile(wheel).namelist()):
|
|
112
|
+
sys.exit(f'::error::{wheel} ships no py.typed marker; downstream type checking would resolve it as Any')
|
|
113
|
+
print(f'{wheel}: py.typed present')
|
|
114
|
+
"
|
|
115
|
+
|
|
116
|
+
- name: Publish to PyPI
|
|
117
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
118
|
+
with:
|
|
119
|
+
print-hash: true
|
|
@@ -77,6 +77,10 @@ repos:
|
|
|
77
77
|
- pytest
|
|
78
78
|
- types-PyYAML
|
|
79
79
|
- paho-mqtt
|
|
80
|
+
# schema-1 parses the parent/child tree with the eBus SDK, which
|
|
81
|
+
# ships py.typed — so the hook needs it installed to resolve those
|
|
82
|
+
# types rather than silently reporting import-not-found.
|
|
83
|
+
- ebus-sdk>=0.18.0
|
|
80
84
|
args: ['--config-file=pyproject.toml']
|
|
81
85
|
exclude: '^src/span_panel_api/generated_client/.*|scripts/.*|tests/.*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*'
|
|
82
86
|
|
|
@@ -92,6 +96,9 @@ repos:
|
|
|
92
96
|
- pytest
|
|
93
97
|
- pyyaml
|
|
94
98
|
- paho-mqtt
|
|
99
|
+
# schema-1 imports the eBus SDK; without it here the hook reports
|
|
100
|
+
# import-error for a dependency that is correctly declared.
|
|
101
|
+
- ebus-sdk>=0.18.0
|
|
95
102
|
exclude: '^src/span_panel_api/generated_client/.*|tests/.*|generate_client\.py|scripts/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*'
|
|
96
103
|
|
|
97
104
|
# Check for common security issues
|
|
@@ -108,7 +115,7 @@ repos:
|
|
|
108
115
|
hooks:
|
|
109
116
|
- id: vulture
|
|
110
117
|
name: vulture
|
|
111
|
-
entry: bash -c 'uv run vulture src/span_panel_api/ --min-confidence 80'
|
|
118
|
+
entry: bash -c 'uv run vulture src/span_panel_api/ packages/schema-0/src/span_panel_api_schema_0/ packages/schema-1/src/span_panel_api_schema_1/ --min-confidence 80'
|
|
112
119
|
language: system
|
|
113
120
|
types: [python]
|
|
114
121
|
pass_filenames: false
|
|
@@ -131,6 +138,6 @@ repos:
|
|
|
131
138
|
name: coverage summary
|
|
132
139
|
entry: bash
|
|
133
140
|
language: system
|
|
134
|
-
args: ['-c', 'output=$(uv run pytest tests/ --cov=src/span_panel_api --cov-config=pyproject.toml --cov-fail-under=85 -q 2>&1); status=$?; echo "$output"; exit "$status"']
|
|
141
|
+
args: ['-c', 'output=$(uv run pytest tests/ --cov=src/span_panel_api --cov=packages/schema-0/src/span_panel_api_schema_0 --cov=packages/schema-1/src/span_panel_api_schema_1 --cov-config=pyproject.toml --cov-fail-under=85 -q 2>&1); status=$?; echo "$output"; exit "$status"']
|
|
135
142
|
pass_filenames: false
|
|
136
143
|
verbose: true
|
|
@@ -4,6 +4,86 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
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).
|
|
6
6
|
|
|
7
|
+
## [3.0.0b2] - 08/2026
|
|
8
|
+
|
|
9
|
+
Pre-release. Releases the reshaped `SchemaAdapter` protocol that `3.0.0b1` predates, and makes the mismatch between the two detectable rather than fatal at construction.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Adapter contract versioning.** `SchemaAdapter` now requires an `ADAPTER_CONTRACT` integer, and discovery rejects any adapter that does not declare this package's `ADAPTER_CONTRACT_VERSION`. Member presence was never the whole contract: a Protocol
|
|
14
|
+
cannot express signatures at runtime, so an adapter carrying every required name and the previous `__init__` arity passed discovery and failed much later inside the transport, as a bare `TypeError` about an argument count — the least actionable moment to
|
|
15
|
+
learn that two installed packages were built against different versions of each other. Adapters must declare the value as a **literal**; one read from the installed bootstrap would agree with every bootstrap, which is the disagreement being looked for.
|
|
16
|
+
- **`SpanPanelAdapterIncompatibleError`**, raised when the adapter a panel needs is installed but unusable. Distinct from `SpanPanelAdapterMissingError` because the remedy inverts: missing means install something, incompatible means installing more cannot
|
|
17
|
+
help. Reporting the second as the first sends someone to install a package they already have. Discovery still only _logs_ a rejection, so one unusable third-party adapter cannot take down a panel whose own adapter is fine; the error surfaces only when
|
|
18
|
+
the rejected adapter turns out to be the one required.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`data-model-version` dispatch is live.** The factory hardcoded `None`, so the guard that refuses a parent/child panel was written, tested and never invoked — every panel resolved to the flat parser regardless of what it reported. The Homie schema is
|
|
23
|
+
now fetched over REST **before** the broker is opened and the version drives adapter selection, which SPAN confirmed is a reliable flat-versus-parent/child signal on that endpoint. A `1.0` panel now raises `SpanPanelAdapterMissingError` naming the
|
|
24
|
+
adapter to install, instead of dying inside the flat parser on a missing `energy.ebus.device.circuit/space` property.
|
|
25
|
+
- **A directly constructed `SpanMqttClient` dispatches too.** Building a client without `create_span_client` previously always resolved the flat adapter, so it carried the same defect the factory path had. Dispatch now happens wherever a parser is built,
|
|
26
|
+
and fills in `data_model_version` / `schema_dispatch_reason` rather than leaving them reading `"not dispatched"`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **BREAKING: `SchemaAdapter.__init__` takes the schema, not a panel size.** `adapter_cls(serial_number, schema)` replaces `adapter_cls(serial_number, panel_size)`. `panel_size` is read out of a block only the flat schema has, so the bootstrap had to
|
|
31
|
+
understand a wire format it is meant to know nothing about, and an adapter whose schema is shaped differently had no way to say so. Each adapter now reads what its own format defines.
|
|
32
|
+
- **BREAKING: `SchemaAdapter.build_field_metadata()` takes no arguments.** It previously received `schema.types` — again a flat-shaped parameter on a format-agnostic protocol. The adapter holds the schema it was constructed with.
|
|
33
|
+
- **`V2HomieSchema.data_model_version`** carries the `dataModelVersion` field, `None` when the panel omits it. Absence is the flat signal and stays distinct from an empty string.
|
|
34
|
+
- **Tier 1 dispatch moved to `span_panel_api.dispatch.select_adapter_key`** from the private `factory._select_adapter_key`, so the transport can dispatch without importing the factory. `adapters.py` continues to answer "what is installed"; the new module
|
|
35
|
+
answers "what does this panel need".
|
|
36
|
+
|
|
37
|
+
## [3.0.0b1] - 08/2026
|
|
38
|
+
|
|
39
|
+
Pre-release. `span-panel-api` becomes a transport and a dispatcher that contains **no parser**. Wire formats ship as separate distributions and register themselves via entry points, so support for a new panel schema arrives by installing a package rather
|
|
40
|
+
than by upgrading the transport. This is prototype work being proven end to end before any decision to land it on `main`.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- **BREAKING: `span-panel-api` no longer contains a parser.** Installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`. Flat-schema panels (firmware `r202603`–`r202627`) need **`span-panel-api-schema-0`** installed
|
|
45
|
+
alongside it:
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **BREAKING: `HomieLifecycle`, `HomiePropertyAccumulator` and `HomieDeviceConsumer` are no longer exported** from `span_panel_api` or `span_panel_api.mqtt`. All three are flat-schema-specific rather than Homie-convention-level: the accumulator filters
|
|
52
|
+
every topic against a single device's prefix and stores `node → prop`, which drops nearly every message under the parent/child model; `HomieLifecycle`'s members are not Homie 5 `$state` values but a consumer-side progression encoding "one description
|
|
53
|
+
received ⇒ ready", which is the flat readiness model. They now live in `span_panel_api_schema_0`.
|
|
54
|
+
- **Removed dead constants** `DEVICE_TOPIC_FMT`, `STATE_TOPIC_FMT`, `DESCRIPTION_TOPIC_FMT`, `PROPERTY_TOPIC_FMT` (unreferenced before the Phase 0 relocation) and `TYPE_PCS` (a real schema type this library does not consume).
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`span_panel_api.adapters.resolve_adapter(key, reason)`** — the single place a missing adapter becomes a named error, used by both Tier 1 dispatch and the transport's default path.
|
|
59
|
+
- **`SpanPanelSchemaVersionError`**, raised when a panel reports a `data-model-version` whose schema major cannot be determined. Distinct from `SpanPanelAdapterMissingError` because the remedy differs: a missing adapter is a known schema with no installed
|
|
60
|
+
parser, while this is a schema no adapter can even be named for.
|
|
61
|
+
- **`SpanPanelAdapterMissingError` and `SpanPanelSchemaVersionError` are now exported** from the top-level package — both are errors a user sees when their panel outruns their install, so catching them should not require reaching into a private module.
|
|
62
|
+
- **`SchemaAdapter.__init__` is declared on the protocol.** Construction was always part of the contract (the transport resolves an adapter class from the registry and calls it), but was previously typed only as a `Callable`, leaving the signature
|
|
63
|
+
unchecked against implementations.
|
|
64
|
+
- **Entry-point validation.** `discover_adapters()` now verifies each loaded object is a class implementing the protocol before registering it, and skips it with a logged reason otherwise. One broken third-party adapter cannot take down a panel whose own
|
|
65
|
+
adapter is fine.
|
|
66
|
+
- **`scripts/verify_adapterless_install.py`** and a CI step that runs it against a venv holding only the bootstrap wheel.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **`SpanMqttClient(adapter_factory=...)` is now optional.** When omitted, the parser is resolved through entry-point discovery at `_build_adapter()` rather than imported. Resolution is lazy by design: constructing a client must not require an adapter to
|
|
71
|
+
be installed, only building a parser must.
|
|
72
|
+
- **Dispatch refuses an unreadable `data-model-version` instead of assuming flat.** Absence still means the flat schema — that is a real signal, since the property was introduced by the firmware that introduced parent/child. A value whose major _can_ be
|
|
73
|
+
read but whose form is non-canonical (`1`, `1.0-beta`) dispatches on that major and logs the deviation. A value with no extractable major now raises. Previously all three fell through to the flat parser, which does not fail — it produces plausible but
|
|
74
|
+
wrong power and energy figures.
|
|
75
|
+
- **Dispatch diagnostics travel through the `SpanMqttClient` constructor**, removing the window where a connected client reported a selected adapter alongside `schema_dispatch_reason='not dispatched'`.
|
|
76
|
+
- **Releases are now per-distribution and the tag no longer sets the version.** A tag selects which distribution to publish — `vX.Y.Z` for `span-panel-api`, `schema-N-vX.Y.Z` for an adapter — and the release fails unless the tagged version matches the one
|
|
77
|
+
committed in that distribution's `pyproject.toml`. The previous workflow rewrote the root version from the tag and built only the root package, which under a two-distribution layout would have published the bootstrap with no adapter alongside it. Version
|
|
78
|
+
numbers are now load-bearing between the two (the adapter declares a floor on the bootstrap), so they belong in the repository rather than being stamped at release time.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Adapter distributions ship a `py.typed` marker.** Without it a consumer's type checker refuses to read the adapter's annotations and resolves every symbol it exports as `Any`, silently erasing the strict typing at the wheel boundary. CI now fails any
|
|
83
|
+
wheel built without one.
|
|
84
|
+
- **Protocol conformance checking no longer depends on member kind.** The required-member set is derived from every public member `SchemaAdapter` declares, not only the callable ones — a `property` or `classmethod` object is not callable, so the previous
|
|
85
|
+
derivation would have quietly stopped requiring such a member the day the protocol declared one.
|
|
86
|
+
|
|
7
87
|
## [2.6.4] - 05/2026
|
|
8
88
|
|
|
9
89
|
### Fixed
|
|
@@ -70,6 +70,28 @@ To install pre-commit hooks:
|
|
|
70
70
|
|
|
71
71
|
This installs dependencies (if needed) and configures git pre-commit hooks.
|
|
72
72
|
|
|
73
|
+
## Workspace layout
|
|
74
|
+
|
|
75
|
+
This repository is a uv workspace publishing more than one distribution: the bootstrap (`span-panel-api`, at the root) and one parser package per panel schema (`packages/schema-N/`). `uv sync` installs the workspace, so the test suite runs against every
|
|
76
|
+
distribution together.
|
|
77
|
+
|
|
78
|
+
To work with the packages individually:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Install the workspace including every member
|
|
82
|
+
uv sync --all-packages
|
|
83
|
+
|
|
84
|
+
# Build every distribution
|
|
85
|
+
uv build --all-packages
|
|
86
|
+
|
|
87
|
+
# Build just one
|
|
88
|
+
uv build --package span-panel-api-schema-0
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Releasing
|
|
92
|
+
|
|
93
|
+
See [RELEASE.md](RELEASE.md) — each distribution versions and publishes independently, and the tag name selects which one is published.
|
|
94
|
+
|
|
73
95
|
## Contributing
|
|
74
96
|
|
|
75
97
|
1. Fork and clone the repository
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0b2
|
|
4
4
|
Summary: A client library for SPAN Panel API
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
6
|
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
@@ -38,10 +38,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
41
|
+
Two packages: the transport, and a parser for your panel's schema. `span-panel-api` contains no parser — installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`.
|
|
42
|
+
|
|
41
43
|
```bash
|
|
42
|
-
pip install span-panel-api
|
|
44
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
`span-panel-api-schema-0` parses the flat schema used by firmware `r202603` through `r202627`, which is every panel in the field today. Panels reporting a `data-model-version` need the adapter for that schema major instead; the error names the one it could
|
|
48
|
+
not find and lists what is installed.
|
|
49
|
+
|
|
50
|
+
Parsers are discovered through the `span_panel_api.schema_adapters` entry-point group, so support for a new panel schema arrives by installing a package rather than by upgrading the transport. The two version independently — see [RELEASE.md](RELEASE.md).
|
|
51
|
+
|
|
45
52
|
### Dependencies
|
|
46
53
|
|
|
47
54
|
- `httpx` — v2 authentication and detection endpoints
|
|
@@ -23,10 +23,17 @@ A Python client library for the SPAN Panel v2 API, using MQTT/Homie for real-tim
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
+
Two packages: the transport, and a parser for your panel's schema. `span-panel-api` contains no parser — installing it alone gives a client that connects and then raises `SpanPanelAdapterMissingError`.
|
|
27
|
+
|
|
26
28
|
```bash
|
|
27
|
-
pip install span-panel-api
|
|
29
|
+
pip install span-panel-api span-panel-api-schema-0
|
|
28
30
|
```
|
|
29
31
|
|
|
32
|
+
`span-panel-api-schema-0` parses the flat schema used by firmware `r202603` through `r202627`, which is every panel in the field today. Panels reporting a `data-model-version` need the adapter for that schema major instead; the error names the one it could
|
|
33
|
+
not find and lists what is installed.
|
|
34
|
+
|
|
35
|
+
Parsers are discovered through the `span_panel_api.schema_adapters` entry-point group, so support for a new panel schema arrives by installing a package rather than by upgrading the transport. The two version independently — see [RELEASE.md](RELEASE.md).
|
|
36
|
+
|
|
30
37
|
### Dependencies
|
|
31
38
|
|
|
32
39
|
- `httpx` — v2 authentication and detection endpoints
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This repository publishes **more than one PyPI distribution** from a single source tree. That makes releasing less obvious than `git tag && push`, so this document is the reference: what lives where, how a tag selects what gets published, and what an
|
|
4
|
+
administrator has to do to release everything.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
One repository, one [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/), independent distributions:
|
|
9
|
+
|
|
10
|
+
| Distribution | Directory | Manifest | Purpose |
|
|
11
|
+
| ------------------------- | -------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
|
|
12
|
+
| `span-panel-api` | repository root | `pyproject.toml` | The **bootstrap** — transport, dispatch, protocols. Contains no parser. |
|
|
13
|
+
| `span-panel-api-schema-0` | `packages/schema-0/` | `packages/schema-0/pyproject.toml` | Flat-schema parser (firmware `r202603`–`r202627`) |
|
|
14
|
+
|
|
15
|
+
Adapters are discovered at runtime through the `span_panel_api.schema_adapters` entry-point group. The bootstrap never imports an adapter, and adding an adapter to the field is an install, not an upgrade. Future adapters follow the same pattern under
|
|
16
|
+
`packages/schema-N/`.
|
|
17
|
+
|
|
18
|
+
Consequences for releasing:
|
|
19
|
+
|
|
20
|
+
- **Each distribution has its own version number** in its own manifest.
|
|
21
|
+
- **Each distribution is its own PyPI project**, with its own trusted publisher.
|
|
22
|
+
- **A release publishes exactly one distribution.** Releasing "the repo" means cutting one release per distribution.
|
|
23
|
+
|
|
24
|
+
## Two version axes
|
|
25
|
+
|
|
26
|
+
The bootstrap and the adapters do not share a version, and this is deliberate rather than an oversight.
|
|
27
|
+
|
|
28
|
+
- **The bootstrap** versions on its own library API — the transport and the `SchemaAdapter` protocol.
|
|
29
|
+
- **An adapter** versions on _its_ library API. The wire format it parses is fixed and is declared by `SUPPORTS_DATA_MODEL_VERSIONS`, not by the version number. A release of `span-panel-api-schema-0` means the parser changed, never that the panel did.
|
|
30
|
+
|
|
31
|
+
So `span-panel-api 3.0.0` and `span-panel-api-schema-0 1.0.0` are unrelated numbers, and either can move without the other.
|
|
32
|
+
|
|
33
|
+
Adapters declare a floor on the bootstrap (`span-panel-api>=3.0.0b1,<4.0`). That dependency is why the versions committed in the manifests are load-bearing: they participate in resolution, so they are not placeholders that a release process may overwrite.
|
|
34
|
+
|
|
35
|
+
## How a tag selects a distribution
|
|
36
|
+
|
|
37
|
+
`.github/workflows/release.yml` runs on `release: published` and derives everything from the tag name. There is no lookup table — the manifest path is computed by convention:
|
|
38
|
+
|
|
39
|
+
| Tag | Distribution published | Manifest read |
|
|
40
|
+
| ----------------- | ------------------------- | ---------------------------------- |
|
|
41
|
+
| `vX.Y.Z` | `span-panel-api` | `pyproject.toml` |
|
|
42
|
+
| `schema-N-vX.Y.Z` | `span-panel-api-schema-N` | `packages/schema-N/pyproject.toml` |
|
|
43
|
+
|
|
44
|
+
Worked example for `schema-0-v1.0.0b1`:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
TAG = schema-0-v1.0.0b1
|
|
48
|
+
${TAG#schema-} → 0-v1.0.0b1 strip leading "schema-"
|
|
49
|
+
${SCHEMA%%-v*} → 0 strip trailing "-v…" ⇒ schema number
|
|
50
|
+
PACKAGE = span-panel-api-schema-0
|
|
51
|
+
MANIFEST = packages/schema-0/pyproject.toml
|
|
52
|
+
VERSION = ${TAG#schema-0-v} → 1.0.0b1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Because the schema number is _extracted_ rather than enumerated, a future `schema-1-v0.1.0` resolves to `packages/schema-1/pyproject.toml` with no change to the workflow.
|
|
56
|
+
|
|
57
|
+
A tag matching neither form (`1.2.3`, `nightly`) fails immediately with a message naming both accepted forms.
|
|
58
|
+
|
|
59
|
+
## The tag does not set the version
|
|
60
|
+
|
|
61
|
+
The workflow **verifies** the version; it does not write it.
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
tag schema-0-v1.0.0b1
|
|
65
|
+
⇒ packages/schema-0/pyproject.toml must declare version = "1.0.0b1"
|
|
66
|
+
⇒ otherwise the job fails without publishing
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This means the release ritual is **bump, commit, then tag** — never tag-and-let-CI-stamp. Earlier versions of this workflow rewrote the version from the tag with `sed`, which cannot work here: there is no single manifest to stamp, and the adapter's
|
|
70
|
+
dependency floor on the bootstrap means a stamped version could silently disagree with what resolution actually uses.
|
|
71
|
+
|
|
72
|
+
A mismatch is a hard failure with both numbers in the message, so the common mistake — tagging before committing the bump — is caught before anything reaches PyPI.
|
|
73
|
+
|
|
74
|
+
## Releasing one distribution
|
|
75
|
+
|
|
76
|
+
1. **Bump the version** in that distribution's manifest, and add a `CHANGELOG.md` entry (the root one for the bootstrap, `packages/schema-N/CHANGELOG.md` for an adapter).
|
|
77
|
+
2. **Merge to `develop`** (or `main`, once this work is no longer prototype) and let CI go green.
|
|
78
|
+
3. **Create a GitHub Release:**
|
|
79
|
+
- **Tag** — `vX.Y.Z` or `schema-N-vX.Y.Z`, per the table above.
|
|
80
|
+
- **Target** — the branch holding the bump. This defaults to the repository's default branch, which is the easiest thing to get wrong; a tag cut from the wrong branch builds the wrong version and fails the verification step.
|
|
81
|
+
- **Set as a pre-release** — tick this for any `aN` / `bN` / `rcN` version.
|
|
82
|
+
4. **Watch the run.** `gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')"`
|
|
83
|
+
|
|
84
|
+
The job prints exactly what it resolved, which is the first thing to read if something looks wrong:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Tag 'schema-0-v1.0.0b1' releases span-panel-api-schema-0 1.0.0b1 from packages/schema-0/pyproject.toml
|
|
88
|
+
Version 1.0.0b1 confirmed.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Releasing every distribution
|
|
92
|
+
|
|
93
|
+
There is no "release everything" button, and that is intentional — the distributions version independently, so a coordinated release is a sequence of single-distribution releases rather than one action.
|
|
94
|
+
|
|
95
|
+
To release the whole workspace:
|
|
96
|
+
|
|
97
|
+
1. Bump every manifest that changed, in one branch, with its changelog entry.
|
|
98
|
+
2. If the bootstrap's version moved and adapters need the new floor, update `span-panel-api>=…` in each adapter manifest **in the same branch**. Do not release an adapter whose floor points at a bootstrap version that is not yet on PyPI.
|
|
99
|
+
3. Merge and let CI go green.
|
|
100
|
+
4. Cut the releases **bootstrap first, then each adapter**:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
v3.0.0b1 → span-panel-api
|
|
104
|
+
schema-0-v1.0.0b1 → span-panel-api-schema-0
|
|
105
|
+
schema-1-v0.1.0 → span-panel-api-schema-1
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
PyPI accepts them in any order, but bootstrap-first means there is never a window in which an adapter is installable and its dependency is not.
|
|
109
|
+
|
|
110
|
+
5. Verify from PyPI rather than from CI — see below.
|
|
111
|
+
|
|
112
|
+
Only bump and release what actually changed. A distribution with no changes does not need a release just because a sibling had one.
|
|
113
|
+
|
|
114
|
+
## Adding a new adapter
|
|
115
|
+
|
|
116
|
+
When `packages/schema-N/` lands, the workflow needs no edit — but PyPI does, and this is the step that will be forgotten:
|
|
117
|
+
|
|
118
|
+
1. **Create the PyPI project and its trusted publisher before the first release.** Because the project does not exist yet, this is a _pending publisher_, added from account/organization publishing settings rather than from the (non-existent) project page:
|
|
119
|
+
|
|
120
|
+
| Field | Value |
|
|
121
|
+
| ----------------- | ------------------------- |
|
|
122
|
+
| PyPI Project Name | `span-panel-api-schema-N` |
|
|
123
|
+
| Owner | `SpanPanel` |
|
|
124
|
+
| Repository name | `span-panel-api` |
|
|
125
|
+
| Workflow name | `release.yml` |
|
|
126
|
+
| Environment name | `release` |
|
|
127
|
+
|
|
128
|
+
Every field except the project name is identical across all distributions here, since they all publish from the same repository and workflow. Once the project exists, the same entry is visible and editable at
|
|
129
|
+
`https://pypi.org/manage/project/<name>/settings/publishing/`.
|
|
130
|
+
|
|
131
|
+
2. **Add the package to the workspace** — it is matched by `members = ["packages/*"]` automatically, but the root `[tool.uv.sources]` and the dev dependency group need an entry if the test suite is to exercise it.
|
|
132
|
+
3. **Ship a `py.typed` marker** in the new package. CI fails the build without it.
|
|
133
|
+
|
|
134
|
+
Trusted publishing verifies repository, workflow filename, and environment — it cannot distinguish _which_ distribution a run is building. That is inherent to a monorepo, and it is why the workflow builds only the tagged package: `dist/` never contains a
|
|
135
|
+
sibling that could be uploaded by accident.
|
|
136
|
+
|
|
137
|
+
## What the workflow checks
|
|
138
|
+
|
|
139
|
+
In order, all before anything is uploaded:
|
|
140
|
+
|
|
141
|
+
1. **Tag names a known distribution** — otherwise fail, naming both accepted forms.
|
|
142
|
+
2. **The derived manifest exists** — catches a `schema-N` tag with no matching directory.
|
|
143
|
+
3. **The tag version equals the committed version** — read with `tomllib`, compared exactly.
|
|
144
|
+
4. **Only the tagged package is built** — `uv build --package <name>`, so `dist/` holds exactly one distribution.
|
|
145
|
+
5. **Every built wheel ships `py.typed`** — a fully annotated distribution that omits it resolves as `Any` for every downstream consumer, silently undoing the strict typing this repository maintains.
|
|
146
|
+
|
|
147
|
+
| Failure | Meaning |
|
|
148
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `Tag '…' names no distribution` | Tag is malformed. Use `vX.Y.Z` or `schema-N-vX.Y.Z`. |
|
|
150
|
+
| `resolves to '…', which does not exist` | Tag names a schema whose directory is not in this commit — usually a tag cut from the wrong branch. |
|
|
151
|
+
| `declares version 'A' but the tag says 'B'` | The bump was not committed, or the release targets the wrong branch. |
|
|
152
|
+
| `ships no py.typed marker` | The new package is missing the marker file. |
|
|
153
|
+
| OIDC / trusted publishing rejection | The PyPI publisher for that project is missing or does not match. Nothing was uploaded; fix and re-run the job. |
|
|
154
|
+
|
|
155
|
+
A failed release is safe. Every check runs before upload, so a failure means nothing reached PyPI and the same tag can be re-run once the cause is fixed.
|
|
156
|
+
|
|
157
|
+
## Verifying a release
|
|
158
|
+
|
|
159
|
+
CI going green proves the build, not the install. The seam this repository is built around — a bootstrap that finds a parser it never imports — can only be exercised across a real package boundary, so verify from PyPI:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 1. The bootstrap alone must fail by name, not with ModuleNotFoundError
|
|
163
|
+
python3 -m venv .solo && ./.solo/bin/pip install --pre span-panel-api
|
|
164
|
+
./.solo/bin/python -c "
|
|
165
|
+
from span_panel_api.adapters import discover_adapters, resolve_adapter, DEFAULT_ADAPTER_KEY
|
|
166
|
+
from span_panel_api.exceptions import SpanPanelAdapterMissingError
|
|
167
|
+
print('adapters:', sorted(discover_adapters()))
|
|
168
|
+
try:
|
|
169
|
+
resolve_adapter(DEFAULT_ADAPTER_KEY, 'release check')
|
|
170
|
+
except SpanPanelAdapterMissingError as exc:
|
|
171
|
+
print('raised as designed:', exc.needed, exc.available)
|
|
172
|
+
"
|
|
173
|
+
|
|
174
|
+
# 2. Both packages: the adapter resolves through discovery
|
|
175
|
+
python3 -m venv .both && ./.both/bin/pip install --pre span-panel-api span-panel-api-schema-0
|
|
176
|
+
./.both/bin/python -c "
|
|
177
|
+
from span_panel_api.adapters import discover_adapters
|
|
178
|
+
print('adapters:', sorted(discover_adapters()))
|
|
179
|
+
"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Expected: `adapters: []` then a named `SpanPanelAdapterMissingError` in the first, `adapters: ['schema_0']` in the second.
|
|
183
|
+
|
|
184
|
+
Drop `--pre` once the versions being verified are not pre-releases.
|
|
185
|
+
|
|
186
|
+
## Pre-releases
|
|
187
|
+
|
|
188
|
+
Versions like `3.0.0b1` are pre-releases in both places that matter:
|
|
189
|
+
|
|
190
|
+
- **PyPI** will not install them without `--pre`, so `pip install span-panel-api` continues to resolve the last stable release.
|
|
191
|
+
- **GitHub** should have "Set as a pre-release" ticked, which keeps them out of the repository's "Latest release" slot.
|
|
192
|
+
|
|
193
|
+
The publish workflow itself does not care — `on: release: published` fires either way.
|