From 8bd263487404985fd0e36de78feb227ac192af9d Mon Sep 17 00:00:00 2001 From: solidiquis Date: Fri, 4 Sep 2026 11:23:52 -0700 Subject: [PATCH 1/2] rust(feature): add bytes channel support Add Value::Bytes to sift_stream and expose it through sift_stream_bindings (ValuePy.Bytes, is_bytes, as_bytes, IngestWithConfigDataChannelValuePy.bytes). Python support lands separately once the bindings are released. Co-Authored-By: Claude Fable 5.1 --- rust/CHANGELOG.md | 19 +++++++++++ rust/crates/sift_stream/src/lib.rs | 2 +- rust/crates/sift_stream/src/stream/channel.rs | 34 +++++++++++++++++++ .../sift_stream_bindings.pyi | 6 ++++ .../src/stream/channel.rs | 25 ++++++++++++++ .../src/stream/request.rs | 7 ++++ 6 files changed, 92 insertions(+), 1 deletion(-) diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index c222041617..fc92a6bc23 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### What's New + +#### Bytes channel values + +`Value` has a new `Bytes(Vec)` variant that ingests as `CHANNEL_DATA_TYPE_BYTES`. A plain +`Vec` or `&[u8]` still converts to `Value::BitField`; wrap it in the new `ChannelBytes` +newtype to send raw bytes: + +```rust +use sift_stream::{ChannelBytes, ChannelValue}; + +let payload = ChannelValue::new("payload", ChannelBytes(vec![0xde, 0xad])); +``` + +`sift_stream_bindings` exposes this as `ValuePy.Bytes`, `ValuePy.is_bytes`, `ValuePy.as_bytes`, +and `IngestWithConfigDataChannelValuePy.bytes`. This fixes bytes channel ingestion in the Python +library, which previously raised `ValueError: Invalid data type: bytes`. + ## [v0.12.0] ### What's New diff --git a/rust/crates/sift_stream/src/lib.rs b/rust/crates/sift_stream/src/lib.rs index f8868e9bef..ce0de5b6cf 100644 --- a/rust/crates/sift_stream/src/lib.rs +++ b/rust/crates/sift_stream/src/lib.rs @@ -598,7 +598,7 @@ pub use stream::{ FileBackupBuilder, IngestionConfigForm, LiveOnlyBuilder, LiveWithBackupsBuilder, RunForm, SiftStreamBuilder, StreamConfigBuilder, }, - channel::{ChannelEnum, ChannelValue, Value}, + channel::{ChannelBytes, ChannelEnum, ChannelValue, Value}, flow::{ChannelIndex, FlowBuilder, FlowDescriptor, FlowDescriptorBuilder}, mode::{ file_backup::FileBackup, diff --git a/rust/crates/sift_stream/src/stream/channel.rs b/rust/crates/sift_stream/src/stream/channel.rs index a52f316ab4..cb6f43197f 100644 --- a/rust/crates/sift_stream/src/stream/channel.rs +++ b/rust/crates/sift_stream/src/stream/channel.rs @@ -38,6 +38,20 @@ pub struct ChannelValue { #[derive(Debug, PartialEq)] pub struct ChannelEnum(pub u32); +/// Wrapper to distinguish raw bytes from bitfield values, since both are +/// backed by `Vec`. A plain `Vec` or `&[u8]` converts to +/// [`Value::BitField`]; wrap it in `ChannelBytes` to produce [`Value::Bytes`]. +/// +/// # Example +/// +/// ``` +/// use sift_stream::{ChannelValue, ChannelBytes}; +/// +/// let bytes_value = ChannelValue::new("payload", ChannelBytes(vec![0xde, 0xad])); +/// ``` +#[derive(Debug, PartialEq)] +pub struct ChannelBytes(pub Vec); + /// Represents a typed value emitted by a channel. /// /// This enum covers all supported data types for telemetry channels. Values can @@ -65,6 +79,7 @@ pub enum Value { Uint64(u64), Enum(u32), BitField(Vec), + Bytes(Vec), } impl Value { @@ -81,6 +96,7 @@ impl Value { Value::Uint64(_) => ChannelDataType::Uint64, Value::Enum(_) => ChannelDataType::Enum, Value::BitField(_) => ChannelDataType::BitField, + Value::Bytes(_) => ChannelDataType::Bytes, } } @@ -97,6 +113,7 @@ impl Value { Value::Uint64(val) => Type::Uint64(*val), Value::Enum(val) => Type::Enum(*val), Value::BitField(val) => Type::BitField(val.clone()), + Value::Bytes(val) => Type::Bytes(val.clone()), } } } @@ -192,6 +209,12 @@ impl From for Value { } } +impl From for Value { + fn from(value: ChannelBytes) -> Self { + Value::Bytes(value.0) + } +} + impl From for Value { fn from(value: u32) -> Self { Value::Uint32(value) @@ -335,4 +358,15 @@ fn test_channel_value_conversion() { }, bitfield_val ); + + let bytes_val = ChannelValue::new("channel", ChannelBytes(vec![0xde, 0xad])); + assert_eq!( + ChannelValue { + name: String::from("channel"), + value: Value::Bytes(vec![0xde, 0xad]) + }, + bytes_val + ); + assert_eq!(bytes_val.value.pb_data_type(), ChannelDataType::Bytes); + assert_eq!(bytes_val.value.pb_value(), Type::Bytes(vec![0xde, 0xad])); } diff --git a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi index a9bf12e693..92243d9022 100644 --- a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi +++ b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi @@ -324,6 +324,8 @@ class IngestWithConfigDataChannelValuePy: @staticmethod def bitfield(value:typing.Sequence[builtins.int]) -> IngestWithConfigDataChannelValuePy: ... @staticmethod + def bytes(value:typing.Sequence[builtins.int]) -> IngestWithConfigDataChannelValuePy: ... + @staticmethod def empty() -> IngestWithConfigDataChannelValuePy: ... @typing.final @@ -798,6 +800,8 @@ class ValuePy: def Enum(value:typing.Optional[builtins.int]) -> ValuePy: ... @staticmethod def BitField(value:typing.Optional[typing.Sequence[builtins.int]]) -> ValuePy: ... + @staticmethod + def Bytes(value:typing.Optional[typing.Sequence[builtins.int]]) -> ValuePy: ... def is_empty(self) -> builtins.bool: ... def is_bool(self) -> builtins.bool: ... def is_string(self) -> builtins.bool: ... @@ -809,6 +813,7 @@ class ValuePy: def is_uint64(self) -> builtins.bool: ... def is_enum(self) -> builtins.bool: ... def is_bitfield(self) -> builtins.bool: ... + def is_bytes(self) -> builtins.bool: ... def as_bool(self) -> builtins.bool: ... def as_string(self) -> builtins.str: ... def as_float(self) -> builtins.float: ... @@ -819,6 +824,7 @@ class ValuePy: def as_uint64(self) -> builtins.int: ... def as_enum(self) -> builtins.int: ... def as_bitfield(self) -> builtins.list[builtins.int]: ... + def as_bytes(self) -> builtins.list[builtins.int]: ... @typing.final class ChannelDataTypePy(Enum): diff --git a/rust/crates/sift_stream_bindings/src/stream/channel.rs b/rust/crates/sift_stream_bindings/src/stream/channel.rs index fa606eb4eb..daae6fdcd9 100644 --- a/rust/crates/sift_stream_bindings/src/stream/channel.rs +++ b/rust/crates/sift_stream_bindings/src/stream/channel.rs @@ -195,6 +195,7 @@ impl From for ChannelValueTypePy { Value::Uint64(val) => Self::uint64(val), Value::Enum(val) => Self::enum_value(val), Value::BitField(val) => Self::bitfield(val), + Value::Bytes(val) => Self::bytes(val), } } } @@ -327,6 +328,17 @@ impl ValuePy { } } + #[staticmethod] + #[allow(non_snake_case)] + pub fn Bytes(value: Option>) -> Self { + match value { + Some(value) => Self { + inner: Value::Bytes(value), + }, + None => Self::Empty(), + } + } + pub fn is_empty(&self) -> bool { matches!(self.inner, Value::Empty) } @@ -371,6 +383,10 @@ impl ValuePy { matches!(self.inner, Value::BitField(_)) } + pub fn is_bytes(&self) -> bool { + matches!(self.inner, Value::Bytes(_)) + } + pub fn as_bool(&self) -> PyResult { match &self.inner { Value::Bool(v) => Ok(*v), @@ -460,6 +476,15 @@ impl ValuePy { )), } } + + pub fn as_bytes(&self) -> PyResult> { + match &self.inner { + Value::Bytes(v) => Ok(v.clone()), + _ => Err(PyErr::new::( + "Value is not Bytes", + )), + } + } } #[gen_stub_pymethods] diff --git a/rust/crates/sift_stream_bindings/src/stream/request.rs b/rust/crates/sift_stream_bindings/src/stream/request.rs index b3b1bb5e05..e459f7a495 100644 --- a/rust/crates/sift_stream_bindings/src/stream/request.rs +++ b/rust/crates/sift_stream_bindings/src/stream/request.rs @@ -172,6 +172,13 @@ impl IngestWithConfigDataChannelValuePy { } } + #[staticmethod] + pub fn bytes(value: Vec) -> Self { + Self { + ty: ChannelValueTypePy::bytes(value), + } + } + #[staticmethod] pub fn empty() -> Self { Self { From a56b1fdccf85db6758960ce18cdce9973a4734f2 Mon Sep 17 00:00:00 2001 From: solidiquis Date: Fri, 4 Sep 2026 11:42:16 -0700 Subject: [PATCH 2/2] rust(chore): rust release prep 0.13.0, sift-stream-bindings 0.5.1 Co-Authored-By: Claude Fable 5.1 --- Cargo.toml | 18 +++++++++--------- rust/CHANGELOG.md | 2 +- rust/crates/sift_stream_bindings/Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af2f5c9676..cd731f95d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ [workspace.package] authors = ["Sift Software Engineers "] -version = "0.12.0" +version = "0.13.0" edition = "2024" categories = ["aerospace", "science::robotics"] homepage = "https://github.com/sift-stack/sift" @@ -87,15 +87,15 @@ tracing-test = { version = "0.2", features = ["no-env-filter"] } uuid = { version = "1.22", features = ["v4"] } zip = "8.2" -sift_connect = { version = "0.12.0", path = "rust/crates/sift_connect" } -sift_rs = { version = "0.12.0", path = "rust/crates/sift_rs" } -sift_error = { version = "0.12.0", path = "rust/crates/sift_error" } -sift_stream = { version = "0.12.0", path = "rust/crates/sift_stream" } -sift_pbfs = { version = "0.12.0", path = "rust/crates/sift_pbfs" } -sift_mcp = { version = "0.12.0", path = "rust/crates/sift_mcp" } -sift_test_util = { version = "0.12.0", path = "rust/crates/sift_test_util" } +sift_connect = { version = "0.13.0", path = "rust/crates/sift_connect" } +sift_rs = { version = "0.13.0", path = "rust/crates/sift_rs" } +sift_error = { version = "0.13.0", path = "rust/crates/sift_error" } +sift_stream = { version = "0.13.0", path = "rust/crates/sift_stream" } +sift_pbfs = { version = "0.13.0", path = "rust/crates/sift_pbfs" } +sift_mcp = { version = "0.13.0", path = "rust/crates/sift_mcp" } +sift_test_util = { version = "0.13.0", path = "rust/crates/sift_test_util" } -sift_stream_bindings = { version = "0.5.0", path = "rust/crates/sift_stream_bindings" } +sift_stream_bindings = { version = "0.5.1", path = "rust/crates/sift_stream_bindings" } # The profile that 'dist' will build with [profile.dist] diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index fc92a6bc23..71c309dbeb 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] +## [v0.13.0] - September 4, 2026 ### What's New #### Bytes channel values diff --git a/rust/crates/sift_stream_bindings/Cargo.toml b/rust/crates/sift_stream_bindings/Cargo.toml index 172547995a..cf09e4655c 100644 --- a/rust/crates/sift_stream_bindings/Cargo.toml +++ b/rust/crates/sift_stream_bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sift-stream-bindings" -version = "0.5.0" +version = "0.5.1" # Released to PyPI as a wheel, never to crates.io. publish = false edition = { workspace = true }