istos 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.
- istos-0.1.0/PKG-INFO +555 -0
- istos-0.1.0/README.md +497 -0
- istos-0.1.0/pyproject.toml +136 -0
- istos-0.1.0/src/istos/__init__.py +79 -0
- istos-0.1.0/src/istos/app/__init__.py +44 -0
- istos-0.1.0/src/istos/app/_base.py +278 -0
- istos-0.1.0/src/istos/app/lifecycle.py +146 -0
- istos-0.1.0/src/istos/app/messaging.py +553 -0
- istos-0.1.0/src/istos/app/queues.py +438 -0
- istos-0.1.0/src/istos/app/streaming.py +368 -0
- istos-0.1.0/src/istos/app/web.py +482 -0
- istos-0.1.0/src/istos/cli.py +134 -0
- istos-0.1.0/src/istos/communication/__init__.py +13 -0
- istos-0.1.0/src/istos/communication/config.py +201 -0
- istos-0.1.0/src/istos/communication/durable.py +133 -0
- istos-0.1.0/src/istos/communication/persist.py +348 -0
- istos-0.1.0/src/istos/communication/sessions.py +381 -0
- istos-0.1.0/src/istos/consistency/__init__.py +22 -0
- istos-0.1.0/src/istos/consistency/config.py +94 -0
- istos-0.1.0/src/istos/consistency/databases.py +100 -0
- istos-0.1.0/src/istos/consistency/redis_storage.py +100 -0
- istos-0.1.0/src/istos/consistency/sqlalchemy_storage.py +278 -0
- istos-0.1.0/src/istos/consistency/storage.py +117 -0
- istos-0.1.0/src/istos/context.py +116 -0
- istos-0.1.0/src/istos/di/__init__.py +25 -0
- istos-0.1.0/src/istos/di/context.py +44 -0
- istos-0.1.0/src/istos/di/depends.py +247 -0
- istos-0.1.0/src/istos/discovery/__init__.py +0 -0
- istos-0.1.0/src/istos/discovery/asyncapi.py +295 -0
- istos-0.1.0/src/istos/errors.py +157 -0
- istos-0.1.0/src/istos/fitness.py +398 -0
- istos-0.1.0/src/istos/http/__init__.py +0 -0
- istos-0.1.0/src/istos/http/asgi.py +46 -0
- istos-0.1.0/src/istos/http/gateway.py +162 -0
- istos-0.1.0/src/istos/http/health.py +52 -0
- istos-0.1.0/src/istos/http/mcp.py +99 -0
- istos-0.1.0/src/istos/logging.py +181 -0
- istos-0.1.0/src/istos/messages/__init__.py +21 -0
- istos-0.1.0/src/istos/messages/serialization.py +148 -0
- istos-0.1.0/src/istos/middleware/__init__.py +4 -0
- istos-0.1.0/src/istos/middleware/base.py +133 -0
- istos-0.1.0/src/istos/middleware/ratelimit.py +67 -0
- istos-0.1.0/src/istos/observability/__init__.py +9 -0
- istos-0.1.0/src/istos/observability/metrics.py +72 -0
- istos-0.1.0/src/istos/observability/tracing.py +83 -0
- istos-0.1.0/src/istos/primitives/__init__.py +0 -0
- istos-0.1.0/src/istos/primitives/channel.py +237 -0
- istos-0.1.0/src/istos/primitives/channel_fabric.py +242 -0
- istos-0.1.0/src/istos/primitives/clients.py +99 -0
- istos-0.1.0/src/istos/primitives/handler.py +293 -0
- istos-0.1.0/src/istos/primitives/liveliness.py +63 -0
- istos-0.1.0/src/istos/primitives/publish.py +146 -0
- istos-0.1.0/src/istos/primitives/query.py +154 -0
- istos-0.1.0/src/istos/primitives/session_store.py +29 -0
- istos-0.1.0/src/istos/primitives/stream.py +178 -0
- istos-0.1.0/src/istos/primitives/subscribe.py +289 -0
- istos-0.1.0/src/istos/py.typed +0 -0
- istos-0.1.0/src/istos/queue/__init__.py +11 -0
- istos-0.1.0/src/istos/queue/cron.py +95 -0
- istos-0.1.0/src/istos/queue/role.py +340 -0
- istos-0.1.0/src/istos/queue/store.py +361 -0
- istos-0.1.0/src/istos/queue/worker.py +187 -0
- istos-0.1.0/src/istos/retry.py +62 -0
- istos-0.1.0/src/istos/routing.py +162 -0
- istos-0.1.0/src/istos/security/__init__.py +0 -0
- istos-0.1.0/src/istos/security/authz.py +325 -0
- istos-0.1.0/src/istos/testing/__init__.py +3 -0
- istos-0.1.0/src/istos/testing/testclient.py +233 -0
- istos-0.1.0/src/istos/validation.py +128 -0
istos-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: istos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A unified Python framework for distributed systems and multi-agent applications over Eclipse Zenoh
|
|
5
|
+
Keywords: zenoh,distributed-systems,pubsub,rpc,microservices,agents,iot
|
|
6
|
+
Author: amirrezaei
|
|
7
|
+
Author-email: amirrezaei <corvology@gmail.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
22
|
+
Requires-Dist: eclipse-zenoh>=1.7.2
|
|
23
|
+
Requires-Dist: msgpack>=1.1.2
|
|
24
|
+
Requires-Dist: pydantic>=2.12.5
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.13.1
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
27
|
+
Requires-Dist: istos[redis,sqlalchemy,otel,s3,jwt] ; extra == 'all'
|
|
28
|
+
Requires-Dist: mypy>=1.19.1 ; extra == 'dev'
|
|
29
|
+
Requires-Dist: pylint>=4.0.5 ; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=1.3.0 ; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-mock>=3.15.1 ; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=6.0.0 ; extra == 'dev'
|
|
34
|
+
Requires-Dist: mkdocs>=1.6.0 ; extra == 'dev'
|
|
35
|
+
Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'dev'
|
|
36
|
+
Requires-Dist: mkdocstrings[python]>=0.27.0 ; extra == 'dev'
|
|
37
|
+
Requires-Dist: pyjwt>=2.8.0 ; extra == 'jwt'
|
|
38
|
+
Requires-Dist: opentelemetry-api>=1.28.0 ; extra == 'otel'
|
|
39
|
+
Requires-Dist: opentelemetry-sdk>=1.28.0 ; extra == 'otel'
|
|
40
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.28.0 ; extra == 'otel'
|
|
41
|
+
Requires-Dist: redis>=5.0.0 ; extra == 'redis'
|
|
42
|
+
Requires-Dist: aioboto3>=13.0.0 ; extra == 's3'
|
|
43
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0 ; extra == 'sqlalchemy'
|
|
44
|
+
Requires-Python: >=3.10, <3.15
|
|
45
|
+
Project-URL: Homepage, https://github.com/0x416d6972/Istos
|
|
46
|
+
Project-URL: Documentation, https://0x416d6972.github.io/Istos/
|
|
47
|
+
Project-URL: Repository, https://github.com/0x416d6972/Istos
|
|
48
|
+
Project-URL: Issues, https://github.com/0x416d6972/Istos/issues
|
|
49
|
+
Project-URL: Changelog, https://github.com/0x416d6972/Istos/blob/main/docs/changelog.md
|
|
50
|
+
Provides-Extra: all
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Provides-Extra: jwt
|
|
53
|
+
Provides-Extra: otel
|
|
54
|
+
Provides-Extra: redis
|
|
55
|
+
Provides-Extra: s3
|
|
56
|
+
Provides-Extra: sqlalchemy
|
|
57
|
+
Description-Content-Type: text/markdown
|
|
58
|
+
|
|
59
|
+
# Istos
|
|
60
|
+
|
|
61
|
+
<p align="center">
|
|
62
|
+
<a href="https://pypi.org/project/istos/"><img alt="PyPI" src="https://img.shields.io/pypi/v/istos.svg"></a>
|
|
63
|
+
<a href="https://github.com/0x416d6972/Istos/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/0x416d6972/Istos/actions/workflows/ci.yml/badge.svg"></a>
|
|
64
|
+
<img alt="Python" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue">
|
|
65
|
+
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-green">
|
|
66
|
+
<img alt="Status" src="https://img.shields.io/badge/status-0.1.0%20Beta-orange">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<em>Decorator-first Python services on <a href="https://zenoh.io/">Eclipse Zenoh</a> — RPC, streaming, duplex channels, work queues, and durable pub/sub.</em>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
**Istos** is for building clean-architecture software and AI agents. A small, explicit decorator surface keeps your codebase maintainable and right-sized — modules that stay easy to analyze, debug, and develop, including with vibe coding, where the machine writes more of the code and readable, well-bounded components are what keep it under control. It maps network operations onto Python decorators so you can wire request/reply, token streams, interactive agent sessions, jobs, and events without standing up a message broker.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Why Istos
|
|
78
|
+
|
|
79
|
+
Distributed systems are hard enough without spending your design budget on
|
|
80
|
+
message brokers. Istos lets you reason about *services that answer requests*
|
|
81
|
+
and *events that fan out* — you draw the boxes and arrows; Istos **is** the
|
|
82
|
+
arrows. A two-service prototype and a hundred-service fleet are the same
|
|
83
|
+
mental model, just more decorators.
|
|
84
|
+
|
|
85
|
+
There is no Kafka, RabbitMQ, NATS, or Redis Streams cluster to provision.
|
|
86
|
+
Durability lives in the *peers* — via [Eclipse Zenoh](https://zenoh.io/)'s
|
|
87
|
+
advanced pub/sub — not in a central log you have to operate. `pip install`,
|
|
88
|
+
write a decorated function, `run()`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Key Features
|
|
93
|
+
|
|
94
|
+
- **Decorators first**: `@handle`, `@stream`, `@channel`, `@publish`, `@subscribe`.
|
|
95
|
+
- **Streaming RPC**: `@stream` yields chunks; `stream_query` / `@stream_client` consume them.
|
|
96
|
+
- **Duplex channels**: `@channel` + `open_channel` / `@channel_client` for multi-turn agents (WebSocket or fabric).
|
|
97
|
+
- **HTTP gateway**: `Istos(http_port=8080)` plus `http=` / `ws=` — JSON, SSE, WebSocket. Co-host inside FastAPI with `istos.asgi.lifespan`. Optional MCP tools from `@handle`.
|
|
98
|
+
- **Smart selectors**: Query params like `?limit=5` land on your Python arguments.
|
|
99
|
+
- **Schema validation**: Type hints / Pydantic at the edge.
|
|
100
|
+
- **Retry policies**: `retry=5` (or a `RetryPolicy`) on queries and subscribers.
|
|
101
|
+
- **Brokerless durability**: `durable=True` replay caches; `persist="s3://…"` / `app.replay(...)` when the producer itself can die.
|
|
102
|
+
- **Security**: TLS/mTLS at the transport; `TokenAuthorizer` / `JWTAuthorizer` / `require_roles` on handlers. Default is still open — use `require_auth=True` when you mean it.
|
|
103
|
+
- **Pluggable storage**: in-memory, Redis, or SQLAlchemy (bring your async driver).
|
|
104
|
+
|
|
105
|
+
## The Mental Model
|
|
106
|
+
|
|
107
|
+
- **`@handle` & `@query`**: 1-to-1 RPC
|
|
108
|
+
- **`@stream`**: 1-to-1 streaming RPC (SLM/LLM tokens)
|
|
109
|
+
- **`@channel`**: full-duplex sessions (agents)
|
|
110
|
+
- **`@publish` & `@subscribe`**: 1-to-many events
|
|
111
|
+
- **`@on_liveliness`**: node discovery & health
|
|
112
|
+
|
|
113
|
+
## Installation
|
|
114
|
+
|
|
115
|
+
This project uses modern Python packaging via [`uv`](https://github.com/astral-sh/uv).
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Standard installation
|
|
119
|
+
uv pip install istos
|
|
120
|
+
|
|
121
|
+
# Or install from source:
|
|
122
|
+
git clone https://github.com/0x416d6972/Istos.git
|
|
123
|
+
cd istos
|
|
124
|
+
uv pip install -e .
|
|
125
|
+
|
|
126
|
+
# Or with the optional backends (Redis, SQLAlchemy, S3 persistence, JWT, OTel):
|
|
127
|
+
uv pip install -e ".[all]"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Quick Start
|
|
131
|
+
|
|
132
|
+
### 1. Registering a Handler
|
|
133
|
+
Handlers sit on the network and respond to incoming queries. Istos automatically parses query parameters into your function's arguments.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from istos import Istos
|
|
137
|
+
|
|
138
|
+
istos = Istos()
|
|
139
|
+
|
|
140
|
+
@istos.handle(prefix="robot/move")
|
|
141
|
+
async def move(distance: int, speed: str = "normal"):
|
|
142
|
+
"""
|
|
143
|
+
Called when a Zenoh Query hits 'robot/move'.
|
|
144
|
+
E.g., Querying 'robot/move?distance=10&speed=fast' automatically binds:
|
|
145
|
+
distance=10, speed='fast'
|
|
146
|
+
"""
|
|
147
|
+
return {"status": "success", "distance": int(distance), "speed": speed}
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
# Blocks and listens for queries
|
|
151
|
+
istos.run()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2. Querying the Network
|
|
155
|
+
You can easily query handlers registered anywhere on the Zenoh network using `kwargs` to build Zenoh Selectors.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from contextlib import asynccontextmanager
|
|
159
|
+
from istos import Istos
|
|
160
|
+
|
|
161
|
+
# Using @istos.query makes it a callable network function
|
|
162
|
+
@istos.query("robot/move")
|
|
163
|
+
async def query_robot(result):
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
# Queries run over the service's shared Zenoh session, so trigger them once the
|
|
167
|
+
# service is running — e.g. from a handler or a lifespan hook:
|
|
168
|
+
@asynccontextmanager
|
|
169
|
+
async def on_start(app):
|
|
170
|
+
reply = await query_robot(distance=15, speed="fast")
|
|
171
|
+
print(f"Robot replied: {reply}")
|
|
172
|
+
yield
|
|
173
|
+
|
|
174
|
+
istos = Istos(lifespan=on_start)
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
istos.run()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> There is no longer a per-call "transient session" fallback: the whole service
|
|
181
|
+
> shares **one** Zenoh session opened by `istos.run()`/`run_async()`. Calling a
|
|
182
|
+
> `@query`/`@publish` before the service is running raises a clear `RuntimeError`.
|
|
183
|
+
|
|
184
|
+
### 3. Publishing & Subscribing (Event-Driven)
|
|
185
|
+
React to real-time events efficiently.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from istos import Istos
|
|
189
|
+
|
|
190
|
+
istos = Istos()
|
|
191
|
+
|
|
192
|
+
# --- Subscriber ---
|
|
193
|
+
@istos.subscribe("drone/telemetry")
|
|
194
|
+
def on_telemetry(data):
|
|
195
|
+
# Triggered automatically when data is pushed to "drone/telemetry"
|
|
196
|
+
print(f"Received telemetry via network: {data}")
|
|
197
|
+
|
|
198
|
+
# --- Publisher ---
|
|
199
|
+
@istos.publish("drone/telemetry")
|
|
200
|
+
def get_telemetry():
|
|
201
|
+
# The return value is automatically published to the network!
|
|
202
|
+
return {"battery": 85, "altitude": 120}
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
# Call the wrapped publisher function to publish the result
|
|
206
|
+
get_telemetry()
|
|
207
|
+
|
|
208
|
+
# Or publish arbitrary data independently
|
|
209
|
+
# await istos.publish_once("drone/telemetry", {"battery": 80})
|
|
210
|
+
|
|
211
|
+
istos.run()
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Brokerless durability.** Add `durable=True` and a subscriber that joins *late*
|
|
215
|
+
still receives everything — replayed peer-to-peer from the producer's cache, with
|
|
216
|
+
no Kafka/NATS broker to run:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
@istos.publish("orders/created", durable=True, cache=1000) # producer keeps a replay log
|
|
220
|
+
async def created(order): return order
|
|
221
|
+
|
|
222
|
+
@istos.subscribe("orders/created", durable=True, replay=1000) # replays history + recovers
|
|
223
|
+
async def on_created(event): ...
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The producer *is* the log (Zenoh advanced pub/sub); pair it with the idempotency
|
|
227
|
+
ledger for effectively-once processing. See [Brokerless Durable Messaging](docs/user-guide/durable-messaging.md).
|
|
228
|
+
|
|
229
|
+
### 4. Liveliness Tracking (Heartbeats)
|
|
230
|
+
Detect when nodes connect or drop off the network without polling.
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# Announce that this node is alive on the network
|
|
234
|
+
istos.declare_liveliness("robot/camera1")
|
|
235
|
+
|
|
236
|
+
# Listen to the network for connection state changes
|
|
237
|
+
@istos.on_liveliness("robot/**")
|
|
238
|
+
def status_changed(key_expr: str, is_alive: bool):
|
|
239
|
+
if is_alive:
|
|
240
|
+
print(f"Node connected: {key_expr}")
|
|
241
|
+
else:
|
|
242
|
+
print(f"ALERT: Node crashed/disconnected -> {key_expr}")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 5. One-Shot Commands & State Clearing
|
|
246
|
+
Use raw async functions when you want to act imperatively rather than relying on events.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# Quickly shoot out a piece of data
|
|
250
|
+
await istos.publish_once("fast/data/pulse", {"system": "ok"})
|
|
251
|
+
|
|
252
|
+
# Clear/erase network states, especially useful if using persistent StoragePlugins
|
|
253
|
+
await istos.delete_once("robot/cache/old_logs")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 6. High-Performance Shared Memory (Zero-Copy)
|
|
257
|
+
When sending large data arrays (like HD video frames) between handlers on the same hardware, enable POSIX shared memory allocations to avoid copies.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@istos.publish("video/feed", use_shm=True)
|
|
261
|
+
def send_frame():
|
|
262
|
+
return large_data_array
|
|
263
|
+
|
|
264
|
+
# The framework automatically manages Zenoh ShmProviders natively!
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 7. Dependency Injection & Pluggability
|
|
268
|
+
Set global defaults on startup, or override them per-endpoint for polyglot persistence and sagas:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from istos import Istos
|
|
272
|
+
from istos.consistency import InMemoryStoragePlugin, SqlAlchemyStoragePlugin
|
|
273
|
+
|
|
274
|
+
istos = Istos(
|
|
275
|
+
storage=InMemoryStoragePlugin(), # Global default
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Each decorator can use its own serializer or storage plugin:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from istos.messages.serialization import MsgPackSerializer
|
|
283
|
+
|
|
284
|
+
# JSON and Memory by default
|
|
285
|
+
@istos.handle("robot/move")
|
|
286
|
+
async def move(distance: int): ...
|
|
287
|
+
|
|
288
|
+
# MsgPack and a durable SQL ledger specifically for this endpoint
|
|
289
|
+
@istos.handle("sensor/data", serializer=MsgPackSerializer(),
|
|
290
|
+
storage=SqlAlchemyStoragePlugin("postgresql+asyncpg://user:pass@db/istos"))
|
|
291
|
+
async def sensor(data): ...
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Dependency injection with `Depends`.** `@handle`, `@subscribe`, `@publish`, `@query`, and `@on_liveliness` can all declare dependencies that Istos resolves per invocation — plain callables, async callables, sub-dependencies, and `yield` dependencies with setup/teardown. Use it as a default value or inside `Annotated`:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from typing import Annotated
|
|
298
|
+
from istos import Istos, Depends
|
|
299
|
+
|
|
300
|
+
istos = Istos()
|
|
301
|
+
|
|
302
|
+
async def get_db():
|
|
303
|
+
conn = await open_conn()
|
|
304
|
+
try:
|
|
305
|
+
yield conn # torn down after the handler replies
|
|
306
|
+
finally:
|
|
307
|
+
await conn.close()
|
|
308
|
+
|
|
309
|
+
@istos.handle("orders/create")
|
|
310
|
+
async def create(order_id: int, db: Annotated[Conn, Depends(get_db)]):
|
|
311
|
+
return await db.insert(order_id)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Dependencies are cached per invocation (`use_cache=True` by default), sync dependencies are offloaded to a thread so they don't block the event loop, circular dependencies raise `DependencyCycleError`, and tests can swap any dependency via `istos.dependency_overrides[get_db] = fake_db`.
|
|
315
|
+
|
|
316
|
+
> **Streaming note:** on `@subscribe`/`@publish`, dependencies resolve **per message** — so a `yield` dependency opens/closes its resource on every message. For an expensive, long-lived resource (DB pool, sensor socket), create it once in the `lifespan` and inject the shared instance with a cheap `Depends` that just returns it; reserve per-message `yield` deps for genuinely per-message setup.
|
|
317
|
+
|
|
318
|
+
### 8. Retry Policies
|
|
319
|
+
Add automatic retries with exponential backoff to any query or subscriber. Pass a simple integer or a full `RetryPolicy` for fine-grained control.
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from istos.core.retry import RetryPolicy
|
|
323
|
+
|
|
324
|
+
# Simple — retry up to 5 times with default exponential backoff
|
|
325
|
+
@istos.query("weather/forecast", retry=5)
|
|
326
|
+
def get_forecast(result):
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
# Subscriber with retries — if processing crashes, it retries before giving up
|
|
330
|
+
@istos.subscribe("sensor/readings", retry=3)
|
|
331
|
+
def on_reading(data):
|
|
332
|
+
save_to_database(data) # retried automatically on transient failures
|
|
333
|
+
|
|
334
|
+
# Advanced — full control over backoff timing and failure handling
|
|
335
|
+
@istos.query("weather/forecast", retry=RetryPolicy(
|
|
336
|
+
max_retries=10,
|
|
337
|
+
delay=1.0,
|
|
338
|
+
backoff_factor=3.0,
|
|
339
|
+
on_failure=lambda e: print(f"Dead letter: {e}")
|
|
340
|
+
))
|
|
341
|
+
def get_forecast(result):
|
|
342
|
+
return result
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### 9. Schema Validation
|
|
346
|
+
Istos automatically validates and type-coerces incoming parameters at the network boundary — before your business logic runs. Supports three modes:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from pydantic import BaseModel
|
|
350
|
+
|
|
351
|
+
# Mode 1: Type hints → auto-coercion
|
|
352
|
+
# Zenoh sends distance="15" (string), Istos casts it to int(15)
|
|
353
|
+
# Zenoh sends distance="hello" → rejected with a validation error reply
|
|
354
|
+
@istos.handle("robot/move")
|
|
355
|
+
async def move(distance: int, speed: str = "normal"):
|
|
356
|
+
return {"moved": distance, "speed": speed}
|
|
357
|
+
|
|
358
|
+
# Mode 2: Pydantic BaseModel → full schema validation
|
|
359
|
+
class MoveRequest(BaseModel):
|
|
360
|
+
distance: int
|
|
361
|
+
speed: str = "normal"
|
|
362
|
+
|
|
363
|
+
@istos.handle("robot/move")
|
|
364
|
+
async def move(request: MoveRequest):
|
|
365
|
+
# request is a fully validated Pydantic object with defaults applied
|
|
366
|
+
return {"moved": request.distance}
|
|
367
|
+
|
|
368
|
+
# Mode 3: No type hints → passthrough (backward compatible)
|
|
369
|
+
@istos.handle("robot/echo")
|
|
370
|
+
async def echo(message):
|
|
371
|
+
return {"echo": message}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 10. Security: Transport, Authentication & Authorization
|
|
375
|
+
|
|
376
|
+
> [!WARNING]
|
|
377
|
+
> **Istos is unauthenticated by default.** With no configuration, a session runs
|
|
378
|
+
> in Zenoh **peer mode** with multicast scouting and no TLS — any peer on the
|
|
379
|
+
> local network can discover your node, invoke every `@handle`, and read every
|
|
380
|
+
> published value. Istos raises an `IstosSecurityWarning` (via `warnings.warn`,
|
|
381
|
+
> like urllib3's `InsecureRequestWarning`) whenever it opens such a session. You
|
|
382
|
+
> can escalate it to a hard failure in CI:
|
|
383
|
+
>
|
|
384
|
+
> ```python
|
|
385
|
+
> import warnings
|
|
386
|
+
> from istos import IstosSecurityWarning
|
|
387
|
+
> warnings.simplefilter("error", IstosSecurityWarning) # insecure config -> exception
|
|
388
|
+
> ```
|
|
389
|
+
>
|
|
390
|
+
> Before deploying, do **both** of the following:
|
|
391
|
+
> 1. **Secure the transport** — authenticate and encrypt the fabric (below).
|
|
392
|
+
> 2. **Authorize handlers** — gate who may invoke them (further below).
|
|
393
|
+
|
|
394
|
+
#### Transport Security & Authentication
|
|
395
|
+
Secure the system without hard-coding secrets. `IstosZenohConfig` loads properties from your environment (`.env` file or environment variables) using `pydantic-settings`.
|
|
396
|
+
|
|
397
|
+
```env
|
|
398
|
+
# .env file
|
|
399
|
+
ISTOS_ZENOH_MODE=client
|
|
400
|
+
# Endpoints accept a JSON array or a comma-separated list:
|
|
401
|
+
ISTOS_ZENOH_CONNECT_ENDPOINTS=["tls/zenoh-router.local:7447"]
|
|
402
|
+
# ISTOS_ZENOH_CONNECT_ENDPOINTS=tls/router-a:7447,tls/router-b:7447
|
|
403
|
+
|
|
404
|
+
# Basic Auth
|
|
405
|
+
ISTOS_ZENOH_USERNAME=robot_1
|
|
406
|
+
ISTOS_ZENOH_PASSWORD=super_secret
|
|
407
|
+
|
|
408
|
+
# TLS / mTLS
|
|
409
|
+
ISTOS_ZENOH_ROOT_CA_CERTIFICATE=/path/to/ca.pem
|
|
410
|
+
# ISTOS_ZENOH_LISTEN_CERTIFICATE=/path/to/cert.pem
|
|
411
|
+
# ISTOS_ZENOH_LISTEN_PRIVATE_KEY=/path/to/key.pem
|
|
412
|
+
# ISTOS_ZENOH_ENABLE_MTLS=true
|
|
413
|
+
|
|
414
|
+
# Lock down discovery: disable UDP multicast scouting and rely on explicit endpoints
|
|
415
|
+
# ISTOS_ZENOH_MULTICAST_SCOUTING=false
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`IstosZenohConfig` is a Pydantic `BaseSettings` model, so it validates **at construction** (via field/model validators) and fails fast on structurally broken security config (malformed endpoints, a TLS cert without its key, mTLS without a CA, an invalid `mode`), warns when credentials are configured without TLS, and rejects unknown `ISTOS_ZENOH_*` variables so a typo can't silently disable auth. Secrets (`password`, `listen_private_key`) are `SecretStr`, so they don't leak into logs or reprs. For knobs the builder doesn't model, deep-merge raw Zenoh config via `additional_config={...}`. Session managers also accept `open_retries`/`open_retry_delay_s` to wait out a router that isn't up yet at startup.
|
|
419
|
+
|
|
420
|
+
Then pass it straight to Istos — it builds the session for you:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from istos import Istos
|
|
424
|
+
from istos.communication.sessions import IstosZenohConfig
|
|
425
|
+
|
|
426
|
+
# Auto-populates from .env!
|
|
427
|
+
config = IstosZenohConfig()
|
|
428
|
+
|
|
429
|
+
istos = Istos(config=config)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`config=` accepts an `IstosZenohConfig` (built via `.build()` at construction) or a raw `zenoh.Config`. It's mutually exclusive with `session_manager=` — pass one or the other. If you need a sync session or full control, wire it yourself instead:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from istos.communication.sessions import AsyncZenohSession
|
|
436
|
+
istos = Istos(session_manager=AsyncZenohSession(config.build()))
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Advanced Security: Vault & Secret Managers (Programmatic Raw PEM)
|
|
440
|
+
For zero-trust environments, Zenoh accepts **raw multiline PEM strings** natively, so you don't need to write files to disk. You can bypass `.env` and pull secrets into `IstosZenohConfig` at startup:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
# 1. Fetch from your secrets manager (HashiCorp Vault, AWS, LDAP, etc.)
|
|
444
|
+
secrets = vault.get_secret("istos/prod")
|
|
445
|
+
|
|
446
|
+
# 2. Inject raw strings directly into the config builder
|
|
447
|
+
config = IstosZenohConfig(
|
|
448
|
+
mode="client",
|
|
449
|
+
connect_endpoints=["tls/zenoh-router.local:7447"],
|
|
450
|
+
username=secrets["zenoh_user"],
|
|
451
|
+
password=secrets["zenoh_pass"],
|
|
452
|
+
root_ca_certificate=secrets["raw_ca_pem_string"], # raw multiline PEM
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
istos = Istos(session_manager=AsyncZenohSession(config.build()))
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### Authorization
|
|
459
|
+
Securing the transport controls *who joins the fabric*. **Authorization** controls *what a joined peer may invoke*. Istos gives every handler an authorization hook. An **authorizer** receives an `AuthContext` (the key expression, parameters, and any attachment/token the caller sent) and returns `True` to allow the request, or `False`/raises `UnauthorizedError` to deny it. Sync and async authorizers are both supported. Denied requests never reach your handler and are answered with an `unauthorized` error.
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
from istos import Istos, TokenAuthorizer, AuthContext, UnauthorizedError
|
|
463
|
+
|
|
464
|
+
# App-wide: every handler (including built-in .istos/* endpoints) requires a token
|
|
465
|
+
istos = Istos(authorizer=TokenAuthorizer("super-secret-token"))
|
|
466
|
+
|
|
467
|
+
# Per-handler override — a custom rule for one endpoint
|
|
468
|
+
def admins_only(ctx: AuthContext) -> bool:
|
|
469
|
+
return ctx.token in {"alice-key", "bob-key"}
|
|
470
|
+
|
|
471
|
+
@istos.handle("fleet/shutdown", authorizer=admins_only)
|
|
472
|
+
async def shutdown():
|
|
473
|
+
return {"stopping": True}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Callers attach their token via `attachment=`:
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
await istos.query_once("fleet/shutdown", attachment="alice-key")
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
> **Built-in endpoints** (`.istos/health`, `.istos/ready`, `.istos/metrics`) and
|
|
483
|
+
> `serve_docs()` inherit the app-wide authorizer. If you leave the app
|
|
484
|
+
> unauthenticated, Istos warns that these endpoints — including the AsyncAPI
|
|
485
|
+
> document that describes your entire API surface — are network-reachable by any
|
|
486
|
+
> peer. Set an `authorizer` (or pass one to `serve_docs(authorizer=...)`) to
|
|
487
|
+
> protect them.
|
|
488
|
+
|
|
489
|
+
#### A note on serialization
|
|
490
|
+
Istos ships `JsonSerializer` (default), `RawSerializer` (bytes/str passthrough for pre-encoded or binary payloads), `MsgPackSerializer`, `PydanticSerializer`, `ProtobufSerializer`, `YamlSerializer`, and a `Base64Serializer` wrapper. `JsonSerializer` tolerates common types like `datetime`, `Decimal`, and `UUID` (stringified), and `MsgPackSerializer` pins string decoding for cross-version consistency.
|
|
491
|
+
|
|
492
|
+
It does **not** ship a pickle-based serializer: `pickle.loads` executes arbitrary code embedded in its input, which on a fabric where any peer can publish to a key is remote code execution by design.
|
|
493
|
+
|
|
494
|
+
## Testing
|
|
495
|
+
|
|
496
|
+
Istos includes `IstosTestClient` for in-process testing without a Zenoh network:
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
from istos import Istos
|
|
500
|
+
from istos.testing import IstosTestClient
|
|
501
|
+
|
|
502
|
+
istos = Istos()
|
|
503
|
+
|
|
504
|
+
@istos.handle("robot/move")
|
|
505
|
+
async def move(distance: int):
|
|
506
|
+
return {"moved": distance}
|
|
507
|
+
|
|
508
|
+
client = IstosTestClient(istos)
|
|
509
|
+
result = await client.query("robot/move", distance=10)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Run the full test suite:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
uv pip install -e ".[dev]"
|
|
516
|
+
pytest tests/ -m "not integration" # unit tests
|
|
517
|
+
pytest tests/ # includes network integration tests
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Production Features
|
|
521
|
+
|
|
522
|
+
- **Logging** — text or JSON under `istos.*`. We don't reconfigure your root logger unless you ask (`configure_logging=True` / `configure_logging()`).
|
|
523
|
+
- **Health** — `.istos/health` / `.istos/ready` (and `/livez` / `/readyz` with `http_port`)
|
|
524
|
+
- **Metrics** — `.istos/metrics` (and `/metrics`)
|
|
525
|
+
- **Capabilities** — `.istos/capabilities` lists handlers/streams with schemas
|
|
526
|
+
- **HTTP / SSE** — `http_port` + `http=` on handle/stream
|
|
527
|
+
- **Tracing** — optional OTel (`istos[otel]`)
|
|
528
|
+
- **Middleware** — correlation IDs, logging, your own
|
|
529
|
+
- **Exceptions** — `@exception_handler`
|
|
530
|
+
- **Shutdown** — SIGINT/SIGTERM
|
|
531
|
+
- **Storage** — Redis, SQLAlchemy, S3 persist, JWT extras
|
|
532
|
+
|
|
533
|
+
[Deployment](docs/user-guide/deployment.md) · [HTTP Gateway](docs/user-guide/http-gateway.md)
|
|
534
|
+
|
|
535
|
+
## CLI
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
istos new my-service # Scaffold a new project
|
|
539
|
+
istos docs # Serve documentation locally
|
|
540
|
+
istos version # Print installed version
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Contributing
|
|
544
|
+
Contributions and pull requests are welcome! Ensure tests pass and type hints are satisfied.
|
|
545
|
+
|
|
546
|
+
1. Fork the repository
|
|
547
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
548
|
+
3. Commit your changes
|
|
549
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
550
|
+
5. Open a Pull Request
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
**License**: Apache-2.0
|
|
554
|
+
|
|
555
|
+
**Python**: 3.10, 3.11, 3.12, 3.13, and 3.14
|