Python bindings for the SDS (Synchronized Data Structures) library
Project description
SDS Library - Python Bindings
Python bindings for the SDS (Synchronized Data Structures) library, providing a Pythonic interface for IoT state synchronization over MQTT.
Installation
pip install sds-library
For development installation:
cd python
pip install -e ".[dev]"
Quick Start
Generate Types from Schema
The code generator creates Python types from your .sds schema file:
# Generate Python types (and optionally C types)
python tools/sds_codegen.py schema.sds --python -o python/
This creates sds_types.py with dataclasses matching your C structs.
Device Node (Using Generated Types)
from sds import SdsNode, Role
from sds_types import SensorData # Generated from schema.sds
with SdsNode("py_sensor_01", "localhost") as node:
# Register with schema bundle - no manual dataclass needed!
table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
@node.on_config("SensorData")
def handle_config(table_type):
print(f"Config: threshold={table.config.threshold}")
while True:
# C-like attribute access
table.state.temperature = 23.5
table.state.humidity = 65.0
table.status.error_code = 0
node.poll(timeout_ms=1000)
Device Node (Manual Schema Definition)
If you prefer not to use the code generator:
from dataclasses import dataclass
from sds import SdsNode, Role, Field
@dataclass
class SensorState:
temperature: float = Field(float32=True)
humidity: float = Field(float32=True)
with SdsNode("py_sensor_01", "localhost") as node:
table = node.register_table(
"SensorData", Role.DEVICE,
state_schema=SensorState,
)
table.state.temperature = 23.5
Owner Node (C-like Syntax)
from sds import SdsNode, Role
with SdsNode("py_owner_01", "localhost") as node:
table = node.register_table(
"SensorData",
Role.OWNER,
status_schema=SensorStatus, # For reading device status
)
# Set config
table.config.threshold = 25.0
@node.on_status("SensorData")
def handle_status(table_type, from_node):
device = table.get_device(from_node)
if device and device.online:
print(f"{from_node}: battery={device.status.battery_percent}%")
while True:
node.poll(timeout_ms=1000)
# Iterate all devices
for node_id, device in table.iter_devices():
print(f"{node_id}: online={device.online}")
Features
- C-like Syntax: Access table data directly (
table.state.temperature = 23.5) - Zero Protocol Drift: Python uses the exact same C implementation
- Pythonic API: Context managers, decorators, and keyword arguments
- Thread-Safe: All operations protected by locks for multi-threaded use
- Cross-Platform: Linux (x86_64, ARM64) and macOS
- Type Hints: Full type annotations for IDE support
- Device Eviction: Automatic cleanup of offline device slots
Device Eviction
When devices disconnect unexpectedly, SDS receives MQTT LWT (Last Will and Testament) messages and can automatically evict them from status slots after a grace period. This prevents slots from being permanently consumed by devices that never reconnect.
# Enable eviction with 60-second grace period
with SdsNode("owner", "localhost", eviction_grace_ms=60000) as node:
table = node.register_table("SensorData", Role.OWNER, schema=SensorData)
@node.on_device_evicted()
def handle_eviction(table_type: str, node_id: str):
print(f"Device {node_id} was evicted from {table_type}")
while True:
node.poll(timeout_ms=1000)
# Check if a device has eviction pending
device = table.get_device("sensor_01")
if device and device.eviction_pending:
print("Device will be evicted soon if it doesn't reconnect")
Thread Safety
SdsNode is fully thread-safe. Multiple threads can safely:
- Call
poll()concurrently - Access table data (
table.state.temperature) - Register tables and callbacks
import threading
from sds import SdsNode, Role
with SdsNode("my_node", "localhost") as node:
table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
def polling_thread():
while True:
node.poll(timeout_ms=100)
def update_thread():
while True:
table.state.temperature = read_sensor()
time.sleep(1)
# Both threads can safely access the node and table
t1 = threading.Thread(target=polling_thread)
t2 = threading.Thread(target=update_thread)
t1.start()
t2.start()
Note: Callbacks are executed while holding the lock. Avoid blocking operations in callbacks to prevent deadlocks.
Logging Configuration
SDS uses Python's standard logging module. By default, log messages go
to a NullHandler (silent). To enable logging:
from sds import configure_logging
import logging
# Quick setup - logs to stderr at INFO level
configure_logging(level=logging.INFO)
# Or configure manually
logging.getLogger("sds").setLevel(logging.DEBUG)
logging.getLogger("sds").addHandler(logging.StreamHandler())
Connection Resilience
SdsNode includes built-in connection retry with exponential backoff:
from sds import SdsNode
node = SdsNode(
"my_node",
"mqtt.example.com",
retry_count=5, # Try 5 times (default: 3)
retry_delay_ms=2000, # Start with 2 second delay (default: 1000)
connect_timeout_ms=10000 # 10 second timeout (default: 5000)
)
On connection failure, retries are attempted with exponential backoff (delay doubles each retry). Non-connection errors are not retried.
Error Handling Best Practices
from sds import (
SdsNode, SdsMqttError, SdsTableError,
SdsValidationError, SdsError
)
try:
with SdsNode("my_node", "localhost") as node:
table = node.register_table("SensorData", Role.DEVICE)
while True:
try:
node.poll()
except SdsMqttError:
# MQTT connection lost - will auto-reconnect
logging.warning("MQTT disconnected, waiting...")
time.sleep(1)
except SdsValidationError as e:
# Invalid node_id or configuration
logging.error(f"Configuration error: {e}")
except SdsMqttError as e:
# Failed to connect after all retries
logging.error(f"Could not connect to MQTT: {e}")
except SdsError as e:
# Other SDS errors
logging.error(f"SDS error: {e}")
Requirements
- Python 3.8+
- MQTT broker (e.g., Mosquitto)
- libpaho-mqtt development headers (for building from source)
Configuration Options
Delta Sync (v0.5.0+)
Enable delta sync to only send changed fields, reducing bandwidth:
with SdsNode(
"sensor_01",
"localhost",
enable_delta_sync=True, # Only send changed fields
delta_float_tolerance=0.01 # Ignore tiny float changes
) as node:
# ...
Benefits:
- Reduced bandwidth (only changed fields transmitted)
- Lower power consumption on battery devices
- Works automatically with codegen-generated tables
Limitations:
- Config messages are always full (retained on broker)
- Status heartbeats are always full (liveness detection)
- Manual schema definitions use full sync
Eviction Grace Period
Configure how long to wait before evicting offline devices:
with SdsNode(
"owner",
"localhost",
eviction_grace_ms=30000 # 30 seconds before eviction
) as node:
@node.on_device_evicted()
def handle_eviction(table_type, node_id):
print(f"Device {node_id} evicted from {table_type}")
Raw MQTT Publish/Subscribe (v0.5.0+)
Send and receive custom MQTT messages through the SDS-managed connection. Useful for logging, diagnostics, or application-specific messages that don't fit the table model.
Publishing:
with SdsNode("sensor_01", "localhost") as node:
# Check connection status
if node.is_connected():
# Publish a log message
node.publish_raw(
f"log/{node.node_id}",
'{"level": "info", "msg": "Sensor started"}',
qos=0,
retained=False
)
# Publish binary data
node.publish_raw("sensor/raw_data", b'\x00\x01\x02\x03')
Subscribing:
def on_log(topic: str, payload: bytes):
print(f"Log from {topic}: {payload.decode()}")
with SdsNode("controller", "localhost") as node:
# Subscribe to all logs (+ matches any single level)
node.subscribe_raw("log/+", on_log)
# Or use # for multi-level wildcard
node.subscribe_raw("sensors/#", on_sensor_data)
# Main loop
while True:
node.poll()
time.sleep(0.1)
# Unsubscribe when done
node.unsubscribe_raw("log/+")
Methods:
| Method | Description |
|---|---|
is_connected() |
Returns True if connected to MQTT broker |
publish_raw(topic, payload, qos=0, retained=False) |
Publish arbitrary MQTT message |
subscribe_raw(topic, callback, qos=0) |
Subscribe to topic pattern with callback |
unsubscribe_raw(topic) |
Unsubscribe from a topic pattern |
Notes:
- Topics starting with
sds/are reserved and will raiseValueError payloadfor publish can bestr(UTF-8 encoded) orbytes- Callback receives
(topic: str, payload: bytes) - Maximum 8 concurrent raw subscriptions
- Wildcard subscriptions (
log/+) count as 1 subscription regardless of matching topics
API Reference
SdsNode
The main class for interacting with SDS.
class SdsNode:
def __init__(self, node_id: str, broker_host: str, port: int = 1883,
username: str | None = None, password: str | None = None,
eviction_grace_ms: int = 0):
"""
Initialize SDS node and connect to MQTT broker.
Args:
eviction_grace_ms: Grace period before evicting offline devices (0 = disabled).
For OWNER roles, when a device disconnects (LWT), an eviction
timer starts. If the device doesn't reconnect within this period,
it's evicted from status slots, freeing the slot for new devices.
"""
def register_table(self, table_type: str, role: Role,
sync_interval_ms: int | None = None,
config_schema: Type | None = None,
state_schema: Type | None = None,
status_schema: Type | None = None) -> SdsTable:
"""Register a table and return SdsTable for C-like access."""
def get_table(self, table_type: str) -> SdsTable:
"""Get a previously registered table."""
def unregister_table(self, table_type: str) -> None:
"""Unregister a table."""
def poll(self, timeout_ms: int = 0) -> None:
"""Process MQTT messages and sync changes."""
def is_ready(self) -> bool:
"""Check if connected to MQTT broker."""
def on_error(self, callback) -> None:
"""Register error callback."""
def on_version_mismatch(self, callback) -> None:
"""Register version mismatch callback."""
SdsTable
Provides C-like attribute access to table sections.
class SdsTable:
table_type: str # The table type name
role: Role # OWNER or DEVICE
# Device role properties
state: SectionProxy # Read/write state section
status: SectionProxy # Read/write status section
config: SectionProxy # Read-only config (from owner)
# Owner role properties
config: SectionProxy # Read/write config section
device_count: int # Number of known devices
def get_device(self, node_id: str) -> DeviceView | None:
"""Get a device's data (owner role only)."""
def iter_devices(self) -> Iterator[tuple[str, DeviceView]]:
"""Iterate over all known devices (owner role only)."""
Role Enum
class Role(Enum):
OWNER = 0 # Publishes config, receives state/status
DEVICE = 1 # Receives config, publishes state/status
LogLevel Enum
class LogLevel(Enum):
NONE = 0 # Disable all logging
ERROR = 1 # Error conditions only
WARN = 2 # Warnings and errors
INFO = 3 # Informational messages
DEBUG = 4 # All messages
# Functions
set_log_level(level: LogLevel) -> None
get_log_level() -> LogLevel
Field Helper
Define field types for schema dataclasses:
from sds import Field, FieldType
@dataclass
class MyConfig:
command: int = Field(uint8=True)
value: float = Field(float32=True)
name: str = Field(string_len=32)
SectionProxy Convenience Methods
# Convert section to dictionary
data = table.state.to_dict()
# {'temperature': 23.5, 'humidity': 65.0}
# Set multiple fields from dictionary
table.state.from_dict({'temperature': 24.0, 'humidity': 60.0})
Exceptions
class SdsError(Exception):
"""Base exception for SDS errors."""
class SdsNotInitializedError(SdsError):
"""Raised when SDS is not initialized."""
class SdsMqttError(SdsError):
"""Raised on MQTT connection errors."""
class SdsTableError(SdsError):
"""Raised on table registration errors."""
class SdsValidationError(SdsError):
"""Raised when input validation fails (e.g., invalid node_id)."""
Building from Source
Prerequisites
Linux (Debian/Ubuntu):
sudo apt-get install libpaho-mqtt-dev python3-dev
Linux (RHEL/CentOS):
sudo yum install paho-c-devel python3-devel
macOS:
brew install libpaho-mqtt
Build
cd python
pip install build
python -m build
Testing
cd python
pip install -e ".[dev]"
pytest
For integration tests (requires MQTT broker):
pytest -m requires_mqtt
License
MIT License - see LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sds_library-0.0.1a6.tar.gz.
File metadata
- Download URL: sds_library-0.0.1a6.tar.gz
- Upload date:
- Size: 48.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96670b18808938c9f604dad5fcaeb386bb353ee9f1ddcfa94b2dd024913c911c
|
|
| MD5 |
ab4f3751dc8e2c5054a8eb5e879d0a1d
|
|
| BLAKE2b-256 |
1e4d1ac47646fc4f053f74ede8e1faa96a9dc2d3239b08d8ab505dae920c8516
|
File details
Details for the file sds_library-0.0.1a6-cp312-cp312-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp312-cp312-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 252.5 kB
- Tags: CPython 3.12, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4378f8e168cbac43167ff6983f7c24c211ed93374506fb55dd82fca066d09b2e
|
|
| MD5 |
e4ab43f331fd355fd243685a1b624ea3
|
|
| BLAKE2b-256 |
c28257f9b3d31f79d5556d5d0aa559cfbe9faf2b48301fa2d84e25798999559a
|
File details
Details for the file sds_library-0.0.1a6-cp312-cp312-macosx_10_9_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp312-cp312-macosx_10_9_x86_64.whl
- Upload date:
- Size: 78.3 kB
- Tags: CPython 3.12, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
404db71ce929dab9df152ca84a364996b067c709f8998cb718d901c762053fd8
|
|
| MD5 |
c85299be1fdfa7c7165aeb768e5c3191
|
|
| BLAKE2b-256 |
5a6cc3d165682865f57531502551f78d67a7eab1c4d019958dc30e82fc2f0cb2
|
File details
Details for the file sds_library-0.0.1a6-cp311-cp311-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp311-cp311-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 251.4 kB
- Tags: CPython 3.11, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e88e1c86f56f10c7b9f7a2565a9ffa3e3d5f49eb88dc04580b5f681fb79a2af
|
|
| MD5 |
abd2aaa2b039e17a7b0a13978cab590f
|
|
| BLAKE2b-256 |
a65303c2553244fbfcbed537ee2930aec4944a9b6d43eba07dd9fdf5bd1b4843
|
File details
Details for the file sds_library-0.0.1a6-cp311-cp311-macosx_10_9_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp311-cp311-macosx_10_9_x86_64.whl
- Upload date:
- Size: 78.0 kB
- Tags: CPython 3.11, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ce9074e7a6b96b8070a6242e240c29aa25f9678fab291aa1d8c2cafc002421b
|
|
| MD5 |
3c8cc434a6fb5aa7c61733fbdd56ef07
|
|
| BLAKE2b-256 |
293e131461f9047dcccfe1c0e5dc027fbca12a94d0cb8f68b1e66112cb566300
|
File details
Details for the file sds_library-0.0.1a6-cp310-cp310-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp310-cp310-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 251.4 kB
- Tags: CPython 3.10, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd918a59198a257000b2b0fe2fbd16bf097dd5dd545795865c771e58c7d4f3a9
|
|
| MD5 |
1ef5cf0d5fb771072a04a32934da66b3
|
|
| BLAKE2b-256 |
9793ae1edbfb404d2d786b7437b3d733cf9b7369b0ac5ca89e8bcf4b0fe66043
|
File details
Details for the file sds_library-0.0.1a6-cp310-cp310-macosx_10_9_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp310-cp310-macosx_10_9_x86_64.whl
- Upload date:
- Size: 78.0 kB
- Tags: CPython 3.10, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a18210df19b9cd85f9a3d9f7f1365f0e132a45c604e4985f03a755178a1435b
|
|
| MD5 |
c1347247fa0daa707da83b002961547b
|
|
| BLAKE2b-256 |
c7aba08a4c58ec4a297720400cef8aff700c94594a546af4a30e7456eddc50b2
|
File details
Details for the file sds_library-0.0.1a6-cp39-cp39-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp39-cp39-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 251.4 kB
- Tags: CPython 3.9, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3cee133a34bf94119ea22869222fa991fb2be6aff27f8928ccd41de07586c7ff
|
|
| MD5 |
1c4c893ee674aba350f43a2d4c6c6799
|
|
| BLAKE2b-256 |
0ce7118e014526af6177f729062348e8b87ebd5b5ab5d5375974ea292b927c32
|
File details
Details for the file sds_library-0.0.1a6-cp39-cp39-macosx_10_9_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp39-cp39-macosx_10_9_x86_64.whl
- Upload date:
- Size: 78.0 kB
- Tags: CPython 3.9, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4344fd14f4872410272c9e6966789804e4451837496fad19a859368884618c7
|
|
| MD5 |
c1969acd1e6d184f3b374bf2b553cf1c
|
|
| BLAKE2b-256 |
4467a1b618b05e6ae1dcc0b74f4942d738d08281e17b5bb24b9be31295daf308
|
File details
Details for the file sds_library-0.0.1a6-cp38-cp38-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp38-cp38-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 251.6 kB
- Tags: CPython 3.8, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2783bbebc9dc55c53d5644a1582300974fbcd86aa858e3e5bec4b6c66c67f73d
|
|
| MD5 |
4fb5436f2864db98b9264446271a8526
|
|
| BLAKE2b-256 |
3b0e4ce0912811baee451f07192fcd4266948a3a1f46f905b598df3b497572c1
|
File details
Details for the file sds_library-0.0.1a6-cp38-cp38-macosx_10_9_x86_64.whl.
File metadata
- Download URL: sds_library-0.0.1a6-cp38-cp38-macosx_10_9_x86_64.whl
- Upload date:
- Size: 74.8 kB
- Tags: CPython 3.8, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c8067e7acdc0b6c278dbc7770cb31ff6ffd5eb191b2e50fd761f3c66bd96816
|
|
| MD5 |
c38bc12d1af767a563ccf1acc1c3218c
|
|
| BLAKE2b-256 |
6b472e0f6a4c2c4dc650d3f32cfc68e3648b4b9b11bbed5c6b1d6decc3060c31
|