Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,40 @@ Example: Declaring key expressions
:start-after: [keyexpr_declare]
:end-before: # [keyexpr_declare]

.. _path-parameters:

Path Parameters
~~~~~~~~~~~~~~~

:class:`zenoh.KeFormat` lets you define key expression patterns with named path parameters,
similar to REST API path templates (e.g. ``/users/{id}``). You can build key expressions by
setting parameter values with a :class:`zenoh.KeFormatter`, or parse key expressions to extract
parameter values.

The format syntax extends key expressions with specification chunks: ``${id:pattern}``,
``${id:pattern#default}``, and similar. For example, ``robot/${sensor_id:*}/reading`` defines
a format with a single parameter ``sensor_id`` that matches any single chunk (``*``).

Example: Building and parsing key expressions with path parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: examples/keyexpr_format.py
:language: python
:start-after: [keyexpr_format]
:end-before: # [keyexpr_format]

Example: Using KeFormat with pub/sub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In pub/sub, the publisher can build key expressions with :class:`zenoh.KeFormatter`, and the
subscriber can parse received :attr:`zenoh.Sample.key_expr` with :meth:`zenoh.KeFormat.parse`
to obtain the path parameter values:

.. literalinclude:: examples/keyexpr_format_pubsub.py
:language: python
:start-after: [keyexpr_format_pubsub]
:end-before: # [keyexpr_format_pubsub]

.. _publish-subscribe:

Publish/Subscribe
Expand Down
14 changes: 14 additions & 0 deletions docs/examples/keyexpr_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import zenoh

# [keyexpr_format]
fmt = zenoh.KeFormat("robot/${sensor_id:*}/reading")

formatter = fmt.formatter()
formatter.set("sensor_id", "temperature")
key = formatter.build()
assert str(key) == "robot/temperature/reading"

parsed = fmt.parse("robot/humidity/reading")
sensor_id = parsed.get("sensor_id")
assert sensor_id == "humidity"
# [keyexpr_format]
31 changes: 31 additions & 0 deletions docs/examples/keyexpr_format_pubsub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import threading
import time

import zenoh

# [keyexpr_format_pubsub]
fmt = zenoh.KeFormat("robot/${sensor_id:*}/reading")

session = zenoh.open(zenoh.Config())
received_sensor_ids = []


def publisher_task():
time.sleep(0.1)
for sensor_id in ("temperature", "humidity", "pressure"):
formatter = fmt.formatter()
formatter.set("sensor_id", sensor_id)
session.put(formatter.build(), f"reading from {sensor_id}")


subscriber = session.declare_subscriber("robot/*/reading")
threading.Thread(target=publisher_task, daemon=True).start()
for sample in subscriber:
parsed = fmt.parse(sample.key_expr)
received_sensor_ids.append(parsed.get("sensor_id"))
if len(received_sensor_ids) >= 3:
break

session.close()
assert set(received_sensor_ids) == {"temperature", "humidity", "pressure"}
# [keyexpr_format_pubsub]
133 changes: 133 additions & 0 deletions src/key_expr_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// Copyright (c) 2024 ZettaScale Technology
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
//
// Contributors:
// ZettaScale Zenoh Team, <zenoh@zettascale.tech>
//

use std::collections::HashMap;

use pyo3::prelude::*;

use crate::{
key_expr::KeyExpr,
utils::{IntoPyErr, IntoRust},
};

#[pyclass]
pub(crate) struct KeFormat(pub(crate) String);

#[pymethods]
impl KeFormat {
#[new]
pub(crate) fn new(format_spec: String) -> PyResult<Self> {
let format = zenoh::key_expr::format::KeFormat::new(&format_spec)
.map_err(IntoPyErr::into_pyerr)?;
// Validate format; we only need to know it parses.
drop(format);
Ok(Self(format_spec))
}

fn formatter(slf: PyRef<Self>) -> KeFormatter {
KeFormatter {
format_spec: slf.0.clone(),
values: HashMap::new(),
}
}

fn parse(&self, key_expr: &Bound<PyAny>) -> PyResult<Parsed> {
let key_expr = KeyExpr::from_py(key_expr)?;
let key_expr_rust = key_expr.into_rust();
let format = zenoh::key_expr::format::KeFormat::new(&self.0)
.map_err(IntoPyErr::into_pyerr)?;
let parsed = format
.parse(key_expr_rust.as_ref())
.map_err(IntoPyErr::into_pyerr)?;
let values: HashMap<String, String> = parsed
.iter()
.map(|(id, value)| {
(
id.to_string(),
value.map(|v| v.to_string()).unwrap_or_default(),
)
})
.collect();
Ok(Parsed { values })
}

fn __repr__(&self) -> String {
format!("KeFormat({:?})", self.0)
}

fn __str__(&self) -> &str {
&self.0
}
}

#[pyclass]
pub(crate) struct Parsed {
pub(crate) values: HashMap<String, String>,
}

#[pymethods]
impl Parsed {
fn get(&self, id: &str) -> PyResult<String> {
self.values
.get(id)
.cloned()
.ok_or_else(|| crate::ZError::new_err(format!("unknown parameter: {}", id)))
}

fn __repr__(&self) -> String {
format!("Parsed({:?})", self.values)
}
}

#[pyclass]
pub(crate) struct KeFormatter {
format_spec: String,
values: HashMap<String, String>,
}

#[pymethods]
impl KeFormatter {
#[pyo3(signature = (id, value))]
fn set(&mut self, id: &str, value: &str) -> PyResult<()> {
let format = zenoh::key_expr::format::KeFormat::new(&self.format_spec)
.map_err(IntoPyErr::into_pyerr)?;
let mut formatter = format.formatter();
formatter.set(id, value).map_err(IntoPyErr::into_pyerr)?;
self.values.insert(id.to_string(), value.to_string());
Ok(())
}

fn get(&self, id: &str) -> Option<String> {
self.values.get(id).cloned()
}

fn build(&self) -> PyResult<KeyExpr> {
let format = zenoh::key_expr::format::KeFormat::new(&self.format_spec)
.map_err(IntoPyErr::into_pyerr)?;
let mut formatter = format.formatter();
for (id, value) in &self.values {
formatter.set(id, value).map_err(IntoPyErr::into_pyerr)?;
}
let key_expr = formatter.build().map_err(IntoPyErr::into_pyerr)?;
Ok(KeyExpr(key_expr.into()))
}

fn clear(&mut self) -> () {
self.values.clear();
}

fn __repr__(&self) -> String {
format!("KeFormatter({:?})", self.format_spec)
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod config;
mod ext;
mod handlers;
mod key_expr;
mod key_expr_format;
mod liveliness;
mod macros;
mod matching;
Expand Down Expand Up @@ -62,6 +63,7 @@ pub(crate) mod zenoh {
config::{Config, WhatAmI, WhatAmIMatcher, ZenohId},
handlers::Handler,
key_expr::{KeyExpr, SetIntersectionLevel},
key_expr_format::{KeFormat, KeFormatter, Parsed},
liveliness::{Liveliness, LivelinessToken},
matching::{MatchingListener, MatchingStatus},
pubsub::{Publisher, Subscriber},
Expand Down
60 changes: 60 additions & 0 deletions zenoh/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,66 @@ class KeyExpr:

def __str__(self) -> str: ...


@final
class KeFormat:
"""Key expression format with path parameters (named chunks).

Similar to REST API path templates (e.g. ``/users/{id}``), KeFormat lets you define
key expression patterns with named parameters. Use :meth:`formatter` to build key
expressions by setting parameter values, or :meth:`parse` to extract parameter
values from a key expression.

Format syntax extends key expressions with specification chunks:
``${id:pattern}``, ``${id:pattern#default}``, ``$#{id:pattern}#``, etc.
For example: ``robot/${sensor_id:*}/reading``.
"""

def __new__(cls, format_spec: str) -> Self:
"""Creates a new KeFormat from a format specification string.
Raises :exc:`ZError` if the format_spec is invalid.
"""

def formatter(self) -> KeFormatter:
"""Constructs a new formatter for building key expressions from this format."""

def parse(self, key_expr: _IntoKeyExpr) -> Parsed:
"""Parses a key expression according to this format and returns the extracted parameter values.
Raises :exc:`ZError` if the key expression does not match the format."""

def __str__(self) -> str: ...


@final
class KeFormatter:
"""Builder for key expressions from a :class:`KeFormat`.
Set parameter values with :meth:`set`, then call :meth:`build` to produce a :class:`KeyExpr`."""

def set(self, id: str, value: str) -> None:
"""Sets the value for the given parameter id.
Raises :exc:`ZError` if the value does not match the parameter pattern."""

def get(self, id: str) -> str | None:
"""Returns the current value for the given parameter id, or None if not set."""

def build(self) -> KeyExpr:
"""Builds a KeyExpr from the format and the currently set parameter values.
Raises :exc:`ZError` if any required parameter is missing or a value is invalid."""

def clear(self) -> None:
"""Clears all set parameter values from this formatter."""


@final
class Parsed:
"""Result of parsing a key expression with :meth:`KeFormat.parse`.
Holds the extracted parameter values."""

def get(self, id: str) -> str:
"""Returns the value for the given parameter id.
Raises :exc:`ZError` if id is not a parameter in the format."""


_IntoKeyExpr = KeyExpr | str

@final
Expand Down
Loading