feat(packaging): nest protocol/ + ocf/ under smartthings_local, add PyPI packaging

Nest the two library packages under a single import namespace so they
can ship as one distribution:

  protocol/ -> smartthings_local/protocol/
  ocf/      -> smartthings_local/ocf/   (git mv, history preserved)

- Rewrite all imports protocol.* -> smartthings_local.protocol.*,
  ocf.* -> smartthings_local.ocf.* across the ocf modules, mqtt_demo/
  (bridge, descriptor, samples), and tests.
- Add pyproject.toml: dist name `smartthings-local`, hatch-vcs versioning
  from v* tags, wheel ships only smartthings_local/.
- Add .github/workflows/publish.yml: build + PyPI Trusted Publishing on
  v* tags (OIDC, no stored token).
- Force-include protocol/ocf_root_ca.pem via [tool.hatch.build] artifacts:
  it is tracked but matches .gitignore's *.pem, so hatchling's VCS file
  selection would drop it — and dtls_session.py loads it at runtime.
- Update mqtt_demo Dockerfile COPY and deploy.sh tar allowlist to the
  single smartthings_local/ package.
- .gitignore: build artifacts (_version.py, dist/, *.egg-info/).

Validated: pytest tests/ (11 passed), python -m build produces sdist +
wheel with the pem bundled, fresh pip install resolves all nested imports
with the pem readable from site-packages.
This commit is contained in:
Jack Nagy
2026-07-06 20:29:19 +01:00
parent 9004bac729
commit 3fdc735141
23 changed files with 155 additions and 37 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
from protocol.coap import (
from smartthings_local.protocol.coap import (
build_coap, parse_coap, encode_options, block_value, fmt_code,
TYPE_CON, METHOD_GET, URI_PATH, ACCEPT, CF_CBOR, BLOCK2,
)
+14 -15
View File
@@ -1,8 +1,8 @@
"""protocol/ and ocf/ must be vendorable on their own — no dependency
on mqtt_demo/. This copies just those two directories into an empty
temp dir and imports every module in them there, so a stray
`from mqtt_demo... import ...` fails loudly instead of silently
passing because mqtt_demo/ happens to also be on sys.path in-repo."""
"""smartthings_local (protocol/ + ocf/) must be vendorable on its own — no
dependency on mqtt_demo/. This copies just the smartthings_local package into
an empty temp dir and imports every module in it there, so a stray
`from mqtt_demo... import ...` fails loudly instead of silently passing
because mqtt_demo/ happens to also be on sys.path in-repo."""
import os
import shutil
import subprocess
@@ -12,17 +12,16 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
def test_protocol_and_ocf_import_without_mqtt_demo_present(tmp_path):
for pkg in ('protocol', 'ocf'):
shutil.copytree(REPO_ROOT / pkg, tmp_path / pkg)
def test_smartthings_local_imports_without_mqtt_demo_present(tmp_path):
shutil.copytree(REPO_ROOT / "smartthings_local", tmp_path / "smartthings_local")
import_lines = [
"import protocol.coap",
"import protocol.dtls_session",
"import ocf.state_cache",
"import ocf.poll_scheduler",
"import ocf.keepalive",
"import ocf.observe_refresh",
"import smartthings_local.protocol.coap",
"import smartthings_local.protocol.dtls_session",
"import smartthings_local.ocf.state_cache",
"import smartthings_local.ocf.poll_scheduler",
"import smartthings_local.ocf.keepalive",
"import smartthings_local.ocf.observe_refresh",
]
script = "\n".join(import_lines) + "\nprint('OK')\n"
@@ -36,7 +35,7 @@ def test_protocol_and_ocf_import_without_mqtt_demo_present(tmp_path):
capture_output=True, text=True,
)
assert result.returncode == 0, (
f"protocol/ocf failed to import without mqtt_demo/ present:\n"
f"smartthings_local failed to import without mqtt_demo/ present:\n"
f"stdout: {result.stdout}\nstderr: {result.stderr}"
)
assert "OK" in result.stdout
+1 -1
View File
@@ -1,5 +1,5 @@
# tests/test_state_cache.py
from ocf.state_cache import StateCache
from smartthings_local.ocf.state_cache import StateCache
class _FakeDescriptor: