rpc-python-toolkit 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.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicola Carpanese
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: rpc-python-toolkit
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability.
5
+ Author: Nicola Carpanese
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/n-car/rpc-python-toolkit
8
+ Project-URL: Repository, https://github.com/n-car/rpc-python-toolkit
9
+ Project-URL: Issues, https://github.com/n-car/rpc-python-toolkit/issues
10
+ Keywords: json-rpc,json-rpc-2.0,rpc,python,http,client,server,safe-mode,iot,embedded
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: jsonschema>=4.0.0
24
+ Dynamic: license-file
25
+
26
+ # RPC Python Toolkit
27
+
28
+ Framework-agnostic JSON-RPC 2.0 toolkit for Python.
29
+
30
+ 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
+
32
+ ## Project Status
33
+
34
+ Small early package with the core JSON-RPC behavior implemented.
35
+
36
+ - Published on PyPI as `rpc-python-toolkit`.
37
+ - Standard JSON-RPC 2.0 remains the default behavior.
38
+ - Safe Mode follows the same `X-RPC-Safe-Enabled` and value marker conventions used by `rpc-express-toolkit`.
39
+ - The public API may still change before a stable Python release.
40
+
41
+ ## Installation
42
+
43
+ Install from PyPI:
44
+
45
+ ```bash
46
+ pip install rpc-python-toolkit
47
+ ```
48
+
49
+ For development snapshots, install from GitHub or from a local checkout:
50
+
51
+ ```bash
52
+ pip install git+https://github.com/n-car/rpc-python-toolkit.git
53
+ ```
54
+
55
+ For local development:
56
+
57
+ ```bash
58
+ pip install -e .
59
+ python -m unittest discover
60
+ ```
61
+
62
+ ## What Is Implemented
63
+
64
+ - Framework-independent `RpcEndpoint`
65
+ - Standard JSON-RPC calls, notifications, and batch requests
66
+ - `RpcClient` and `RpcSafeClient` over HTTP using Python standard library
67
+ - JSON Schema validation through `jsonschema`
68
+ - Optional RPC Toolkit Safe Mode over HTTP headers
69
+ - Method introspection with `__rpc.*` methods
70
+
71
+ ## What Is Not Implemented Yet
72
+
73
+ - Built-in ASGI/WSGI adapters for FastAPI, Starlette, Flask, or Django
74
+ - Async endpoint handlers
75
+ - OpenRPC export
76
+ - Built-in HTTP server adapters
77
+
78
+ ## Quick Start
79
+
80
+ ```python
81
+ from rpc_python_toolkit import RpcEndpoint
82
+
83
+ rpc = RpcEndpoint(enable_introspection=True)
84
+
85
+ def test(request, context, params):
86
+ return {"ok": True, "params": params}
87
+
88
+ rpc.add_method("test", test)
89
+
90
+ response = rpc.handle_payload({
91
+ "jsonrpc": "2.0",
92
+ "method": "test",
93
+ "params": {"value": 123},
94
+ "id": 1,
95
+ })
96
+
97
+ print(response.body)
98
+ ```
99
+
100
+ Result:
101
+
102
+ ```json
103
+ {"jsonrpc": "2.0", "id": 1, "result": {"ok": true, "params": {"value": 123}}}
104
+ ```
105
+
106
+ ## HTTP Client
107
+
108
+ ```python
109
+ from rpc_python_toolkit import RpcClient
110
+
111
+ client = RpcClient("http://localhost:3000/api")
112
+ result = client.call("test", {"value": 123})
113
+ ```
114
+
115
+ ## Safe Mode
116
+
117
+ Use `RpcSafeEndpoint` and `RpcSafeClient` when both sides support RPC Toolkit Safe Mode:
118
+
119
+ ```python
120
+ from rpc_python_toolkit import RpcSafeEndpoint, RpcSafeClient
121
+
122
+ rpc = RpcSafeEndpoint()
123
+ client = RpcSafeClient("http://localhost:3000/api")
124
+ ```
125
+
126
+ Safe Mode enables `X-RPC-Safe-Enabled` negotiation and recursive value encoding/decoding:
127
+
128
+ - strings are encoded as `S:<value>`
129
+ - datetimes are encoded as `D:<iso-date>`
130
+ - large integers are encoded as `<digits>n`
131
+
132
+ Python exposes arbitrary precision integers as `int`. BigInt marker strings from other RPC Toolkit implementations are decoded to Python `int` values when they are real markers, while literal marker-like strings are protected by the `S:` prefix in Safe Mode.
133
+
134
+ ## Examples
135
+
136
+ Runnable examples are available in [`examples/`](examples/):
137
+
138
+ - [`basic_server.py`](examples/basic_server.py) - direct endpoint usage without an HTTP adapter.
139
+ - [`basic_client.py`](examples/basic_client.py) - standard HTTP client call against an in-process endpoint.
140
+ - [`schema_validation.py`](examples/schema_validation.py) - method schema validation and JSON-RPC error output.
141
+ - [`safe_mode.py`](examples/safe_mode.py) - `RpcSafeClient` to `RpcSafeEndpoint` round-trip for strings, dates, and large integers.
142
+
143
+ For local checkout usage:
144
+
145
+ ```bash
146
+ PYTHONPATH=src python3 examples/basic_server.py
147
+ PYTHONPATH=src python3 examples/basic_client.py
148
+ PYTHONPATH=src python3 examples/schema_validation.py
149
+ PYTHONPATH=src python3 examples/safe_mode.py
150
+ ```
151
+
152
+ ## Related Projects
153
+
154
+ - [rpc-express-toolkit](https://github.com/n-car/rpc-express-toolkit)
155
+ - [rpc-node-toolkit](https://github.com/n-car/rpc-node-toolkit)
156
+ - [rpc-toolkit-js-client](https://github.com/n-car/rpc-toolkit-js-client)
157
+ - [rpc-dotnet-toolkit](https://github.com/n-car/rpc-dotnet-toolkit)
158
+ - [rpc-java-toolkit](https://github.com/n-car/rpc-java-toolkit)
159
+ - [rpc-php-toolkit](https://github.com/n-car/rpc-php-toolkit)
160
+ - [rpc-arduino-toolkit](https://github.com/n-car/rpc-arduino-toolkit)
161
+ - [node-red-contrib-rpc-toolkit](https://github.com/n-car/node-red-contrib-rpc-toolkit)
162
+
163
+ ## License
164
+
165
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,140 @@
1
+ # RPC Python Toolkit
2
+
3
+ Framework-agnostic JSON-RPC 2.0 toolkit for Python.
4
+
5
+ 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
+
7
+ ## Project Status
8
+
9
+ Small early package with the core JSON-RPC behavior implemented.
10
+
11
+ - Published on PyPI as `rpc-python-toolkit`.
12
+ - Standard JSON-RPC 2.0 remains the default behavior.
13
+ - Safe Mode follows the same `X-RPC-Safe-Enabled` and value marker conventions used by `rpc-express-toolkit`.
14
+ - The public API may still change before a stable Python release.
15
+
16
+ ## Installation
17
+
18
+ Install from PyPI:
19
+
20
+ ```bash
21
+ pip install rpc-python-toolkit
22
+ ```
23
+
24
+ For development snapshots, install from GitHub or from a local checkout:
25
+
26
+ ```bash
27
+ pip install git+https://github.com/n-car/rpc-python-toolkit.git
28
+ ```
29
+
30
+ For local development:
31
+
32
+ ```bash
33
+ pip install -e .
34
+ python -m unittest discover
35
+ ```
36
+
37
+ ## What Is Implemented
38
+
39
+ - Framework-independent `RpcEndpoint`
40
+ - Standard JSON-RPC calls, notifications, and batch requests
41
+ - `RpcClient` and `RpcSafeClient` over HTTP using Python standard library
42
+ - JSON Schema validation through `jsonschema`
43
+ - Optional RPC Toolkit Safe Mode over HTTP headers
44
+ - Method introspection with `__rpc.*` methods
45
+
46
+ ## What Is Not Implemented Yet
47
+
48
+ - Built-in ASGI/WSGI adapters for FastAPI, Starlette, Flask, or Django
49
+ - Async endpoint handlers
50
+ - OpenRPC export
51
+ - Built-in HTTP server adapters
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from rpc_python_toolkit import RpcEndpoint
57
+
58
+ rpc = RpcEndpoint(enable_introspection=True)
59
+
60
+ def test(request, context, params):
61
+ return {"ok": True, "params": params}
62
+
63
+ rpc.add_method("test", test)
64
+
65
+ response = rpc.handle_payload({
66
+ "jsonrpc": "2.0",
67
+ "method": "test",
68
+ "params": {"value": 123},
69
+ "id": 1,
70
+ })
71
+
72
+ print(response.body)
73
+ ```
74
+
75
+ Result:
76
+
77
+ ```json
78
+ {"jsonrpc": "2.0", "id": 1, "result": {"ok": true, "params": {"value": 123}}}
79
+ ```
80
+
81
+ ## HTTP Client
82
+
83
+ ```python
84
+ from rpc_python_toolkit import RpcClient
85
+
86
+ client = RpcClient("http://localhost:3000/api")
87
+ result = client.call("test", {"value": 123})
88
+ ```
89
+
90
+ ## Safe Mode
91
+
92
+ Use `RpcSafeEndpoint` and `RpcSafeClient` when both sides support RPC Toolkit Safe Mode:
93
+
94
+ ```python
95
+ from rpc_python_toolkit import RpcSafeEndpoint, RpcSafeClient
96
+
97
+ rpc = RpcSafeEndpoint()
98
+ client = RpcSafeClient("http://localhost:3000/api")
99
+ ```
100
+
101
+ Safe Mode enables `X-RPC-Safe-Enabled` negotiation and recursive value encoding/decoding:
102
+
103
+ - strings are encoded as `S:<value>`
104
+ - datetimes are encoded as `D:<iso-date>`
105
+ - large integers are encoded as `<digits>n`
106
+
107
+ Python exposes arbitrary precision integers as `int`. BigInt marker strings from other RPC Toolkit implementations are decoded to Python `int` values when they are real markers, while literal marker-like strings are protected by the `S:` prefix in Safe Mode.
108
+
109
+ ## Examples
110
+
111
+ Runnable examples are available in [`examples/`](examples/):
112
+
113
+ - [`basic_server.py`](examples/basic_server.py) - direct endpoint usage without an HTTP adapter.
114
+ - [`basic_client.py`](examples/basic_client.py) - standard HTTP client call against an in-process endpoint.
115
+ - [`schema_validation.py`](examples/schema_validation.py) - method schema validation and JSON-RPC error output.
116
+ - [`safe_mode.py`](examples/safe_mode.py) - `RpcSafeClient` to `RpcSafeEndpoint` round-trip for strings, dates, and large integers.
117
+
118
+ For local checkout usage:
119
+
120
+ ```bash
121
+ PYTHONPATH=src python3 examples/basic_server.py
122
+ PYTHONPATH=src python3 examples/basic_client.py
123
+ PYTHONPATH=src python3 examples/schema_validation.py
124
+ PYTHONPATH=src python3 examples/safe_mode.py
125
+ ```
126
+
127
+ ## Related Projects
128
+
129
+ - [rpc-express-toolkit](https://github.com/n-car/rpc-express-toolkit)
130
+ - [rpc-node-toolkit](https://github.com/n-car/rpc-node-toolkit)
131
+ - [rpc-toolkit-js-client](https://github.com/n-car/rpc-toolkit-js-client)
132
+ - [rpc-dotnet-toolkit](https://github.com/n-car/rpc-dotnet-toolkit)
133
+ - [rpc-java-toolkit](https://github.com/n-car/rpc-java-toolkit)
134
+ - [rpc-php-toolkit](https://github.com/n-car/rpc-php-toolkit)
135
+ - [rpc-arduino-toolkit](https://github.com/n-car/rpc-arduino-toolkit)
136
+ - [node-red-contrib-rpc-toolkit](https://github.com/n-car/node-red-contrib-rpc-toolkit)
137
+
138
+ ## License
139
+
140
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rpc-python-toolkit"
7
+ version = "0.1.0"
8
+ description = "Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Nicola Carpanese" }
14
+ ]
15
+ keywords = [
16
+ "json-rpc",
17
+ "json-rpc-2.0",
18
+ "rpc",
19
+ "python",
20
+ "http",
21
+ "client",
22
+ "server",
23
+ "safe-mode",
24
+ "iot",
25
+ "embedded"
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 3 - Alpha",
29
+ "Intended Audience :: Developers",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Internet :: WWW/HTTP",
36
+ "Topic :: Software Development :: Libraries"
37
+ ]
38
+ dependencies = [
39
+ "jsonschema>=4.0.0"
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/n-car/rpc-python-toolkit"
44
+ Repository = "https://github.com/n-car/rpc-python-toolkit"
45
+ Issues = "https://github.com/n-car/rpc-python-toolkit/issues"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from .client import RpcClient, RpcSafeClient
2
+ from .endpoint import RpcEndpoint, RpcHttpResponse, RpcSafeEndpoint
3
+ from .errors import RpcError, RpcRemoteError
4
+ from .serialization import deserialize_value, serialize_value
5
+ from .version import __version__
6
+
7
+ __all__ = [
8
+ "__version__",
9
+ "deserialize_value",
10
+ "serialize_value",
11
+ "RpcClient",
12
+ "RpcEndpoint",
13
+ "RpcError",
14
+ "RpcHttpResponse",
15
+ "RpcRemoteError",
16
+ "RpcSafeClient",
17
+ "RpcSafeEndpoint",
18
+ ]
19
+
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.request
6
+ from typing import Any
7
+
8
+ from .errors import RpcError, RpcRemoteError
9
+ from .serialization import deserialize_value, serialize_value
10
+
11
+
12
+ class RpcClient:
13
+ def __init__(
14
+ self,
15
+ url: str,
16
+ *,
17
+ headers: dict[str, str] | None = None,
18
+ timeout: float = 30,
19
+ safe_enabled: bool = False,
20
+ require_safe_header: bool = True,
21
+ ):
22
+ self.url = url
23
+ self.headers = headers or {}
24
+ self.timeout = timeout
25
+ self.safe_enabled = safe_enabled
26
+ self.require_safe_header = require_safe_header
27
+ self._next_id = 1
28
+
29
+ def call(self, method: str, params: Any = None, *, request_id: Any = None) -> Any:
30
+ request = {
31
+ "jsonrpc": "2.0",
32
+ "method": method,
33
+ "id": self._make_id() if request_id is None else request_id,
34
+ }
35
+ if params is not None:
36
+ request["params"] = self._encode(params)
37
+
38
+ response = self._send(request, expect_response=True)
39
+ if "error" in response:
40
+ error = response["error"]
41
+ data = self._decode(error.get("data"), response.get("_safe_enabled", False))
42
+ raise RpcRemoteError(error.get("message", "RPC error"), error.get("code", -32603), data)
43
+
44
+ return self._decode(response.get("result"), response.get("_safe_enabled", False))
45
+
46
+ def notify(self, method: str, params: Any = None) -> None:
47
+ request = {
48
+ "jsonrpc": "2.0",
49
+ "method": method,
50
+ }
51
+ if params is not None:
52
+ request["params"] = self._encode(params)
53
+ self._send(request, expect_response=False)
54
+
55
+ def batch(self, requests: list[dict[str, Any]]) -> list[Any]:
56
+ payload = []
57
+ for item in requests:
58
+ request = {
59
+ "jsonrpc": "2.0",
60
+ "method": item["method"],
61
+ }
62
+ if "id" in item:
63
+ request["id"] = item["id"]
64
+ if "params" in item:
65
+ request["params"] = self._encode(item["params"])
66
+ payload.append(request)
67
+
68
+ response = self._send(payload, expect_response=True)
69
+ if not response:
70
+ return []
71
+
72
+ responses = response if isinstance(response, list) else [response]
73
+ results = []
74
+ for item in responses:
75
+ if "error" in item:
76
+ error = item["error"]
77
+ data = self._decode(error.get("data"), item.get("_safe_enabled", False))
78
+ raise RpcRemoteError(error.get("message", "RPC error"), error.get("code", -32603), data)
79
+ results.append(self._decode(item.get("result"), item.get("_safe_enabled", False)))
80
+ return results
81
+
82
+ def _send(self, payload: Any, *, expect_response: bool) -> Any:
83
+ body = json.dumps(payload, separators=(",", ":"), default=str).encode("utf-8")
84
+ headers = {
85
+ "content-type": "application/json",
86
+ **self.headers,
87
+ }
88
+
89
+ if self.safe_enabled:
90
+ headers["x-rpc-safe-enabled"] = "true"
91
+ else:
92
+ headers.setdefault("x-rpc-safe-enabled", "false")
93
+
94
+ request = urllib.request.Request(self.url, data=body, headers=headers, method="POST")
95
+
96
+ try:
97
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
98
+ response_body = response.read().decode("utf-8")
99
+ server_safe_header = response.headers.get("X-RPC-Safe-Enabled")
100
+ status = response.status
101
+ except urllib.error.HTTPError as exc:
102
+ response_body = exc.read().decode("utf-8")
103
+ server_safe_header = exc.headers.get("X-RPC-Safe-Enabled")
104
+ status = exc.code
105
+
106
+ if status == 204 or response_body == "":
107
+ return [] if not expect_response else {}
108
+
109
+ if self.safe_enabled and self.require_safe_header and server_safe_header is None:
110
+ raise RpcError(
111
+ "RPC Compatibility Error: safe client expected X-RPC-Safe-Enabled response header",
112
+ -32603,
113
+ )
114
+
115
+ server_safe_enabled = str(server_safe_header).lower() == "true"
116
+ decoded = json.loads(response_body)
117
+ self._attach_safe_flag(decoded, server_safe_enabled)
118
+ return decoded
119
+
120
+ def _encode(self, value: Any) -> Any:
121
+ return serialize_value(value, safe_enabled=self.safe_enabled)
122
+
123
+ @staticmethod
124
+ def _decode(value: Any, safe_enabled: bool) -> Any:
125
+ return deserialize_value(value, safe_enabled=safe_enabled)
126
+
127
+ def _make_id(self) -> int:
128
+ request_id = self._next_id
129
+ self._next_id += 1
130
+ return request_id
131
+
132
+ @staticmethod
133
+ def _attach_safe_flag(value: Any, safe_enabled: bool) -> None:
134
+ if isinstance(value, list):
135
+ for item in value:
136
+ if isinstance(item, dict):
137
+ item["_safe_enabled"] = safe_enabled
138
+ elif isinstance(value, dict):
139
+ value["_safe_enabled"] = safe_enabled
140
+
141
+
142
+ class RpcSafeClient(RpcClient):
143
+ def __init__(self, url: str, **options: Any):
144
+ options.setdefault("safe_enabled", True)
145
+ options.setdefault("require_safe_header", True)
146
+ super().__init__(url, **options)