SpiriSynq 0.1.0__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,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: SpiriSynq
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: deepdiff>=9.0.0
8
+ Requires-Dist: eclipse-zenoh>=1.8.0
9
+ Requires-Dist: loguru>=0.7.3
10
+ Requires-Dist: psygnal>=0.15.1
11
+ Requires-Dist: rich>=14.3.3
12
+ Requires-Dist: ruamel-yaml>=0.19.1
13
+ Requires-Dist: typer>=0.24.1
14
+ Provides-Extra: docs
15
+ Requires-Dist: sphinx>=8.0; extra == "docs"
16
+ Requires-Dist: sphinx-rtd-theme>=3.0; extra == "docs"
17
+ Requires-Dist: myst-parser>=4.0; extra == "docs"
18
+ Requires-Dist: autodoc-pydantic>=2.0; extra == "docs"
19
+
20
+ # SpiriSynq
21
+
22
+ SpiriSynq keeps Python dataclass instances in sync across processes, machines, and languages over a [Zenoh](https://zenoh.io) pub/sub network. Define a typed dataclass, point multiple instances at the same topic, and field changes flow between them automatically.
23
+
24
+ ```python
25
+ from dataclasses import dataclass
26
+ from SpiriSynq.syncable_objects import SyncableObject
27
+
28
+ @dataclass
29
+ class Telemetry(SyncableObject):
30
+ altitude: float = 0.0
31
+ battery: int = 100
32
+
33
+ # Process A — owns the data
34
+ t = Telemetry("drone/telemetry", synq_authoritive=True)
35
+ t.altitude = 42.5 # published to the network automatically
36
+
37
+ # Process B — mirrors it
38
+ mirror = Telemetry.from_topic("drone/telemetry")
39
+ print(mirror.altitude) # 42.5
40
+ ```
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install SpiriSynq
46
+ ```
47
+
48
+ Requires Python 3.13+. A Zenoh router is optional for local use — peers discover each other directly.
49
+
50
+ ## How it works
51
+
52
+ Each syncable field maps to a Zenoh key expression `<topic>/<field>`. When a field changes, psygnal fires a signal, SpiriSynq serialises the new value to YAML, and publishes it. All subscribers on the same topic receive the update and apply it in-place without echo-looping it back.
53
+
54
+ Nested dataclass fields sync at the sub-field level: a change to `robot.gps.latitude` publishes to `<topic>/gps/latitude`, not `<topic>/gps`. Inherit from `SubSyncableDataclass` to make a nested type fully evented without Zenoh overhead; alternatively, use `@dataclass(frozen=True)` for immutable value objects that are replaced atomically.
55
+
56
+ ## Authoritative vs mirror
57
+
58
+ Sync is **bidirectional** — every instance both publishes its own changes and receives changes from others. `synq_authoritive` controls only the queryable side:
59
+
60
+ - **Authoritative** (`synq_authoritive=True`): registers Zenoh queryables for full-state rehydration on connect, `@remote_method` RPCs, and schema/discovery endpoints. Sets the base topic prefix from the hostname.
61
+ - **Mirror** (default): publishes and receives field changes just like an authoritative instance, but forwards RPC calls to the authoritative node rather than running them locally.
62
+
63
+ ```python
64
+ # Both sides can write — sync is bidirectional
65
+ counter = Counter("myapp/counter", synq_authoritive=True)
66
+ mirror = Counter.from_topic("myapp/counter")
67
+ mirror.value = 99 # received by counter
68
+ ```
69
+
70
+ ## Remote methods
71
+
72
+ `@remote_method` exposes a method as a Zenoh queryable. Mirrors call it transparently — the call is routed to the authoritative node and the return value is sent back.
73
+
74
+ ```python
75
+ from SpiriSynq.remote_callables import remote_method
76
+
77
+ @dataclass
78
+ class Robot(SyncableObject):
79
+ status: str = "idle"
80
+
81
+ @remote_method
82
+ def arm(self, mode: str = "auto") -> str:
83
+ self.status = "armed"
84
+ return f"armed in {mode} mode"
85
+
86
+ # From a mirror — works exactly like a local call
87
+ robot = Robot.from_topic("fleet/robot1")
88
+ result = robot.arm(mode="manual") # "armed in manual mode"
89
+ ```
90
+
91
+ Generator and async generator methods are supported. The mirror receives a regular Python generator that streams values as each reply arrives.
92
+
93
+ Custom timeouts: `robot.arm.timeout(5.0)(mode="manual")`.
94
+
95
+ ## Discovery
96
+
97
+ ```python
98
+ from SpiriSynq.session import current_session
99
+
100
+ session = current_session.get()
101
+ for metadata in session.list_topics():
102
+ print(metadata["topic"], metadata["classes"])
103
+
104
+ # Filter by type
105
+ for metadata in session.list_topics(type_filter="Robot"):
106
+ robot = Robot.from_topic(metadata["topic"])
107
+ ```
108
+
109
+ ## Cross-language compatibility
110
+
111
+ The wire format is plain Zenoh with YAML payloads. Any node that implements the [SpiriSynq protocol](docs/protocol.md) — four mandatory queryables and per-field puts — is a first-class participant. No library required on the other end.
112
+
113
+ ## Key configuration options
114
+
115
+ | Field | Default | Purpose |
116
+ |---|---|---|
117
+ | `synq_authoritive` | `False` | Register queryables; answer rehydration and RPC calls |
118
+ | `synq_publish` | `True` | Publish local changes to the network |
119
+ | `synq_receive` | `True` | Apply incoming changes from the network |
120
+ | `sync_lazy_publish` | `False` | Skip publishing when no subscribers are present |
121
+ | `synq_auto_start` | `True` | Call `sync()` automatically on construction |
122
+
123
+ ## Docs
124
+
125
+ - [Overview](docs/overview.md)
126
+ - [Getting Started](docs/getting_started.md)
127
+ - [Concepts](docs/concepts.md)
128
+ - [Protocol Specification](docs/protocol.md)
@@ -0,0 +1,109 @@
1
+ # SpiriSynq
2
+
3
+ SpiriSynq keeps Python dataclass instances in sync across processes, machines, and languages over a [Zenoh](https://zenoh.io) pub/sub network. Define a typed dataclass, point multiple instances at the same topic, and field changes flow between them automatically.
4
+
5
+ ```python
6
+ from dataclasses import dataclass
7
+ from SpiriSynq.syncable_objects import SyncableObject
8
+
9
+ @dataclass
10
+ class Telemetry(SyncableObject):
11
+ altitude: float = 0.0
12
+ battery: int = 100
13
+
14
+ # Process A — owns the data
15
+ t = Telemetry("drone/telemetry", synq_authoritive=True)
16
+ t.altitude = 42.5 # published to the network automatically
17
+
18
+ # Process B — mirrors it
19
+ mirror = Telemetry.from_topic("drone/telemetry")
20
+ print(mirror.altitude) # 42.5
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install SpiriSynq
27
+ ```
28
+
29
+ Requires Python 3.13+. A Zenoh router is optional for local use — peers discover each other directly.
30
+
31
+ ## How it works
32
+
33
+ Each syncable field maps to a Zenoh key expression `<topic>/<field>`. When a field changes, psygnal fires a signal, SpiriSynq serialises the new value to YAML, and publishes it. All subscribers on the same topic receive the update and apply it in-place without echo-looping it back.
34
+
35
+ Nested dataclass fields sync at the sub-field level: a change to `robot.gps.latitude` publishes to `<topic>/gps/latitude`, not `<topic>/gps`. Inherit from `SubSyncableDataclass` to make a nested type fully evented without Zenoh overhead; alternatively, use `@dataclass(frozen=True)` for immutable value objects that are replaced atomically.
36
+
37
+ ## Authoritative vs mirror
38
+
39
+ Sync is **bidirectional** — every instance both publishes its own changes and receives changes from others. `synq_authoritive` controls only the queryable side:
40
+
41
+ - **Authoritative** (`synq_authoritive=True`): registers Zenoh queryables for full-state rehydration on connect, `@remote_method` RPCs, and schema/discovery endpoints. Sets the base topic prefix from the hostname.
42
+ - **Mirror** (default): publishes and receives field changes just like an authoritative instance, but forwards RPC calls to the authoritative node rather than running them locally.
43
+
44
+ ```python
45
+ # Both sides can write — sync is bidirectional
46
+ counter = Counter("myapp/counter", synq_authoritive=True)
47
+ mirror = Counter.from_topic("myapp/counter")
48
+ mirror.value = 99 # received by counter
49
+ ```
50
+
51
+ ## Remote methods
52
+
53
+ `@remote_method` exposes a method as a Zenoh queryable. Mirrors call it transparently — the call is routed to the authoritative node and the return value is sent back.
54
+
55
+ ```python
56
+ from SpiriSynq.remote_callables import remote_method
57
+
58
+ @dataclass
59
+ class Robot(SyncableObject):
60
+ status: str = "idle"
61
+
62
+ @remote_method
63
+ def arm(self, mode: str = "auto") -> str:
64
+ self.status = "armed"
65
+ return f"armed in {mode} mode"
66
+
67
+ # From a mirror — works exactly like a local call
68
+ robot = Robot.from_topic("fleet/robot1")
69
+ result = robot.arm(mode="manual") # "armed in manual mode"
70
+ ```
71
+
72
+ Generator and async generator methods are supported. The mirror receives a regular Python generator that streams values as each reply arrives.
73
+
74
+ Custom timeouts: `robot.arm.timeout(5.0)(mode="manual")`.
75
+
76
+ ## Discovery
77
+
78
+ ```python
79
+ from SpiriSynq.session import current_session
80
+
81
+ session = current_session.get()
82
+ for metadata in session.list_topics():
83
+ print(metadata["topic"], metadata["classes"])
84
+
85
+ # Filter by type
86
+ for metadata in session.list_topics(type_filter="Robot"):
87
+ robot = Robot.from_topic(metadata["topic"])
88
+ ```
89
+
90
+ ## Cross-language compatibility
91
+
92
+ The wire format is plain Zenoh with YAML payloads. Any node that implements the [SpiriSynq protocol](docs/protocol.md) — four mandatory queryables and per-field puts — is a first-class participant. No library required on the other end.
93
+
94
+ ## Key configuration options
95
+
96
+ | Field | Default | Purpose |
97
+ |---|---|---|
98
+ | `synq_authoritive` | `False` | Register queryables; answer rehydration and RPC calls |
99
+ | `synq_publish` | `True` | Publish local changes to the network |
100
+ | `synq_receive` | `True` | Apply incoming changes from the network |
101
+ | `sync_lazy_publish` | `False` | Skip publishing when no subscribers are present |
102
+ | `synq_auto_start` | `True` | Call `sync()` automatically on construction |
103
+
104
+ ## Docs
105
+
106
+ - [Overview](docs/overview.md)
107
+ - [Getting Started](docs/getting_started.md)
108
+ - [Concepts](docs/concepts.md)
109
+ - [Protocol Specification](docs/protocol.md)