feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding
scripts/c6-presence-watcher.py and friends carry a Python port of
`wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical
SOTA replacement — a PyO3 binding over the published Rust crate so
the runtime can pivot to the same enum semantics every other consumer
of `wifi-densepose-bfld 0.3.0` already uses.
New file: `python/src/bindings/privacy_gate.rs` (~155 LOC)
- `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}`
- `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters
- `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors
- free fns `allows_hap`, `allows_network`, `allows_matter`
- registered in `python/src/lib.rs` via `bindings::privacy_gate::register`
Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }`
as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.
ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility
mirrors Matter eligibility (Anonymous and Restricted only); a single
`PrivacyClass::from(*self).allows_matter()` call is the gate truth-source.
Verification: `cargo check -p wifi-densepose-py` on the workspace
compiles cleanly with the new binding linking against the published
crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking
wifi-densepose-py v2.0.0-alpha.1 ✓).
Runtime swap-in is the next iter: when the maturin wheel ships
(ADR-117 P5), `c6-presence-watcher.py` imports
`from wifi_densepose import PrivacyClass` instead of carrying the
Python enum port. Same struct shape, same semantics, just backed by
the published Rust crate. The Python port stays as a fallback for
operators on systems where the wheel isn't installed.
Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
c19742d71a
commit
de0712d435
|
|
@ -39,6 +39,13 @@ wifi-densepose-core = { version = "0.3.0", path = "../v2/crates/wifi-densepose-c
|
|||
# no tokio (Q5 audited 2026-05-24); safe to wrap in py.allow_threads.
|
||||
wifi-densepose-vitals = { version = "0.3.0", path = "../v2/crates/wifi-densepose-vitals" }
|
||||
|
||||
# ADR-118 BFLD core — PrivacyClass enum + identity_risk scoring +
|
||||
# privacy gate. Exposed to Python via bindings/privacy_gate.rs so the
|
||||
# c6-presence-watcher.py runtime (currently using a Python port of the
|
||||
# same semantics) can switch to the canonical Rust implementation when
|
||||
# the wheel ships. ADR-125 §2.1.d invariant enforcement lives here.
|
||||
wifi-densepose-bfld = { version = "0.3.0", path = "../v2/crates/wifi-densepose-bfld" }
|
||||
|
||||
# numpy bridge — needed for P3.5 BfldFrame (Complex64 ndarray) and for
|
||||
# the future P3 CsiFrame numpy round-trip.
|
||||
numpy = "0.22"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
//! ADR-118 / ADR-125 §2.1.d — Python binding for the BFLD `PrivacyClass`
|
||||
//! enum and the HAP-eligibility gate.
|
||||
//!
|
||||
//! Python:
|
||||
//! ```python
|
||||
//! from wifi_densepose import PrivacyClass, allows_hap, allows_matter, allows_network
|
||||
//!
|
||||
//! PrivacyClass.Anonymous # → 2
|
||||
//! allows_hap(PrivacyClass.Raw) # → False (I1 invariant)
|
||||
//! allows_hap(PrivacyClass.Anonymous)# → True
|
||||
//! allows_matter(PrivacyClass.Restricted) # → True (ADR-122 §2.4)
|
||||
//! ```
|
||||
//!
|
||||
//! This is the SOTA replacement for the Python port that ships in
|
||||
//! `scripts/c6-presence-watcher.py::PrivacyClass`. When the
|
||||
//! `wifi-densepose` PyPI wheel lands (ADR-117 P5), runtimes flip from
|
||||
//! the Python port to this Rust-backed binding and get the same enum
|
||||
//! semantics as every other consumer of the published
|
||||
//! `wifi-densepose-bfld 0.3.0` crate.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use wifi_densepose_bfld::PrivacyClass;
|
||||
|
||||
/// Python-facing wrapper for [`wifi_densepose_bfld::PrivacyClass`].
|
||||
///
|
||||
/// Repr matches the Rust enum byte values 0..=3.
|
||||
#[pyclass(eq, eq_int, hash, frozen, name = "PrivacyClass", module = "wifi_densepose")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum PyPrivacyClass {
|
||||
Raw = 0,
|
||||
Derived = 1,
|
||||
Anonymous = 2,
|
||||
Restricted = 3,
|
||||
}
|
||||
|
||||
impl From<PrivacyClass> for PyPrivacyClass {
|
||||
fn from(c: PrivacyClass) -> Self {
|
||||
match c {
|
||||
PrivacyClass::Raw => Self::Raw,
|
||||
PrivacyClass::Derived => Self::Derived,
|
||||
PrivacyClass::Anonymous => Self::Anonymous,
|
||||
PrivacyClass::Restricted => Self::Restricted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PyPrivacyClass> for PrivacyClass {
|
||||
fn from(c: PyPrivacyClass) -> Self {
|
||||
match c {
|
||||
PyPrivacyClass::Raw => Self::Raw,
|
||||
PyPrivacyClass::Derived => Self::Derived,
|
||||
PyPrivacyClass::Anonymous => Self::Anonymous,
|
||||
PyPrivacyClass::Restricted => Self::Restricted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPrivacyClass {
|
||||
/// True if frames of this class may cross a `NetworkSink`.
|
||||
/// Class 0 (`Raw`) is local-only by structural invariant I1
|
||||
/// (ADR-118 §2.2).
|
||||
#[getter]
|
||||
fn allows_network(&self) -> bool {
|
||||
PrivacyClass::from(*self).allows_network()
|
||||
}
|
||||
|
||||
/// True if frames of this class may cross the Matter boundary.
|
||||
/// Only classes 2 (`Anonymous`) and 3 (`Restricted`) qualify per
|
||||
/// ADR-122 §2.4 / ADR-125 §2.1.d.
|
||||
#[getter]
|
||||
fn allows_matter(&self) -> bool {
|
||||
PrivacyClass::from(*self).allows_matter()
|
||||
}
|
||||
|
||||
/// True if frames of this class may cross the HomeKit Accessory
|
||||
/// Protocol boundary. Same set as `allows_matter` — class 2 or 3.
|
||||
#[getter]
|
||||
fn allows_hap(&self) -> bool {
|
||||
// HAP eligibility is the same shape as Matter eligibility per
|
||||
// ADR-125 §2.1.d; we don't add a separate Rust method until
|
||||
// there's a divergence to justify it.
|
||||
PrivacyClass::from(*self).allows_matter()
|
||||
}
|
||||
|
||||
/// Byte value (0..=3) for serialization.
|
||||
#[getter]
|
||||
fn as_u8(&self) -> u8 {
|
||||
PrivacyClass::from(*self).as_u8()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
match self {
|
||||
Self::Raw => "PrivacyClass.Raw",
|
||||
Self::Derived => "PrivacyClass.Derived",
|
||||
Self::Anonymous => "PrivacyClass.Anonymous",
|
||||
Self::Restricted => "PrivacyClass.Restricted",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Map a byte value 0..=3 to the corresponding `PrivacyClass`.
|
||||
/// Raises `ValueError` on out-of-range input.
|
||||
#[staticmethod]
|
||||
fn from_u8(v: u8) -> PyResult<Self> {
|
||||
PrivacyClass::try_from(v)
|
||||
.map(Self::from)
|
||||
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Map a string ("raw" / "derived" / "anonymous" / "restricted",
|
||||
/// case-insensitive) to the corresponding `PrivacyClass`. Raises
|
||||
/// `ValueError` on unknown names.
|
||||
#[staticmethod]
|
||||
fn from_str(s: &str) -> PyResult<Self> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"raw" => Ok(Self::Raw),
|
||||
"derived" => Ok(Self::Derived),
|
||||
"anonymous" => Ok(Self::Anonymous),
|
||||
"restricted" => Ok(Self::Restricted),
|
||||
_ => Err(pyo3::exceptions::PyValueError::new_err(format!(
|
||||
"invalid PrivacyClass name: {s:?} (expected raw/derived/anonymous/restricted)"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Free-function helper: `True` iff `c` may cross the HAP boundary.
|
||||
/// Convenience wrapper so Python callers can write
|
||||
/// `allows_hap(PrivacyClass.Anonymous)` without method-call syntax.
|
||||
#[pyfunction]
|
||||
fn allows_hap(c: PyPrivacyClass) -> bool {
|
||||
c.allows_hap()
|
||||
}
|
||||
|
||||
/// Free-function helper: `True` iff `c` may cross a `NetworkSink`.
|
||||
#[pyfunction]
|
||||
fn allows_network(c: PyPrivacyClass) -> bool {
|
||||
c.allows_network()
|
||||
}
|
||||
|
||||
/// Free-function helper: `True` iff `c` may cross the Matter boundary.
|
||||
#[pyfunction]
|
||||
fn allows_matter(c: PyPrivacyClass) -> bool {
|
||||
c.allows_matter()
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyPrivacyClass>()?;
|
||||
m.add_function(wrap_pyfunction!(allows_hap, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(allows_network, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(allows_matter, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ mod bindings {
|
|||
pub mod bfld;
|
||||
pub mod keypoint;
|
||||
pub mod pose;
|
||||
pub mod privacy_gate;
|
||||
pub mod vitals;
|
||||
}
|
||||
|
||||
|
|
@ -80,5 +81,9 @@ fn wifi_densepose_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||
// P3.5 — BFLD bindings (stub Rust; future wifi-densepose-bfld crate
|
||||
// will replace the stub without changing the Python API).
|
||||
bindings::bfld::register(m)?;
|
||||
// ADR-118 PrivacyClass + HAP/Matter eligibility gates (SOTA — backed by
|
||||
// the published `wifi-densepose-bfld 0.3.0` crate, not the Python port).
|
||||
// Closes ADR-125 §2.1.d at the binding boundary.
|
||||
bindings::privacy_gate::register(m)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue