From a1ba3efddaa2f06dba389c0932601f23d2f1d028 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:20:05 +0100 Subject: [PATCH 1/4] fix(packaging): ship the devices and session subpackages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2.0 refactor moved the implementation into src/devices/ and src/session/, but [tool.setuptools] packages still described the 1.x flat layout, so setuptools never copied either directory into the build. The published 2.0.0b1 wheel and sdist therefore contain only api/, helper/, data/, hive.py and the flat deprecation shims — and every one of those entry points imports from the two missing subpackages: apyhiveapi/__init__.py:29 from .hive import Hive apyhiveapi/hive.py:8 from .devices.action import HiveAction ModuleNotFoundError: No module named 'apyhiveapi.devices' All three flavours (apyhiveapi, pyhive, pyhiveapi) fail at import, so 2.0.0b1 is unusable: it pip-installs cleanly and then breaks Home Assistant at integration setup. Add the four missing package names, and note in a comment why the list is explicit rather than find: (package-dir maps two distribution names onto one source tree) and why the sync flavour is absent from it (it is generated by the unasync build_py cmdclass). MANIFEST.in was stale for the same reason: it referenced a top-level pyhiveapi/ and data/ that have not existed since the move to src/, which is why SOURCES.txt listed no devices/ or session/ entries. The test suite could not catch this — it runs against src/ via an editable install and never touches the built artifact. Add scripts/verify_dist.py, which installs the wheel into a throwaway venv and imports every module in every flavour, and wire it into the tests workflow and both publish workflows so a build like this cannot reach PyPI again. Verified: it fails on the published 2.0.0b1 wheel and passes on this build (132 modules, 3 flavours). Co-Authored-By: Claude Opus 5 --- .github/workflows/dev-publish.yml | 3 + .github/workflows/python-publish.yml | 3 + .github/workflows/tests.yml | 25 ++++ MANIFEST.in | 7 +- Makefile | 9 +- pyproject.toml | 18 ++- scripts/verify_dist.py | 187 +++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 scripts/verify_dist.py diff --git a/.github/workflows/dev-publish.yml b/.github/workflows/dev-publish.yml index 1f6fce7f..76571fd3 100644 --- a/.github/workflows/dev-publish.yml +++ b/.github/workflows/dev-publish.yml @@ -37,6 +37,9 @@ jobs: - name: Build package run: python -m build + - name: Verify built distribution is importable + run: python scripts/verify_dist.py dist + - name: Upload build artifacts uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index d4f73407..10256b38 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -23,6 +23,9 @@ jobs: python -m pip install build python -m build + - name: Verify built distribution is importable + run: python scripts/verify_dist.py dist + - name: Upload wheel to GitHub Release uses: ncipollo/release-action@v1 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a48c48a..4c81d213 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,15 +8,19 @@ on: paths: - 'src/**' - 'tests/**' + - 'scripts/**' - 'setup.py' - 'pyproject.toml' + - 'MANIFEST.in' - '.github/workflows/tests.yml' pull_request: paths: - 'src/**' - 'tests/**' + - 'scripts/**' - 'setup.py' - 'pyproject.toml' + - 'MANIFEST.in' - '.github/workflows/tests.yml' jobs: @@ -43,3 +47,24 @@ jobs: - name: Run tests run: pytest tests/ --tb=short --no-cov + + packaging: + name: Packaging + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: "3.x" + + - name: Build distributions + run: | + python -m pip install build + python -m build + + # The test suite runs against src/ via an editable install, so it cannot + # see packaging mistakes. This installs the built wheel in a clean venv + # and imports every module in it. + - name: Verify built distribution is importable + run: python scripts/verify_dist.py dist diff --git a/MANIFEST.in b/MANIFEST.in index fc098183..a772c8ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,5 @@ -recursive-include pyhiveapi * -recursive-include data * \ No newline at end of file +recursive-include src *.py +recursive-include src/data *.json +include LICENSE README.md +global-exclude .DS_Store +global-exclude *.py[cod] diff --git a/Makefile b/Makefile index 031e2586..28328130 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: setup test lint sync +.PHONY: setup test lint sync verify-dist setup: pip install -e ".[dev]" @@ -15,3 +15,10 @@ lint: sync: python setup.py build_py + +# Build the wheel and prove it is importable in a clean venv. The test suite +# runs against src/, so only this catches packaging regressions. +verify-dist: + rm -rf dist + python -m build + python scripts/verify_dist.py dist diff --git a/pyproject.toml b/pyproject.toml index 87554ac6..e368b149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,23 @@ dev = [ ] [tool.setuptools] -packages = ["apyhiveapi", "apyhiveapi.api", "apyhiveapi.helper", "pyhive", "pyhive.api", "pyhive.helper"] +# Keep this list in sync with the package directories under src/. It is explicit +# rather than auto-discovered because package-dir maps two distribution names +# (apyhiveapi, pyhive) onto the same source tree, which find: cannot express. +# The sync flavour (pyhiveapi) is generated at build time by the unasync +# build_py cmdclass in setup.py and so is not listed here. +packages = [ + "apyhiveapi", + "apyhiveapi.api", + "apyhiveapi.devices", + "apyhiveapi.helper", + "apyhiveapi.session", + "pyhive", + "pyhive.api", + "pyhive.devices", + "pyhive.helper", + "pyhive.session", +] [tool.setuptools.package-dir] apyhiveapi = "src" diff --git a/scripts/verify_dist.py b/scripts/verify_dist.py new file mode 100644 index 00000000..66aca45a --- /dev/null +++ b/scripts/verify_dist.py @@ -0,0 +1,187 @@ +"""Verify a built distribution is importable and complete. + +The test suite runs against ``src/`` via an editable install, so it cannot +catch packaging mistakes — a missing entry in ``[tool.setuptools] packages`` +produces a wheel that installs cleanly and then fails at ``import``. This +script closes that gap by installing the built wheel into a throwaway +virtualenv and importing every module in every flavour. + +Usage: + python scripts/verify_dist.py [dist_dir] + +Exits non-zero with a description of what is missing or unimportable. +""" + +import json +import subprocess +import sys +import sysconfig +import tempfile +import venv +from pathlib import Path + +# Flavours shipped by this distribution: the async original, its published +# alias, and the unasync-generated sync build. +FLAVOURS = ("apyhiveapi", "pyhive", "pyhiveapi") + +# Subpackages that must be present in each flavour. Guards against the +# packages list in pyproject.toml drifting behind the source layout. +REQUIRED_SUBPACKAGES = ("api", "devices", "helper", "session") + +# Package data that must ship alongside the async flavours. +REQUIRED_DATA = {"apyhiveapi": ["data/data.json"], "pyhive": ["data/data.json"]} + +# Entry points the Home Assistant integration depends on. A build that +# imports but has lost these is still a broken release. +REQUIRED_ATTRS = { + "apyhiveapi": ["API", "Auth", "Hive", "HiveReauthRequired"], + "pyhive": ["API", "Auth", "Hive"], + "pyhiveapi": ["API", "Auth", "Hive"], +} + +# Runs inside the throwaway venv, so it must stay stdlib-only. +PROBE = """ +import importlib +import importlib.util +import json +import pathlib +import pkgutil +import sys +import warnings + +warnings.simplefilter("ignore", DeprecationWarning) + +flavours = json.loads(sys.argv[1]) +required_subpackages = json.loads(sys.argv[2]) +required_data = json.loads(sys.argv[3]) +required_attrs = json.loads(sys.argv[4]) + +failures = [] +imported = 0 + +for flavour in flavours: + try: + top = importlib.import_module(flavour) + except Exception as err: # noqa: BLE001 - report, do not raise + failures.append(f"{flavour}: cannot import: {err!r}") + continue + + root = top.__path__[0] + for sub in required_subpackages: + if not importlib.util.find_spec(f"{flavour}.{sub}"): + failures.append(f"{flavour}.{sub}: subpackage not shipped") + + for rel in required_data.get(flavour, []): + if not (pathlib.Path(root) / rel).is_file(): + failures.append(f"{flavour}/{rel}: package data not shipped") + + for attr in required_attrs.get(flavour, []): + if not hasattr(top, attr): + failures.append(f"{flavour}.{attr}: missing from public API") + + for mod in pkgutil.walk_packages(top.__path__, flavour + "."): + imported += 1 + try: + importlib.import_module(mod.name) + except Exception as err: # noqa: BLE001 - report, do not raise + failures.append(f"{mod.name}: {type(err).__name__}: {err}") + +print(json.dumps({"imported": imported, "failures": failures})) +""" + + +def find_wheel(dist_dir: Path) -> Path: + """Return the single wheel in ``dist_dir``. + + Args: + dist_dir: Directory holding built distributions. + + Returns: + Path to the wheel. + + Raises: + SystemExit: No wheel, or more than one, was found. + """ + wheels = sorted(dist_dir.glob("*.whl")) + if not wheels: + raise SystemExit( + f"error: no wheel found in {dist_dir}/ — run `python -m build`" + ) + if len(wheels) > 1: + names = ", ".join(w.name for w in wheels) + raise SystemExit(f"error: expected one wheel in {dist_dir}/, found: {names}") + return wheels[0] + + +def main(argv: list[str]) -> int: + """Install the built wheel in a clean venv and import everything in it. + + Args: + argv: Command line arguments; ``argv[0]`` may be the dist directory. + + Returns: + 0 when the distribution is complete and importable, 1 otherwise. + """ + dist_dir = Path(argv[0] if argv else "dist") + wheel = find_wheel(dist_dir) + print(f"verifying {wheel.name}") + + with tempfile.TemporaryDirectory() as tmp: + env_dir = Path(tmp) / "venv" + venv.create(env_dir, with_pip=True, clear=True) + bin_dir = "Scripts" if sysconfig.get_platform().startswith("win") else "bin" + python = env_dir / bin_dir / "python" + + install = subprocess.run( + [str(python), "-m", "pip", "install", "--quiet", str(wheel)], + capture_output=True, + text=True, + check=False, + ) + if install.returncode != 0: + print(install.stdout + install.stderr, file=sys.stderr) + print("error: wheel failed to install", file=sys.stderr) + return 1 + + # Run from the temp dir so a sibling src/ tree cannot shadow the + # installed package. + probe = subprocess.run( + [ + str(python), + "-c", + PROBE, + json.dumps(FLAVOURS), + json.dumps(REQUIRED_SUBPACKAGES), + json.dumps(REQUIRED_DATA), + json.dumps(REQUIRED_ATTRS), + ], + capture_output=True, + text=True, + cwd=tmp, + check=False, + ) + if probe.returncode != 0 or not probe.stdout.strip(): + print(probe.stdout + probe.stderr, file=sys.stderr) + print("error: verification probe crashed", file=sys.stderr) + return 1 + + result = json.loads(probe.stdout) + + failures = result["failures"] + print(f"imported {result['imported']} modules across {len(FLAVOURS)} flavours") + if failures: + print(f"\nerror: {len(failures)} problem(s) in the built distribution:") + for failure in failures: + print(f" - {failure}") + print( + "\nIf a subpackage is missing, add it to `[tool.setuptools] packages` " + "in pyproject.toml.", + ) + return 1 + + print("distribution is complete and importable") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) From 0902663bce78a2a249dd8b7ccbf7d641383e33ab Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:23:15 +0100 Subject: [PATCH 2/4] ci: align packaging job build deps with the tests job verify_dist.py installs the built wheel, which resolves pyquery -> lxml. The packaging job runs on "3.x" (latest stable), which is exactly where a prebuilt lxml wheel may not yet exist, so install the same headers the tests job does rather than depending on wheel availability. Co-Authored-By: Claude Opus 5 --- .github/workflows/tests.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4c81d213..cf90e81c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -58,10 +58,16 @@ jobs: with: python-version: "3.x" - - name: Build distributions + # verify_dist.py installs the wheel, which pulls pyquery -> lxml. Match the + # tests job so a Python version without prebuilt lxml wheels can still build. + - name: Install build dependencies run: | + sudo apt-get update && sudo apt-get install -y \ + libxml2-dev libxslt1-dev python3-dev build-essential python -m pip install build - python -m build + + - name: Build distributions + run: python -m build # The test suite runs against src/ via an editable install, so it cannot # see packaging mistakes. This installs the built wheel in a clean venv From aa6931d0f30aaa6197fbe69deeafeb1f8e5cacee Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:59:00 +0100 Subject: [PATCH 3/4] chore: update repository URLs from Pyhive to Pyhass organization Update all GitHub repository references from github.com/Pyhive/Pyhiveapi to github.com/Pyhass/Pyhive in pyproject.toml, README.md badges/links, and LICENSE link. Also skip Claude code review workflow for fork PRs since they lack secrets/OIDC tokens for authentication, and enhance the review prompt to use gh CLI commands and MCP inline comment tool for more targeted feedback. --- .github/workflows/claude-code-review.yml | 17 +++++++++++++++-- README.md | 8 ++++---- pyproject.toml | 6 +++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 65ad6b97..46d80674 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -6,7 +6,11 @@ on: jobs: claude-review: - if: github.event.pull_request.draft == false + # Fork PRs get no secrets or OIDC token on `pull_request`, so the action + # cannot authenticate there — skip them (use @claude via claude.yml instead). + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: contents: read @@ -26,10 +30,19 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + Review this pull request for the pyhive-integration Python library. Check for: - Correctness: logic errors, incorrect async/await usage, blocking calls in async context - Type annotations: missing or incorrect types on public methods - Security: no secrets in code, safe HTTP and Cognito API call patterns - Style: snake_case naming, no unused imports, ruff/pylint compliance - Tests: are new features or bug fixes covered by tests? - Post inline comments on specific lines where relevant. Be concise. + + Use `gh pr diff` and `gh pr view` to inspect the changes. + Post inline comments on specific lines with + mcp__github_inline_comment__create_inline_comment, then post a short + overall summary with `gh pr comment`. Be concise. + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Read,Glob,Grep" diff --git a/README.md b/README.md index 652df809..97c310dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyhive-integration -![CI](https://github.com/Pyhive/Pyhiveapi/actions/workflows/ci.yml/badge.svg) ![PyPI](https://img.shields.io/pypi/v/pyhive-integration) ![Python](https://img.shields.io/pypi/pyversions/pyhive-integration) ![License](https://img.shields.io/github/license/Pyhive/Pyhiveapi) +![CI](https://github.com/Pyhass/Pyhive/actions/workflows/ci.yml/badge.svg) ![PyPI](https://img.shields.io/pypi/v/pyhive-integration) ![Python](https://img.shields.io/pypi/pyversions/pyhive-integration) ![License](https://img.shields.io/github/license/Pyhass/Pyhive) A Python library for interfacing with the [Hive](https://www.hivehome.com/) smart home platform. Provides both async (`apyhiveapi`) and sync (`pyhiveapi`) APIs, and is designed primarily for use with [Home Assistant](https://www.home-assistant.io/) — though it works standalone too. @@ -175,11 +175,11 @@ python setup.py build_py ## Links - [PyPI](https://pypi.org/project/pyhive-integration/) -- [Source](https://github.com/Pyhive/Pyhiveapi) -- [Issue Tracker](https://github.com/Pyhive/Pyhiveapi/issues) +- [Source](https://github.com/Pyhass/Pyhive) +- [Issue Tracker](https://github.com/Pyhass/Pyhive/issues) --- ## License -MIT License — see [LICENSE](LICENSE) for details. +MIT License — see [LICENSE](https://github.com/Pyhass/Pyhive/blob/master/LICENSE) for details. diff --git a/pyproject.toml b/pyproject.toml index e368b149..8c0b2643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/Pyhive/pyhiveapi" -Source = "https://github.com/Pyhive/Pyhiveapi" -"Issue Tracker" = "https://github.com/Pyhive/Pyhiveapi/issues" +Homepage = "https://github.com/Pyhass/Pyhive" +Source = "https://github.com/Pyhass/Pyhive" +"Issue Tracker" = "https://github.com/Pyhass/Pyhive/issues" [project.optional-dependencies] dev = [ From b1cd3a8abaa12332318e7e2a3f3d11b817511ea6 Mon Sep 17 00:00:00 2001 From: Khole Jones <29937485+KJonline@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:01:44 +0100 Subject: [PATCH 4/4] chore: refresh detect-secrets baseline line numbers Co-Authored-By: Claude Opus 5.5 --- .secrets.baseline | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 28e0d7bd..83a918e4 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -268,28 +268,28 @@ "filename": "src/api/hive_auth_async.py", "hashed_secret": "5dc786e32e3a0a4611daaf397721c6ef64cd71b0", "is_verified": false, - "line_number": 49 + "line_number": 50 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "ac9f290e69cee683ba3c63461f1f3fa02765032a", "is_verified": false, - "line_number": 50 + "line_number": 51 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "351b174ccf89601f6f4bd3f3970a4aba7d17c98e", "is_verified": false, - "line_number": 53 + "line_number": 54 }, { "type": "Secret Keyword", "filename": "src/api/hive_auth_async.py", "hashed_secret": "576956b5291ac38d04ef5f82cc974286a857f0b2", "is_verified": false, - "line_number": 112 + "line_number": 113 } ], "src/api/srp_crypto.py": [ @@ -580,5 +580,5 @@ } ] }, - "generated_at": "2026-09-25T09:40:50Z" + "generated_at": "2026-09-26T17:01:30Z" }