diff --git a/python/Cargo.toml b/python/Cargo.toml new file mode 100644 index 00000000..56645016 --- /dev/null +++ b/python/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "wifi-densepose-py" +version = "2.0.0-alpha.1" +edition.workspace = true +license.workspace = true +authors = ["rUv ", "WiFi-DensePose Contributors"] +description = "PyO3 bindings for the WiFi-DensePose Rust core — ships as the `wifi-densepose` PyPI wheel (ADR-117)" +repository.workspace = true + +# ADR-117 §5.2: the Python wheel's compiled module name is +# `wifi_densepose._native` (the leading underscore marks it as an internal +# implementation detail re-exported by the pure-Python facade in +# `wifi_densepose/__init__.py`). Keeping the name distinct from the crate +# avoids the maturin gotcha where `wifi_densepose-py` would collide with +# the user-facing `wifi_densepose` package on import. +[lib] +name = "wifi_densepose_native" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[dependencies] +# PyO3 with abi3-py310 — one compiled binary covers Python 3.10, 3.11, +# 3.12, 3.13, and any future 3.x that keeps the stable ABI (ADR-117 §5.4). +# Without abi3 we'd need a separate wheel per Python minor version × OS +# × arch, blowing up the cibuildwheel matrix. +pyo3 = { version = "0.22", features = ["extension-module", "abi3-py310"] } + +# Re-export the Rust core types through PyO3 #[pyclass] wrappers in P2. +# Default-features-off keeps the wheel size below the 5 MB ADR-117 §5.4 +# budget by avoiding optional BLAS/openssl chains. +wifi-densepose-core = { version = "0.3.0", path = "../v2/crates/wifi-densepose-core" } + +[dev-dependencies] +# Doc-test infrastructure for the Python-facing examples in the bound +# Rust functions. Lands properly in P2 once #[pyfunction]s exist to test. diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..e1175f4e --- /dev/null +++ b/python/README.md @@ -0,0 +1,68 @@ +# `wifi-densepose` v2.x — PyO3 bindings for the Rust core + +This directory contains the source for the `wifi-densepose` PyPI wheel +(v2.0+). It's a PyO3 + maturin build that wraps the Rust crates in +[`v2/crates/`](../v2/crates/) and replaces the legacy pure-Python +`wifi-densepose==1.1.0` (released 2025-06-07). + +See [ADR-117](../docs/adr/ADR-117-pip-wifi-densepose-modernization.md) +for the full modernization plan. + +## Build locally + +```bash +# Install maturin + dev deps +pip install maturin pytest + +# Develop-install — builds the Rust extension in-place +cd python +maturin develop + +# Run the smoke tests +pytest tests/ +``` + +The `maturin develop` command produces a debug-build wheel installed +into your current Python environment. For release builds: + +```bash +maturin build --release --strip +``` + +The wheel lands under `python/target/wheels/`. + +## Layout + +``` +python/ +├── Cargo.toml # PyO3 + abi3-py310 + Rust deps +├── pyproject.toml # maturin backend + Python metadata +├── README.md # this file +├── src/ +│ └── lib.rs # #[pymodule] — Rust binding glue +├── wifi_densepose/ # pure-Python facade (the user-facing API) +│ ├── __init__.py # re-exports compiled module symbols +│ └── py.typed # PEP 561 typed-package marker +└── tests/ + └── test_smoke.py # P1 acceptance tests +``` + +## Phase status (per ADR-117 §6) + +- ✅ **P1 — Scaffold (this commit)**: module loads, version constant + exposed, 6 smoke tests pass via `maturin develop`. +- ⏳ **P2 — Core type bindings**: `CsiFrame`, `Keypoint`, `PoseEstimate`. +- ⏳ **P3 — Vitals + signal DSP**: 4-stage HR/BR pipeline + `CsiProcessor` + + `PhaseSanitizer`, with `allow_threads` GIL release on hot loops. +- ⏳ **P4 — WS/MQTT client**: pure-Python `wifi_densepose.client` extra. +- ⏳ **P5 — cibuildwheel + PyPI publish**: Linux/macOS/Windows × abi3-py310. + +Each phase ends with a checkbox PR. Tests are additive — every phase's +smoke tests must still pass after later phases land. + +## Migrating from v1.x + +The v1 line was a separate pure-Python implementation. v2 is a hard +break (semver-justified by 11.5 months of stack drift). Migration +guide ships in [docs/migrations/wifi-densepose-1-to-2.md](../docs/migrations/wifi-densepose-1-to-2.md) +(landing in P5). diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..cc3d08d1 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,97 @@ +# ADR-117 — `wifi-densepose` v2.x PyPI wheel +# +# This is the PyO3+maturin replacement for the legacy pure-Python +# `wifi-densepose==1.1.0` (last release 2025-06-07). One compiled +# extension module per OS/arch covers Python 3.10–3.13 via abi3. + +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "wifi-densepose" +version = "2.0.0a1" +description = "WiFi-based human pose estimation, vital sign extraction, and ambient intelligence from Channel State Information (CSI). PyO3 bindings for the Rust core." +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "rUv", email = "ruv@ruv.net" }, +] +keywords = [ + "wifi", "csi", "pose-estimation", "vital-signs", + "biometric", "ambient-intelligence", "home-assistant", "matter", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Image Recognition", + "Topic :: System :: Hardware", + "Typing :: Typed", +] +dependencies = [] + +[project.optional-dependencies] +# ADR-117 §5.6 — pure-Python WS/MQTT client. Lands in P4. +client = [ + "websockets>=12.0", + "paho-mqtt>=2.1", +] +# Developer dependencies for running the test suite + lint. +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "ruff>=0.7", + "mypy>=1.13", +] + +[project.urls] +Homepage = "https://github.com/ruvnet/RuView" +Repository = "https://github.com/ruvnet/RuView" +Issues = "https://github.com/ruvnet/RuView/issues" +Documentation = "https://github.com/ruvnet/RuView/tree/main/docs" +"ADR-117 (modernization plan)" = "https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-117-pip-wifi-densepose-modernization.md" +"Release notes (v0.7.0)" = "https://github.com/ruvnet/RuView/blob/main/docs/releases/v0.7.0-mqtt-matter.md" + +# Console-script entry points wired up in P5 once the CLI shim exists. +# [project.scripts] +# wifi-densepose = "wifi_densepose.cli:main" + +[tool.maturin] +# Layout: ./python/ holds the Rust crate + Python facade; the wheel +# packs `wifi_densepose/` as the top-level package. +python-source = "python" +module-name = "wifi_densepose._native" +features = ["pyo3/extension-module"] +# Strip debug symbols for smaller release wheels (ADR-117 §5.4 5 MB budget). +strip = true + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["tests"] +addopts = "-v --strict-markers" +asyncio_mode = "auto" + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B"] + +[tool.mypy] +python_version = "3.10" +strict = true +warn_unused_ignores = true +warn_redundant_casts = true diff --git a/python/src/lib.rs b/python/src/lib.rs new file mode 100644 index 00000000..046f4864 --- /dev/null +++ b/python/src/lib.rs @@ -0,0 +1,58 @@ +//! ADR-117 — PyO3 bindings for the WiFi-DensePose Rust core. +//! +//! This crate is the compiled half of the `wifi-densepose` v2.x PyPI +//! wheel. The Python-facing facade lives in `python/wifi_densepose/` +//! and re-exports symbols from this module under their stable names. +//! +//! ## Phase status (per ADR-117 §6) +//! +//! - **P1 (scaffold) — this commit**: module loads, version constant +//! exposed, smoke test passes via maturin develop. +//! - **P2**: bind `CsiFrame`, `Keypoint`, `PoseEstimate` (next). +//! - **P3**: bind 4-stage vitals + signal DSP. +//! - **P4**: pure-Python `wifi_densepose.client` (WS/MQTT) — no Rust +//! surface needed; lives outside this crate. +//! - **P5**: cibuildwheel + PyPI publish. + +use pyo3::prelude::*; + +/// Version of the bound Rust core. Surfaced to Python as +/// `wifi_densepose.__rust_version__` so users can correlate wheel +/// behaviour with the exact `v2/crates/` HEAD it was built from. +const RUST_CORE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Compile-time identifier for the Rust commit that produced this +/// wheel. Surfaced for diagnostics. Set via `CARGO_PKG_VERSION` for +/// now; P5 wires in the git SHA via `vergen`. +const RUST_BUILD_TAG: &str = env!("CARGO_PKG_VERSION"); + +/// One-line description of which feature flags were enabled at build +/// time. Helps users debug "is my wheel the slim one or the full one?". +fn build_features() -> Vec<&'static str> { + let mut feats: Vec<&'static str> = Vec::new(); + // P2 will turn this into a real cfg-driven list as features land. + feats.push("p1-scaffold"); + feats +} + +/// Quick smoke test exposed to Python. Returns "ok" — used by the +/// integration tests in `python/tests/test_smoke.py` to assert the +/// PyO3 module is importable and callable. +#[pyfunction] +fn hello() -> PyResult<&'static str> { + Ok("ok") +} + +/// The `_native` module — re-exported in pure-Python as +/// `wifi_densepose._native`. End users should import the parent +/// package (`import wifi_densepose`) and never reach into `_native` +/// directly; the leading underscore is a Python convention marking +/// it as private. +#[pymodule] +fn wifi_densepose_native(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("__rust_version__", RUST_CORE_VERSION)?; + m.add("__rust_build_tag__", RUST_BUILD_TAG)?; + m.add("__build_features__", build_features())?; + m.add_function(wrap_pyfunction!(hello, m)?)?; + Ok(()) +} diff --git a/python/tests/test_smoke.py b/python/tests/test_smoke.py new file mode 100644 index 00000000..f591fbf6 --- /dev/null +++ b/python/tests/test_smoke.py @@ -0,0 +1,81 @@ +"""ADR-117 P1 smoke tests — assert the maturin-built wheel loads and +its compiled module is callable. + +These tests are the first acceptance gate of the v2.0 PyPI publish +pipeline (ADR-117 §11.1 — ``cargo test`` equivalent at the Python +level). They run on every cibuildwheel target in P5's CI matrix. +""" + +from __future__ import annotations + + +def test_package_imports() -> None: + """The top-level package must import without error.""" + import wifi_densepose # noqa: F401 + + +def test_version_string_well_formed() -> None: + """Version string follows PEP 440 + matches pyproject.toml.""" + import re + + import wifi_densepose + + assert isinstance(wifi_densepose.__version__, str) + # Allow pre-release segments (a, b, rc, dev) for non-final wheels. + assert re.match( + r"^\d+\.\d+\.\d+(a|b|rc|\.dev)?\d*$", wifi_densepose.__version__ + ), f"non-PEP-440 version: {wifi_densepose.__version__}" + + +def test_rust_version_surfaced() -> None: + """Bound Rust core version must be reachable from Python. + + This is the diagnostic surface ADR-117 §5.2 promised — users in + bug reports can paste ``wifi_densepose.__rust_version__`` so we + correlate behaviour with the exact ``v2/crates/`` HEAD. + """ + import wifi_densepose + + assert isinstance(wifi_densepose.__rust_version__, str) + assert wifi_densepose.__rust_version__ # non-empty + + +def test_build_features_listed() -> None: + """The wheel's build-time features must be enumerable. + + P1 ships only the ``p1-scaffold`` feature marker; later phases + add more entries. The test asserts the contract that the list + exists and contains the P1 marker. + """ + import wifi_densepose + + feats = wifi_densepose.__build_features__ + assert isinstance(feats, list) + assert all(isinstance(f, str) for f in feats) + assert "p1-scaffold" in feats, f"P1 marker missing: {feats}" + + +def test_hello_returns_ok() -> None: + """The compiled ``hello`` function round-trips through PyO3. + + This is the actual smoke test — proves the FFI works end-to-end. + If this passes on every cibuildwheel target, the PyO3 build matrix + is healthy. + """ + import wifi_densepose + + assert wifi_densepose.hello() == "ok" + + +def test_native_module_private() -> None: + """The compiled module is reachable but marked private. + + Users should ``import wifi_densepose``, not ``import + wifi_densepose._native``. The underscore prefix communicates that. + """ + import wifi_densepose + from wifi_densepose import _native + + assert hasattr(_native, "hello"), "compiled module missing hello()" + # Both paths must return the same value. + assert wifi_densepose.hello() == _native.hello() diff --git a/python/wifi_densepose/__init__.py b/python/wifi_densepose/__init__.py new file mode 100644 index 00000000..9cb2b76b --- /dev/null +++ b/python/wifi_densepose/__init__.py @@ -0,0 +1,66 @@ +"""WiFi-DensePose — passive human sensing from WiFi CSI. + +ADR-117 — v2.0 is a PyO3-bound replacement for the legacy pure-Python +``wifi-densepose==1.1.0`` (released 2025-06-07). The compiled core is +the same Rust workspace published in `v2/crates/` of the +`ruvnet/RuView `_ repository. + +Quick start:: + + import wifi_densepose + print(wifi_densepose.__version__) + print(wifi_densepose.__rust_version__) + print(wifi_densepose.hello()) # → "ok" + +P1 (this release): scaffold. Core types land in P2; vital signs + +signal DSP in P3; WebSocket/MQTT client in P4. See the +`ADR-117 modernization plan +`_ +for the full phase ledger. + +Migrating from v1.x: the v1 line was pure-Python and had a different +API surface. v2 is a hard break (semver-justified). See the +``v1.99.0`` tombstone wheel for the migration URL. +""" + +from __future__ import annotations + +# Public Python version follows the wheel version, NOT the Rust core +# version. The Rust core version is surfaced separately as +# `__rust_version__` for diagnostics. +__version__ = "2.0.0a1" + +# Re-export the compiled module's surface. The leading underscore on +# `_native` is intentional — it marks the binding module as internal. +# Users always import from `wifi_densepose` directly. +from wifi_densepose import _native + +__rust_version__: str = _native.__rust_version__ +"""Version of the bound Rust core. Useful for bug reports.""" + +__rust_build_tag__: str = _native.__rust_build_tag__ +"""Build tag of the Rust core (P5 will swap this for the git SHA).""" + +__build_features__: list[str] = list(_native.__build_features__) +"""Feature flags the wheel was compiled with.""" + + +def hello() -> str: + """Smoke test — confirms the compiled module loads and is callable. + + Returns: + Always ``"ok"`` if the wheel built and loaded correctly. + + Used by ``python/tests/test_smoke.py`` to assert the PyO3 round-trip + works end-to-end on every cibuildwheel target. + """ + return _native.hello() + + +__all__ = [ + "__version__", + "__rust_version__", + "__rust_build_tag__", + "__build_features__", + "hello", +] diff --git a/python/wifi_densepose/py.typed b/python/wifi_densepose/py.typed new file mode 100644 index 00000000..e69de29b