rpc-python-toolkit 0.1.0__py3-none-any.whl
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/__init__.py +19 -0
- rpc_python_toolkit/client.py +146 -0
- rpc_python_toolkit/endpoint.py +314 -0
- rpc_python_toolkit/errors.py +16 -0
- rpc_python_toolkit/protocol.py +42 -0
- rpc_python_toolkit/serialization.py +222 -0
- rpc_python_toolkit/validation.py +26 -0
- rpc_python_toolkit/version.py +2 -0
- rpc_python_toolkit-0.1.0.dist-info/METADATA +165 -0
- rpc_python_toolkit-0.1.0.dist-info/RECORD +13 -0
- rpc_python_toolkit-0.1.0.dist-info/WHEEL +5 -0
- rpc_python_toolkit-0.1.0.dist-info/licenses/LICENSE +22 -0
- rpc_python_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import RpcError
|
|
10
|
+
from .protocol import has_id, make_response, validate_request_envelope
|
|
11
|
+
from .serialization import deserialize_value, serialize_value
|
|
12
|
+
from .validation import SchemaValidator
|
|
13
|
+
from .version import __version__
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Handler = Callable[..., Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RpcHttpResponse:
|
|
21
|
+
status: int
|
|
22
|
+
headers: dict[str, str]
|
|
23
|
+
body: Any | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MethodConfig:
|
|
28
|
+
handler: Handler
|
|
29
|
+
schema: dict[str, Any] | None = None
|
|
30
|
+
description: str = ""
|
|
31
|
+
expose_schema: bool = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RpcEndpoint:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
context: Any = None,
|
|
38
|
+
*,
|
|
39
|
+
safe_enabled: bool = False,
|
|
40
|
+
strict_mode: bool = True,
|
|
41
|
+
enable_introspection: bool = False,
|
|
42
|
+
introspection_prefix: str = "__rpc",
|
|
43
|
+
max_serialization_depth: int = 100,
|
|
44
|
+
max_deserialization_depth: int = 100,
|
|
45
|
+
):
|
|
46
|
+
self.context = context
|
|
47
|
+
self.methods: dict[str, MethodConfig] = {}
|
|
48
|
+
self.safe_enabled = safe_enabled
|
|
49
|
+
self.strict_mode = strict_mode
|
|
50
|
+
self.enable_introspection = enable_introspection
|
|
51
|
+
self.introspection_prefix = introspection_prefix
|
|
52
|
+
self.max_serialization_depth = max_serialization_depth
|
|
53
|
+
self.max_deserialization_depth = max_deserialization_depth
|
|
54
|
+
self.validator = SchemaValidator()
|
|
55
|
+
|
|
56
|
+
if self.enable_introspection:
|
|
57
|
+
self._register_introspection_methods()
|
|
58
|
+
|
|
59
|
+
def add_method(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
handler: Handler,
|
|
63
|
+
*,
|
|
64
|
+
schema: dict[str, Any] | None = None,
|
|
65
|
+
description: str = "",
|
|
66
|
+
expose_schema: bool = False,
|
|
67
|
+
) -> None:
|
|
68
|
+
if not isinstance(name, str) or not name:
|
|
69
|
+
raise ValueError("Method name must be a non-empty string")
|
|
70
|
+
|
|
71
|
+
self.methods[name] = MethodConfig(
|
|
72
|
+
handler=handler,
|
|
73
|
+
schema=schema,
|
|
74
|
+
description=description,
|
|
75
|
+
expose_schema=expose_schema,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def remove_method(self, name: str) -> None:
|
|
79
|
+
self.methods.pop(name, None)
|
|
80
|
+
|
|
81
|
+
def list_methods(self) -> list[str]:
|
|
82
|
+
return list(self.methods.keys())
|
|
83
|
+
|
|
84
|
+
def handle_request(self, payload: str | bytes | dict[str, Any] | list[Any], context: dict[str, Any] | None = None) -> str:
|
|
85
|
+
response = self.handle_payload(payload, context)
|
|
86
|
+
if response.status == 204 or response.body is None:
|
|
87
|
+
return ""
|
|
88
|
+
return json.dumps(response.body, separators=(",", ":"), default=str)
|
|
89
|
+
|
|
90
|
+
def handle_payload(
|
|
91
|
+
self,
|
|
92
|
+
payload: str | bytes | dict[str, Any] | list[Any],
|
|
93
|
+
context: dict[str, Any] | None = None,
|
|
94
|
+
) -> RpcHttpResponse:
|
|
95
|
+
headers = {
|
|
96
|
+
"content-type": "application/json",
|
|
97
|
+
"x-rpc-safe-enabled": "true" if self.safe_enabled else "false",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
request = self._decode_payload(payload)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
return RpcHttpResponse(
|
|
104
|
+
200,
|
|
105
|
+
headers,
|
|
106
|
+
make_response(
|
|
107
|
+
None,
|
|
108
|
+
error={
|
|
109
|
+
"code": -32700,
|
|
110
|
+
"message": "Parse error",
|
|
111
|
+
"data": {"message": str(exc)},
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
request_context = context or {}
|
|
117
|
+
try:
|
|
118
|
+
if isinstance(request, list):
|
|
119
|
+
return self._process_batch(request, request_context, headers)
|
|
120
|
+
|
|
121
|
+
result = self._process_single(request, request_context)
|
|
122
|
+
if result is None:
|
|
123
|
+
return RpcHttpResponse(204, headers)
|
|
124
|
+
|
|
125
|
+
return RpcHttpResponse(200, headers, result)
|
|
126
|
+
except RpcError as exc:
|
|
127
|
+
return RpcHttpResponse(200, headers, self._make_error_response(None, exc))
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
return RpcHttpResponse(
|
|
130
|
+
200,
|
|
131
|
+
headers,
|
|
132
|
+
self._make_error_response(None, RpcError(str(exc), -32603)),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def serialize_bigints_and_dates(self, value: Any) -> Any:
|
|
136
|
+
return serialize_value(
|
|
137
|
+
value,
|
|
138
|
+
safe_enabled=self.safe_enabled,
|
|
139
|
+
max_depth=self.max_serialization_depth,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def deserialize_bigints_and_dates(self, value: Any, *, safe_enabled: bool | None = None) -> Any:
|
|
143
|
+
return deserialize_value(
|
|
144
|
+
value,
|
|
145
|
+
safe_enabled=self.safe_enabled if safe_enabled is None else safe_enabled,
|
|
146
|
+
max_depth=self.max_deserialization_depth,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _process_batch(
|
|
150
|
+
self,
|
|
151
|
+
batch: list[Any],
|
|
152
|
+
context: dict[str, Any],
|
|
153
|
+
headers: dict[str, str],
|
|
154
|
+
) -> RpcHttpResponse:
|
|
155
|
+
if not batch:
|
|
156
|
+
return RpcHttpResponse(
|
|
157
|
+
200,
|
|
158
|
+
headers,
|
|
159
|
+
make_response(None, error={"code": -32600, "message": "Invalid Request"}),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
responses = []
|
|
163
|
+
for item in batch:
|
|
164
|
+
result = self._process_single(item, context)
|
|
165
|
+
if result is not None:
|
|
166
|
+
responses.append(result)
|
|
167
|
+
|
|
168
|
+
if not responses:
|
|
169
|
+
return RpcHttpResponse(204, headers)
|
|
170
|
+
|
|
171
|
+
return RpcHttpResponse(200, headers, responses)
|
|
172
|
+
|
|
173
|
+
def _process_single(self, request: Any, context: dict[str, Any]) -> dict[str, Any] | None:
|
|
174
|
+
try:
|
|
175
|
+
validate_request_envelope(request)
|
|
176
|
+
client_safe_enabled = self._client_safe_enabled(context)
|
|
177
|
+
|
|
178
|
+
if self.safe_enabled and self.strict_mode and not self._has_safe_header(context):
|
|
179
|
+
raise RpcError("Safe Mode requires X-RPC-Safe-Enabled header", -32600)
|
|
180
|
+
|
|
181
|
+
method_name = request["method"]
|
|
182
|
+
method = self.methods.get(method_name)
|
|
183
|
+
if method is None:
|
|
184
|
+
raise RpcError(f"Method not found: {method_name}", -32601)
|
|
185
|
+
|
|
186
|
+
params = request.get("params", {})
|
|
187
|
+
if client_safe_enabled:
|
|
188
|
+
params = self.deserialize_bigints_and_dates(params, safe_enabled=True)
|
|
189
|
+
|
|
190
|
+
self.validator.validate(params, method.schema)
|
|
191
|
+
result = self._execute_handler(method.handler, request, context, params)
|
|
192
|
+
|
|
193
|
+
if not has_id(request):
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
return make_response(
|
|
197
|
+
request.get("id"),
|
|
198
|
+
result=self.serialize_bigints_and_dates(result),
|
|
199
|
+
)
|
|
200
|
+
except RpcError as exc:
|
|
201
|
+
if isinstance(request, dict) and not has_id(request):
|
|
202
|
+
return None
|
|
203
|
+
request_id = request.get("id") if isinstance(request, dict) else None
|
|
204
|
+
return self._make_error_response(request_id, exc)
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
if isinstance(request, dict) and not has_id(request):
|
|
207
|
+
return None
|
|
208
|
+
request_id = request.get("id") if isinstance(request, dict) else None
|
|
209
|
+
return self._make_error_response(request_id, RpcError(str(exc), -32603))
|
|
210
|
+
|
|
211
|
+
def _make_error_response(self, request_id: Any, error: RpcError) -> dict[str, Any]:
|
|
212
|
+
body = {
|
|
213
|
+
"code": error.code,
|
|
214
|
+
"message": str(error),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if error.data is not None:
|
|
218
|
+
body["data"] = self.serialize_bigints_and_dates(error.data)
|
|
219
|
+
|
|
220
|
+
return make_response(request_id, error=body)
|
|
221
|
+
|
|
222
|
+
def _execute_handler(
|
|
223
|
+
self,
|
|
224
|
+
handler: Handler,
|
|
225
|
+
request: dict[str, Any],
|
|
226
|
+
context: dict[str, Any],
|
|
227
|
+
params: Any,
|
|
228
|
+
) -> Any:
|
|
229
|
+
result = handler(request, self.context if self.context is not None else context, params)
|
|
230
|
+
if inspect.isawaitable(result):
|
|
231
|
+
raise RpcError("Async handlers require an async adapter", -32603)
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def _register_introspection_methods(self) -> None:
|
|
235
|
+
prefix = self.introspection_prefix
|
|
236
|
+
|
|
237
|
+
self.add_method(
|
|
238
|
+
f"{prefix}.listMethods",
|
|
239
|
+
lambda _request, _context, _params: [
|
|
240
|
+
name for name in self.methods if not name.startswith(f"{prefix}.")
|
|
241
|
+
],
|
|
242
|
+
)
|
|
243
|
+
self.add_method(
|
|
244
|
+
f"{prefix}.version",
|
|
245
|
+
lambda _request, _context, _params: {
|
|
246
|
+
"toolkit": "rpc-python-toolkit",
|
|
247
|
+
"version": __version__,
|
|
248
|
+
"methodCount": len(
|
|
249
|
+
[name for name in self.methods if not name.startswith(f"{prefix}.")]
|
|
250
|
+
),
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
self.add_method(f"{prefix}.describe", self._describe_method)
|
|
254
|
+
self.add_method(
|
|
255
|
+
f"{prefix}.capabilities",
|
|
256
|
+
lambda _request, _context, _params: {
|
|
257
|
+
"jsonrpc": "2.0",
|
|
258
|
+
"batch": True,
|
|
259
|
+
"notifications": True,
|
|
260
|
+
"introspection": True,
|
|
261
|
+
"safeMode": self.safe_enabled,
|
|
262
|
+
"transport": "python-http",
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _describe_method(self, _request: dict[str, Any], _context: Any, params: Any) -> dict[str, Any]:
|
|
267
|
+
if not isinstance(params, dict) or not isinstance(params.get("method"), str):
|
|
268
|
+
raise RpcError("Invalid params: method name required", -32602)
|
|
269
|
+
|
|
270
|
+
method_name = params["method"]
|
|
271
|
+
if method_name.startswith(f"{self.introspection_prefix}."):
|
|
272
|
+
raise RpcError("Cannot describe introspection methods", -32601)
|
|
273
|
+
|
|
274
|
+
method = self.methods.get(method_name)
|
|
275
|
+
if method is None:
|
|
276
|
+
raise RpcError(f"Method not found: {method_name}", -32601)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"name": method_name,
|
|
280
|
+
"description": method.description,
|
|
281
|
+
"schema": method.schema if method.expose_schema else None,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _decode_payload(payload: str | bytes | dict[str, Any] | list[Any]) -> Any:
|
|
286
|
+
if isinstance(payload, bytes):
|
|
287
|
+
payload = payload.decode("utf-8")
|
|
288
|
+
|
|
289
|
+
if isinstance(payload, str):
|
|
290
|
+
return json.loads(payload)
|
|
291
|
+
|
|
292
|
+
return payload
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _normalized_headers(context: dict[str, Any]) -> dict[str, str]:
|
|
296
|
+
headers = context.get("headers", {}) or {}
|
|
297
|
+
return {str(key).lower(): str(value) for key, value in headers.items()}
|
|
298
|
+
|
|
299
|
+
def _has_safe_header(self, context: dict[str, Any]) -> bool:
|
|
300
|
+
headers = self._normalized_headers(context)
|
|
301
|
+
return "x-rpc-safe-enabled" in headers or "x-rpc-safe" in headers
|
|
302
|
+
|
|
303
|
+
def _client_safe_enabled(self, context: dict[str, Any]) -> bool:
|
|
304
|
+
headers = self._normalized_headers(context)
|
|
305
|
+
value = headers.get("x-rpc-safe-enabled", headers.get("x-rpc-safe"))
|
|
306
|
+
return str(value).lower() == "true"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class RpcSafeEndpoint(RpcEndpoint):
|
|
310
|
+
def __init__(self, context: Any = None, **options: Any):
|
|
311
|
+
options.setdefault("safe_enabled", True)
|
|
312
|
+
options.setdefault("strict_mode", True)
|
|
313
|
+
super().__init__(context, **options)
|
|
314
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RpcError(Exception):
|
|
5
|
+
"""JSON-RPC error with code and optional data."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: int = -32603, data: object = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.code = code
|
|
10
|
+
self.data = data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RpcRemoteError(RpcError):
|
|
14
|
+
"""Error returned by a remote JSON-RPC endpoint."""
|
|
15
|
+
|
|
16
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .errors import RpcError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def has_id(request: dict[str, Any]) -> bool:
|
|
9
|
+
return "id" in request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_response(
|
|
13
|
+
request_id: Any,
|
|
14
|
+
*,
|
|
15
|
+
result: Any = None,
|
|
16
|
+
error: dict[str, Any] | None = None,
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
response = {"jsonrpc": "2.0", "id": request_id}
|
|
19
|
+
if error is not None:
|
|
20
|
+
response["error"] = error
|
|
21
|
+
else:
|
|
22
|
+
response["result"] = result
|
|
23
|
+
return response
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_request_envelope(request: Any) -> None:
|
|
27
|
+
if not isinstance(request, dict):
|
|
28
|
+
raise RpcError("Invalid Request", -32600)
|
|
29
|
+
|
|
30
|
+
if request.get("jsonrpc") != "2.0":
|
|
31
|
+
raise RpcError("Invalid JSON-RPC version", -32600)
|
|
32
|
+
|
|
33
|
+
method = request.get("method")
|
|
34
|
+
if not isinstance(method, str) or not method:
|
|
35
|
+
raise RpcError("Method not specified or invalid", -32600)
|
|
36
|
+
|
|
37
|
+
if "params" in request and not isinstance(request["params"], (dict, list)):
|
|
38
|
+
raise RpcError("Invalid params", -32602)
|
|
39
|
+
|
|
40
|
+
if "id" in request and not isinstance(request["id"], (str, int, float, type(None))):
|
|
41
|
+
raise RpcError("Invalid request id", -32600)
|
|
42
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import is_dataclass, asdict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
JS_SAFE_INTEGER_MAX = 9007199254740991
|
|
10
|
+
DEFAULT_MAX_DEPTH = 100
|
|
11
|
+
_BIGINT_RE = re.compile(r"^-?\d+n$")
|
|
12
|
+
_ISO_DATE_RE = re.compile(
|
|
13
|
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def serialize_value(
|
|
18
|
+
value: Any,
|
|
19
|
+
*,
|
|
20
|
+
safe_enabled: bool = False,
|
|
21
|
+
max_depth: int = DEFAULT_MAX_DEPTH,
|
|
22
|
+
_depth: int = 0,
|
|
23
|
+
_seen: set[int] | None = None,
|
|
24
|
+
) -> Any:
|
|
25
|
+
"""Recursively encode values using RPC Toolkit Safe Mode conventions."""
|
|
26
|
+
|
|
27
|
+
if _depth > max_depth:
|
|
28
|
+
raise ValueError("Serialization depth limit exceeded")
|
|
29
|
+
|
|
30
|
+
seen = _seen if _seen is not None else set()
|
|
31
|
+
|
|
32
|
+
if isinstance(value, _dt.datetime):
|
|
33
|
+
text = value.isoformat()
|
|
34
|
+
return f"D:{text}" if safe_enabled else text
|
|
35
|
+
|
|
36
|
+
if isinstance(value, _dt.date):
|
|
37
|
+
text = _dt.datetime.combine(value, _dt.time()).isoformat()
|
|
38
|
+
return f"D:{text}" if safe_enabled else text
|
|
39
|
+
|
|
40
|
+
if isinstance(value, bool) or value is None:
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
if isinstance(value, int):
|
|
44
|
+
if abs(value) > JS_SAFE_INTEGER_MAX:
|
|
45
|
+
return f"{value}n"
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
return f"S:{value}" if safe_enabled else value
|
|
50
|
+
|
|
51
|
+
if isinstance(value, Mapping):
|
|
52
|
+
return _serialize_mapping(value, safe_enabled, max_depth, _depth, seen)
|
|
53
|
+
|
|
54
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
55
|
+
return _serialize_sequence(value, safe_enabled, max_depth, _depth, seen)
|
|
56
|
+
|
|
57
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
58
|
+
return serialize_value(
|
|
59
|
+
asdict(value),
|
|
60
|
+
safe_enabled=safe_enabled,
|
|
61
|
+
max_depth=max_depth,
|
|
62
|
+
_depth=_depth + 1,
|
|
63
|
+
_seen=seen,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def deserialize_value(
|
|
70
|
+
value: Any,
|
|
71
|
+
*,
|
|
72
|
+
safe_enabled: bool = False,
|
|
73
|
+
max_depth: int = DEFAULT_MAX_DEPTH,
|
|
74
|
+
_depth: int = 0,
|
|
75
|
+
_seen: set[int] | None = None,
|
|
76
|
+
) -> Any:
|
|
77
|
+
"""Recursively decode values using RPC Toolkit Safe Mode conventions."""
|
|
78
|
+
|
|
79
|
+
if _depth > max_depth:
|
|
80
|
+
raise ValueError("Deserialization depth limit exceeded")
|
|
81
|
+
|
|
82
|
+
seen = _seen if _seen is not None else set()
|
|
83
|
+
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
if safe_enabled and value.startswith("S:"):
|
|
86
|
+
return value[2:]
|
|
87
|
+
|
|
88
|
+
if safe_enabled and value.startswith("D:"):
|
|
89
|
+
return _parse_datetime(value[2:], fallback=value)
|
|
90
|
+
|
|
91
|
+
if _BIGINT_RE.match(value):
|
|
92
|
+
return int(value[:-1])
|
|
93
|
+
|
|
94
|
+
if not safe_enabled and _ISO_DATE_RE.match(value):
|
|
95
|
+
return _parse_datetime(value, fallback=value)
|
|
96
|
+
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
if isinstance(value, Mapping):
|
|
100
|
+
return _deserialize_mapping(value, safe_enabled, max_depth, _depth, seen)
|
|
101
|
+
|
|
102
|
+
if isinstance(value, list):
|
|
103
|
+
return _deserialize_sequence(value, safe_enabled, max_depth, _depth, seen)
|
|
104
|
+
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _serialize_mapping(
|
|
109
|
+
value: Mapping[Any, Any],
|
|
110
|
+
safe_enabled: bool,
|
|
111
|
+
max_depth: int,
|
|
112
|
+
depth: int,
|
|
113
|
+
seen: set[int],
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
marker = id(value)
|
|
116
|
+
if marker in seen:
|
|
117
|
+
raise ValueError("Circular reference detected during serialization")
|
|
118
|
+
|
|
119
|
+
seen.add(marker)
|
|
120
|
+
try:
|
|
121
|
+
return {
|
|
122
|
+
str(key): serialize_value(
|
|
123
|
+
item,
|
|
124
|
+
safe_enabled=safe_enabled,
|
|
125
|
+
max_depth=max_depth,
|
|
126
|
+
_depth=depth + 1,
|
|
127
|
+
_seen=seen,
|
|
128
|
+
)
|
|
129
|
+
for key, item in value.items()
|
|
130
|
+
}
|
|
131
|
+
finally:
|
|
132
|
+
seen.remove(marker)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _serialize_sequence(
|
|
136
|
+
value: Sequence[Any],
|
|
137
|
+
safe_enabled: bool,
|
|
138
|
+
max_depth: int,
|
|
139
|
+
depth: int,
|
|
140
|
+
seen: set[int],
|
|
141
|
+
) -> list[Any]:
|
|
142
|
+
marker = id(value)
|
|
143
|
+
if marker in seen:
|
|
144
|
+
raise ValueError("Circular reference detected during serialization")
|
|
145
|
+
|
|
146
|
+
seen.add(marker)
|
|
147
|
+
try:
|
|
148
|
+
return [
|
|
149
|
+
serialize_value(
|
|
150
|
+
item,
|
|
151
|
+
safe_enabled=safe_enabled,
|
|
152
|
+
max_depth=max_depth,
|
|
153
|
+
_depth=depth + 1,
|
|
154
|
+
_seen=seen,
|
|
155
|
+
)
|
|
156
|
+
for item in value
|
|
157
|
+
]
|
|
158
|
+
finally:
|
|
159
|
+
seen.remove(marker)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _deserialize_mapping(
|
|
163
|
+
value: Mapping[Any, Any],
|
|
164
|
+
safe_enabled: bool,
|
|
165
|
+
max_depth: int,
|
|
166
|
+
depth: int,
|
|
167
|
+
seen: set[int],
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
marker = id(value)
|
|
170
|
+
if marker in seen:
|
|
171
|
+
raise ValueError("Circular reference detected during deserialization")
|
|
172
|
+
|
|
173
|
+
seen.add(marker)
|
|
174
|
+
try:
|
|
175
|
+
return {
|
|
176
|
+
str(key): deserialize_value(
|
|
177
|
+
item,
|
|
178
|
+
safe_enabled=safe_enabled,
|
|
179
|
+
max_depth=max_depth,
|
|
180
|
+
_depth=depth + 1,
|
|
181
|
+
_seen=seen,
|
|
182
|
+
)
|
|
183
|
+
for key, item in value.items()
|
|
184
|
+
}
|
|
185
|
+
finally:
|
|
186
|
+
seen.remove(marker)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _deserialize_sequence(
|
|
190
|
+
value: list[Any],
|
|
191
|
+
safe_enabled: bool,
|
|
192
|
+
max_depth: int,
|
|
193
|
+
depth: int,
|
|
194
|
+
seen: set[int],
|
|
195
|
+
) -> list[Any]:
|
|
196
|
+
marker = id(value)
|
|
197
|
+
if marker in seen:
|
|
198
|
+
raise ValueError("Circular reference detected during deserialization")
|
|
199
|
+
|
|
200
|
+
seen.add(marker)
|
|
201
|
+
try:
|
|
202
|
+
return [
|
|
203
|
+
deserialize_value(
|
|
204
|
+
item,
|
|
205
|
+
safe_enabled=safe_enabled,
|
|
206
|
+
max_depth=max_depth,
|
|
207
|
+
_depth=depth + 1,
|
|
208
|
+
_seen=seen,
|
|
209
|
+
)
|
|
210
|
+
for item in value
|
|
211
|
+
]
|
|
212
|
+
finally:
|
|
213
|
+
seen.remove(marker)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _parse_datetime(value: str, *, fallback: str) -> _dt.datetime | str:
|
|
217
|
+
try:
|
|
218
|
+
normalized = value.replace("Z", "+00:00")
|
|
219
|
+
return _dt.datetime.fromisoformat(normalized)
|
|
220
|
+
except ValueError:
|
|
221
|
+
return fallback
|
|
222
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import jsonschema
|
|
6
|
+
|
|
7
|
+
from .errors import RpcError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SchemaValidator:
|
|
11
|
+
def validate(self, params: Any, schema: dict[str, Any] | None) -> None:
|
|
12
|
+
if not schema:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
jsonschema.validate(instance=params, schema=schema)
|
|
17
|
+
except jsonschema.ValidationError as exc:
|
|
18
|
+
raise RpcError(
|
|
19
|
+
"Validation failed",
|
|
20
|
+
-32602,
|
|
21
|
+
{
|
|
22
|
+
"message": exc.message,
|
|
23
|
+
"path": list(exc.path),
|
|
24
|
+
},
|
|
25
|
+
) from exc
|
|
26
|
+
|
|
@@ -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,13 @@
|
|
|
1
|
+
rpc_python_toolkit/__init__.py,sha256=ph_tY5IvR-ihwa9ubr-vKWIhS4ru73RmHVbRjt-7YME,477
|
|
2
|
+
rpc_python_toolkit/client.py,sha256=QD3vf8OSdDe42522nwWEy2gEyNAjNJzayezvqN6jP5A,5276
|
|
3
|
+
rpc_python_toolkit/endpoint.py,sha256=vMizSI0lQH2HpocWyZS4238NE2wK2peQmkjSvYArVXo,10867
|
|
4
|
+
rpc_python_toolkit/errors.py,sha256=en8mmO56IeT8OfRCTheMxNOUN09RpdLqR8X4BQTB6gk,374
|
|
5
|
+
rpc_python_toolkit/protocol.py,sha256=MMQ1tF1waizZ0dlq3LZc_Dx9tVN00KFLaWh70wR9g7A,1145
|
|
6
|
+
rpc_python_toolkit/serialization.py,sha256=gWI0UyxzNj-EATaBkSG9i-b-De-bH49igqBmzkfE9fM,5755
|
|
7
|
+
rpc_python_toolkit/validation.py,sha256=MXibLokBpBq_mDo7yxZ_64S6Mc_S1LWPM6UYsdxTXZg,617
|
|
8
|
+
rpc_python_toolkit/version.py,sha256=p4MxVO8ih2-hnh7INVBkrqMKBDGTkMso1LEbM6mx3jg,23
|
|
9
|
+
rpc_python_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=-JOXJbeUglpNN6FaH-5OkUacdU1e8cXsttGJxO8EAAY,1074
|
|
10
|
+
rpc_python_toolkit-0.1.0.dist-info/METADATA,sha256=6SM4BQFhVt9QUCPJmp3k20qKy5Zcugd0hEhYQPPkm08,5317
|
|
11
|
+
rpc_python_toolkit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
rpc_python_toolkit-0.1.0.dist-info/top_level.txt,sha256=KeHhMmqjDgfOlP13qPd9_FhW3vXFe2xTL3HzuDAqiv4,19
|
|
13
|
+
rpc_python_toolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
rpc_python_toolkit
|