Skip to main content

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 raise ValueError
  • payload for publish can be str (UTF-8 encoded) or bytes
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sds_library-0.0.1a6.tar.gz (48.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

sds_library-0.0.1a6-cp312-cp312-manylinux_2_34_x86_64.whl (252.5 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.34+ x86-64

sds_library-0.0.1a6-cp312-cp312-macosx_10_9_x86_64.whl (78.3 kB view details)

Uploaded CPython 3.12macOS 10.9+ x86-64

sds_library-0.0.1a6-cp311-cp311-manylinux_2_34_x86_64.whl (251.4 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.34+ x86-64

sds_library-0.0.1a6-cp311-cp311-macosx_10_9_x86_64.whl (78.0 kB view details)

Uploaded CPython 3.11macOS 10.9+ x86-64

sds_library-0.0.1a6-cp310-cp310-manylinux_2_34_x86_64.whl (251.4 kB view details)

Uploaded CPython 3.10manylinux: glibc 2.34+ x86-64

sds_library-0.0.1a6-cp310-cp310-macosx_10_9_x86_64.whl (78.0 kB view details)

Uploaded CPython 3.10macOS 10.9+ x86-64

sds_library-0.0.1a6-cp39-cp39-manylinux_2_34_x86_64.whl (251.4 kB view details)

Uploaded CPython 3.9manylinux: glibc 2.34+ x86-64

sds_library-0.0.1a6-cp39-cp39-macosx_10_9_x86_64.whl (78.0 kB view details)

Uploaded CPython 3.9macOS 10.9+ x86-64

sds_library-0.0.1a6-cp38-cp38-manylinux_2_34_x86_64.whl (251.6 kB view details)

Uploaded CPython 3.8manylinux: glibc 2.34+ x86-64

sds_library-0.0.1a6-cp38-cp38-macosx_10_9_x86_64.whl (74.8 kB view details)

Uploaded CPython 3.8macOS 10.9+ x86-64

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

Hashes for sds_library-0.0.1a6.tar.gz
Algorithm Hash digest
SHA256 96670b18808938c9f604dad5fcaeb386bb353ee9f1ddcfa94b2dd024913c911c
MD5 ab4f3751dc8e2c5054a8eb5e879d0a1d
BLAKE2b-256 1e4d1ac47646fc4f053f74ede8e1faa96a9dc2d3239b08d8ab505dae920c8516

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp312-cp312-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp312-cp312-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 4378f8e168cbac43167ff6983f7c24c211ed93374506fb55dd82fca066d09b2e
MD5 e4ab43f331fd355fd243685a1b624ea3
BLAKE2b-256 c28257f9b3d31f79d5556d5d0aa559cfbe9faf2b48301fa2d84e25798999559a

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp312-cp312-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp312-cp312-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 404db71ce929dab9df152ca84a364996b067c709f8998cb718d901c762053fd8
MD5 c85299be1fdfa7c7165aeb768e5c3191
BLAKE2b-256 5a6cc3d165682865f57531502551f78d67a7eab1c4d019958dc30e82fc2f0cb2

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp311-cp311-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp311-cp311-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 2e88e1c86f56f10c7b9f7a2565a9ffa3e3d5f49eb88dc04580b5f681fb79a2af
MD5 abd2aaa2b039e17a7b0a13978cab590f
BLAKE2b-256 a65303c2553244fbfcbed537ee2930aec4944a9b6d43eba07dd9fdf5bd1b4843

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp311-cp311-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp311-cp311-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 4ce9074e7a6b96b8070a6242e240c29aa25f9678fab291aa1d8c2cafc002421b
MD5 3c8cc434a6fb5aa7c61733fbdd56ef07
BLAKE2b-256 293e131461f9047dcccfe1c0e5dc027fbca12a94d0cb8f68b1e66112cb566300

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp310-cp310-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp310-cp310-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 cd918a59198a257000b2b0fe2fbd16bf097dd5dd545795865c771e58c7d4f3a9
MD5 1ef5cf0d5fb771072a04a32934da66b3
BLAKE2b-256 9793ae1edbfb404d2d786b7437b3d733cf9b7369b0ac5ca89e8bcf4b0fe66043

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp310-cp310-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp310-cp310-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 1a18210df19b9cd85f9a3d9f7f1365f0e132a45c604e4985f03a755178a1435b
MD5 c1347247fa0daa707da83b002961547b
BLAKE2b-256 c7aba08a4c58ec4a297720400cef8aff700c94594a546af4a30e7456eddc50b2

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp39-cp39-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp39-cp39-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 3cee133a34bf94119ea22869222fa991fb2be6aff27f8928ccd41de07586c7ff
MD5 1c4c893ee674aba350f43a2d4c6c6799
BLAKE2b-256 0ce7118e014526af6177f729062348e8b87ebd5b5ab5d5375974ea292b927c32

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp39-cp39-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp39-cp39-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 a4344fd14f4872410272c9e6966789804e4451837496fad19a859368884618c7
MD5 c1969acd1e6d184f3b374bf2b553cf1c
BLAKE2b-256 4467a1b618b05e6ae1dcc0b74f4942d738d08281e17b5bb24b9be31295daf308

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp38-cp38-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp38-cp38-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 2783bbebc9dc55c53d5644a1582300974fbcd86aa858e3e5bec4b6c66c67f73d
MD5 4fb5436f2864db98b9264446271a8526
BLAKE2b-256 3b0e4ce0912811baee451f07192fcd4266948a3a1f46f905b598df3b497572c1

See more details on using hashes here.

File details

Details for the file sds_library-0.0.1a6-cp38-cp38-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for sds_library-0.0.1a6-cp38-cp38-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 6c8067e7acdc0b6c278dbc7770cb31ff6ffd5eb191b2e50fd761f3c66bd96816
MD5 c38bc12d1af767a563ccf1acc1c3218c
BLAKE2b-256 6b472e0f6a4c2c4dc650d3f32cfc68e3648b4b9b11bbed5c6b1d6decc3060c31

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page