sds-library 0.0.1a6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ # Include C source files for building from source
2
+ recursive-include ../src *.c
3
+ recursive-include ../include *.h
4
+ recursive-include ../platform *.c *.cpp
5
+
6
+ # Include the CFFI definitions
7
+ include sds/_cdefs.h
8
+
9
+ # Include documentation
10
+ include README.md
11
+ include ../LICENSE
@@ -0,0 +1,559 @@
1
+ Metadata-Version: 2.4
2
+ Name: sds-library
3
+ Version: 0.0.1a6
4
+ Summary: Python bindings for the SDS (Synchronized Data Structures) library
5
+ Author: SDS Team
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/EdgeKVM-Inc/sds-library
8
+ Project-URL: Documentation, https://github.com/EdgeKVM-Inc/sds-library#readme
9
+ Project-URL: Repository, https://github.com/EdgeKVM-Inc/sds-library
10
+ Keywords: iot,mqtt,embedded,synchronization,dds
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS :: MacOS X
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Networking
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: cffi>=1.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-cov; extra == "dev"
30
+ Requires-Dist: mypy; extra == "dev"
31
+ Requires-Dist: black; extra == "dev"
32
+ Requires-Dist: ruff; extra == "dev"
33
+
34
+ # SDS Library - Python Bindings
35
+
36
+ Python bindings for the SDS (Synchronized Data Structures) library, providing
37
+ a Pythonic interface for IoT state synchronization over MQTT.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install sds-library
43
+ ```
44
+
45
+ For development installation:
46
+
47
+ ```bash
48
+ cd python
49
+ pip install -e ".[dev]"
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ### Generate Types from Schema
55
+
56
+ The code generator creates Python types from your `.sds` schema file:
57
+
58
+ ```bash
59
+ # Generate Python types (and optionally C types)
60
+ python tools/sds_codegen.py schema.sds --python -o python/
61
+ ```
62
+
63
+ This creates `sds_types.py` with dataclasses matching your C structs.
64
+
65
+ ### Device Node (Using Generated Types)
66
+
67
+ ```python
68
+ from sds import SdsNode, Role
69
+ from sds_types import SensorData # Generated from schema.sds
70
+
71
+ with SdsNode("py_sensor_01", "localhost") as node:
72
+ # Register with schema bundle - no manual dataclass needed!
73
+ table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
74
+
75
+ @node.on_config("SensorData")
76
+ def handle_config(table_type):
77
+ print(f"Config: threshold={table.config.threshold}")
78
+
79
+ while True:
80
+ # C-like attribute access
81
+ table.state.temperature = 23.5
82
+ table.state.humidity = 65.0
83
+ table.status.error_code = 0
84
+
85
+ node.poll(timeout_ms=1000)
86
+ ```
87
+
88
+ ### Device Node (Manual Schema Definition)
89
+
90
+ If you prefer not to use the code generator:
91
+
92
+ ```python
93
+ from dataclasses import dataclass
94
+ from sds import SdsNode, Role, Field
95
+
96
+ @dataclass
97
+ class SensorState:
98
+ temperature: float = Field(float32=True)
99
+ humidity: float = Field(float32=True)
100
+
101
+ with SdsNode("py_sensor_01", "localhost") as node:
102
+ table = node.register_table(
103
+ "SensorData", Role.DEVICE,
104
+ state_schema=SensorState,
105
+ )
106
+ table.state.temperature = 23.5
107
+ ```
108
+
109
+ ### Owner Node (C-like Syntax)
110
+
111
+ ```python
112
+ from sds import SdsNode, Role
113
+
114
+ with SdsNode("py_owner_01", "localhost") as node:
115
+ table = node.register_table(
116
+ "SensorData",
117
+ Role.OWNER,
118
+ status_schema=SensorStatus, # For reading device status
119
+ )
120
+
121
+ # Set config
122
+ table.config.threshold = 25.0
123
+
124
+ @node.on_status("SensorData")
125
+ def handle_status(table_type, from_node):
126
+ device = table.get_device(from_node)
127
+ if device and device.online:
128
+ print(f"{from_node}: battery={device.status.battery_percent}%")
129
+
130
+ while True:
131
+ node.poll(timeout_ms=1000)
132
+
133
+ # Iterate all devices
134
+ for node_id, device in table.iter_devices():
135
+ print(f"{node_id}: online={device.online}")
136
+ ```
137
+
138
+ ## Features
139
+
140
+ - **C-like Syntax**: Access table data directly (`table.state.temperature = 23.5`)
141
+ - **Zero Protocol Drift**: Python uses the exact same C implementation
142
+ - **Pythonic API**: Context managers, decorators, and keyword arguments
143
+ - **Thread-Safe**: All operations protected by locks for multi-threaded use
144
+ - **Cross-Platform**: Linux (x86_64, ARM64) and macOS
145
+ - **Type Hints**: Full type annotations for IDE support
146
+ - **Device Eviction**: Automatic cleanup of offline device slots
147
+
148
+ ## Device Eviction
149
+
150
+ When devices disconnect unexpectedly, SDS receives MQTT LWT (Last Will and Testament)
151
+ messages and can automatically evict them from status slots after a grace period.
152
+ This prevents slots from being permanently consumed by devices that never reconnect.
153
+
154
+ ```python
155
+ # Enable eviction with 60-second grace period
156
+ with SdsNode("owner", "localhost", eviction_grace_ms=60000) as node:
157
+ table = node.register_table("SensorData", Role.OWNER, schema=SensorData)
158
+
159
+ @node.on_device_evicted()
160
+ def handle_eviction(table_type: str, node_id: str):
161
+ print(f"Device {node_id} was evicted from {table_type}")
162
+
163
+ while True:
164
+ node.poll(timeout_ms=1000)
165
+
166
+ # Check if a device has eviction pending
167
+ device = table.get_device("sensor_01")
168
+ if device and device.eviction_pending:
169
+ print("Device will be evicted soon if it doesn't reconnect")
170
+ ```
171
+
172
+ ## Thread Safety
173
+
174
+ SdsNode is fully thread-safe. Multiple threads can safely:
175
+ - Call `poll()` concurrently
176
+ - Access table data (`table.state.temperature`)
177
+ - Register tables and callbacks
178
+
179
+ ```python
180
+ import threading
181
+ from sds import SdsNode, Role
182
+
183
+ with SdsNode("my_node", "localhost") as node:
184
+ table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
185
+
186
+ def polling_thread():
187
+ while True:
188
+ node.poll(timeout_ms=100)
189
+
190
+ def update_thread():
191
+ while True:
192
+ table.state.temperature = read_sensor()
193
+ time.sleep(1)
194
+
195
+ # Both threads can safely access the node and table
196
+ t1 = threading.Thread(target=polling_thread)
197
+ t2 = threading.Thread(target=update_thread)
198
+ t1.start()
199
+ t2.start()
200
+ ```
201
+
202
+ **Note:** Callbacks are executed while holding the lock. Avoid blocking
203
+ operations in callbacks to prevent deadlocks.
204
+
205
+ ## Logging Configuration
206
+
207
+ SDS uses Python's standard `logging` module. By default, log messages go
208
+ to a `NullHandler` (silent). To enable logging:
209
+
210
+ ```python
211
+ from sds import configure_logging
212
+ import logging
213
+
214
+ # Quick setup - logs to stderr at INFO level
215
+ configure_logging(level=logging.INFO)
216
+
217
+ # Or configure manually
218
+ logging.getLogger("sds").setLevel(logging.DEBUG)
219
+ logging.getLogger("sds").addHandler(logging.StreamHandler())
220
+ ```
221
+
222
+ ## Connection Resilience
223
+
224
+ SdsNode includes built-in connection retry with exponential backoff:
225
+
226
+ ```python
227
+ from sds import SdsNode
228
+
229
+ node = SdsNode(
230
+ "my_node",
231
+ "mqtt.example.com",
232
+ retry_count=5, # Try 5 times (default: 3)
233
+ retry_delay_ms=2000, # Start with 2 second delay (default: 1000)
234
+ connect_timeout_ms=10000 # 10 second timeout (default: 5000)
235
+ )
236
+ ```
237
+
238
+ On connection failure, retries are attempted with exponential backoff
239
+ (delay doubles each retry). Non-connection errors are not retried.
240
+
241
+ ## Error Handling Best Practices
242
+
243
+ ```python
244
+ from sds import (
245
+ SdsNode, SdsMqttError, SdsTableError,
246
+ SdsValidationError, SdsError
247
+ )
248
+
249
+ try:
250
+ with SdsNode("my_node", "localhost") as node:
251
+ table = node.register_table("SensorData", Role.DEVICE)
252
+
253
+ while True:
254
+ try:
255
+ node.poll()
256
+ except SdsMqttError:
257
+ # MQTT connection lost - will auto-reconnect
258
+ logging.warning("MQTT disconnected, waiting...")
259
+ time.sleep(1)
260
+
261
+ except SdsValidationError as e:
262
+ # Invalid node_id or configuration
263
+ logging.error(f"Configuration error: {e}")
264
+ except SdsMqttError as e:
265
+ # Failed to connect after all retries
266
+ logging.error(f"Could not connect to MQTT: {e}")
267
+ except SdsError as e:
268
+ # Other SDS errors
269
+ logging.error(f"SDS error: {e}")
270
+ ```
271
+
272
+ ## Requirements
273
+
274
+ - Python 3.8+
275
+ - MQTT broker (e.g., Mosquitto)
276
+ - libpaho-mqtt development headers (for building from source)
277
+
278
+ ## Configuration Options
279
+
280
+ ### Delta Sync (v0.5.0+)
281
+
282
+ Enable delta sync to only send changed fields, reducing bandwidth:
283
+
284
+ ```python
285
+ with SdsNode(
286
+ "sensor_01",
287
+ "localhost",
288
+ enable_delta_sync=True, # Only send changed fields
289
+ delta_float_tolerance=0.01 # Ignore tiny float changes
290
+ ) as node:
291
+ # ...
292
+ ```
293
+
294
+ **Benefits:**
295
+ - Reduced bandwidth (only changed fields transmitted)
296
+ - Lower power consumption on battery devices
297
+ - Works automatically with codegen-generated tables
298
+
299
+ **Limitations:**
300
+ - Config messages are always full (retained on broker)
301
+ - Status heartbeats are always full (liveness detection)
302
+ - Manual schema definitions use full sync
303
+
304
+ ### Eviction Grace Period
305
+
306
+ Configure how long to wait before evicting offline devices:
307
+
308
+ ```python
309
+ with SdsNode(
310
+ "owner",
311
+ "localhost",
312
+ eviction_grace_ms=30000 # 30 seconds before eviction
313
+ ) as node:
314
+ @node.on_device_evicted()
315
+ def handle_eviction(table_type, node_id):
316
+ print(f"Device {node_id} evicted from {table_type}")
317
+ ```
318
+
319
+ ### Raw MQTT Publish/Subscribe (v0.5.0+)
320
+
321
+ Send and receive custom MQTT messages through the SDS-managed connection. Useful for
322
+ logging, diagnostics, or application-specific messages that don't fit the table model.
323
+
324
+ **Publishing:**
325
+ ```python
326
+ with SdsNode("sensor_01", "localhost") as node:
327
+ # Check connection status
328
+ if node.is_connected():
329
+ # Publish a log message
330
+ node.publish_raw(
331
+ f"log/{node.node_id}",
332
+ '{"level": "info", "msg": "Sensor started"}',
333
+ qos=0,
334
+ retained=False
335
+ )
336
+
337
+ # Publish binary data
338
+ node.publish_raw("sensor/raw_data", b'\x00\x01\x02\x03')
339
+ ```
340
+
341
+ **Subscribing:**
342
+ ```python
343
+ def on_log(topic: str, payload: bytes):
344
+ print(f"Log from {topic}: {payload.decode()}")
345
+
346
+ with SdsNode("controller", "localhost") as node:
347
+ # Subscribe to all logs (+ matches any single level)
348
+ node.subscribe_raw("log/+", on_log)
349
+
350
+ # Or use # for multi-level wildcard
351
+ node.subscribe_raw("sensors/#", on_sensor_data)
352
+
353
+ # Main loop
354
+ while True:
355
+ node.poll()
356
+ time.sleep(0.1)
357
+
358
+ # Unsubscribe when done
359
+ node.unsubscribe_raw("log/+")
360
+ ```
361
+
362
+ **Methods:**
363
+
364
+ | Method | Description |
365
+ |--------|-------------|
366
+ | `is_connected()` | Returns `True` if connected to MQTT broker |
367
+ | `publish_raw(topic, payload, qos=0, retained=False)` | Publish arbitrary MQTT message |
368
+ | `subscribe_raw(topic, callback, qos=0)` | Subscribe to topic pattern with callback |
369
+ | `unsubscribe_raw(topic)` | Unsubscribe from a topic pattern |
370
+
371
+ **Notes:**
372
+ - Topics starting with `sds/` are reserved and will raise `ValueError`
373
+ - `payload` for publish can be `str` (UTF-8 encoded) or `bytes`
374
+ - Callback receives `(topic: str, payload: bytes)`
375
+ - Maximum 8 concurrent raw subscriptions
376
+ - Wildcard subscriptions (`log/+`) count as 1 subscription regardless of matching topics
377
+
378
+ ## API Reference
379
+
380
+ ### SdsNode
381
+
382
+ The main class for interacting with SDS.
383
+
384
+ ```python
385
+ class SdsNode:
386
+ def __init__(self, node_id: str, broker_host: str, port: int = 1883,
387
+ username: str | None = None, password: str | None = None,
388
+ eviction_grace_ms: int = 0):
389
+ """
390
+ Initialize SDS node and connect to MQTT broker.
391
+
392
+ Args:
393
+ eviction_grace_ms: Grace period before evicting offline devices (0 = disabled).
394
+ For OWNER roles, when a device disconnects (LWT), an eviction
395
+ timer starts. If the device doesn't reconnect within this period,
396
+ it's evicted from status slots, freeing the slot for new devices.
397
+ """
398
+
399
+ def register_table(self, table_type: str, role: Role,
400
+ sync_interval_ms: int | None = None,
401
+ config_schema: Type | None = None,
402
+ state_schema: Type | None = None,
403
+ status_schema: Type | None = None) -> SdsTable:
404
+ """Register a table and return SdsTable for C-like access."""
405
+
406
+ def get_table(self, table_type: str) -> SdsTable:
407
+ """Get a previously registered table."""
408
+
409
+ def unregister_table(self, table_type: str) -> None:
410
+ """Unregister a table."""
411
+
412
+ def poll(self, timeout_ms: int = 0) -> None:
413
+ """Process MQTT messages and sync changes."""
414
+
415
+ def is_ready(self) -> bool:
416
+ """Check if connected to MQTT broker."""
417
+
418
+ def on_error(self, callback) -> None:
419
+ """Register error callback."""
420
+
421
+ def on_version_mismatch(self, callback) -> None:
422
+ """Register version mismatch callback."""
423
+ ```
424
+
425
+ ### SdsTable
426
+
427
+ Provides C-like attribute access to table sections.
428
+
429
+ ```python
430
+ class SdsTable:
431
+ table_type: str # The table type name
432
+ role: Role # OWNER or DEVICE
433
+
434
+ # Device role properties
435
+ state: SectionProxy # Read/write state section
436
+ status: SectionProxy # Read/write status section
437
+ config: SectionProxy # Read-only config (from owner)
438
+
439
+ # Owner role properties
440
+ config: SectionProxy # Read/write config section
441
+ device_count: int # Number of known devices
442
+
443
+ def get_device(self, node_id: str) -> DeviceView | None:
444
+ """Get a device's data (owner role only)."""
445
+
446
+ def iter_devices(self) -> Iterator[tuple[str, DeviceView]]:
447
+ """Iterate over all known devices (owner role only)."""
448
+ ```
449
+
450
+ ### Role Enum
451
+
452
+ ```python
453
+ class Role(Enum):
454
+ OWNER = 0 # Publishes config, receives state/status
455
+ DEVICE = 1 # Receives config, publishes state/status
456
+ ```
457
+
458
+ ### LogLevel Enum
459
+
460
+ ```python
461
+ class LogLevel(Enum):
462
+ NONE = 0 # Disable all logging
463
+ ERROR = 1 # Error conditions only
464
+ WARN = 2 # Warnings and errors
465
+ INFO = 3 # Informational messages
466
+ DEBUG = 4 # All messages
467
+
468
+ # Functions
469
+ set_log_level(level: LogLevel) -> None
470
+ get_log_level() -> LogLevel
471
+ ```
472
+
473
+ ### Field Helper
474
+
475
+ Define field types for schema dataclasses:
476
+
477
+ ```python
478
+ from sds import Field, FieldType
479
+
480
+ @dataclass
481
+ class MyConfig:
482
+ command: int = Field(uint8=True)
483
+ value: float = Field(float32=True)
484
+ name: str = Field(string_len=32)
485
+ ```
486
+
487
+ ### SectionProxy Convenience Methods
488
+
489
+ ```python
490
+ # Convert section to dictionary
491
+ data = table.state.to_dict()
492
+ # {'temperature': 23.5, 'humidity': 65.0}
493
+
494
+ # Set multiple fields from dictionary
495
+ table.state.from_dict({'temperature': 24.0, 'humidity': 60.0})
496
+ ```
497
+
498
+ ### Exceptions
499
+
500
+ ```python
501
+ class SdsError(Exception):
502
+ """Base exception for SDS errors."""
503
+
504
+ class SdsNotInitializedError(SdsError):
505
+ """Raised when SDS is not initialized."""
506
+
507
+ class SdsMqttError(SdsError):
508
+ """Raised on MQTT connection errors."""
509
+
510
+ class SdsTableError(SdsError):
511
+ """Raised on table registration errors."""
512
+
513
+ class SdsValidationError(SdsError):
514
+ """Raised when input validation fails (e.g., invalid node_id)."""
515
+ ```
516
+
517
+ ## Building from Source
518
+
519
+ ### Prerequisites
520
+
521
+ **Linux (Debian/Ubuntu):**
522
+ ```bash
523
+ sudo apt-get install libpaho-mqtt-dev python3-dev
524
+ ```
525
+
526
+ **Linux (RHEL/CentOS):**
527
+ ```bash
528
+ sudo yum install paho-c-devel python3-devel
529
+ ```
530
+
531
+ **macOS:**
532
+ ```bash
533
+ brew install libpaho-mqtt
534
+ ```
535
+
536
+ ### Build
537
+
538
+ ```bash
539
+ cd python
540
+ pip install build
541
+ python -m build
542
+ ```
543
+
544
+ ## Testing
545
+
546
+ ```bash
547
+ cd python
548
+ pip install -e ".[dev]"
549
+ pytest
550
+ ```
551
+
552
+ For integration tests (requires MQTT broker):
553
+ ```bash
554
+ pytest -m requires_mqtt
555
+ ```
556
+
557
+ ## License
558
+
559
+ MIT License - see LICENSE file for details.