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.
- rpc_python_toolkit-0.1.0/LICENSE +22 -0
- rpc_python_toolkit-0.1.0/PKG-INFO +165 -0
- rpc_python_toolkit-0.1.0/README.md +140 -0
- rpc_python_toolkit-0.1.0/pyproject.toml +48 -0
- rpc_python_toolkit-0.1.0/setup.cfg +4 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/__init__.py +19 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/client.py +146 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/endpoint.py +314 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/errors.py +16 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/protocol.py +42 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/serialization.py +222 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/validation.py +26 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit/version.py +2 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info/PKG-INFO +165 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info/SOURCES.txt +17 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info/dependency_links.txt +1 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info/requires.txt +1 -0
- rpc_python_toolkit-0.1.0/src/rpc_python_toolkit.egg-info/top_level.txt +1 -0
- rpc_python_toolkit-0.1.0/tests/test_core.py +297 -0
|
@@ -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,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)
|