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.
Files changed (20) hide show
  1. {rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info → rpc_python_toolkit-0.2.0}/PKG-INFO +19 -3
  2. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/README.md +18 -2
  3. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/pyproject.toml +1 -1
  4. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/__init__.py +2 -2
  5. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/endpoint.py +54 -3
  6. rpc_python_toolkit-0.2.0/src/rpc_python_toolkit/version.py +1 -0
  7. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0/src/rpc_python_toolkit.egg-info}/PKG-INFO +19 -3
  8. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/tests/test_core.py +62 -0
  9. rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/version.py +0 -2
  10. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/LICENSE +0 -0
  11. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/setup.cfg +0 -0
  12. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/client.py +0 -0
  13. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/errors.py +0 -0
  14. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/protocol.py +0 -0
  15. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/serialization.py +0 -0
  16. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit/validation.py +0 -0
  17. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/SOURCES.txt +0 -0
  18. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/dependency_links.txt +0 -0
  19. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/requires.txt +0 -0
  20. {rpc_python_toolkit-0.1.0 → rpc_python_toolkit-0.2.0}/src/rpc_python_toolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpc-python-toolkit
3
- Version: 0.1.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
+ [![CI](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
29
+ [![PyPI Version](https://img.shields.io/pypi/v/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
30
+ [![Python Versions](https://img.shields.io/pypi/pyversions/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
31
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
32
+ [![Status](https://img.shields.io/badge/status-alpha-orange.svg)](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
- Small early package with the core JSON-RPC behavior implemented.
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(enable_introspection=True)
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
+ [![CI](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
4
+ [![PyPI Version](https://img.shields.io/pypi/v/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
5
+ [![Python Versions](https://img.shields.io/pypi/pyversions/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
6
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![Status](https://img.shields.io/badge/status-alpha-orange.svg)](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
- Small early package with the core JSON-RPC behavior implemented.
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(enable_introspection=True)
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.1.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 if self.context is not None else context, params)
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": True,
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpc-python-toolkit
3
- Version: 0.1.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
+ [![CI](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
29
+ [![PyPI Version](https://img.shields.io/pypi/v/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
30
+ [![Python Versions](https://img.shields.io/pypi/pyversions/rpc-python-toolkit.svg)](https://pypi.org/project/rpc-python-toolkit/)
31
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
32
+ [![Status](https://img.shields.io/badge/status-alpha-orange.svg)](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
- Small early package with the core JSON-RPC behavior implemented.
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(enable_introspection=True)
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)
@@ -1,2 +0,0 @@
1
- __version__ = "0.1.0"
2
-