aiorainpoint 0.1.0__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.
Files changed (49) hide show
  1. aiorainpoint-0.1.0/.gitattributes +2 -0
  2. aiorainpoint-0.1.0/.github/dependabot.yml +44 -0
  3. aiorainpoint-0.1.0/.github/rulesets/main.json +64 -0
  4. aiorainpoint-0.1.0/.github/workflows/catalog-freshness.yml +204 -0
  5. aiorainpoint-0.1.0/.github/workflows/constraints.yml +186 -0
  6. aiorainpoint-0.1.0/.github/workflows/release-please.yml +121 -0
  7. aiorainpoint-0.1.0/.github/workflows/tests.yml +229 -0
  8. aiorainpoint-0.1.0/.gitignore +15 -0
  9. aiorainpoint-0.1.0/.pre-commit-config.yaml +28 -0
  10. aiorainpoint-0.1.0/.release-please-manifest.json +3 -0
  11. aiorainpoint-0.1.0/CHANGELOG.md +10 -0
  12. aiorainpoint-0.1.0/LICENSE +22 -0
  13. aiorainpoint-0.1.0/PKG-INFO +57 -0
  14. aiorainpoint-0.1.0/README.md +45 -0
  15. aiorainpoint-0.1.0/codecov.yml +9 -0
  16. aiorainpoint-0.1.0/pyproject.toml +70 -0
  17. aiorainpoint-0.1.0/release-please-config.json +27 -0
  18. aiorainpoint-0.1.0/requirements-test.txt +5 -0
  19. aiorainpoint-0.1.0/scripts/check_dist.py +83 -0
  20. aiorainpoint-0.1.0/scripts/dump_push_envelopes.py +391 -0
  21. aiorainpoint-0.1.0/scripts/probe_app_endpoints.py +114 -0
  22. aiorainpoint-0.1.0/scripts/refresh_product_catalog.py +493 -0
  23. aiorainpoint-0.1.0/scripts/smoke_installed.py +37 -0
  24. aiorainpoint-0.1.0/scripts/trial_control_work_mode.py +299 -0
  25. aiorainpoint-0.1.0/sonar-project.properties +12 -0
  26. aiorainpoint-0.1.0/src/aiorainpoint/__init__.py +157 -0
  27. aiorainpoint-0.1.0/src/aiorainpoint/certs/ali_iot_ca.crt +24 -0
  28. aiorainpoint-0.1.0/src/aiorainpoint/client.py +930 -0
  29. aiorainpoint-0.1.0/src/aiorainpoint/data/product_catalog.json +6411 -0
  30. aiorainpoint-0.1.0/src/aiorainpoint/decoders.py +1775 -0
  31. aiorainpoint-0.1.0/src/aiorainpoint/generic_decoder.py +475 -0
  32. aiorainpoint-0.1.0/src/aiorainpoint/models.py +103 -0
  33. aiorainpoint-0.1.0/src/aiorainpoint/mqtt.py +1155 -0
  34. aiorainpoint-0.1.0/src/aiorainpoint/product_catalog.py +268 -0
  35. aiorainpoint-0.1.0/src/aiorainpoint/py.typed +0 -0
  36. aiorainpoint-0.1.0/src/aiorainpoint/trust.py +66 -0
  37. aiorainpoint-0.1.0/src/aiorainpoint/utils.py +705 -0
  38. aiorainpoint-0.1.0/src/aiorainpoint/validators.py +98 -0
  39. aiorainpoint-0.1.0/tests/__init__.py +1 -0
  40. aiorainpoint-0.1.0/tests/helpers.py +40 -0
  41. aiorainpoint-0.1.0/tests/payload_samples.py +494 -0
  42. aiorainpoint-0.1.0/tests/test_client.py +3650 -0
  43. aiorainpoint-0.1.0/tests/test_decoders.py +3176 -0
  44. aiorainpoint-0.1.0/tests/test_generic_decoder.py +848 -0
  45. aiorainpoint-0.1.0/tests/test_mqtt.py +2748 -0
  46. aiorainpoint-0.1.0/tests/test_product_catalog.py +457 -0
  47. aiorainpoint-0.1.0/tests/test_trust.py +231 -0
  48. aiorainpoint-0.1.0/tests/test_utils.py +1053 -0
  49. aiorainpoint-0.1.0/tests/test_validators.py +203 -0
@@ -0,0 +1,2 @@
1
+ *.crt -text
2
+ *.json -text
@@ -0,0 +1,44 @@
1
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
2
+
3
+ version: 2
4
+ updates:
5
+ # Monitor GitHub Actions if you add workflows
6
+ - package-ecosystem: "github-actions"
7
+ directory: "/"
8
+ schedule:
9
+ interval: "weekly"
10
+ # Hold new releases a few days so a hijacked publish is pulled before it is
11
+ # proposed. Security updates bypass this.
12
+ cooldown:
13
+ default-days: 5
14
+ groups:
15
+ actions:
16
+ patterns:
17
+ - "*"
18
+ commit-message:
19
+ prefix: "ci"
20
+ labels:
21
+ - "dependencies"
22
+ - "github-actions"
23
+
24
+ # Monitor Python (pip) dependencies
25
+ - package-ecosystem: "pip"
26
+ directory: "/"
27
+ schedule:
28
+ interval: "weekly"
29
+ cooldown:
30
+ default-days: 5
31
+ semver-major-days: 14
32
+ # One PR for minor and patch bumps; majors still arrive one PR each.
33
+ groups:
34
+ pip:
35
+ patterns:
36
+ - "*"
37
+ update-types:
38
+ - "minor"
39
+ - "patch"
40
+ commit-message:
41
+ prefix: "chore"
42
+ labels:
43
+ - "dependencies"
44
+ - "python"
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "main",
3
+ "target": "branch",
4
+ "enforcement": "active",
5
+ "bypass_actors": [],
6
+ "conditions": {
7
+ "ref_name": {
8
+ "include": [
9
+ "~DEFAULT_BRANCH"
10
+ ],
11
+ "exclude": []
12
+ }
13
+ },
14
+ "rules": [
15
+ {
16
+ "type": "deletion"
17
+ },
18
+ {
19
+ "type": "non_fast_forward"
20
+ },
21
+ {
22
+ "type": "pull_request",
23
+ "parameters": {
24
+ "required_approving_review_count": 0,
25
+ "dismiss_stale_reviews_on_push": false,
26
+ "require_code_owner_review": false,
27
+ "require_last_push_approval": false,
28
+ "required_review_thread_resolution": false
29
+ }
30
+ },
31
+ {
32
+ "type": "required_status_checks",
33
+ "parameters": {
34
+ "strict_required_status_checks_policy": false,
35
+ "required_status_checks": [
36
+ {
37
+ "context": "ruff",
38
+ "integration_id": 15368
39
+ },
40
+ {
41
+ "context": "mypy",
42
+ "integration_id": 15368
43
+ },
44
+ {
45
+ "context": "test",
46
+ "integration_id": 15368
47
+ },
48
+ {
49
+ "context": "test-min",
50
+ "integration_id": 15368
51
+ },
52
+ {
53
+ "context": "wheel",
54
+ "integration_id": 15368
55
+ },
56
+ {
57
+ "context": "ha-constraints-min",
58
+ "integration_id": 15368
59
+ }
60
+ ]
61
+ }
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,204 @@
1
+ name: Catalog Freshness
2
+
3
+ # Writes are done with a GitHub App token rather than GITHUB_TOKEN. A branch
4
+ # pushed with GITHUB_TOKEN does not raise workflow events, so the PR this job
5
+ # opens would sit with no checks on it forever. An App-authored push raises
6
+ # them normally, so the catalog PR gets the same checks as a hand-pushed one.
7
+ # The App and its two secrets are the shared release bot's.
8
+ permissions:
9
+ contents: read
10
+
11
+ on:
12
+ schedule:
13
+ # An hour after ha-rainpoint's run, which uses the same RainPoint account.
14
+ - cron: "0 7 * * 1"
15
+ workflow_dispatch:
16
+
17
+ # A manual dispatch that overlaps the weekly cron would otherwise race two
18
+ # runs into the same branch and the same PR-creation probe.
19
+ concurrency:
20
+ group: ${{ github.workflow }}
21
+ cancel-in-progress: true
22
+
23
+ env:
24
+ BRANCH: automated/product-catalog-sync
25
+
26
+ jobs:
27
+ freshness-check:
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 10
30
+
31
+ steps:
32
+ - name: Generate app token
33
+ id: app-token
34
+ uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
35
+ with:
36
+ client-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
37
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
38
+ # Scoped to exactly what the push and the PR need. Requesting a
39
+ # permission the installation lacks fails the token request, so a run
40
+ # that starts erroring here wants the specific scope added back rather
41
+ # than this block removed.
42
+ permission-contents: write
43
+ permission-pull-requests: write
44
+
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ with:
47
+ # Pinned to main rather than left to default, because a dispatch run
48
+ # takes the ref picked under "Use workflow from". Off main, the branch
49
+ # the PR step creates would be based on that ref, so the catalog PR
50
+ # would carry its unrelated commits while claiming to be a snapshot
51
+ # refresh. The scheduled run is already on main; this makes the
52
+ # dispatch behave the same.
53
+ ref: main
54
+ # Persisted on purpose, unlike the other workflows here: the drift
55
+ # step pushes with it.
56
+ token: ${{ steps.app-token.outputs.token }}
57
+
58
+ - name: Set up Python 3.13
59
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
60
+ with:
61
+ python-version: "3.13"
62
+
63
+ - name: Set up uv
64
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
65
+ with:
66
+ enable-cache: false
67
+
68
+ - name: Install dependencies
69
+ run: uv pip install --system .
70
+
71
+ - name: Check RainPoint credentials are present
72
+ id: creds
73
+ env:
74
+ RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
75
+ RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
76
+ run: |
77
+ if [ -z "$RAINPOINT_EMAIL" ] || [ -z "$RAINPOINT_PASSWORD" ]; then
78
+ echo "::warning::RainPoint credentials are not set; the catalog was not checked."
79
+ echo "skip=true" >> "$GITHUB_OUTPUT"
80
+ else
81
+ echo "skip=false" >> "$GITHUB_OUTPUT"
82
+ fi
83
+
84
+ # RAINPOINT_AREA_CODE is a repository variable, not a secret. It is a phone
85
+ # dial code (1 for the US), which is not sensitive, and holding it as a secret
86
+ # made the runner redact every occurrence of that digit in the log: the drift
87
+ # report came back reading "Fetched ***04 model entries" and listing models as
88
+ # "HTV2***3FRF", which is ambiguous and defeats the point of the report. An
89
+ # unset variable is an empty string, and the script already falls back to 1.
90
+ - name: Run catalog freshness check
91
+ id: check
92
+ if: steps.creds.outputs.skip == 'false'
93
+ env:
94
+ RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
95
+ RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
96
+ RAINPOINT_AREA_CODE: ${{ vars.RAINPOINT_AREA_CODE }}
97
+ run: |
98
+ # The default step shell is "bash -e", so drift (a deliberate exit 1)
99
+ # would abort the step before the status could be read. -e goes off
100
+ # around the call and back on straight after.
101
+ set +e
102
+ # --check writes its drift summary to stdout and its progress lines to
103
+ # stderr, so redirecting stdout alone captures the summary cleanly. It
104
+ # is worth capturing rather than leaving in the log: it splits changed
105
+ # models into datapoint changes and metadata-only changes, which is the
106
+ # difference between "commit this" and "read this carefully", and that
107
+ # split is what the PR body is for.
108
+ python scripts/refresh_product_catalog.py --check > drift.txt
109
+ status=$?
110
+ set -e
111
+ cat drift.txt
112
+ if [ "$status" -eq 0 ]; then
113
+ echo "drift=false" >> "$GITHUB_OUTPUT"
114
+ exit 0
115
+ fi
116
+ # Exit 3 is the server's rate limiter refusing every login attempt,
117
+ # after the script has already waited out the cooldown and retried.
118
+ # It says nothing about the snapshot, so it must not be reported as
119
+ # drift and must not reach the regenerate step. Left red rather than
120
+ # silently green, so a week with no check is visible, but named so
121
+ # nobody has to read the log to find out why.
122
+ if [ "$status" -eq 3 ]; then
123
+ echo "::error::RainPoint rate-limited every login attempt; the catalog was not checked. Re-run this workflow later."
124
+ exit 1
125
+ fi
126
+ # --check also exits 1 when the live pull comes back empty, which is a
127
+ # fetch failure rather than drift. It reports that on stderr, leaving
128
+ # stdout empty, so an empty summary here is the failure case and has to
129
+ # stay a red run.
130
+ if ! grep -q '[^[:space:]]' drift.txt; then
131
+ echo "::error::Catalog check failed without producing a drift summary."
132
+ exit 1
133
+ fi
134
+ echo "drift=true" >> "$GITHUB_OUTPUT"
135
+
136
+ # The write is a second live pull rather than a reuse of the one above
137
+ # because write mode is where the guards live: it refuses to overwrite the
138
+ # committed catalog with an empty pull, or with one holding under half the
139
+ # models. Fetching twice costs a login and about half a megabyte, once a
140
+ # week.
141
+ - name: Regenerate catalog
142
+ if: steps.check.outputs.drift == 'true'
143
+ env:
144
+ RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
145
+ RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
146
+ RAINPOINT_AREA_CODE: ${{ vars.RAINPOINT_AREA_CODE }}
147
+ run: python scripts/refresh_product_catalog.py
148
+
149
+ - name: Open or update the catalog PR
150
+ if: steps.check.outputs.drift == 'true'
151
+ env:
152
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
153
+ APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
154
+ CATALOG: src/aiorainpoint/data/product_catalog.json
155
+ run: |
156
+ set -euo pipefail
157
+ # The drift report and the written file are two separate pulls, so a
158
+ # report with nothing behind it is possible and must not become an
159
+ # empty commit.
160
+ if git diff --quiet -- "$CATALOG"; then
161
+ echo "Drift was reported but the regenerated catalog is unchanged; nothing to open."
162
+ exit 0
163
+ fi
164
+ git config user.name "${APP_SLUG}[bot]"
165
+ git config user.email "${APP_SLUG}[bot]@users.noreply.github.com"
166
+ # -C resets the branch onto main every run (the checkout is pinned to
167
+ # it), so it carries exactly one commit and never accumulates stale
168
+ # ones.
169
+ git switch -C "$BRANCH"
170
+ git add "$CATALOG"
171
+ git commit -m "fix(catalog): refresh the product catalog snapshot"
172
+ # The checkout is single-branch, so nothing here has a remote-tracking
173
+ # ref for this branch, and a bare --force-with-lease refuses with
174
+ # "stale info" from the second push onwards. Fetching the branch is
175
+ # necessary but not sufficient: the bare form still refuses, because it
176
+ # wants a value it observed rather than one it just created. Stating
177
+ # the expected value explicitly is the documented, non-experimental
178
+ # form of the lease, and it still refuses if anyone else moved the
179
+ # branch. An absent branch leaves the expectation empty, which git
180
+ # reads as "must not exist yet", so the first run is covered too.
181
+ git fetch --no-tags origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>/dev/null || true
182
+ expected="$(git rev-parse --verify --quiet "refs/remotes/origin/${BRANCH}" || true)"
183
+ git push --force-with-lease="refs/heads/${BRANCH}:${expected}" origin "$BRANCH"
184
+
185
+ {
186
+ echo "The bundled snapshot of RainPoint's product list no longer matches a live pull, so it has been regenerated."
187
+ echo
188
+ echo '```'
189
+ cat drift.txt
190
+ echo '```'
191
+ } > pr-body.md
192
+
193
+ number="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')"
194
+ if [ -z "$number" ]; then
195
+ gh pr create --draft --base main --head "$BRANCH" \
196
+ --title "fix(catalog): refresh the product catalog snapshot" \
197
+ --body-file pr-body.md
198
+ else
199
+ # gh pr edit --body fails on the maintainer's repos with a Projects-classic
200
+ # GraphQL error and changes nothing, so the body is patched through
201
+ # the REST API instead.
202
+ gh api -X PATCH "repos/${GITHUB_REPOSITORY}/pulls/${number}" -F body=@pr-body.md > /dev/null
203
+ echo "PR #${number} for $BRANCH already open; force-push and body update applied."
204
+ fi
@@ -0,0 +1,186 @@
1
+ name: HA Constraints
2
+ permissions:
3
+ contents: read
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ branches: [main]
10
+ schedule:
11
+ - cron: "0 8 * * 1"
12
+
13
+ # Cancel a superseded pull request run. Every other run gets its own group,
14
+ # since a pending run in a shared group is replaced by the next one queued.
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
17
+ cancel-in-progress: true
18
+
19
+ # Checks that the built wheel installs under Home Assistant's pinned dependencies,
20
+ # so a release here cannot make the integration uninstallable.
21
+ jobs:
22
+ # Required check.
23
+ ha-constraints-min:
24
+ runs-on: ubuntu-latest
25
+ timeout-minutes: 10
26
+ env:
27
+ # The oldest Home Assistant ha-rainpoint supports (its hacs.json minimum); moves when that does.
28
+ HA_REF: "2026.2.0"
29
+
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - name: Set up Python 3.13
36
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
37
+ with:
38
+ python-version: "3.13"
39
+
40
+ - name: Set up uv
41
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
42
+ with:
43
+ enable-cache: false
44
+
45
+ - name: Build the wheel
46
+ run: uv build --wheel
47
+
48
+ - name: Fetch Home Assistant's package constraints
49
+ run: |
50
+ url="https://raw.githubusercontent.com/home-assistant/core/${HA_REF}/homeassistant/package_constraints.txt"
51
+ if ! curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 "$url" -o package_constraints.txt; then
52
+ echo "::error::Could not download package_constraints.txt for Home Assistant ${HA_REF}"
53
+ exit 1
54
+ fi
55
+ if [ ! -s package_constraints.txt ]; then
56
+ echo "::error::package_constraints.txt for Home Assistant ${HA_REF} is empty"
57
+ exit 1
58
+ fi
59
+
60
+ # Home Assistant's own install flags, minus --quiet because the output is read below,
61
+ # plus --dry-run. A venv because uv refuses a bare interpreter.
62
+ - name: Resolve the wheel under those constraints
63
+ run: |
64
+ uv venv "$RUNNER_TEMP/resolve" --python 3.13
65
+ status=0
66
+ uv pip install --dry-run --python "$RUNNER_TEMP/resolve/bin/python" \
67
+ --index-strategy unsafe-first-match --upgrade \
68
+ --constraint package_constraints.txt dist/*.whl > resolve.txt 2>&1 || status=$?
69
+ cat resolve.txt
70
+ if [ "$status" -ne 0 ]; then
71
+ echo "::error::The wheel does not resolve under Home Assistant ${HA_REF} constraints"
72
+ exit 1
73
+ fi
74
+
75
+ - name: Check the resolved pins match Home Assistant's
76
+ run: |
77
+ for pkg in aiohttp paho-mqtt; do
78
+ pin=$(grep -m1 -iE "^${pkg}==" package_constraints.txt || true)
79
+ if [ -z "$pin" ]; then
80
+ echo "::error::Home Assistant ${HA_REF} no longer pins $pkg"
81
+ exit 1
82
+ fi
83
+ if ! grep -qxF " + $pin" resolve.txt; then
84
+ echo "::error::$pkg did not resolve to $pin, the pin in Home Assistant ${HA_REF}"
85
+ exit 1
86
+ fi
87
+ done
88
+
89
+ # Not a required check: dev moves daily.
90
+ ha-constraints-dev:
91
+ runs-on: ubuntu-latest
92
+ timeout-minutes: 10
93
+ env:
94
+ HA_REF: dev
95
+
96
+ steps:
97
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
98
+ with:
99
+ persist-credentials: false
100
+
101
+ - name: Set up Python 3.14
102
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
103
+ with:
104
+ python-version: "3.14"
105
+
106
+ - name: Set up uv
107
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
108
+ with:
109
+ enable-cache: false
110
+
111
+ - name: Build the wheel
112
+ run: uv build --wheel
113
+
114
+ - name: Fetch Home Assistant's package constraints
115
+ run: |
116
+ url="https://raw.githubusercontent.com/home-assistant/core/${HA_REF}/homeassistant/package_constraints.txt"
117
+ if ! curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 "$url" -o package_constraints.txt; then
118
+ echo "::error::Could not download package_constraints.txt for Home Assistant ${HA_REF}"
119
+ exit 1
120
+ fi
121
+ if [ ! -s package_constraints.txt ]; then
122
+ echo "::error::package_constraints.txt for Home Assistant ${HA_REF} is empty"
123
+ exit 1
124
+ fi
125
+
126
+ # Home Assistant's own install flags, minus --quiet because the output is read below,
127
+ # plus --dry-run. A venv because uv refuses a bare interpreter.
128
+ - name: Resolve the wheel under those constraints
129
+ run: |
130
+ uv venv "$RUNNER_TEMP/resolve" --python 3.14
131
+ status=0
132
+ uv pip install --dry-run --python "$RUNNER_TEMP/resolve/bin/python" \
133
+ --index-strategy unsafe-first-match --upgrade \
134
+ --constraint package_constraints.txt dist/*.whl > resolve.txt 2>&1 || status=$?
135
+ cat resolve.txt
136
+ if [ "$status" -ne 0 ]; then
137
+ echo "::error::The wheel does not resolve under Home Assistant ${HA_REF} constraints"
138
+ exit 1
139
+ fi
140
+
141
+ - name: Check the resolved pins match Home Assistant's
142
+ run: |
143
+ for pkg in aiohttp paho-mqtt; do
144
+ pin=$(grep -m1 -iE "^${pkg}==" package_constraints.txt || true)
145
+ if [ -z "$pin" ]; then
146
+ echo "::error::Home Assistant ${HA_REF} no longer pins $pkg"
147
+ exit 1
148
+ fi
149
+ if ! grep -qxF " + $pin" resolve.txt; then
150
+ echo "::error::$pkg did not resolve to $pin, the pin in Home Assistant ${HA_REF}"
151
+ exit 1
152
+ fi
153
+ done
154
+
155
+ # The only job with a write token. It runs on the schedule alone and checks out nothing.
156
+ report-scheduled-failure:
157
+ needs: [ha-constraints-min, ha-constraints-dev]
158
+ if: ${{ !cancelled() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
159
+ runs-on: ubuntu-latest
160
+ timeout-minutes: 5
161
+ permissions:
162
+ issues: write
163
+ env:
164
+ GH_TOKEN: ${{ github.token }}
165
+ GH_REPO: ${{ github.repository }}
166
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
167
+ MIN_RESULT: ${{ needs.ha-constraints-min.result }}
168
+ DEV_RESULT: ${{ needs.ha-constraints-dev.result }}
169
+
170
+ steps:
171
+ - name: Open or update the failure issue
172
+ run: |
173
+ title="Weekly Home Assistant constraints check failing"
174
+ targets=""
175
+ [ "$MIN_RESULT" = "failure" ] && targets="ha-constraints-min"
176
+ [ "$DEV_RESULT" = "failure" ] && targets="${targets:+$targets, }ha-constraints-dev"
177
+ [ -n "$targets" ] || exit 0
178
+ body="Target: $targets
179
+ Run: $RUN_URL"
180
+ number=$(gh issue list --state open --limit 100 --json number,title \
181
+ | jq -r --arg t "$title" 'map(select(.title == $t))[0].number // empty')
182
+ if [ -n "$number" ]; then
183
+ gh issue comment "$number" --body "$body"
184
+ else
185
+ gh issue create --title "$title" --body "$body"
186
+ fi
@@ -0,0 +1,121 @@
1
+ name: Release Please
2
+
3
+ # One file on purpose: the PyPI and TestPyPI publisher records bind to this
4
+ # filename and to the pypi environment, and neither accepts a reusable workflow.
5
+ # There is no concurrency group, so a release run waiting on the pypi reviewer
6
+ # never cancels or queues a TestPyPI rehearsal dispatch.
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - main
12
+ # A push event is not guaranteed to arrive. A dispatch with an empty ref
13
+ # re-runs release-please alone and is the recovery path.
14
+ workflow_dispatch:
15
+ inputs:
16
+ ref:
17
+ description: Commit, branch or tag to build and upload to TestPyPI. Leave empty for a normal release run.
18
+ type: string
19
+ required: false
20
+ default: ""
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ release-please:
27
+ runs-on: ubuntu-latest
28
+ outputs:
29
+ release_created: ${{ steps.release.outputs.release_created }}
30
+ tag_name: ${{ steps.release.outputs.tag_name }}
31
+ version: ${{ steps.release.outputs.version }}
32
+ steps:
33
+ - name: Generate app token
34
+ id: app-token
35
+ uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
36
+ with:
37
+ client-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
38
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
39
+ # Scoped to what release-please uses: contents for the version bump,
40
+ # tag and release; pull-requests for the release PR and its labels.
41
+ permission-contents: write
42
+ permission-pull-requests: write
43
+
44
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
45
+ id: release
46
+ with:
47
+ token: ${{ steps.app-token.outputs.token }}
48
+
49
+ # Runs the repository's code and suite, so it holds no id-token permission.
50
+ build:
51
+ needs: release-please
52
+ if: >-
53
+ needs.release-please.outputs.release_created == 'true'
54
+ || (github.event_name == 'workflow_dispatch' && inputs.ref != '')
55
+ runs-on: ubuntu-latest
56
+
57
+ steps:
58
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
59
+ with:
60
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || needs.release-please.outputs.tag_name }}
61
+ persist-credentials: false
62
+
63
+ - name: Set up Python 3.13
64
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
65
+ with:
66
+ python-version: "3.13"
67
+
68
+ - name: Set up uv
69
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
70
+ with:
71
+ enable-cache: false
72
+
73
+ - name: Build the sdist and the wheel
74
+ run: uv build
75
+
76
+ - name: Check the archives
77
+ run: python scripts/check_dist.py dist
78
+
79
+ - name: Install the wheel into a clean venv
80
+ run: |
81
+ uv venv "$RUNNER_TEMP/wheel-venv" --python 3.13
82
+ uv pip install --python "$RUNNER_TEMP/wheel-venv/bin/python" dist/*.whl -r requirements-test.txt
83
+
84
+ # EXPECTED_VERSION is empty on a dispatch, so only a release asserts the version.
85
+ - name: Smoke-test the installed wheel
86
+ working-directory: ${{ runner.temp }}
87
+ env:
88
+ EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
89
+ run: |
90
+ "$RUNNER_TEMP/wheel-venv/bin/python" -I "$GITHUB_WORKSPACE/scripts/smoke_installed.py" ${EXPECTED_VERSION:+"$EXPECTED_VERSION"}
91
+
92
+ - name: Run the whole suite against the wheel
93
+ run: |
94
+ mkdir "$RUNNER_TEMP/wheel-run"
95
+ cp -r tests scripts pyproject.toml "$RUNNER_TEMP/wheel-run/"
96
+ cd "$RUNNER_TEMP/wheel-run"
97
+ "$RUNNER_TEMP/wheel-venv/bin/python" -m pytest --no-cov --tb=short
98
+
99
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
100
+ with:
101
+ name: dist
102
+ path: dist/
103
+ if-no-files-found: error
104
+
105
+ # Only the job that mints the OIDC token, and it runs no repository code.
106
+ publish:
107
+ needs: build
108
+ runs-on: ubuntu-latest
109
+ environment: pypi
110
+ permissions:
111
+ id-token: write
112
+
113
+ steps:
114
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
115
+ with:
116
+ name: dist
117
+ path: dist/
118
+
119
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
120
+ with:
121
+ repository-url: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}