diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index b7b8aef..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Publish Python 🐍 distribution 📦 to PyPI - -on: - push: - tags: - - "v*.*.*" - -jobs: - release: - name: Release - uses: ./.github/workflows/release.yml - with: - pypi-project-url: https://pypi.org/p/teslemetry-stream - permissions: - id-token: write - contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a261e7..03a5215 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,22 +1,14 @@ name: Release gate & publish on: - workflow_call: - inputs: - python-version: - description: Python version to build/test/publish with - required: false - type: string - default: "3.13" - pypi-project-url: - description: PyPI project URL shown on the environment deployment - required: true - type: string - environment-name: - description: Protected GitHub environment gating the publish step - required: false - type: string - default: pypi + push: + tags: + - "v*.*.*" + +env: + PYTHON_VERSION: "3.13" + PYPI_PROJECT_URL: https://pypi.org/p/teslemetry-stream + ENVIRONMENT_NAME: pypi jobs: lint: @@ -75,8 +67,8 @@ jobs: uses: astral-sh/setup-uv@v5 with: enable-cache: true - - name: Set up Python ${{ inputs.python-version }} - run: uv python install ${{ inputs.python-version }} + - name: Set up Python ${{ env.PYTHON_VERSION }} + run: uv python install ${{ env.PYTHON_VERSION }} - name: Build run: uv build - name: Check distribution @@ -92,8 +84,8 @@ jobs: needs: build runs-on: ubuntu-latest environment: - name: ${{ inputs.environment-name }} - url: ${{ inputs.pypi-project-url }} + name: ${{ env.ENVIRONMENT_NAME }} + url: ${{ env.PYPI_PROJECT_URL }} permissions: id-token: write steps: @@ -104,9 +96,6 @@ jobs: path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - # Reusable workflow: PEP 740 attestations sign under the top-level caller's identity, which PyPI's publisher-identity check rejects with a 400. - attestations: false github-release: name: Upload distribution to GitHub Release diff --git a/AGENTS.md b/AGENTS.md index 6e335df..9472246 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ This file is the project's committed home for project-intrinsic agent knowledge: - Energy site events (`teslemetry_stream/energysite.py`) are shaped differently from vehicle signals: `live_status`/`site_info` are flat top-level envelopes (`{createdAt, site_id, isCache?, live_status|site_info}`), not nested under `data`, and the payload is a full opaque document rather than a field delta - there is no per-field config to enable, the server auto-polls subscribed sites. Contract source: Teslemetry/api PR 310 (`src/routes/sse/index.ts`, `liveStatusSchema.ts`, `siteInfoSchema.ts`), flag-gated server-side as of this writing - `tests/test_energysite_events.py` fixtures mirror that PR's schemas. - `energy_totals` (Teslemetry/api PR 316, trimmed by PR 321) is shaped differently again: the site id rides the `id` field, not `site_id` - filter on `id` and `totals`, not `site_id`. It carries a compact cumulative `totals` object (`EnergyHistoryTotals` in `const.py`) instead of a document, fires only when the server's periodic `calendar_history` poll detects a change (silence is not staleness), and has no snapshot-on-connect delivery. As of PR 321 the wire payload is trimmed to `id`/`createdAt`/`totals` plus `isCache` only when true (`product_type`/`topic`/`url` were dropped as redundant with the event's own topic name and site id); `Key.PRODUCT_TYPE`/`Key.TOPIC`/`Key.URL` in `const.py` remain defined for other event kinds but are no longer part of the energy_totals filter. - `site_info` events no longer carry `tariff_content`/`tariff_content_v2` (Teslemetry/api PR 318); the V2 tariff is its own `tariff_content_v2` event/listener (`listen_TariffContentV2`), same envelope shape as `site_info`, with a `None` body meaning an explicit server-side removal rather than "not received yet". Both share the same silence-means-no-change contract - freshness lives in REST, never in event cadence. There is deliberately no library helper recombining `site_info` and `tariff_content_v2` into one document - that would only ever cover the V2 tariff (legacy V1 `tariff_content` has no SSE topic and stays REST-only by design), so it can't actually promise the whole REST-shaped document; a consumer wanting both tariffs together should use the REST site_info endpoint. -- Releases (tag `v*.*.*`) go through `.github/workflows/release.yml`, a reusable `workflow_call` gate: `lint` + the full `test` python-version matrix (mirrors `ci.yml`) must pass on the exact release SHA before `build` (single Python, build+twine) runs, and only then do the `pypi` environment's protection rules (and its trusted-publishing OIDC) allow `publish-to-pypi`; `python-publish.yml` just supplies the tag trigger and calls it. The `pypi` GitHub environment itself (required reviewers, deployment branches) is admin-configured outside this repo's files. +- Releases (tag `v*.*.*`) go through `.github/workflows/release.yml` directly - it's the sole top-level workflow, triggered on the tag push: `lint` + the full `test` python-version matrix (mirrors `ci.yml`) must pass on the exact release SHA before `build` (single Python, build+twine) runs, and only then do the `pypi` environment's protection rules (and its trusted-publishing OIDC) allow `publish-to-pypi`. It must stay a top-level workflow, not a `workflow_call` reusable one - PyPI's trusted publisher is configured for the `release.yml` + `pypi` environment identity, and a reusable-workflow caller signs PEP 740 attestations under the caller's identity instead, which that publisher check rejects. The `pypi` GitHub environment itself (required reviewers, deployment branches) is admin-configured outside this repo's files. - `TeslemetryStream(topics=...)` (Teslemetry/api PR 319) is an optional exact SSE wire-event allowlist sent as the connection's `topics` query param; `SseTopic` in `const.py` is the closed set the server recognizes (must stay in sync with the api's `SSE_TOPICS`), and `SSE_VEHICLE_TOPICS`/`SSE_ENERGY_TOPICS`/`SSE_ALL_TOPICS` are client-side presets - flat per-product-kind lists of exact wire names, deliberately not further split by whether a topic happens to have a connect-time snapshot server-side; that's upstream server behavior, not something this library encodes. Omitting `topics` (`None`) is legacy-all forever - every applicable event delivered unfiltered - and existing callers that never pass it are unaffected. An explicitly empty iterable is rejected with `ValueError` at construction time rather than silently falling back to legacy-all - "no topics" must not mean "all topics", mirroring the server's own 400 on an empty `topics` value. A bare `str`/`SseTopic` is accepted as a single topic rather than iterated character-by-character - `topics` type-checks `str | Iterable[str] | None` precisely because a lone string also satisfies `Iterable[str]`, the classic footgun. `tests/test_sse_topics.py` covers the tariff listener, its null-removal signal, the `topics` param's URL construction, the empty-iterable rejection, and the bare-string/bare-`SseTopic` case. ## Maintaining this file