cbor-rpc 1.0.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.
- cbor_rpc-1.0.0/PKG-INFO +274 -0
- cbor_rpc-1.0.0/README.md +249 -0
- cbor_rpc-1.0.0/cbor_rpc/__init__.py +70 -0
- cbor_rpc-1.0.0/cbor_rpc/event/__init__.py +1 -0
- cbor_rpc-1.0.0/cbor_rpc/event/emitter.py +68 -0
- cbor_rpc-1.0.0/cbor_rpc/pipe/__init__.py +3 -0
- cbor_rpc-1.0.0/cbor_rpc/pipe/aio_pipe.py +198 -0
- cbor_rpc-1.0.0/cbor_rpc/pipe/event_pipe.py +75 -0
- cbor_rpc-1.0.0/cbor_rpc/pipe/pipe.py +120 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/__init__.py +8 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/context.py +16 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/logging.py +49 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_base.py +68 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_call.py +115 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_server.py +78 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_v1.py +285 -0
- cbor_rpc-1.0.0/cbor_rpc/rpc/server_base.py +90 -0
- cbor_rpc-1.0.0/cbor_rpc/ssh/__init__.py +1 -0
- cbor_rpc-1.0.0/cbor_rpc/ssh/ssh_pipe.py +198 -0
- cbor_rpc-1.0.0/cbor_rpc/stdio/__init__.py +3 -0
- cbor_rpc-1.0.0/cbor_rpc/stdio/stdio_pipe.py +76 -0
- cbor_rpc-1.0.0/cbor_rpc/tcp/__init__.py +3 -0
- cbor_rpc-1.0.0/cbor_rpc/tcp/tcp.py +267 -0
- cbor_rpc-1.0.0/cbor_rpc/timed_promise.py +60 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/__init__.py +3 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/base/__init__.py +4 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/base/base_exception.py +2 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/base/event_transformer_pipe.py +79 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/base/transformer_base.py +107 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/base/transformer_pipe.py +89 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/cbor_transformer.py +94 -0
- cbor_rpc-1.0.0/cbor_rpc/transformer/json_transformer.py +85 -0
- cbor_rpc-1.0.0/cbor_rpc.egg-info/PKG-INFO +274 -0
- cbor_rpc-1.0.0/cbor_rpc.egg-info/SOURCES.txt +38 -0
- cbor_rpc-1.0.0/cbor_rpc.egg-info/dependency_links.txt +1 -0
- cbor_rpc-1.0.0/cbor_rpc.egg-info/requires.txt +14 -0
- cbor_rpc-1.0.0/cbor_rpc.egg-info/top_level.txt +1 -0
- cbor_rpc-1.0.0/pyproject.toml +63 -0
- cbor_rpc-1.0.0/setup.cfg +4 -0
- cbor_rpc-1.0.0/setup.py +5 -0
cbor_rpc-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cbor-rpc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An async-compatible CBOR-based RPC system
|
|
5
|
+
Author-email: Sudip Bhattarai <sudip@bhattarai.me>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/mesudip/cbor-rpc-py
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: asyncssh>=2.14.0
|
|
14
|
+
Requires-Dist: bcrypt
|
|
15
|
+
Requires-Dist: cbor2
|
|
16
|
+
Requires-Dist: ijson[yajl]
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=8.3.2; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "test"
|
|
21
|
+
Requires-Dist: docker; extra == "test"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
cbor-rpc
|
|
27
|
+
========
|
|
28
|
+
[](https://codecov.io/github/mesudip/cbor-rpc-py)
|
|
29
|
+
|
|
30
|
+
A lightweight, event-based RPC framework for Python using CBOR (optionally JSON) over various transport layers.
|
|
31
|
+
|
|
32
|
+
## Table of Contents
|
|
33
|
+
- [RPC System](#rpc-system)
|
|
34
|
+
- [Capabilities](#capabilities)
|
|
35
|
+
- [Creating a Server](#creating-a-server)
|
|
36
|
+
- [Creating a Client](#creating-a-client)
|
|
37
|
+
- [Pipes and Event Pipes](#pipes-and-event-pipes)
|
|
38
|
+
- [Transformers](#transformers)
|
|
39
|
+
- [High-level Pipes](#high-level-pipes)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## RPC System
|
|
44
|
+
|
|
45
|
+
The RPC system is built on top of `EventPipe` and `Transformers`.
|
|
46
|
+
|
|
47
|
+
### Capabilities
|
|
48
|
+
- **Bidirectional**: Both sides call rpc methods, both side can emit events.
|
|
49
|
+
- **Logs & Progress**: Real-time streaming of log messages and progress updates during a rpc call.
|
|
50
|
+
- **Events**: Broadcast and listen to topics.
|
|
51
|
+
- **Async/Await**: Native support for Python's `asyncio`.
|
|
52
|
+
- **Method Cancellation**: Long-running calls can be cancelled by the caller.
|
|
53
|
+
|
|
54
|
+
### Creating a Server
|
|
55
|
+
Extend `RpcV1Server` and implement `handle_method_call`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import asyncio
|
|
59
|
+
from cbor_rpc import RpcV1Server, TcpServer, CborStreamTransformer,RpcCallContext
|
|
60
|
+
|
|
61
|
+
class MyService(RpcV1Server):
|
|
62
|
+
def __init__(self, tcp_server: TcpServer):
|
|
63
|
+
super().__init__()
|
|
64
|
+
# Configure server to handle new connections
|
|
65
|
+
tcp_server.on("connection", self.on_connection)
|
|
66
|
+
|
|
67
|
+
async def on_connection(self, tcp_pipe):
|
|
68
|
+
print(f"New connection from {tcp_pipe.get_peer_info()}")
|
|
69
|
+
rpc_pipe = CborStreamTransformer().apply_transformer(tcp_pipe)
|
|
70
|
+
|
|
71
|
+
# Add connection to RPC system
|
|
72
|
+
conn_id = str(tcp_pipe.get_peer_info())
|
|
73
|
+
await self.add_connection(conn_id, rpc_pipe)
|
|
74
|
+
|
|
75
|
+
async def handle_method_call(self, connection_id, context: RpcCallContext, method, args):
|
|
76
|
+
if method == "add":
|
|
77
|
+
return args[0] + args[1]
|
|
78
|
+
raise Exception("Unknown method")
|
|
79
|
+
|
|
80
|
+
async def run_server():
|
|
81
|
+
server = await TcpServer.create("0.0.0.0", 9000)
|
|
82
|
+
service = MyService(server)
|
|
83
|
+
print("Server running on 9000...")
|
|
84
|
+
await asyncio.Future() # block forever
|
|
85
|
+
|
|
86
|
+
# asyncio.run(run_server())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Creating a Client
|
|
90
|
+
Use the `RpcV1` class to wrap an object-oriented pipe.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
from typing import Any, List
|
|
95
|
+
from cbor_rpc import RpcV1, RpcCallContext, TcpPipe, CborStreamTransformer
|
|
96
|
+
|
|
97
|
+
# 1. Define Client with Methods (Bidirectional)
|
|
98
|
+
class MyClient(RpcV1):
|
|
99
|
+
def get_id(self) -> str:
|
|
100
|
+
return "client-node"
|
|
101
|
+
|
|
102
|
+
async def handle_method_call(self, context: RpcCallContext, method: str, args: List[Any]) -> Any:
|
|
103
|
+
# Handle calls FROM the server
|
|
104
|
+
if method == "ping":
|
|
105
|
+
return "pong"
|
|
106
|
+
raise Exception(f"Unknown method {method}")
|
|
107
|
+
|
|
108
|
+
async def on_event(self, topic: str, message: Any) -> None:
|
|
109
|
+
print(f"Event: {topic} -> {message}")
|
|
110
|
+
|
|
111
|
+
async def run_client():
|
|
112
|
+
# 2. Connect via TCP
|
|
113
|
+
tcp_pipe = await TcpPipe.create_connection("localhost", 9000)
|
|
114
|
+
|
|
115
|
+
# 3. Apply CBOR Transformer
|
|
116
|
+
cbor_pipe = CborStreamTransformer().apply_transformer(tcp_pipe)
|
|
117
|
+
|
|
118
|
+
# 4. Instantiate Custom Client
|
|
119
|
+
client = MyClient(cbor_pipe)
|
|
120
|
+
|
|
121
|
+
# 5. Make calls (Client -> Server)
|
|
122
|
+
result = await client.call_method("add", 5, 10)
|
|
123
|
+
print(f"5 + 10 = {result}")
|
|
124
|
+
|
|
125
|
+
# Call with logs and progress
|
|
126
|
+
handle = client.create_call("long_task")
|
|
127
|
+
handle.on_log(lambda level, msg: print(f"LOG: {msg}"))
|
|
128
|
+
handle.on_progress(lambda val, meta: print(f"Progress: {val}%"))
|
|
129
|
+
|
|
130
|
+
# await handle.call()
|
|
131
|
+
|
|
132
|
+
# asyncio.run(run_client())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Pipes and Event Pipes
|
|
138
|
+
|
|
139
|
+
The core of `cbor-rpc` is the **Pipe** abstraction. Unlike traditional unidirectional pipes, a **Pipe** in this framework represents a **duplex connection**. It allows you to both:
|
|
140
|
+
- **Write** messages to the remote side.
|
|
141
|
+
- **Read** replies or incoming messages from the remote side.
|
|
142
|
+
|
|
143
|
+
This abstraction provides a consistent interface for bidirectional communication across different transport layers (TCP streams, SSH channels, Stdio, etc.).
|
|
144
|
+
|
|
145
|
+
### Basic Usage (`EventPipe`)
|
|
146
|
+
Most "real-world" pipes (TCP, SSH, Stdio) are `EventPipe`s. They are event-driven, meaning you register listeners for incoming data instead of polling.
|
|
147
|
+
|
|
148
|
+
#### Consuming Data
|
|
149
|
+
There are two ways to listen for data:
|
|
150
|
+
1. **`pipeline("data", handler)`**: Used for serial processing. Handlers are awaited in order. If a pipeline handler throws an error, it stops the chain and emits an `"error"`.
|
|
151
|
+
2. **`on("data", handler)`**: Simple pub/sub. The handler is called whenever data arrives. If it's a coroutine, it's run in the background.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Simple listener
|
|
155
|
+
pipe.on("data", lambda chunk: print(f"Received {len(chunk)} bytes"))
|
|
156
|
+
|
|
157
|
+
# Serial processing (e.g., for transformers)
|
|
158
|
+
async def process_data(chunk):
|
|
159
|
+
# This is awaited before the next chunk is processed
|
|
160
|
+
await do_something(chunk)
|
|
161
|
+
|
|
162
|
+
pipe.pipeline("data", process_data)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Sending Data
|
|
166
|
+
Use the `write()` method to send data through the pipe.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
await pipe.write(b"Request data")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Converting a `Pipe` to an `EventPipe`
|
|
173
|
+
If you have a raw `Pipe` (which uses `read()`/`write()`), you can convert it to an `EventPipe` using `make_event_based()`:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
event_pipe = raw_pipe.make_event_based()
|
|
177
|
+
event_pipe.on("data", handle_incoming)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Transformers
|
|
181
|
+
|
|
182
|
+
Transformers allow you to convert raw data (typically `bytes`) into high-level Python objects and vice-versa.
|
|
183
|
+
|
|
184
|
+
### Available Transformers
|
|
185
|
+
The library comes with two built-in transformer types:
|
|
186
|
+
- **`JsonTransformer`** / **`JsonStreamTransformer`**: Encodes/decodes JSON data.
|
|
187
|
+
- **`CborTransformer`** / **`CborStreamTransformer`**: Encodes/decodes CBOR data (ideal for binary efficiency).
|
|
188
|
+
|
|
189
|
+
### Stream Support
|
|
190
|
+
It is important to note that **not all transformers support streams**.
|
|
191
|
+
- A standard **`Transformer`** (like `JsonTransformer`) expects a complete message in each chunk. It maps 1 input -> 1 output.
|
|
192
|
+
- A **Stream Transformer** (like `JsonStreamTransformer`) is designed to handle fragmented data (e.g., from TCP). It buffers incoming bytes until a complete message can be decoded.
|
|
193
|
+
|
|
194
|
+
**When using TCP or SSH pipes, you almost always want to use a `...StreamTransformer`.**
|
|
195
|
+
|
|
196
|
+
### Using Transformers
|
|
197
|
+
You can wrap a byte-based pipe with a transformer to create an object-based pipe.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from cbor_rpc.transformer.json_transformer import JsonStreamTransformer
|
|
201
|
+
|
|
202
|
+
# Wrap a raw pipe
|
|
203
|
+
object_pipe = JsonStreamTransformer().apply_transformer(raw_pipe)
|
|
204
|
+
|
|
205
|
+
# Now 'data' events emit Python objects
|
|
206
|
+
object_pipe.on("data", lambda obj: print(f"Received object: {obj}"))
|
|
207
|
+
await object_pipe.write({"method": "hello", "params": []})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Making a Custom Transformer
|
|
211
|
+
To create your own transformer, subclass `Transformer` (for single packets) or `AsyncTransformer` (for streams):
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from cbor_rpc.transformer.base import Transformer
|
|
215
|
+
|
|
216
|
+
class MyUpperTransformer(Transformer[str, str]):
|
|
217
|
+
def encode(self, data: str) -> str:
|
|
218
|
+
return data.upper()
|
|
219
|
+
|
|
220
|
+
def decode(self, data: str) -> str:
|
|
221
|
+
return data.lower()
|
|
222
|
+
```
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## High-level Pipes
|
|
226
|
+
|
|
227
|
+
`cbor-rpc` provides several ready-to-use pipe implementations for different transport layers.
|
|
228
|
+
|
|
229
|
+
### TCP Pipe (`TcpPipe`)
|
|
230
|
+
Used for network communication over TCP.
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from cbor_rpc.tcp import TcpPipe
|
|
234
|
+
|
|
235
|
+
# Client
|
|
236
|
+
pipe = await TcpPipe.create_connection("localhost", 8000)
|
|
237
|
+
|
|
238
|
+
# Server
|
|
239
|
+
from cbor_rpc.tcp import TcpServer
|
|
240
|
+
class MyServer(TcpServer):
|
|
241
|
+
async def accept(self, pipe: TcpPipe) -> bool:
|
|
242
|
+
print("New connection!")
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
server = await MyServer.create("0.0.0.0", 8000)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### SSH Pipe (`SshPipe`)
|
|
249
|
+
Tunneling through SSH using `asyncssh`.
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from cbor_rpc.ssh import SshPipe
|
|
253
|
+
# Used typically to run a command on a remote host and communicate with it
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Stdio Pipe (`StdioPipe`)
|
|
257
|
+
Communicate with subprocesses via stdin/stdout.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from cbor_rpc.stdio import StdioPipe
|
|
261
|
+
|
|
262
|
+
# Start a subprocess
|
|
263
|
+
pipe = await StdioPipe.start_process("python3", "worker.py")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
Development
|
|
269
|
+
-----------
|
|
270
|
+
Enable local git hooks to auto-format on commit:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
pre-commit install
|
|
274
|
+
```
|
cbor_rpc-1.0.0/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
cbor-rpc
|
|
2
|
+
========
|
|
3
|
+
[](https://codecov.io/github/mesudip/cbor-rpc-py)
|
|
4
|
+
|
|
5
|
+
A lightweight, event-based RPC framework for Python using CBOR (optionally JSON) over various transport layers.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
- [RPC System](#rpc-system)
|
|
9
|
+
- [Capabilities](#capabilities)
|
|
10
|
+
- [Creating a Server](#creating-a-server)
|
|
11
|
+
- [Creating a Client](#creating-a-client)
|
|
12
|
+
- [Pipes and Event Pipes](#pipes-and-event-pipes)
|
|
13
|
+
- [Transformers](#transformers)
|
|
14
|
+
- [High-level Pipes](#high-level-pipes)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## RPC System
|
|
19
|
+
|
|
20
|
+
The RPC system is built on top of `EventPipe` and `Transformers`.
|
|
21
|
+
|
|
22
|
+
### Capabilities
|
|
23
|
+
- **Bidirectional**: Both sides call rpc methods, both side can emit events.
|
|
24
|
+
- **Logs & Progress**: Real-time streaming of log messages and progress updates during a rpc call.
|
|
25
|
+
- **Events**: Broadcast and listen to topics.
|
|
26
|
+
- **Async/Await**: Native support for Python's `asyncio`.
|
|
27
|
+
- **Method Cancellation**: Long-running calls can be cancelled by the caller.
|
|
28
|
+
|
|
29
|
+
### Creating a Server
|
|
30
|
+
Extend `RpcV1Server` and implement `handle_method_call`.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from cbor_rpc import RpcV1Server, TcpServer, CborStreamTransformer,RpcCallContext
|
|
35
|
+
|
|
36
|
+
class MyService(RpcV1Server):
|
|
37
|
+
def __init__(self, tcp_server: TcpServer):
|
|
38
|
+
super().__init__()
|
|
39
|
+
# Configure server to handle new connections
|
|
40
|
+
tcp_server.on("connection", self.on_connection)
|
|
41
|
+
|
|
42
|
+
async def on_connection(self, tcp_pipe):
|
|
43
|
+
print(f"New connection from {tcp_pipe.get_peer_info()}")
|
|
44
|
+
rpc_pipe = CborStreamTransformer().apply_transformer(tcp_pipe)
|
|
45
|
+
|
|
46
|
+
# Add connection to RPC system
|
|
47
|
+
conn_id = str(tcp_pipe.get_peer_info())
|
|
48
|
+
await self.add_connection(conn_id, rpc_pipe)
|
|
49
|
+
|
|
50
|
+
async def handle_method_call(self, connection_id, context: RpcCallContext, method, args):
|
|
51
|
+
if method == "add":
|
|
52
|
+
return args[0] + args[1]
|
|
53
|
+
raise Exception("Unknown method")
|
|
54
|
+
|
|
55
|
+
async def run_server():
|
|
56
|
+
server = await TcpServer.create("0.0.0.0", 9000)
|
|
57
|
+
service = MyService(server)
|
|
58
|
+
print("Server running on 9000...")
|
|
59
|
+
await asyncio.Future() # block forever
|
|
60
|
+
|
|
61
|
+
# asyncio.run(run_server())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Creating a Client
|
|
65
|
+
Use the `RpcV1` class to wrap an object-oriented pipe.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
from typing import Any, List
|
|
70
|
+
from cbor_rpc import RpcV1, RpcCallContext, TcpPipe, CborStreamTransformer
|
|
71
|
+
|
|
72
|
+
# 1. Define Client with Methods (Bidirectional)
|
|
73
|
+
class MyClient(RpcV1):
|
|
74
|
+
def get_id(self) -> str:
|
|
75
|
+
return "client-node"
|
|
76
|
+
|
|
77
|
+
async def handle_method_call(self, context: RpcCallContext, method: str, args: List[Any]) -> Any:
|
|
78
|
+
# Handle calls FROM the server
|
|
79
|
+
if method == "ping":
|
|
80
|
+
return "pong"
|
|
81
|
+
raise Exception(f"Unknown method {method}")
|
|
82
|
+
|
|
83
|
+
async def on_event(self, topic: str, message: Any) -> None:
|
|
84
|
+
print(f"Event: {topic} -> {message}")
|
|
85
|
+
|
|
86
|
+
async def run_client():
|
|
87
|
+
# 2. Connect via TCP
|
|
88
|
+
tcp_pipe = await TcpPipe.create_connection("localhost", 9000)
|
|
89
|
+
|
|
90
|
+
# 3. Apply CBOR Transformer
|
|
91
|
+
cbor_pipe = CborStreamTransformer().apply_transformer(tcp_pipe)
|
|
92
|
+
|
|
93
|
+
# 4. Instantiate Custom Client
|
|
94
|
+
client = MyClient(cbor_pipe)
|
|
95
|
+
|
|
96
|
+
# 5. Make calls (Client -> Server)
|
|
97
|
+
result = await client.call_method("add", 5, 10)
|
|
98
|
+
print(f"5 + 10 = {result}")
|
|
99
|
+
|
|
100
|
+
# Call with logs and progress
|
|
101
|
+
handle = client.create_call("long_task")
|
|
102
|
+
handle.on_log(lambda level, msg: print(f"LOG: {msg}"))
|
|
103
|
+
handle.on_progress(lambda val, meta: print(f"Progress: {val}%"))
|
|
104
|
+
|
|
105
|
+
# await handle.call()
|
|
106
|
+
|
|
107
|
+
# asyncio.run(run_client())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Pipes and Event Pipes
|
|
113
|
+
|
|
114
|
+
The core of `cbor-rpc` is the **Pipe** abstraction. Unlike traditional unidirectional pipes, a **Pipe** in this framework represents a **duplex connection**. It allows you to both:
|
|
115
|
+
- **Write** messages to the remote side.
|
|
116
|
+
- **Read** replies or incoming messages from the remote side.
|
|
117
|
+
|
|
118
|
+
This abstraction provides a consistent interface for bidirectional communication across different transport layers (TCP streams, SSH channels, Stdio, etc.).
|
|
119
|
+
|
|
120
|
+
### Basic Usage (`EventPipe`)
|
|
121
|
+
Most "real-world" pipes (TCP, SSH, Stdio) are `EventPipe`s. They are event-driven, meaning you register listeners for incoming data instead of polling.
|
|
122
|
+
|
|
123
|
+
#### Consuming Data
|
|
124
|
+
There are two ways to listen for data:
|
|
125
|
+
1. **`pipeline("data", handler)`**: Used for serial processing. Handlers are awaited in order. If a pipeline handler throws an error, it stops the chain and emits an `"error"`.
|
|
126
|
+
2. **`on("data", handler)`**: Simple pub/sub. The handler is called whenever data arrives. If it's a coroutine, it's run in the background.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Simple listener
|
|
130
|
+
pipe.on("data", lambda chunk: print(f"Received {len(chunk)} bytes"))
|
|
131
|
+
|
|
132
|
+
# Serial processing (e.g., for transformers)
|
|
133
|
+
async def process_data(chunk):
|
|
134
|
+
# This is awaited before the next chunk is processed
|
|
135
|
+
await do_something(chunk)
|
|
136
|
+
|
|
137
|
+
pipe.pipeline("data", process_data)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Sending Data
|
|
141
|
+
Use the `write()` method to send data through the pipe.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
await pipe.write(b"Request data")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Converting a `Pipe` to an `EventPipe`
|
|
148
|
+
If you have a raw `Pipe` (which uses `read()`/`write()`), you can convert it to an `EventPipe` using `make_event_based()`:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
event_pipe = raw_pipe.make_event_based()
|
|
152
|
+
event_pipe.on("data", handle_incoming)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Transformers
|
|
156
|
+
|
|
157
|
+
Transformers allow you to convert raw data (typically `bytes`) into high-level Python objects and vice-versa.
|
|
158
|
+
|
|
159
|
+
### Available Transformers
|
|
160
|
+
The library comes with two built-in transformer types:
|
|
161
|
+
- **`JsonTransformer`** / **`JsonStreamTransformer`**: Encodes/decodes JSON data.
|
|
162
|
+
- **`CborTransformer`** / **`CborStreamTransformer`**: Encodes/decodes CBOR data (ideal for binary efficiency).
|
|
163
|
+
|
|
164
|
+
### Stream Support
|
|
165
|
+
It is important to note that **not all transformers support streams**.
|
|
166
|
+
- A standard **`Transformer`** (like `JsonTransformer`) expects a complete message in each chunk. It maps 1 input -> 1 output.
|
|
167
|
+
- A **Stream Transformer** (like `JsonStreamTransformer`) is designed to handle fragmented data (e.g., from TCP). It buffers incoming bytes until a complete message can be decoded.
|
|
168
|
+
|
|
169
|
+
**When using TCP or SSH pipes, you almost always want to use a `...StreamTransformer`.**
|
|
170
|
+
|
|
171
|
+
### Using Transformers
|
|
172
|
+
You can wrap a byte-based pipe with a transformer to create an object-based pipe.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from cbor_rpc.transformer.json_transformer import JsonStreamTransformer
|
|
176
|
+
|
|
177
|
+
# Wrap a raw pipe
|
|
178
|
+
object_pipe = JsonStreamTransformer().apply_transformer(raw_pipe)
|
|
179
|
+
|
|
180
|
+
# Now 'data' events emit Python objects
|
|
181
|
+
object_pipe.on("data", lambda obj: print(f"Received object: {obj}"))
|
|
182
|
+
await object_pipe.write({"method": "hello", "params": []})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Making a Custom Transformer
|
|
186
|
+
To create your own transformer, subclass `Transformer` (for single packets) or `AsyncTransformer` (for streams):
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from cbor_rpc.transformer.base import Transformer
|
|
190
|
+
|
|
191
|
+
class MyUpperTransformer(Transformer[str, str]):
|
|
192
|
+
def encode(self, data: str) -> str:
|
|
193
|
+
return data.upper()
|
|
194
|
+
|
|
195
|
+
def decode(self, data: str) -> str:
|
|
196
|
+
return data.lower()
|
|
197
|
+
```
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## High-level Pipes
|
|
201
|
+
|
|
202
|
+
`cbor-rpc` provides several ready-to-use pipe implementations for different transport layers.
|
|
203
|
+
|
|
204
|
+
### TCP Pipe (`TcpPipe`)
|
|
205
|
+
Used for network communication over TCP.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from cbor_rpc.tcp import TcpPipe
|
|
209
|
+
|
|
210
|
+
# Client
|
|
211
|
+
pipe = await TcpPipe.create_connection("localhost", 8000)
|
|
212
|
+
|
|
213
|
+
# Server
|
|
214
|
+
from cbor_rpc.tcp import TcpServer
|
|
215
|
+
class MyServer(TcpServer):
|
|
216
|
+
async def accept(self, pipe: TcpPipe) -> bool:
|
|
217
|
+
print("New connection!")
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
server = await MyServer.create("0.0.0.0", 8000)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### SSH Pipe (`SshPipe`)
|
|
224
|
+
Tunneling through SSH using `asyncssh`.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from cbor_rpc.ssh import SshPipe
|
|
228
|
+
# Used typically to run a command on a remote host and communicate with it
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Stdio Pipe (`StdioPipe`)
|
|
232
|
+
Communicate with subprocesses via stdin/stdout.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from cbor_rpc.stdio import StdioPipe
|
|
236
|
+
|
|
237
|
+
# Start a subprocess
|
|
238
|
+
pipe = await StdioPipe.start_process("python3", "worker.py")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
Development
|
|
244
|
+
-----------
|
|
245
|
+
Enable local git hooks to auto-format on commit:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
pre-commit install
|
|
249
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CBOR-RPC: An async-compatible CBOR-based RPC system
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .event import AbstractEmitter
|
|
6
|
+
from .pipe import AioPipe, EventPipe, Pipe
|
|
7
|
+
from .timed_promise import TimedPromise
|
|
8
|
+
from .tcp import TcpPipe, TcpServer
|
|
9
|
+
from .stdio import StdioPipe
|
|
10
|
+
from .ssh import SshPipe, SshServer
|
|
11
|
+
from .transformer import (
|
|
12
|
+
AsyncTransformer,
|
|
13
|
+
CborStreamTransformer,
|
|
14
|
+
CborTransformer,
|
|
15
|
+
EventTransformerPipe,
|
|
16
|
+
JsonStreamTransformer,
|
|
17
|
+
JsonTransformer,
|
|
18
|
+
Transformer,
|
|
19
|
+
TransformerPipe,
|
|
20
|
+
)
|
|
21
|
+
from .rpc import (
|
|
22
|
+
RpcInitClient,
|
|
23
|
+
RpcClient,
|
|
24
|
+
RpcServer,
|
|
25
|
+
RpcV1,
|
|
26
|
+
RpcV1Server,
|
|
27
|
+
Server,
|
|
28
|
+
RpcCallContext,
|
|
29
|
+
RpcLogger,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Promise
|
|
34
|
+
"TimedPromise",
|
|
35
|
+
# Emitter
|
|
36
|
+
"AbstractEmitter",
|
|
37
|
+
# Pipe abstract classes
|
|
38
|
+
"AioPipe",
|
|
39
|
+
"EventPipe",
|
|
40
|
+
"Pipe",
|
|
41
|
+
# Server abstract classes
|
|
42
|
+
"Server",
|
|
43
|
+
# Rpc abstract classes
|
|
44
|
+
"RpcInitClient",
|
|
45
|
+
"RpcClient",
|
|
46
|
+
"RpcServer",
|
|
47
|
+
# Rpc base implementation
|
|
48
|
+
"RpcV1",
|
|
49
|
+
"RpcV1Server",
|
|
50
|
+
# Rpc high level
|
|
51
|
+
"RpcCallContext",
|
|
52
|
+
"RpcLogger",
|
|
53
|
+
# Pipe implementations
|
|
54
|
+
"TcpPipe",
|
|
55
|
+
"TcpServer",
|
|
56
|
+
"StdioPipe",
|
|
57
|
+
"SshPipe",
|
|
58
|
+
"SshServer",
|
|
59
|
+
# Transformers
|
|
60
|
+
"Transformer",
|
|
61
|
+
"AsyncTransformer",
|
|
62
|
+
"JsonTransformer",
|
|
63
|
+
"JsonStreamTransformer",
|
|
64
|
+
"CborTransformer",
|
|
65
|
+
"CborStreamTransformer",
|
|
66
|
+
"TransformerPipe",
|
|
67
|
+
"EventTransformerPipe",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .emitter import AbstractEmitter
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Callable
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import traceback
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AbstractEmitter(ABC):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._pipelines: Dict[str, List[Callable]] = {}
|
|
12
|
+
self._subscribers: Dict[str, List[Callable]] = {}
|
|
13
|
+
|
|
14
|
+
def unsubscribe(self, event: str, handler: Callable) -> None:
|
|
15
|
+
if event in self._subscribers:
|
|
16
|
+
self._subscribers[event] = [h for h in self._subscribers[event] if h != handler]
|
|
17
|
+
if event in self._pipelines:
|
|
18
|
+
self._pipelines[event] = [h for h in self._pipelines[event] if h != handler]
|
|
19
|
+
|
|
20
|
+
def replace_on_handler(self, event_type: str, handler: Callable) -> None:
|
|
21
|
+
self._subscribers[event_type] = [handler]
|
|
22
|
+
|
|
23
|
+
def _run_background_task(self, coro: Callable[..., Any], *args: Any) -> None:
|
|
24
|
+
async def runner():
|
|
25
|
+
try:
|
|
26
|
+
await coro(*args)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
traceback.print_exc()
|
|
29
|
+
warnings.warn(f"Background task error in handler: {e}", RuntimeWarning)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
loop = asyncio.get_running_loop()
|
|
33
|
+
except RuntimeError:
|
|
34
|
+
warnings.warn("Background task skipped: no running event loop", RuntimeWarning)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
loop.create_task(runner())
|
|
38
|
+
|
|
39
|
+
def _emit(self, event_type: str, *args: Any) -> None:
|
|
40
|
+
for sub in self._subscribers.get(event_type, []):
|
|
41
|
+
try:
|
|
42
|
+
if inspect.iscoroutinefunction(sub):
|
|
43
|
+
self._run_background_task(sub, *args)
|
|
44
|
+
else:
|
|
45
|
+
sub(*args)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
traceback.print_exc()
|
|
48
|
+
warnings.warn(f"Synchronous error in handler: {e}", RuntimeWarning)
|
|
49
|
+
|
|
50
|
+
async def _notify(self, event_type: str, *args: Any) -> None:
|
|
51
|
+
""" """
|
|
52
|
+
for pipeline in self._pipelines.get(event_type, []):
|
|
53
|
+
try:
|
|
54
|
+
if inspect.iscoroutinefunction(pipeline):
|
|
55
|
+
await pipeline(*args)
|
|
56
|
+
else:
|
|
57
|
+
pipeline(*args)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self._emit("error", e)
|
|
60
|
+
raise e
|
|
61
|
+
|
|
62
|
+
self._emit(event_type, *args)
|
|
63
|
+
|
|
64
|
+
def on(self, event: str, handler: Callable) -> None:
|
|
65
|
+
self._subscribers.setdefault(event, []).append(handler)
|
|
66
|
+
|
|
67
|
+
def pipeline(self, event: str, handler: Callable) -> None:
|
|
68
|
+
self._pipelines.setdefault(event, []).append(handler)
|