Compare commits

...
1 Commits
Author SHA1 Message Date
Evan 7ed3eaa617 json state proxy 2026-05-07 15:36:03 +01:00
7 changed files with 140 additions and 79 deletions

No files matched your search

Generated
+1
View File
@@ -795,6 +795,7 @@ dependencies = [
"pyo3-log",
"pyo3-stub-gen",
"rand 0.10.1",
"serde_json",
"tokio",
"zenoh",
"zerompk",
+1
View File
@@ -60,3 +60,4 @@ env_logger.workspace = true
zenoh.workspace = true
zerompk.workspace = true
rand = "0.10.1"
serde_json = "1.0.149"
+3 -64
View File
@@ -1,72 +1,11 @@
# This file is automatically generated by pyo3_stub_gen
# ruff: noqa: E501, F401
import builtins
import typing
@typing.final
class Keypair:
r"""
Identity keypair of a node.
"""
class StateProxy:
@staticmethod
def generate() -> Keypair:
r"""
Generate a new Ed25519 keypair.
"""
@staticmethod
def from_bytes(bytes: bytes) -> Keypair:
r"""
Construct an Ed25519 keypair from secret key bytes
"""
def to_bytes(self) -> bytes:
r"""
Get the secret key bytes underlying the keypair
"""
def to_node_id(self) -> builtins.str:
r"""
Convert the `Keypair` into the corresponding `PeerId` string, which we use as our `NodeId`.
"""
@typing.final
class NetworkingHandle:
def __new__(cls, identity: Keypair, bootstrap_peers: typing.Sequence[builtins.str], listen_port: builtins.int) -> NetworkingHandle: ...
async def gossipsub_subscribe(self, topic: builtins.str) -> builtins.bool:
r"""
Subscribe to a `GossipSub` topic.
Returns `True` if the subscription worked. Returns `False` if we were already subscribed.
"""
async def gossipsub_unsubscribe(self, topic: builtins.str) -> builtins.bool:
r"""
Unsubscribes from a `GossipSub` topic.
Returns `True` if we were subscribed to this topic. Returns `False` if we were not subscribed.
"""
async def gossipsub_publish(self, topic: builtins.str, data: bytes) -> None:
r"""
Publishes a message with multiple topics to the `GossipSub` network.
If no peers are found that subscribe to this topic, throws `NoPeersSubscribedToTopicError` exception.
"""
async def recv(self) -> PyFromSwarm: ...
class PyFromSwarm:
@typing.final
class Connection(PyFromSwarm):
__match_args__ = ("connected",)
@property
def connected(self) -> builtins.bool: ...
def __new__(cls, connected: builtins.bool) -> PyFromSwarm.Connection: ...
@typing.final
class Message(PyFromSwarm):
__match_args__ = ("topic", "data",)
@property
def topic(self) -> builtins.str: ...
@property
def data(self) -> bytes: ...
def __new__(cls, topic: builtins.str, data: bytes) -> PyFromSwarm.Message: ...
...
async def init() -> StateProxy: ...
async def snapshot(self) -> str: ...
+8 -13
View File
@@ -5,21 +5,15 @@
//!
mod allow_threading;
mod ident;
mod networking;
// mod ident;
// mod networking;
mod state;
use crate::ident::PyKeypair;
use crate::networking::networking_submodule;
use crate::state::snapshot_module;
use pyo3::prelude::PyModule;
use pyo3::types::PyModuleMethods;
use pyo3::{Bound, PyResult, pyclass, pymodule};
use pyo3::{Bound, PyResult, pymodule};
use pyo3_stub_gen::define_stub_info_gatherer;
/// Namespace for all the constants used by this crate.
pub(crate) mod r#const {
pub const MPSC_CHANNEL_SIZE: usize = 1024;
}
/// Namespace for crate-wide extension traits/methods
pub(crate) mod ext {
use crate::allow_threading::AllowThreads;
@@ -162,8 +156,9 @@ fn main_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
// TODO: for now this is all NOT a submodule, but figure out how to make the submodule system
// work with maturin, where the types generate correctly, in the right folder, without
// too many importing issues...
m.add_class::<PyKeypair>()?;
networking_submodule(m)?;
// m.add_class::<PyKeypair>()?;
// networking_submodule(m)?;
snapshot_module(m)?;
// top-level constructs
// TODO: ...
+110
View File
@@ -0,0 +1,110 @@
use networking::Session;
use pyo3::{
exceptions::{PyRuntimeError, PyValueError},
prelude::*,
};
use pyo3_stub_gen::derive::{gen_methods_from_python, gen_stub_pyclass, gen_stub_pymethods};
use serde_json::{Map, Value};
use zenoh::{Result, Session as ZSession, query::QueryTarget, sample::SampleFields};
pyo3_stub_gen::inventory::submit! {
gen_methods_from_python! {
r#"
class StateProxy:
@staticmethod
async def init() -> StateProxy: ...
async def snapshot(self) -> str: ...
"#
}
}
pub fn snapshot_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<StateProxy>()?;
Ok(())
}
#[gen_stub_pyclass]
#[pyclass]
pub struct StateProxy {
session: Session,
}
#[gen_stub_pymethods]
#[pymethods]
impl StateProxy {
#[staticmethod]
#[gen_stub(skip)]
pub fn init<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
Ok(Self {
session: networking::open(
networking::cfg(rand::random(), 0).expect("default cfg is valid"),
)
.await
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?,
})
})
}
#[gen_stub(skip)]
pub fn snapshot<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, {
let session = self.session.clone();
async move {
Self::_snapshot(session)
.await
.map_err(|e| PyValueError::new_err(e.to_string()))
.map(|v| v.to_string())
}
})
}
}
impl StateProxy {
async fn _snapshot(session: ZSession) -> Result<Value> {
let q = session.get("storage/mem1/**").await?;
let mut v = Value::Object(Map::default());
while let Ok(sample) = q.recv_async().await {
let mut cur_v = &mut v;
let Ok(sample) = sample.into_result() else {
continue;
};
// skip storage/mem1
let SampleFields {
payload, key_expr, ..
} = sample.into();
let mut iter = key_expr.split('/').skip(2).peekable();
loop {
let Some(p) = iter.next() else {
break;
};
if iter.peek().is_none() {
// terminal; write value into json
let existing = cur_v
.as_object_mut()
.expect("path terminated unexpectedly - value stored at some/path and some/path/two")
.insert(p.to_owned(), Value::String(payload.try_to_string()?.to_string()));
if let Some(value) = existing {
assert!(value.is_string())
// could log, but string overwrites are fine
}
} else {
// non-terminal; ensure key exists in v, then replace cur with that object
cur_v = cur_v
.as_object_mut()
.expect("path terminated unexpectedly - value stored at some/path and some/path/two")
.entry(p)
.or_insert(Value::Object(Map::default()));
assert!(
cur_v.is_object(),
"path terminated unexpectedly - value stored at some/path and some/path/two"
)
}
}
}
Ok(v)
}
}
+4 -2
View File
@@ -2,9 +2,11 @@ use std::{
env,
ops::{Deref, DerefMut},
panic,
sync::Arc,
};
use netwatcher::WatchHandle;
use parking_lot::Mutex;
use tokio::{sync::mpsc, task::JoinHandle};
use zenoh::{Result, Session as ZSession, config::WhatAmI, internal::runtime::Runtime};
use zenoh_plugin_storage_manager::StoragesPlugin;
@@ -51,7 +53,7 @@ pub async fn open(cfg: zenoh::Config) -> Result<Session> {
.await?;
let session = zenoh::session::init(runtime.clone().into()).await?;
runtime.start().await?;
let _watch_all_handle = watch_all(runtime).await?;
let _watch_all_handle = Arc::new(Mutex::new(watch_all(runtime).await?));
Ok(Session {
session,
_watch_all_handle,
@@ -91,7 +93,7 @@ async fn watch_all(runtime: Runtime) -> Result<WatchAllHandle> {
pub struct Session {
pub session: ZSession,
_watch_all_handle: WatchAllHandle,
_watch_all_handle: Arc<Mutex<WatchAllHandle>>,
}
impl Deref for Session {
type Target = ZSession;
+13
View File
@@ -0,0 +1,13 @@
from exo_pyo3_bindings import StateProxy
import anyio
async def main():
sp = await StateProxy.init()
while True:
data = await sp.snapshot()
if data != "{}":
print(data)
await anyio.sleep(1)
if __name__ == "__main__":
anyio.run(main)