rpc-python-toolkit 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.
- {rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info → rpc_python_toolkit-0.2.0}/PKG-INFO +19 -3
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/README.md +18 -2
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/pyproject.toml +1 -1
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/__init__.py +2 -2
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/endpoint.py +54 -3
- rpc_python_toolkit-0.2.0/src/rpc_python_toolkit/version.py +1 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0/src/rpc_python_toolkit.egg-info}/PKG-INFO +19 -3
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/tests/test_core.py +62 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/version.py +0 -2
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/LICENSE +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/setup.cfg +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/client.py +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/errors.py +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/protocol.py +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/serialization.py +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/validation.py +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/SOURCES.txt +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/dependency_links.txt +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/requires.txt +0 -0
- {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/top_level.txt +0 -0
{rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info → rpc_python_toolkit-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpc-python-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability.
|
|
5
5
|
Author: Nicola Carpanese
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,13 +25,19 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
# RPC Python Toolkit
|
|
27
27
|
|
|
28
|
+
[](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
30
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
33
|
+
|
|
28
34
|
Framework-agnostic JSON-RPC 2.0 toolkit for Python.
|
|
29
35
|
|
|
30
36
|
Use this package when you need a small Python JSON-RPC 2.0 client/server core that can be embedded directly or adapted to HTTP frameworks. It follows the same protocol conventions used across the RPC Toolkit ecosystem.
|
|
31
37
|
|
|
32
38
|
## Project Status
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
Alpha package with the core JSON-RPC behavior implemented.
|
|
35
41
|
|
|
36
42
|
- Published on PyPI as `rpc-python-toolkit`.
|
|
37
43
|
- Standard JSON-RPC 2.0 remains the default behavior.
|
|
@@ -80,7 +86,11 @@ python -m unittest discover
|
|
|
80
86
|
```python
|
|
81
87
|
from rpc_python_toolkit import RpcEndpoint
|
|
82
88
|
|
|
83
|
-
rpc = RpcEndpoint(
|
|
89
|
+
rpc = RpcEndpoint(
|
|
90
|
+
enable_introspection=True,
|
|
91
|
+
enable_batch=True,
|
|
92
|
+
max_batch_size=100,
|
|
93
|
+
)
|
|
84
94
|
|
|
85
95
|
def test(request, context, params):
|
|
86
96
|
return {"ok": True, "params": params}
|
|
@@ -97,6 +107,12 @@ response = rpc.handle_payload({
|
|
|
97
107
|
print(response.body)
|
|
98
108
|
```
|
|
99
109
|
|
|
110
|
+
Batch can be disabled with `enable_batch=False`. Introspection remains opt-in.
|
|
111
|
+
When both application and request contexts are dictionaries, handlers receive a
|
|
112
|
+
merged dictionary and request values take precedence. For a non-dictionary
|
|
113
|
+
application context plus request data, handlers receive `RpcCallContext` with
|
|
114
|
+
separate `application` and `request` attributes.
|
|
115
|
+
|
|
100
116
|
Result:
|
|
101
117
|
|
|
102
118
|
```json
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# RPC Python Toolkit
|
|
2
2
|
|
|
3
|
+
[](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
5
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
8
|
+
|
|
3
9
|
Framework-agnostic JSON-RPC 2.0 toolkit for Python.
|
|
4
10
|
|
|
5
11
|
Use this package when you need a small Python JSON-RPC 2.0 client/server core that can be embedded directly or adapted to HTTP frameworks. It follows the same protocol conventions used across the RPC Toolkit ecosystem.
|
|
6
12
|
|
|
7
13
|
## Project Status
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
Alpha package with the core JSON-RPC behavior implemented.
|
|
10
16
|
|
|
11
17
|
- Published on PyPI as `rpc-python-toolkit`.
|
|
12
18
|
- Standard JSON-RPC 2.0 remains the default behavior.
|
|
@@ -55,7 +61,11 @@ python -m unittest discover
|
|
|
55
61
|
```python
|
|
56
62
|
from rpc_python_toolkit import RpcEndpoint
|
|
57
63
|
|
|
58
|
-
rpc = RpcEndpoint(
|
|
64
|
+
rpc = RpcEndpoint(
|
|
65
|
+
enable_introspection=True,
|
|
66
|
+
enable_batch=True,
|
|
67
|
+
max_batch_size=100,
|
|
68
|
+
)
|
|
59
69
|
|
|
60
70
|
def test(request, context, params):
|
|
61
71
|
return {"ok": True, "params": params}
|
|
@@ -72,6 +82,12 @@ response = rpc.handle_payload({
|
|
|
72
82
|
print(response.body)
|
|
73
83
|
```
|
|
74
84
|
|
|
85
|
+
Batch can be disabled with `enable_batch=False`. Introspection remains opt-in.
|
|
86
|
+
When both application and request contexts are dictionaries, handlers receive a
|
|
87
|
+
merged dictionary and request values take precedence. For a non-dictionary
|
|
88
|
+
application context plus request data, handlers receive `RpcCallContext` with
|
|
89
|
+
separate `application` and `request` attributes.
|
|
90
|
+
|
|
75
91
|
Result:
|
|
76
92
|
|
|
77
93
|
```json
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rpc-python-toolkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .client import RpcClient, RpcSafeClient
|
|
2
|
-
from .endpoint import RpcEndpoint, RpcHttpResponse, RpcSafeEndpoint
|
|
2
|
+
from .endpoint import RpcCallContext, RpcEndpoint, RpcHttpResponse, RpcSafeEndpoint
|
|
3
3
|
from .errors import RpcError, RpcRemoteError
|
|
4
4
|
from .serialization import deserialize_value, serialize_value
|
|
5
5
|
from .version import __version__
|
|
@@ -10,10 +10,10 @@ __all__ = [
|
|
|
10
10
|
"serialize_value",
|
|
11
11
|
"RpcClient",
|
|
12
12
|
"RpcEndpoint",
|
|
13
|
+
"RpcCallContext",
|
|
13
14
|
"RpcError",
|
|
14
15
|
"RpcHttpResponse",
|
|
15
16
|
"RpcRemoteError",
|
|
16
17
|
"RpcSafeClient",
|
|
17
18
|
"RpcSafeEndpoint",
|
|
18
19
|
]
|
|
19
|
-
|
|
@@ -23,6 +23,19 @@ class RpcHttpResponse:
|
|
|
23
23
|
body: Any | None = None
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class RpcCallContext:
|
|
28
|
+
"""Context used when application state is not a mapping.
|
|
29
|
+
|
|
30
|
+
Mapping application contexts are merged with request data for backward
|
|
31
|
+
compatibility. For other application context types, this wrapper keeps
|
|
32
|
+
application and request scopes explicit.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
application: Any
|
|
36
|
+
request: dict[str, Any]
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
@dataclass
|
|
27
40
|
class MethodConfig:
|
|
28
41
|
handler: Handler
|
|
@@ -40,6 +53,8 @@ class RpcEndpoint:
|
|
|
40
53
|
strict_mode: bool = True,
|
|
41
54
|
enable_introspection: bool = False,
|
|
42
55
|
introspection_prefix: str = "__rpc",
|
|
56
|
+
enable_batch: bool = True,
|
|
57
|
+
max_batch_size: int = 100,
|
|
43
58
|
max_serialization_depth: int = 100,
|
|
44
59
|
max_deserialization_depth: int = 100,
|
|
45
60
|
):
|
|
@@ -49,6 +64,8 @@ class RpcEndpoint:
|
|
|
49
64
|
self.strict_mode = strict_mode
|
|
50
65
|
self.enable_introspection = enable_introspection
|
|
51
66
|
self.introspection_prefix = introspection_prefix
|
|
67
|
+
self.enable_batch = enable_batch
|
|
68
|
+
self.max_batch_size = max_batch_size
|
|
52
69
|
self.max_serialization_depth = max_serialization_depth
|
|
53
70
|
self.max_deserialization_depth = max_deserialization_depth
|
|
54
71
|
self.validator = SchemaValidator()
|
|
@@ -159,6 +176,29 @@ class RpcEndpoint:
|
|
|
159
176
|
make_response(None, error={"code": -32600, "message": "Invalid Request"}),
|
|
160
177
|
)
|
|
161
178
|
|
|
179
|
+
if not self.enable_batch:
|
|
180
|
+
return RpcHttpResponse(
|
|
181
|
+
200,
|
|
182
|
+
headers,
|
|
183
|
+
make_response(
|
|
184
|
+
None,
|
|
185
|
+
error={"code": -32600, "message": "Batch requests are not enabled"},
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if self.max_batch_size > 0 and len(batch) > self.max_batch_size:
|
|
190
|
+
return RpcHttpResponse(
|
|
191
|
+
200,
|
|
192
|
+
headers,
|
|
193
|
+
make_response(
|
|
194
|
+
None,
|
|
195
|
+
error={
|
|
196
|
+
"code": -32600,
|
|
197
|
+
"message": f"Batch size exceeds maximum of {self.max_batch_size}",
|
|
198
|
+
},
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
162
202
|
responses = []
|
|
163
203
|
for item in batch:
|
|
164
204
|
result = self._process_single(item, context)
|
|
@@ -226,11 +266,23 @@ class RpcEndpoint:
|
|
|
226
266
|
context: dict[str, Any],
|
|
227
267
|
params: Any,
|
|
228
268
|
) -> Any:
|
|
229
|
-
result = handler(request, self.context
|
|
269
|
+
result = handler(request, self._resolve_handler_context(context), params)
|
|
230
270
|
if inspect.isawaitable(result):
|
|
231
271
|
raise RpcError("Async handlers require an async adapter", -32603)
|
|
232
272
|
return result
|
|
233
273
|
|
|
274
|
+
def _resolve_handler_context(self, request_context: dict[str, Any]) -> Any:
|
|
275
|
+
if self.context is None:
|
|
276
|
+
return request_context
|
|
277
|
+
|
|
278
|
+
if isinstance(self.context, dict):
|
|
279
|
+
return {**self.context, **request_context}
|
|
280
|
+
|
|
281
|
+
if request_context:
|
|
282
|
+
return RpcCallContext(application=self.context, request=request_context)
|
|
283
|
+
|
|
284
|
+
return self.context
|
|
285
|
+
|
|
234
286
|
def _register_introspection_methods(self) -> None:
|
|
235
287
|
prefix = self.introspection_prefix
|
|
236
288
|
|
|
@@ -255,7 +307,7 @@ class RpcEndpoint:
|
|
|
255
307
|
f"{prefix}.capabilities",
|
|
256
308
|
lambda _request, _context, _params: {
|
|
257
309
|
"jsonrpc": "2.0",
|
|
258
|
-
"batch":
|
|
310
|
+
"batch": self.enable_batch,
|
|
259
311
|
"notifications": True,
|
|
260
312
|
"introspection": True,
|
|
261
313
|
"safeMode": self.safe_enabled,
|
|
@@ -311,4 +363,3 @@ class RpcSafeEndpoint(RpcEndpoint):
|
|
|
311
363
|
options.setdefault("safe_enabled", True)
|
|
312
364
|
options.setdefault("strict_mode", True)
|
|
313
365
|
super().__init__(context, **options)
|
|
314
|
-
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
{rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0/src/rpc_python_toolkit.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpc-python-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability.
|
|
5
5
|
Author: Nicola Carpanese
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,13 +25,19 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
# RPC Python Toolkit
|
|
27
27
|
|
|
28
|
+
[](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
30
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
33
|
+
|
|
28
34
|
Framework-agnostic JSON-RPC 2.0 toolkit for Python.
|
|
29
35
|
|
|
30
36
|
Use this package when you need a small Python JSON-RPC 2.0 client/server core that can be embedded directly or adapted to HTTP frameworks. It follows the same protocol conventions used across the RPC Toolkit ecosystem.
|
|
31
37
|
|
|
32
38
|
## Project Status
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
Alpha package with the core JSON-RPC behavior implemented.
|
|
35
41
|
|
|
36
42
|
- Published on PyPI as `rpc-python-toolkit`.
|
|
37
43
|
- Standard JSON-RPC 2.0 remains the default behavior.
|
|
@@ -80,7 +86,11 @@ python -m unittest discover
|
|
|
80
86
|
```python
|
|
81
87
|
from rpc_python_toolkit import RpcEndpoint
|
|
82
88
|
|
|
83
|
-
rpc = RpcEndpoint(
|
|
89
|
+
rpc = RpcEndpoint(
|
|
90
|
+
enable_introspection=True,
|
|
91
|
+
enable_batch=True,
|
|
92
|
+
max_batch_size=100,
|
|
93
|
+
)
|
|
84
94
|
|
|
85
95
|
def test(request, context, params):
|
|
86
96
|
return {"ok": True, "params": params}
|
|
@@ -97,6 +107,12 @@ response = rpc.handle_payload({
|
|
|
97
107
|
print(response.body)
|
|
98
108
|
```
|
|
99
109
|
|
|
110
|
+
Batch can be disabled with `enable_batch=False`. Introspection remains opt-in.
|
|
111
|
+
When both application and request contexts are dictionaries, handlers receive a
|
|
112
|
+
merged dictionary and request values take precedence. For a non-dictionary
|
|
113
|
+
application context plus request data, handlers receive `RpcCallContext` with
|
|
114
|
+
separate `application` and `request` attributes.
|
|
115
|
+
|
|
100
116
|
Result:
|
|
101
117
|
|
|
102
118
|
```json
|
|
@@ -8,6 +8,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
8
8
|
|
|
9
9
|
from rpc_python_toolkit import (
|
|
10
10
|
RpcClient,
|
|
11
|
+
RpcCallContext,
|
|
11
12
|
RpcEndpoint,
|
|
12
13
|
RpcRemoteError,
|
|
13
14
|
RpcSafeClient,
|
|
@@ -68,6 +69,67 @@ class CoreTests(unittest.TestCase):
|
|
|
68
69
|
[{"jsonrpc": "2.0", "id": "a", "result": {"value": 1}}],
|
|
69
70
|
)
|
|
70
71
|
|
|
72
|
+
def test_can_disable_batch_requests(self):
|
|
73
|
+
rpc = RpcEndpoint(enable_batch=False)
|
|
74
|
+
rpc.add_method("echo", lambda _request, _context, params: params)
|
|
75
|
+
|
|
76
|
+
response = rpc.handle_payload(
|
|
77
|
+
[{"jsonrpc": "2.0", "method": "echo", "id": 1}]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.assertEqual(response.body["error"]["code"], -32600)
|
|
81
|
+
self.assertIn("not enabled", response.body["error"]["message"])
|
|
82
|
+
|
|
83
|
+
def test_enforces_maximum_batch_size(self):
|
|
84
|
+
rpc = RpcEndpoint(max_batch_size=1)
|
|
85
|
+
rpc.add_method("echo", lambda _request, _context, params: params)
|
|
86
|
+
|
|
87
|
+
response = rpc.handle_payload(
|
|
88
|
+
[
|
|
89
|
+
{"jsonrpc": "2.0", "method": "echo", "id": 1},
|
|
90
|
+
{"jsonrpc": "2.0", "method": "echo", "id": 2},
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.assertEqual(response.body["error"]["code"], -32600)
|
|
95
|
+
self.assertIn("maximum of 1", response.body["error"]["message"])
|
|
96
|
+
|
|
97
|
+
def test_merges_application_and_request_mapping_contexts(self):
|
|
98
|
+
rpc = RpcEndpoint({"service": "orders", "user": "application"})
|
|
99
|
+
rpc.add_method("context", lambda _request, context, _params: context)
|
|
100
|
+
|
|
101
|
+
response = rpc.handle_payload(
|
|
102
|
+
{"jsonrpc": "2.0", "method": "context", "id": 1},
|
|
103
|
+
{"user": "request", "request_id": "abc"},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.assertEqual(
|
|
107
|
+
response.body["result"],
|
|
108
|
+
{"service": "orders", "user": "request", "request_id": "abc"},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_preserves_both_context_scopes_for_non_mapping_application_context(self):
|
|
112
|
+
application = object()
|
|
113
|
+
rpc = RpcEndpoint(application)
|
|
114
|
+
captured = []
|
|
115
|
+
|
|
116
|
+
def capture_context(_request, context, _params):
|
|
117
|
+
captured.append(context)
|
|
118
|
+
return "ok"
|
|
119
|
+
|
|
120
|
+
rpc.add_method("context", capture_context)
|
|
121
|
+
|
|
122
|
+
response = rpc.handle_payload(
|
|
123
|
+
{"jsonrpc": "2.0", "method": "context", "id": 1},
|
|
124
|
+
{"request_id": "abc"},
|
|
125
|
+
)
|
|
126
|
+
context = captured[0]
|
|
127
|
+
|
|
128
|
+
self.assertEqual(response.body["result"], "ok")
|
|
129
|
+
self.assertIsInstance(context, RpcCallContext)
|
|
130
|
+
self.assertIs(context.application, application)
|
|
131
|
+
self.assertEqual(context.request["request_id"], "abc")
|
|
132
|
+
|
|
71
133
|
def test_enforces_safe_mode_header_in_strict_mode(self):
|
|
72
134
|
rpc = RpcSafeEndpoint()
|
|
73
135
|
rpc.add_method("echo", lambda _request, _context, params: params)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|