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.
Files changed (40) hide show
  1. cbor_rpc-1.0.0/PKG-INFO +274 -0
  2. cbor_rpc-1.0.0/README.md +249 -0
  3. cbor_rpc-1.0.0/cbor_rpc/__init__.py +70 -0
  4. cbor_rpc-1.0.0/cbor_rpc/event/__init__.py +1 -0
  5. cbor_rpc-1.0.0/cbor_rpc/event/emitter.py +68 -0
  6. cbor_rpc-1.0.0/cbor_rpc/pipe/__init__.py +3 -0
  7. cbor_rpc-1.0.0/cbor_rpc/pipe/aio_pipe.py +198 -0
  8. cbor_rpc-1.0.0/cbor_rpc/pipe/event_pipe.py +75 -0
  9. cbor_rpc-1.0.0/cbor_rpc/pipe/pipe.py +120 -0
  10. cbor_rpc-1.0.0/cbor_rpc/rpc/__init__.py +8 -0
  11. cbor_rpc-1.0.0/cbor_rpc/rpc/context.py +16 -0
  12. cbor_rpc-1.0.0/cbor_rpc/rpc/logging.py +49 -0
  13. cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_base.py +68 -0
  14. cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_call.py +115 -0
  15. cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_server.py +78 -0
  16. cbor_rpc-1.0.0/cbor_rpc/rpc/rpc_v1.py +285 -0
  17. cbor_rpc-1.0.0/cbor_rpc/rpc/server_base.py +90 -0
  18. cbor_rpc-1.0.0/cbor_rpc/ssh/__init__.py +1 -0
  19. cbor_rpc-1.0.0/cbor_rpc/ssh/ssh_pipe.py +198 -0
  20. cbor_rpc-1.0.0/cbor_rpc/stdio/__init__.py +3 -0
  21. cbor_rpc-1.0.0/cbor_rpc/stdio/stdio_pipe.py +76 -0
  22. cbor_rpc-1.0.0/cbor_rpc/tcp/__init__.py +3 -0
  23. cbor_rpc-1.0.0/cbor_rpc/tcp/tcp.py +267 -0
  24. cbor_rpc-1.0.0/cbor_rpc/timed_promise.py +60 -0
  25. cbor_rpc-1.0.0/cbor_rpc/transformer/__init__.py +3 -0
  26. cbor_rpc-1.0.0/cbor_rpc/transformer/base/__init__.py +4 -0
  27. cbor_rpc-1.0.0/cbor_rpc/transformer/base/base_exception.py +2 -0
  28. cbor_rpc-1.0.0/cbor_rpc/transformer/base/event_transformer_pipe.py +79 -0
  29. cbor_rpc-1.0.0/cbor_rpc/transformer/base/transformer_base.py +107 -0
  30. cbor_rpc-1.0.0/cbor_rpc/transformer/base/transformer_pipe.py +89 -0
  31. cbor_rpc-1.0.0/cbor_rpc/transformer/cbor_transformer.py +94 -0
  32. cbor_rpc-1.0.0/cbor_rpc/transformer/json_transformer.py +85 -0
  33. cbor_rpc-1.0.0/cbor_rpc.egg-info/PKG-INFO +274 -0
  34. cbor_rpc-1.0.0/cbor_rpc.egg-info/SOURCES.txt +38 -0
  35. cbor_rpc-1.0.0/cbor_rpc.egg-info/dependency_links.txt +1 -0
  36. cbor_rpc-1.0.0/cbor_rpc.egg-info/requires.txt +14 -0
  37. cbor_rpc-1.0.0/cbor_rpc.egg-info/top_level.txt +1 -0
  38. cbor_rpc-1.0.0/pyproject.toml +63 -0
  39. cbor_rpc-1.0.0/setup.cfg +4 -0
  40. cbor_rpc-1.0.0/setup.py +5 -0
@@ -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
+ [![codecov](https://codecov.io/github/mesudip/cbor-rpc-py/graph/badge.svg)](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
+ ```
@@ -0,0 +1,249 @@
1
+ cbor-rpc
2
+ ========
3
+ [![codecov](https://codecov.io/github/mesudip/cbor-rpc-py/graph/badge.svg)](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)
@@ -0,0 +1,3 @@
1
+ from .event_pipe import EventPipe
2
+ from .pipe import Pipe
3
+ from .aio_pipe import AioPipe