vention-communication 0.1.0__tar.gz → 0.2.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.
- {vention_communication-0.1.0 → vention_communication-0.2.0}/PKG-INFO +114 -83
- {vention_communication-0.1.0 → vention_communication-0.2.0}/README.md +113 -82
- {vention_communication-0.1.0 → vention_communication-0.2.0}/pyproject.toml +1 -1
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/app.py +1 -1
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/codegen.py +0 -148
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/connect_router.py +0 -25
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/__init__.py +0 -0
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/decorators.py +0 -0
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/entries.py +0 -0
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/errors.py +0 -0
- {vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/typing_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vention-communication
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A framework for storing and managing component and application data for machine apps.
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: VentionCo
|
|
@@ -28,26 +28,31 @@ A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatib
|
|
|
28
28
|
|
|
29
29
|
## ✨ Features
|
|
30
30
|
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
31
|
+
- **Zero boilerplate RPCs**: Expose any async Python function as a network API with a single decorator.
|
|
32
|
+
- **Strong typing**: Request and response models derived directly from Python annotations.
|
|
33
|
+
- **Built-in schema emission**: Generates a .proto file at runtime, ready for SDK code generation.
|
|
34
34
|
- **Single service surface**: All methods exposed under `/rpc/<package.Service>/<Method>`.
|
|
35
|
-
- **
|
|
35
|
+
- **Connect-compatible transport**: Works seamlessly with `@connectrpc/connect-web` and `connectrpc-python`.
|
|
36
36
|
|
|
37
37
|
## 🧠 Concepts & Overview
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### The Problem
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Machine-app developers write Python daily but shouldn’t need to know REST, gRPC, or frontend networking.
|
|
42
|
+
They just need a way to say “this function should be callable from anywhere.”
|
|
42
43
|
|
|
43
|
-
###
|
|
44
|
+
### The Solution
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
vention-communication bridges that gap by turning annotated Python functions into typed RPC endpoints automatically.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
`@action()` → defines a one-request / one-response method.
|
|
49
|
+
|
|
50
|
+
`@stream()` → defines a live telemetry or event stream that other services can subscribe to.
|
|
51
|
+
|
|
52
|
+
`VentionApp.finalize()` → scans for decorators, builds a Connect router, and emits a .proto schema.
|
|
53
|
+
|
|
54
|
+
Once the .proto exists, SDKs for TypeScript, Python, or Go can be generated using Buf.
|
|
55
|
+
The result: your frontend gets auto-completed methods for every RPC, no HTTP or JSON code required.
|
|
51
56
|
|
|
52
57
|
### Core Concepts
|
|
53
58
|
|
|
@@ -56,52 +61,6 @@ A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatib
|
|
|
56
61
|
- **Service Surface** — all actions and streams belong to one service, e.g. `vention.app.v1.<YourAppName>Service`, with routes mounted under `/rpc`.
|
|
57
62
|
- **Proto Generation** — `VentionApp.finalize()` writes a .proto to disk, capturing all decorated RPCs, inferred models, and scalar wrappers. SDK generation (via Buf) is handled externally.
|
|
58
63
|
|
|
59
|
-
### Stream Configuration Options
|
|
60
|
-
|
|
61
|
-
When creating a stream with `@stream()`, you can configure how updates are delivered to subscribers:
|
|
62
|
-
|
|
63
|
-
#### `replay` (default: `True`)
|
|
64
|
-
|
|
65
|
-
Controls whether new subscribers receive the last published value immediately when they subscribe.
|
|
66
|
-
|
|
67
|
-
- **`replay=True`**: New subscribers instantly receive the most recent value (if one exists). Useful for state streams where clients need the current state immediately upon connection.
|
|
68
|
-
- **`replay=False`**: New subscribers only receive values published after they subscribe. Useful for event streams where you only want to see new events.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#### `queue_maxsize` (default: `1`)
|
|
72
|
-
|
|
73
|
-
The maximum number of items that can be queued for each subscriber before the delivery policy kicks in.
|
|
74
|
-
|
|
75
|
-
- **`queue_maxsize=1`**: Only the latest value is kept. Perfect for state streams where you only care about the current state.
|
|
76
|
-
- **`queue_maxsize=N`** (N > 1): Allows buffering up to N items. Useful when subscribers might process items slower than they're published, but you still want to limit memory usage.
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
# Only keep latest temperature reading
|
|
80
|
-
@stream(name="Temperature", payload=Temperature, queue_maxsize=1)
|
|
81
|
-
|
|
82
|
-
# Buffer up to 10 sensor readings
|
|
83
|
-
@stream(name="SensorData", payload=SensorReading, queue_maxsize=10)
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
#### `policy` (default: `"latest"`)
|
|
87
|
-
|
|
88
|
-
Controls what happens when a subscriber's queue is full and a new value is published.
|
|
89
|
-
|
|
90
|
-
- **`policy="latest"`**: Drops the oldest item and adds the new one. Ensures subscribers always have the most recent data. Best for state streams where you only care about the current value.
|
|
91
|
-
- **`policy="fifo"`**: Waits for the subscriber to process items and make space in the queue before adding new items. Ensures no data is lost but may cause backpressure if subscribers are slow. Best for event streams where you need to process every event.
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
# Latest-wins: always show current state
|
|
95
|
-
@stream(name="Status", payload=Status, policy="latest", queue_maxsize=1)
|
|
96
|
-
|
|
97
|
-
# FIFO: process every event, even if slow
|
|
98
|
-
@stream(name="Events", payload=Event, policy="fifo", queue_maxsize=100)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**Common Combinations:**
|
|
102
|
-
|
|
103
|
-
- **State monitoring** (default): `replay=True`, `queue_maxsize=1`, `policy="latest"` — subscribers get current state immediately and always see the latest value.
|
|
104
|
-
- **Event streaming**: `replay=False`, `queue_maxsize=100`, `policy="fifo"` — subscribers only see new events and process them in order.
|
|
105
64
|
|
|
106
65
|
## ⚙️ Installation & Setup
|
|
107
66
|
|
|
@@ -123,46 +82,38 @@ pip install vention-communication
|
|
|
123
82
|
- Python: `connectrpc` with `httpx.AsyncClient`
|
|
124
83
|
|
|
125
84
|
## 🚀 Quickstart Tutorial
|
|
85
|
+
A complete "hello world" in three steps.
|
|
126
86
|
|
|
127
87
|
### 1. Define your RPCs
|
|
128
88
|
|
|
129
89
|
```python
|
|
130
|
-
# demo/main.py
|
|
131
90
|
from pydantic import BaseModel
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
from vention_communication.decorators import action, stream
|
|
135
|
-
|
|
91
|
+
from vention_communication import VentionApp, action, stream
|
|
92
|
+
import time, random
|
|
136
93
|
|
|
137
94
|
class PingRequest(BaseModel):
|
|
138
95
|
message: str
|
|
139
96
|
|
|
140
|
-
|
|
141
97
|
class PingResponse(BaseModel):
|
|
142
98
|
message: str
|
|
143
99
|
|
|
144
|
-
|
|
145
100
|
class Heartbeat(BaseModel):
|
|
146
101
|
value: str
|
|
147
102
|
timestamp: int
|
|
148
103
|
|
|
149
|
-
|
|
150
|
-
app = VentionApp(name="DemoApp", emit_proto=True, proto_path="proto/app.proto")
|
|
151
|
-
|
|
104
|
+
app = VentionApp(name="DemoApp", emit_proto=True)
|
|
152
105
|
|
|
153
106
|
@action()
|
|
154
107
|
async def ping(req: PingRequest) -> PingResponse:
|
|
155
108
|
return PingResponse(message=f"Pong: {req.message}")
|
|
156
109
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
import random
|
|
162
|
-
return Heartbeat(value=f"{random.random():.2f}", timestamp=int(time()))
|
|
163
|
-
|
|
110
|
+
@stream(name="heartbeat", payload=Heartbeat, replay=True)
|
|
111
|
+
async def heartbeat():
|
|
112
|
+
"""Broadcast a live heartbeat value to all subscribers."""
|
|
113
|
+
return Heartbeat(value=f"{random.uniform(0,100):.2f}", timestamp=int(time.time()))
|
|
164
114
|
|
|
165
115
|
app.finalize()
|
|
116
|
+
|
|
166
117
|
```
|
|
167
118
|
|
|
168
119
|
**Run:**
|
|
@@ -171,10 +122,7 @@ app.finalize()
|
|
|
171
122
|
uvicorn demo.main:app --reload
|
|
172
123
|
```
|
|
173
124
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- `/rpc/vention.app.v1.DemoAppService/Ping`
|
|
177
|
-
- `/rpc/vention.app.v1.DemoAppService/Heartbeat`
|
|
125
|
+
Endpoints are automatically registered under `/rpc/vention.app.v1.DemoAppService.`
|
|
178
126
|
|
|
179
127
|
### 2. Generated .proto
|
|
180
128
|
|
|
@@ -252,7 +200,7 @@ VentionApp(
|
|
|
252
200
|
|
|
253
201
|
**Methods:**
|
|
254
202
|
|
|
255
|
-
- `.
|
|
203
|
+
- `.register_rpc_plugin(bundle: RpcBundle)` — merges external action/stream definitions (e.g., from state-machine or storage).
|
|
256
204
|
- `.finalize()` — registers routes, emits .proto, and makes publishers available.
|
|
257
205
|
|
|
258
206
|
**Attributes:**
|
|
@@ -284,7 +232,90 @@ VentionApp(
|
|
|
284
232
|
- `queue_maxsize`: Maximum items per subscriber queue (default: `1`)
|
|
285
233
|
- `policy`: Delivery policy when queue is full - `"latest"` drops old items, `"fifo"` waits for space (default: `"latest"`)
|
|
286
234
|
|
|
287
|
-
|
|
235
|
+
### Stream Configuration Options
|
|
236
|
+
|
|
237
|
+
When creating a stream with `@stream()`, you can configure how updates are delivered to subscribers:
|
|
238
|
+
|
|
239
|
+
#### `replay` (default: `True`)
|
|
240
|
+
|
|
241
|
+
Controls whether new subscribers receive the last published value immediately when they subscribe.
|
|
242
|
+
|
|
243
|
+
- **`replay=True`**: New subscribers instantly receive the most recent value (if one exists). Useful for state streams where clients need the current state immediately upon connection.
|
|
244
|
+
- **`replay=False`**: New subscribers only receive values published after they subscribe. Useful for event streams where you only want to see new events.
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
#### `queue_maxsize` (default: `1`)
|
|
248
|
+
|
|
249
|
+
The maximum number of items that can be queued for each subscriber before the delivery policy kicks in.
|
|
250
|
+
|
|
251
|
+
- **`queue_maxsize=1`**: Only the latest value is kept. Perfect for state streams where you only care about the current state.
|
|
252
|
+
- **`queue_maxsize=N`** (N > 1): Allows buffering up to N items. Useful when subscribers might process items slower than they're published, but you still want to limit memory usage.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# Only keep latest temperature reading
|
|
256
|
+
@stream(name="Temperature", payload=Temperature, queue_maxsize=1)
|
|
257
|
+
|
|
258
|
+
# Buffer up to 10 sensor readings
|
|
259
|
+
@stream(name="SensorData", payload=SensorReading, queue_maxsize=10)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `policy` (default: `"latest"`)
|
|
263
|
+
|
|
264
|
+
Defines what happens when a subscriber’s queue is full and a new value is published.
|
|
265
|
+
|
|
266
|
+
Each subscriber maintains its own in-memory queue of pending messages.
|
|
267
|
+
When you publish faster than a client can consume, the queue eventually fills — the policy determines what happens next.
|
|
268
|
+
|
|
269
|
+
`policy="latest"` — “drop oldest, never block”
|
|
270
|
+
|
|
271
|
+
- The publisher never waits.
|
|
272
|
+
- If a subscriber’s queue is full, the oldest item is dropped and the new one is inserted immediately.
|
|
273
|
+
- Fast subscribers receive every message; slow subscribers skip intermediate values but always see the most recent state.
|
|
274
|
+
|
|
275
|
+
✅ Pros
|
|
276
|
+
- Zero backpressure — publisher performance unaffected by slow clients.
|
|
277
|
+
- Keeps UI dashboards and telemetry feeds current (“latest value always wins”).
|
|
278
|
+
- Ideal for high-frequency data (positions, sensor readings, machine state).
|
|
279
|
+
|
|
280
|
+
⚠️ Cons
|
|
281
|
+
- Drops messages for slow clients (they may miss intermediate updates).
|
|
282
|
+
- Subscribers can diverge — one may receive more updates than another.
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
```python
|
|
286
|
+
@stream(name="Temperature", payload=TempReading,
|
|
287
|
+
policy="latest", queue_maxsize=1)
|
|
288
|
+
# → publisher never blocks; subscribers always see the most recent temperature
|
|
289
|
+
```
|
|
290
|
+
`policy="fifo"` — “deliver all, may block”
|
|
291
|
+
|
|
292
|
+
- The publisher awaits until there’s space in every subscriber’s queue.
|
|
293
|
+
- Guarantees that all messages are delivered in order to every subscriber.
|
|
294
|
+
- A slow subscriber can stall the entire stream, because the distributor
|
|
295
|
+
waits for that subscriber’s queue to make room before continuing.
|
|
296
|
+
|
|
297
|
+
✅ Pros
|
|
298
|
+
- Preserves every event and strict ordering.
|
|
299
|
+
- Reliable for command sequences, audit logs, and event-driven logic.
|
|
300
|
+
|
|
301
|
+
⚠️ Cons
|
|
302
|
+
- One slow or paused subscriber can block all others.
|
|
303
|
+
- Publishing rate is limited by the slowest client.
|
|
304
|
+
- In extreme cases, a throttled browser or dropped connection can cause the
|
|
305
|
+
distributor to stall until the queue frees or the subscriber is removed.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
```python
|
|
309
|
+
@stream(name="Events", payload=MachineEvent,
|
|
310
|
+
policy="fifo", queue_maxsize=100)
|
|
311
|
+
# → guarantees ordered delivery but can back-pressure the publisher
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Common Combinations:**
|
|
315
|
+
|
|
316
|
+
- **State monitoring** (default): `replay=True`, `queue_maxsize=1`, `policy="latest"` — subscribers get current state immediately and always see the latest value.
|
|
317
|
+
- **Event streaming**: `replay=False`, `queue_maxsize=100`, `policy="fifo"` — subscribers only see new events and process them in order.
|
|
318
|
+
|
|
288
319
|
|
|
289
320
|
## 🔍 Troubleshooting & FAQ
|
|
290
321
|
|
|
@@ -298,5 +329,5 @@ Ensure `app.finalize()` has been called before publishing or subscribing.
|
|
|
298
329
|
|
|
299
330
|
**Q: How do I integrate this with other libraries (state machine, storage, etc.)?**
|
|
300
331
|
|
|
301
|
-
Use `app.
|
|
332
|
+
Use `app.register_rpc_plugin()` to merge additional RPC definitions before calling `.finalize()`.
|
|
302
333
|
|
|
@@ -14,26 +14,31 @@ A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatib
|
|
|
14
14
|
|
|
15
15
|
## ✨ Features
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
17
|
+
- **Zero boilerplate RPCs**: Expose any async Python function as a network API with a single decorator.
|
|
18
|
+
- **Strong typing**: Request and response models derived directly from Python annotations.
|
|
19
|
+
- **Built-in schema emission**: Generates a .proto file at runtime, ready for SDK code generation.
|
|
20
20
|
- **Single service surface**: All methods exposed under `/rpc/<package.Service>/<Method>`.
|
|
21
|
-
- **
|
|
21
|
+
- **Connect-compatible transport**: Works seamlessly with `@connectrpc/connect-web` and `connectrpc-python`.
|
|
22
22
|
|
|
23
23
|
## 🧠 Concepts & Overview
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### The Problem
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Machine-app developers write Python daily but shouldn’t need to know REST, gRPC, or frontend networking.
|
|
28
|
+
They just need a way to say “this function should be callable from anywhere.”
|
|
28
29
|
|
|
29
|
-
###
|
|
30
|
+
### The Solution
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
vention-communication bridges that gap by turning annotated Python functions into typed RPC endpoints automatically.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
`@action()` → defines a one-request / one-response method.
|
|
35
|
+
|
|
36
|
+
`@stream()` → defines a live telemetry or event stream that other services can subscribe to.
|
|
37
|
+
|
|
38
|
+
`VentionApp.finalize()` → scans for decorators, builds a Connect router, and emits a .proto schema.
|
|
39
|
+
|
|
40
|
+
Once the .proto exists, SDKs for TypeScript, Python, or Go can be generated using Buf.
|
|
41
|
+
The result: your frontend gets auto-completed methods for every RPC, no HTTP or JSON code required.
|
|
37
42
|
|
|
38
43
|
### Core Concepts
|
|
39
44
|
|
|
@@ -42,52 +47,6 @@ A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatib
|
|
|
42
47
|
- **Service Surface** — all actions and streams belong to one service, e.g. `vention.app.v1.<YourAppName>Service`, with routes mounted under `/rpc`.
|
|
43
48
|
- **Proto Generation** — `VentionApp.finalize()` writes a .proto to disk, capturing all decorated RPCs, inferred models, and scalar wrappers. SDK generation (via Buf) is handled externally.
|
|
44
49
|
|
|
45
|
-
### Stream Configuration Options
|
|
46
|
-
|
|
47
|
-
When creating a stream with `@stream()`, you can configure how updates are delivered to subscribers:
|
|
48
|
-
|
|
49
|
-
#### `replay` (default: `True`)
|
|
50
|
-
|
|
51
|
-
Controls whether new subscribers receive the last published value immediately when they subscribe.
|
|
52
|
-
|
|
53
|
-
- **`replay=True`**: New subscribers instantly receive the most recent value (if one exists). Useful for state streams where clients need the current state immediately upon connection.
|
|
54
|
-
- **`replay=False`**: New subscribers only receive values published after they subscribe. Useful for event streams where you only want to see new events.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#### `queue_maxsize` (default: `1`)
|
|
58
|
-
|
|
59
|
-
The maximum number of items that can be queued for each subscriber before the delivery policy kicks in.
|
|
60
|
-
|
|
61
|
-
- **`queue_maxsize=1`**: Only the latest value is kept. Perfect for state streams where you only care about the current state.
|
|
62
|
-
- **`queue_maxsize=N`** (N > 1): Allows buffering up to N items. Useful when subscribers might process items slower than they're published, but you still want to limit memory usage.
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
# Only keep latest temperature reading
|
|
66
|
-
@stream(name="Temperature", payload=Temperature, queue_maxsize=1)
|
|
67
|
-
|
|
68
|
-
# Buffer up to 10 sensor readings
|
|
69
|
-
@stream(name="SensorData", payload=SensorReading, queue_maxsize=10)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
#### `policy` (default: `"latest"`)
|
|
73
|
-
|
|
74
|
-
Controls what happens when a subscriber's queue is full and a new value is published.
|
|
75
|
-
|
|
76
|
-
- **`policy="latest"`**: Drops the oldest item and adds the new one. Ensures subscribers always have the most recent data. Best for state streams where you only care about the current value.
|
|
77
|
-
- **`policy="fifo"`**: Waits for the subscriber to process items and make space in the queue before adding new items. Ensures no data is lost but may cause backpressure if subscribers are slow. Best for event streams where you need to process every event.
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
# Latest-wins: always show current state
|
|
81
|
-
@stream(name="Status", payload=Status, policy="latest", queue_maxsize=1)
|
|
82
|
-
|
|
83
|
-
# FIFO: process every event, even if slow
|
|
84
|
-
@stream(name="Events", payload=Event, policy="fifo", queue_maxsize=100)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**Common Combinations:**
|
|
88
|
-
|
|
89
|
-
- **State monitoring** (default): `replay=True`, `queue_maxsize=1`, `policy="latest"` — subscribers get current state immediately and always see the latest value.
|
|
90
|
-
- **Event streaming**: `replay=False`, `queue_maxsize=100`, `policy="fifo"` — subscribers only see new events and process them in order.
|
|
91
50
|
|
|
92
51
|
## ⚙️ Installation & Setup
|
|
93
52
|
|
|
@@ -109,46 +68,38 @@ pip install vention-communication
|
|
|
109
68
|
- Python: `connectrpc` with `httpx.AsyncClient`
|
|
110
69
|
|
|
111
70
|
## 🚀 Quickstart Tutorial
|
|
71
|
+
A complete "hello world" in three steps.
|
|
112
72
|
|
|
113
73
|
### 1. Define your RPCs
|
|
114
74
|
|
|
115
75
|
```python
|
|
116
|
-
# demo/main.py
|
|
117
76
|
from pydantic import BaseModel
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
from vention_communication.decorators import action, stream
|
|
121
|
-
|
|
77
|
+
from vention_communication import VentionApp, action, stream
|
|
78
|
+
import time, random
|
|
122
79
|
|
|
123
80
|
class PingRequest(BaseModel):
|
|
124
81
|
message: str
|
|
125
82
|
|
|
126
|
-
|
|
127
83
|
class PingResponse(BaseModel):
|
|
128
84
|
message: str
|
|
129
85
|
|
|
130
|
-
|
|
131
86
|
class Heartbeat(BaseModel):
|
|
132
87
|
value: str
|
|
133
88
|
timestamp: int
|
|
134
89
|
|
|
135
|
-
|
|
136
|
-
app = VentionApp(name="DemoApp", emit_proto=True, proto_path="proto/app.proto")
|
|
137
|
-
|
|
90
|
+
app = VentionApp(name="DemoApp", emit_proto=True)
|
|
138
91
|
|
|
139
92
|
@action()
|
|
140
93
|
async def ping(req: PingRequest) -> PingResponse:
|
|
141
94
|
return PingResponse(message=f"Pong: {req.message}")
|
|
142
95
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
import random
|
|
148
|
-
return Heartbeat(value=f"{random.random():.2f}", timestamp=int(time()))
|
|
149
|
-
|
|
96
|
+
@stream(name="heartbeat", payload=Heartbeat, replay=True)
|
|
97
|
+
async def heartbeat():
|
|
98
|
+
"""Broadcast a live heartbeat value to all subscribers."""
|
|
99
|
+
return Heartbeat(value=f"{random.uniform(0,100):.2f}", timestamp=int(time.time()))
|
|
150
100
|
|
|
151
101
|
app.finalize()
|
|
102
|
+
|
|
152
103
|
```
|
|
153
104
|
|
|
154
105
|
**Run:**
|
|
@@ -157,10 +108,7 @@ app.finalize()
|
|
|
157
108
|
uvicorn demo.main:app --reload
|
|
158
109
|
```
|
|
159
110
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- `/rpc/vention.app.v1.DemoAppService/Ping`
|
|
163
|
-
- `/rpc/vention.app.v1.DemoAppService/Heartbeat`
|
|
111
|
+
Endpoints are automatically registered under `/rpc/vention.app.v1.DemoAppService.`
|
|
164
112
|
|
|
165
113
|
### 2. Generated .proto
|
|
166
114
|
|
|
@@ -238,7 +186,7 @@ VentionApp(
|
|
|
238
186
|
|
|
239
187
|
**Methods:**
|
|
240
188
|
|
|
241
|
-
- `.
|
|
189
|
+
- `.register_rpc_plugin(bundle: RpcBundle)` — merges external action/stream definitions (e.g., from state-machine or storage).
|
|
242
190
|
- `.finalize()` — registers routes, emits .proto, and makes publishers available.
|
|
243
191
|
|
|
244
192
|
**Attributes:**
|
|
@@ -270,7 +218,90 @@ VentionApp(
|
|
|
270
218
|
- `queue_maxsize`: Maximum items per subscriber queue (default: `1`)
|
|
271
219
|
- `policy`: Delivery policy when queue is full - `"latest"` drops old items, `"fifo"` waits for space (default: `"latest"`)
|
|
272
220
|
|
|
273
|
-
|
|
221
|
+
### Stream Configuration Options
|
|
222
|
+
|
|
223
|
+
When creating a stream with `@stream()`, you can configure how updates are delivered to subscribers:
|
|
224
|
+
|
|
225
|
+
#### `replay` (default: `True`)
|
|
226
|
+
|
|
227
|
+
Controls whether new subscribers receive the last published value immediately when they subscribe.
|
|
228
|
+
|
|
229
|
+
- **`replay=True`**: New subscribers instantly receive the most recent value (if one exists). Useful for state streams where clients need the current state immediately upon connection.
|
|
230
|
+
- **`replay=False`**: New subscribers only receive values published after they subscribe. Useful for event streams where you only want to see new events.
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
#### `queue_maxsize` (default: `1`)
|
|
234
|
+
|
|
235
|
+
The maximum number of items that can be queued for each subscriber before the delivery policy kicks in.
|
|
236
|
+
|
|
237
|
+
- **`queue_maxsize=1`**: Only the latest value is kept. Perfect for state streams where you only care about the current state.
|
|
238
|
+
- **`queue_maxsize=N`** (N > 1): Allows buffering up to N items. Useful when subscribers might process items slower than they're published, but you still want to limit memory usage.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
# Only keep latest temperature reading
|
|
242
|
+
@stream(name="Temperature", payload=Temperature, queue_maxsize=1)
|
|
243
|
+
|
|
244
|
+
# Buffer up to 10 sensor readings
|
|
245
|
+
@stream(name="SensorData", payload=SensorReading, queue_maxsize=10)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### `policy` (default: `"latest"`)
|
|
249
|
+
|
|
250
|
+
Defines what happens when a subscriber’s queue is full and a new value is published.
|
|
251
|
+
|
|
252
|
+
Each subscriber maintains its own in-memory queue of pending messages.
|
|
253
|
+
When you publish faster than a client can consume, the queue eventually fills — the policy determines what happens next.
|
|
254
|
+
|
|
255
|
+
`policy="latest"` — “drop oldest, never block”
|
|
256
|
+
|
|
257
|
+
- The publisher never waits.
|
|
258
|
+
- If a subscriber’s queue is full, the oldest item is dropped and the new one is inserted immediately.
|
|
259
|
+
- Fast subscribers receive every message; slow subscribers skip intermediate values but always see the most recent state.
|
|
260
|
+
|
|
261
|
+
✅ Pros
|
|
262
|
+
- Zero backpressure — publisher performance unaffected by slow clients.
|
|
263
|
+
- Keeps UI dashboards and telemetry feeds current (“latest value always wins”).
|
|
264
|
+
- Ideal for high-frequency data (positions, sensor readings, machine state).
|
|
265
|
+
|
|
266
|
+
⚠️ Cons
|
|
267
|
+
- Drops messages for slow clients (they may miss intermediate updates).
|
|
268
|
+
- Subscribers can diverge — one may receive more updates than another.
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
```python
|
|
272
|
+
@stream(name="Temperature", payload=TempReading,
|
|
273
|
+
policy="latest", queue_maxsize=1)
|
|
274
|
+
# → publisher never blocks; subscribers always see the most recent temperature
|
|
275
|
+
```
|
|
276
|
+
`policy="fifo"` — “deliver all, may block”
|
|
277
|
+
|
|
278
|
+
- The publisher awaits until there’s space in every subscriber’s queue.
|
|
279
|
+
- Guarantees that all messages are delivered in order to every subscriber.
|
|
280
|
+
- A slow subscriber can stall the entire stream, because the distributor
|
|
281
|
+
waits for that subscriber’s queue to make room before continuing.
|
|
282
|
+
|
|
283
|
+
✅ Pros
|
|
284
|
+
- Preserves every event and strict ordering.
|
|
285
|
+
- Reliable for command sequences, audit logs, and event-driven logic.
|
|
286
|
+
|
|
287
|
+
⚠️ Cons
|
|
288
|
+
- One slow or paused subscriber can block all others.
|
|
289
|
+
- Publishing rate is limited by the slowest client.
|
|
290
|
+
- In extreme cases, a throttled browser or dropped connection can cause the
|
|
291
|
+
distributor to stall until the queue frees or the subscriber is removed.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
```python
|
|
295
|
+
@stream(name="Events", payload=MachineEvent,
|
|
296
|
+
policy="fifo", queue_maxsize=100)
|
|
297
|
+
# → guarantees ordered delivery but can back-pressure the publisher
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Common Combinations:**
|
|
301
|
+
|
|
302
|
+
- **State monitoring** (default): `replay=True`, `queue_maxsize=1`, `policy="latest"` — subscribers get current state immediately and always see the latest value.
|
|
303
|
+
- **Event streaming**: `replay=False`, `queue_maxsize=100`, `policy="fifo"` — subscribers only see new events and process them in order.
|
|
304
|
+
|
|
274
305
|
|
|
275
306
|
## 🔍 Troubleshooting & FAQ
|
|
276
307
|
|
|
@@ -284,4 +315,4 @@ Ensure `app.finalize()` has been called before publishing or subscribing.
|
|
|
284
315
|
|
|
285
316
|
**Q: How do I integrate this with other libraries (state machine, storage, etc.)?**
|
|
286
317
|
|
|
287
|
-
Use `app.
|
|
318
|
+
Use `app.register_rpc_plugin()` to merge additional RPC definitions before calling `.finalize()`.
|
|
@@ -38,7 +38,7 @@ class VentionApp(FastAPI):
|
|
|
38
38
|
self.connect_router = ConnectRouter()
|
|
39
39
|
self._extra_bundles: List[RpcBundle] = []
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def register_rpc_plugin(self, bundle: RpcBundle) -> None:
|
|
42
42
|
"""Add RPCs/streams provided by external libraries.
|
|
43
43
|
|
|
44
44
|
Must be called before finalize().
|
|
@@ -21,19 +21,9 @@ import "google/protobuf/empty.proto";
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _unwrap_optional(type_annotation: Any) -> tuple[Any, bool]:
|
|
24
|
-
"""Unwrap Optional[T] or Union[T, None] to T.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
type_annotation: Type annotation that may be Optional
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
Tuple of (inner_type, is_optional)
|
|
31
|
-
"""
|
|
32
24
|
origin = get_origin(type_annotation)
|
|
33
|
-
# Check if it's a Union type (Optional is Union[T, None])
|
|
34
25
|
if origin is Union:
|
|
35
26
|
args = get_args(type_annotation)
|
|
36
|
-
# Filter out NoneType
|
|
37
27
|
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
38
28
|
if len(non_none_args) == 1:
|
|
39
29
|
return (non_none_args[0], True)
|
|
@@ -41,14 +31,6 @@ def _unwrap_optional(type_annotation: Any) -> tuple[Any, bool]:
|
|
|
41
31
|
|
|
42
32
|
|
|
43
33
|
def _unwrap_list(type_annotation: Any) -> tuple[Any, bool]:
|
|
44
|
-
"""Unwrap List[T] or list[T] to T.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
type_annotation: Type annotation that may be a list
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Tuple of (inner_type, is_list)
|
|
51
|
-
"""
|
|
52
34
|
origin = get_origin(type_annotation)
|
|
53
35
|
if origin in (list, List):
|
|
54
36
|
args = get_args(type_annotation)
|
|
@@ -58,14 +40,6 @@ def _unwrap_list(type_annotation: Any) -> tuple[Any, bool]:
|
|
|
58
40
|
|
|
59
41
|
|
|
60
42
|
def _msg_name_for_scalar_stream(stream_name: str) -> str:
|
|
61
|
-
"""Generate a message name for a scalar stream payload.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
stream_name: Name of the stream
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Message name for the scalar wrapper
|
|
68
|
-
"""
|
|
69
43
|
return f"{stream_name}Message"
|
|
70
44
|
|
|
71
45
|
|
|
@@ -74,19 +48,6 @@ def _determine_proto_type_for_field(
|
|
|
74
48
|
seen_models: set[str],
|
|
75
49
|
lines: list[str],
|
|
76
50
|
) -> str:
|
|
77
|
-
"""Determine the proto type name for a field's inner type.
|
|
78
|
-
|
|
79
|
-
Handles scalars, nested Pydantic models, and fallback to string.
|
|
80
|
-
Recursively generates nested model definitions if needed.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
inner_type: The unwrapped inner type (after Optional/List)
|
|
84
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
85
|
-
lines: List to append nested message definitions to
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
Proto type name string
|
|
89
|
-
"""
|
|
90
51
|
if inner_type in _SCALAR_MAP:
|
|
91
52
|
return _SCALAR_MAP[inner_type]
|
|
92
53
|
|
|
@@ -109,28 +70,11 @@ def _process_pydantic_field(
|
|
|
109
70
|
seen_models: set[str],
|
|
110
71
|
lines: list[str],
|
|
111
72
|
) -> str:
|
|
112
|
-
"""Process a single Pydantic field and generate its proto definition.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
field_name: Name of the field
|
|
116
|
-
field_type: Type annotation of the field
|
|
117
|
-
field_index: Proto field number (1-indexed)
|
|
118
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
119
|
-
lines: List to append nested message definitions to
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Proto field definition line (e.g., " string name = 1;")
|
|
123
|
-
"""
|
|
124
|
-
# Unwrap Optional first
|
|
125
73
|
inner_type, _ = _unwrap_optional(field_type)
|
|
126
|
-
|
|
127
|
-
# Unwrap List
|
|
128
74
|
list_inner_type, is_list = _unwrap_list(inner_type)
|
|
129
75
|
|
|
130
|
-
# Determine proto type (handles nested models recursively)
|
|
131
76
|
proto_type = _determine_proto_type_for_field(list_inner_type, seen_models, lines)
|
|
132
77
|
|
|
133
|
-
# Add "repeated" prefix for lists
|
|
134
78
|
if is_list:
|
|
135
79
|
proto_type = f"repeated {proto_type}"
|
|
136
80
|
|
|
@@ -142,16 +86,6 @@ def _generate_pydantic_message(
|
|
|
142
86
|
seen_models: set[str],
|
|
143
87
|
lines: list[str],
|
|
144
88
|
) -> list[str]:
|
|
145
|
-
"""Generate proto message definition for a Pydantic model.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
type_annotation: Pydantic model type
|
|
149
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
150
|
-
lines: List to append nested message definitions to
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
List of lines for the message definition
|
|
154
|
-
"""
|
|
155
89
|
model_name = type_annotation.__name__
|
|
156
90
|
fields = []
|
|
157
91
|
field_index = 1
|
|
@@ -176,15 +110,6 @@ def _generate_pydantic_message(
|
|
|
176
110
|
def _generate_scalar_wrapper_message(
|
|
177
111
|
stream_name: str, payload_type: Type[Any]
|
|
178
112
|
) -> list[str]:
|
|
179
|
-
"""Generate proto message definition for a scalar stream payload.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
stream_name: Name of the stream
|
|
183
|
-
payload_type: Scalar type (int, float, str, bool)
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
List of lines for the wrapper message definition
|
|
187
|
-
"""
|
|
188
113
|
wrapper_name = _msg_name_for_scalar_stream(stream_name)
|
|
189
114
|
lines = [
|
|
190
115
|
f"message {wrapper_name} {{",
|
|
@@ -199,30 +124,16 @@ def _proto_type_name(
|
|
|
199
124
|
scalar_wrappers: Dict[str, str],
|
|
200
125
|
stream_name: Optional[str] = None,
|
|
201
126
|
) -> str:
|
|
202
|
-
"""Map a Python type to its proto type name.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
type_annotation: Type to map
|
|
206
|
-
scalar_wrappers: Dictionary mapping stream names to wrapper message names
|
|
207
|
-
stream_name: Optional stream name for scalar wrapper lookup
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
Proto type name string
|
|
211
|
-
"""
|
|
212
127
|
if type_annotation is None:
|
|
213
128
|
return "google.protobuf.Empty"
|
|
214
129
|
|
|
215
|
-
# Unwrap Optional
|
|
216
130
|
inner_type, _ = _unwrap_optional(type_annotation)
|
|
217
131
|
|
|
218
|
-
# Unwrap List (for streams, lists become scalar wrappers)
|
|
219
132
|
list_inner_type, is_list = _unwrap_list(inner_type)
|
|
220
133
|
|
|
221
134
|
if list_inner_type in _SCALAR_MAP:
|
|
222
135
|
if stream_name and stream_name in scalar_wrappers:
|
|
223
136
|
return scalar_wrappers[stream_name]
|
|
224
|
-
# For streams, if it's a list, we don't use "repeated" in the return type
|
|
225
|
-
# The stream itself provides the repetition
|
|
226
137
|
return str(_SCALAR_MAP[list_inner_type])
|
|
227
138
|
|
|
228
139
|
if is_pydantic_model(list_inner_type):
|
|
@@ -236,30 +147,17 @@ def _register_pydantic_model(
|
|
|
236
147
|
seen_models: set[str],
|
|
237
148
|
lines: list[str],
|
|
238
149
|
) -> None:
|
|
239
|
-
"""Register a Pydantic model and recursively register nested models.
|
|
240
|
-
|
|
241
|
-
Unwraps Optional and List types to find the underlying Pydantic model,
|
|
242
|
-
then generates its proto definition if not already seen.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
type_annotation: Type annotation that may contain a Pydantic model
|
|
246
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
247
|
-
lines: List to append message definitions to
|
|
248
|
-
"""
|
|
249
150
|
if type_annotation is None:
|
|
250
151
|
return
|
|
251
152
|
|
|
252
|
-
# Unwrap Optional
|
|
253
153
|
inner_type, _ = _unwrap_optional(type_annotation)
|
|
254
154
|
|
|
255
|
-
# Unwrap List to get inner type
|
|
256
155
|
list_inner_type, _ = _unwrap_list(inner_type)
|
|
257
156
|
|
|
258
157
|
if is_pydantic_model(list_inner_type):
|
|
259
158
|
model_name = list_inner_type.__name__
|
|
260
159
|
if model_name not in seen_models:
|
|
261
160
|
seen_models.add(model_name)
|
|
262
|
-
# This will recursively register nested models via _generate_pydantic_message
|
|
263
161
|
lines.extend(
|
|
264
162
|
_generate_pydantic_message(list_inner_type, seen_models, lines)
|
|
265
163
|
)
|
|
@@ -271,15 +169,6 @@ def _process_stream_payload(
|
|
|
271
169
|
lines: list[str],
|
|
272
170
|
scalar_wrappers: Dict[str, str],
|
|
273
171
|
) -> None:
|
|
274
|
-
"""Process a stream's payload type and generate necessary message definitions.
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
stream_entry: Stream entry containing payload type information
|
|
278
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
279
|
-
lines: List to append message definitions to
|
|
280
|
-
scalar_wrappers: Dictionary to populate with scalar wrapper mappings
|
|
281
|
-
"""
|
|
282
|
-
# Unwrap Optional and List for stream payloads
|
|
283
172
|
inner_type, _ = _unwrap_optional(stream_entry.payload_type)
|
|
284
173
|
list_inner_type, _ = _unwrap_list(inner_type)
|
|
285
174
|
|
|
@@ -299,20 +188,10 @@ def _collect_message_types(
|
|
|
299
188
|
seen_models: set[str],
|
|
300
189
|
scalar_wrappers: Dict[str, str],
|
|
301
190
|
) -> None:
|
|
302
|
-
"""Collect and generate all message types needed for the proto.
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
bundle: RPC bundle containing actions and streams
|
|
306
|
-
lines: List to append message definitions to
|
|
307
|
-
seen_models: Set of already-seen model names to avoid duplicates
|
|
308
|
-
scalar_wrappers: Dictionary to populate with scalar wrapper mappings
|
|
309
|
-
"""
|
|
310
|
-
# Process action input/output types
|
|
311
191
|
for action_entry in bundle.actions:
|
|
312
192
|
_register_pydantic_model(action_entry.input_type, seen_models, lines)
|
|
313
193
|
_register_pydantic_model(action_entry.output_type, seen_models, lines)
|
|
314
194
|
|
|
315
|
-
# Process stream payload types
|
|
316
195
|
for stream_entry in bundle.streams:
|
|
317
196
|
_process_stream_payload(stream_entry, seen_models, lines, scalar_wrappers)
|
|
318
197
|
|
|
@@ -320,13 +199,6 @@ def _collect_message_types(
|
|
|
320
199
|
def _generate_service_rpcs(
|
|
321
200
|
bundle: RpcBundle, lines: list[str], scalar_wrappers: Dict[str, str]
|
|
322
201
|
) -> None:
|
|
323
|
-
"""Generate RPC method definitions for actions and streams.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
bundle: RPC bundle containing actions and streams
|
|
327
|
-
lines: List to append RPC definitions to
|
|
328
|
-
scalar_wrappers: Dictionary mapping stream names to wrapper message names
|
|
329
|
-
"""
|
|
330
202
|
rpc_prefix = " rpc"
|
|
331
203
|
|
|
332
204
|
for action_entry in bundle.actions:
|
|
@@ -346,15 +218,6 @@ def _generate_service_rpcs(
|
|
|
346
218
|
|
|
347
219
|
|
|
348
220
|
def generate_proto(app_name: str, *, bundle: Optional[RpcBundle] = None) -> str:
|
|
349
|
-
"""Generate a Protocol Buffer definition from RPC actions and streams.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
app_name: Name of the application (used for service name)
|
|
353
|
-
bundle: Optional RPC bundle. If not provided, collects from decorators.
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
Complete proto3 file content as a string
|
|
357
|
-
"""
|
|
358
221
|
if bundle is None:
|
|
359
222
|
bundle = collect_bundle()
|
|
360
223
|
|
|
@@ -373,17 +236,6 @@ def generate_proto(app_name: str, *, bundle: Optional[RpcBundle] = None) -> str:
|
|
|
373
236
|
|
|
374
237
|
|
|
375
238
|
def sanitize_service_name(name: str) -> str:
|
|
376
|
-
"""Sanitize an application name for use as a service name.
|
|
377
|
-
|
|
378
|
-
Extracts alphanumeric parts, capitalizes each part, and joins them.
|
|
379
|
-
Invalid characters are dropped.
|
|
380
|
-
|
|
381
|
-
Args:
|
|
382
|
-
name: Application name to sanitize
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
Sanitized service name, or "VentionApp" if no valid parts found
|
|
386
|
-
"""
|
|
387
239
|
import re
|
|
388
240
|
|
|
389
241
|
parts = re.findall(r"[A-Za-z0-9]+", name)
|
{vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/connect_router.py
RENAMED
|
@@ -18,14 +18,6 @@ CONTENT_TYPE = "application/connect+json"
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _frame(payload: Dict[str, Any], *, trailer: bool = False) -> bytes:
|
|
21
|
-
"""Create a Connect protocol frame for streaming responses.
|
|
22
|
-
|
|
23
|
-
Connect uses a binary framing format for streaming JSON over HTTP. Each frame
|
|
24
|
-
consists of:
|
|
25
|
-
- 1 byte: Flags (0x00 for data, 0x80 for trailer/end-of-stream)
|
|
26
|
-
- 4 bytes: Body length in big-endian format
|
|
27
|
-
- N bytes: JSON-encoded payload
|
|
28
|
-
"""
|
|
29
21
|
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode(
|
|
30
22
|
"utf-8"
|
|
31
23
|
)
|
|
@@ -52,7 +44,6 @@ class StreamManager:
|
|
|
52
44
|
"""
|
|
53
45
|
|
|
54
46
|
def __init__(self) -> None:
|
|
55
|
-
"""Initialize the StreamManager with an empty topic registry."""
|
|
56
47
|
self._topics: Dict[str, Dict[str, Any]] = {}
|
|
57
48
|
|
|
58
49
|
def ensure_topic(self, entry: StreamEntry) -> None:
|
|
@@ -157,14 +148,6 @@ class StreamManager:
|
|
|
157
148
|
topic["subscribers"].discard(subscriber)
|
|
158
149
|
|
|
159
150
|
async def _distributor(self, stream_name: str) -> None:
|
|
160
|
-
"""Distribute published items to all subscribers of a stream.
|
|
161
|
-
|
|
162
|
-
For FIFO policy, waits for queue space. For latest-wins policy,
|
|
163
|
-
drops oldest items when queues are full.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
stream_name: Name of the stream topic to distribute
|
|
167
|
-
"""
|
|
168
151
|
topic = self._topics[stream_name]
|
|
169
152
|
publish_queue: asyncio.Queue[Any] = topic["publish_queue"]
|
|
170
153
|
subscribers: set[_Subscriber] = topic["subscribers"]
|
|
@@ -312,14 +295,6 @@ def _serialize_stream_item(item: Any) -> Dict[str, Any]:
|
|
|
312
295
|
|
|
313
296
|
|
|
314
297
|
async def _maybe_await(value: Any) -> Any:
|
|
315
|
-
"""Await a value if it's a coroutine or Future, otherwise return it.
|
|
316
|
-
|
|
317
|
-
Args:
|
|
318
|
-
value: Value that may or may not be awaitable
|
|
319
|
-
|
|
320
|
-
Returns:
|
|
321
|
-
The result of awaiting or the value itself
|
|
322
|
-
"""
|
|
323
298
|
if asyncio.iscoroutine(value) or isinstance(value, asyncio.Future):
|
|
324
299
|
return await value
|
|
325
300
|
return value
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vention_communication-0.1.0 → vention_communication-0.2.0}/src/communication/typing_utils.py
RENAMED
|
File without changes
|