rpc-python-toolkit 0.1.0__py3-none-any.whl → 0.2.1__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 +2 -2
- rpc_python_toolkit/endpoint.py +75 -18
- rpc_python_toolkit/protocol.py +11 -4
- rpc_python_toolkit/version.py +1 -2
- {rpc_python_toolkit-0.1.0.dist-info → rpc_python_toolkit-0.2.1.dist-info}/METADATA +48 -3
- rpc_python_toolkit-0.2.1.dist-info/RECORD +13 -0
- {rpc_python_toolkit-0.1.0.dist-info → rpc_python_toolkit-0.2.1.dist-info}/WHEEL +1 -1
- rpc_python_toolkit-0.1.0.dist-info/RECORD +0 -13
- {rpc_python_toolkit-0.1.0.dist-info → rpc_python_toolkit-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {rpc_python_toolkit-0.1.0.dist-info → rpc_python_toolkit-0.2.1.dist-info}/top_level.txt +0 -0
rpc_python_toolkit/__init__.py
CHANGED
|
@@ -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
|
-
|
rpc_python_toolkit/endpoint.py
CHANGED
|
@@ -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,8 +53,11 @@ 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,
|
|
60
|
+
sanitize_errors: bool = True,
|
|
45
61
|
):
|
|
46
62
|
self.context = context
|
|
47
63
|
self.methods: dict[str, MethodConfig] = {}
|
|
@@ -49,8 +65,11 @@ class RpcEndpoint:
|
|
|
49
65
|
self.strict_mode = strict_mode
|
|
50
66
|
self.enable_introspection = enable_introspection
|
|
51
67
|
self.introspection_prefix = introspection_prefix
|
|
68
|
+
self.enable_batch = enable_batch
|
|
69
|
+
self.max_batch_size = max_batch_size
|
|
52
70
|
self.max_serialization_depth = max_serialization_depth
|
|
53
71
|
self.max_deserialization_depth = max_deserialization_depth
|
|
72
|
+
self.sanitize_errors = sanitize_errors
|
|
54
73
|
self.validator = SchemaValidator()
|
|
55
74
|
|
|
56
75
|
if self.enable_introspection:
|
|
@@ -100,17 +119,13 @@ class RpcEndpoint:
|
|
|
100
119
|
try:
|
|
101
120
|
request = self._decode_payload(payload)
|
|
102
121
|
except Exception as exc:
|
|
122
|
+
error = {"code": -32700, "message": "Parse error"}
|
|
123
|
+
if not self.sanitize_errors:
|
|
124
|
+
error["data"] = {"message": str(exc)}
|
|
103
125
|
return RpcHttpResponse(
|
|
104
126
|
200,
|
|
105
127
|
headers,
|
|
106
|
-
make_response(
|
|
107
|
-
None,
|
|
108
|
-
error={
|
|
109
|
-
"code": -32700,
|
|
110
|
-
"message": "Parse error",
|
|
111
|
-
"data": {"message": str(exc)},
|
|
112
|
-
},
|
|
113
|
-
),
|
|
128
|
+
make_response(None, error=error),
|
|
114
129
|
)
|
|
115
130
|
|
|
116
131
|
request_context = context or {}
|
|
@@ -159,6 +174,29 @@ class RpcEndpoint:
|
|
|
159
174
|
make_response(None, error={"code": -32600, "message": "Invalid Request"}),
|
|
160
175
|
)
|
|
161
176
|
|
|
177
|
+
if not self.enable_batch:
|
|
178
|
+
return RpcHttpResponse(
|
|
179
|
+
200,
|
|
180
|
+
headers,
|
|
181
|
+
make_response(
|
|
182
|
+
None,
|
|
183
|
+
error={"code": -32600, "message": "Batch requests are not enabled"},
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if self.max_batch_size > 0 and len(batch) > self.max_batch_size:
|
|
188
|
+
return RpcHttpResponse(
|
|
189
|
+
200,
|
|
190
|
+
headers,
|
|
191
|
+
make_response(
|
|
192
|
+
None,
|
|
193
|
+
error={
|
|
194
|
+
"code": -32600,
|
|
195
|
+
"message": f"Batch size exceeds maximum of {self.max_batch_size}",
|
|
196
|
+
},
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
162
200
|
responses = []
|
|
163
201
|
for item in batch:
|
|
164
202
|
result = self._process_single(item, context)
|
|
@@ -171,8 +209,12 @@ class RpcEndpoint:
|
|
|
171
209
|
return RpcHttpResponse(200, headers, responses)
|
|
172
210
|
|
|
173
211
|
def _process_single(self, request: Any, context: dict[str, Any]) -> dict[str, Any] | None:
|
|
212
|
+
# An invalid envelope is not a notification, even when it has no id.
|
|
213
|
+
# Do not echo an untrusted id until the complete envelope is validated.
|
|
214
|
+
valid_request = False
|
|
174
215
|
try:
|
|
175
216
|
validate_request_envelope(request)
|
|
217
|
+
valid_request = True
|
|
176
218
|
client_safe_enabled = self._client_safe_enabled(context)
|
|
177
219
|
|
|
178
220
|
if self.safe_enabled and self.strict_mode and not self._has_safe_header(context):
|
|
@@ -198,23 +240,24 @@ class RpcEndpoint:
|
|
|
198
240
|
result=self.serialize_bigints_and_dates(result),
|
|
199
241
|
)
|
|
200
242
|
except RpcError as exc:
|
|
201
|
-
if
|
|
243
|
+
if valid_request and not has_id(request):
|
|
202
244
|
return None
|
|
203
|
-
request_id = request.get("id") if
|
|
245
|
+
request_id = request.get("id") if valid_request else None
|
|
204
246
|
return self._make_error_response(request_id, exc)
|
|
205
247
|
except Exception as exc:
|
|
206
|
-
if
|
|
248
|
+
if valid_request and not has_id(request):
|
|
207
249
|
return None
|
|
208
|
-
request_id = request.get("id") if
|
|
250
|
+
request_id = request.get("id") if valid_request else None
|
|
209
251
|
return self._make_error_response(request_id, RpcError(str(exc), -32603))
|
|
210
252
|
|
|
211
253
|
def _make_error_response(self, request_id: Any, error: RpcError) -> dict[str, Any]:
|
|
254
|
+
internal_error = self.sanitize_errors and error.code == -32603
|
|
212
255
|
body = {
|
|
213
256
|
"code": error.code,
|
|
214
|
-
"message": str(error),
|
|
257
|
+
"message": "Internal error" if internal_error else str(error),
|
|
215
258
|
}
|
|
216
259
|
|
|
217
|
-
if error.data is not None:
|
|
260
|
+
if not internal_error and error.data is not None:
|
|
218
261
|
body["data"] = self.serialize_bigints_and_dates(error.data)
|
|
219
262
|
|
|
220
263
|
return make_response(request_id, error=body)
|
|
@@ -226,11 +269,23 @@ class RpcEndpoint:
|
|
|
226
269
|
context: dict[str, Any],
|
|
227
270
|
params: Any,
|
|
228
271
|
) -> Any:
|
|
229
|
-
result = handler(request, self.context
|
|
272
|
+
result = handler(request, self._resolve_handler_context(context), params)
|
|
230
273
|
if inspect.isawaitable(result):
|
|
231
274
|
raise RpcError("Async handlers require an async adapter", -32603)
|
|
232
275
|
return result
|
|
233
276
|
|
|
277
|
+
def _resolve_handler_context(self, request_context: dict[str, Any]) -> Any:
|
|
278
|
+
if self.context is None:
|
|
279
|
+
return request_context
|
|
280
|
+
|
|
281
|
+
if isinstance(self.context, dict):
|
|
282
|
+
return {**self.context, **request_context}
|
|
283
|
+
|
|
284
|
+
if request_context:
|
|
285
|
+
return RpcCallContext(application=self.context, request=request_context)
|
|
286
|
+
|
|
287
|
+
return self.context
|
|
288
|
+
|
|
234
289
|
def _register_introspection_methods(self) -> None:
|
|
235
290
|
prefix = self.introspection_prefix
|
|
236
291
|
|
|
@@ -255,7 +310,7 @@ class RpcEndpoint:
|
|
|
255
310
|
f"{prefix}.capabilities",
|
|
256
311
|
lambda _request, _context, _params: {
|
|
257
312
|
"jsonrpc": "2.0",
|
|
258
|
-
"batch":
|
|
313
|
+
"batch": self.enable_batch,
|
|
259
314
|
"notifications": True,
|
|
260
315
|
"introspection": True,
|
|
261
316
|
"safeMode": self.safe_enabled,
|
|
@@ -287,7 +342,10 @@ class RpcEndpoint:
|
|
|
287
342
|
payload = payload.decode("utf-8")
|
|
288
343
|
|
|
289
344
|
if isinstance(payload, str):
|
|
290
|
-
|
|
345
|
+
def reject_non_json_constant(value: str) -> None:
|
|
346
|
+
raise ValueError(f"Invalid JSON constant: {value}")
|
|
347
|
+
|
|
348
|
+
return json.loads(payload, parse_constant=reject_non_json_constant)
|
|
291
349
|
|
|
292
350
|
return payload
|
|
293
351
|
|
|
@@ -311,4 +369,3 @@ class RpcSafeEndpoint(RpcEndpoint):
|
|
|
311
369
|
options.setdefault("safe_enabled", True)
|
|
312
370
|
options.setdefault("strict_mode", True)
|
|
313
371
|
super().__init__(context, **options)
|
|
314
|
-
|
rpc_python_toolkit/protocol.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
from .errors import RpcError
|
|
@@ -35,8 +36,14 @@ def validate_request_envelope(request: Any) -> None:
|
|
|
35
36
|
raise RpcError("Method not specified or invalid", -32600)
|
|
36
37
|
|
|
37
38
|
if "params" in request and not isinstance(request["params"], (dict, list)):
|
|
38
|
-
raise RpcError("Invalid params", -
|
|
39
|
-
|
|
40
|
-
if "id" in request
|
|
41
|
-
|
|
39
|
+
raise RpcError("Invalid params container", -32600)
|
|
40
|
+
|
|
41
|
+
if "id" in request:
|
|
42
|
+
request_id = request["id"]
|
|
43
|
+
if (
|
|
44
|
+
isinstance(request_id, bool)
|
|
45
|
+
or not isinstance(request_id, (str, int, float, type(None)))
|
|
46
|
+
or (isinstance(request_id, float) and not math.isfinite(request_id))
|
|
47
|
+
):
|
|
48
|
+
raise RpcError("Invalid request id", -32600)
|
|
42
49
|
|
rpc_python_toolkit/version.py
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
__version__ = "0.1
|
|
2
|
-
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpc-python-toolkit
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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,19 +25,34 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
# RPC Python Toolkit
|
|
27
27
|
|
|
28
|
+
[](https://github.com/n-car/rpc-python-toolkit/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
30
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://pypi.org/project/rpc-python-toolkit/)
|
|
33
|
+
|
|
28
34
|
Framework-agnostic JSON-RPC 2.0 toolkit for Python.
|
|
29
35
|
|
|
30
36
|
Use this package when you need a small Python JSON-RPC 2.0 client/server core that can be embedded directly or adapted to HTTP frameworks. It follows the same protocol conventions used across the RPC Toolkit ecosystem.
|
|
31
37
|
|
|
32
38
|
## Project Status
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
Alpha package with the core JSON-RPC behavior implemented.
|
|
35
41
|
|
|
36
42
|
- Published on PyPI as `rpc-python-toolkit`.
|
|
37
43
|
- Standard JSON-RPC 2.0 remains the default behavior.
|
|
38
44
|
- Safe Mode follows the same `X-RPC-Safe-Enabled` and value marker conventions used by `rpc-express-toolkit`.
|
|
39
45
|
- The public API may still change before a stable Python release.
|
|
40
46
|
|
|
47
|
+
## Release Notes
|
|
48
|
+
|
|
49
|
+
### 0.2.1 - 2026-10-12
|
|
50
|
+
|
|
51
|
+
- Sanitize unexpected errors and `RpcError(-32603)` by default, removing internal messages and data while preserving explicit application errors.
|
|
52
|
+
- Validate request envelopes and reject non-JSON numeric constants, scalar/null parameters, boolean IDs, and non-finite IDs.
|
|
53
|
+
- Return errors for invalid batch elements, distinguish explicit null IDs from notifications, and preserve JSON payload shapes.
|
|
54
|
+
- Add error-handling and protocol regression tests, and document the tightened wire contract and trusted handler context.
|
|
55
|
+
|
|
41
56
|
## Installation
|
|
42
57
|
|
|
43
58
|
Install from PyPI:
|
|
@@ -80,7 +95,11 @@ python -m unittest discover
|
|
|
80
95
|
```python
|
|
81
96
|
from rpc_python_toolkit import RpcEndpoint
|
|
82
97
|
|
|
83
|
-
rpc = RpcEndpoint(
|
|
98
|
+
rpc = RpcEndpoint(
|
|
99
|
+
enable_introspection=True,
|
|
100
|
+
enable_batch=True,
|
|
101
|
+
max_batch_size=100,
|
|
102
|
+
)
|
|
84
103
|
|
|
85
104
|
def test(request, context, params):
|
|
86
105
|
return {"ok": True, "params": params}
|
|
@@ -97,6 +116,28 @@ response = rpc.handle_payload({
|
|
|
97
116
|
print(response.body)
|
|
98
117
|
```
|
|
99
118
|
|
|
119
|
+
Batch can be disabled with `enable_batch=False`. Introspection remains opt-in.
|
|
120
|
+
Unexpected errors and `RpcError` instances with code `-32603` return only
|
|
121
|
+
`Internal error` by default, without internal messages or data. Other explicit
|
|
122
|
+
`RpcError` codes, messages, and data are preserved. Parse errors omit diagnostic
|
|
123
|
+
data. Use `sanitize_errors=False` only for local debugging to restore details.
|
|
124
|
+
|
|
125
|
+
Only valid requests without an `id` are notifications. An explicit `id: null`
|
|
126
|
+
receives a response; malformed envelopes (including scalar/null `params` or
|
|
127
|
+
boolean/non-finite IDs) return `-32600` with `id: null`. Method parameter schema
|
|
128
|
+
errors remain `-32602`.
|
|
129
|
+
Malformed JSON, including the non-JSON constants `NaN`, `Infinity`, and `-Infinity`,
|
|
130
|
+
returns `-32700`. Invalid elements in a batch produce individual error responses;
|
|
131
|
+
valid notifications remain silent even when their method fails. Objects, arrays,
|
|
132
|
+
and null results retain their JSON shapes.
|
|
133
|
+
|
|
134
|
+
When both application and request contexts are dictionaries, handlers receive a
|
|
135
|
+
merged dictionary and request values take precedence. For a non-dictionary
|
|
136
|
+
application context plus request data, handlers receive `RpcCallContext` with
|
|
137
|
+
separate `application` and `request` attributes.
|
|
138
|
+
These contexts are supplied by application/adapter code. Client `params` remain a
|
|
139
|
+
separate third handler argument and cannot replace trusted context.
|
|
140
|
+
|
|
100
141
|
Result:
|
|
101
142
|
|
|
102
143
|
```json
|
|
@@ -131,6 +172,10 @@ Safe Mode enables `X-RPC-Safe-Enabled` negotiation and recursive value encoding/
|
|
|
131
172
|
|
|
132
173
|
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
174
|
|
|
175
|
+
Safe Mode transforms payload values, not method names, request IDs, error codes,
|
|
176
|
+
or error messages. Application `RpcError.data` is encoded like a result; sanitized
|
|
177
|
+
internal errors expose neither their original message nor their data.
|
|
178
|
+
|
|
134
179
|
## Examples
|
|
135
180
|
|
|
136
181
|
Runnable examples are available in [`examples/`](examples/):
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
rpc_python_toolkit/__init__.py,sha256=OR3yfeiE-KEwC6CqAql59PljqW_VzHDyTwCZ5h7iJwU,514
|
|
2
|
+
rpc_python_toolkit/client.py,sha256=QD3vf8OSdDe42522nwWEy2gEyNAjNJzayezvqN6jP5A,5276
|
|
3
|
+
rpc_python_toolkit/endpoint.py,sha256=nucv7Q9gEeiFWQlL8ajdHFcgxjouK2EqqMMP-K_lOs0,12997
|
|
4
|
+
rpc_python_toolkit/errors.py,sha256=en8mmO56IeT8OfRCTheMxNOUN09RpdLqR8X4BQTB6gk,374
|
|
5
|
+
rpc_python_toolkit/protocol.py,sha256=CfI316lATY5qWH0LER5lnqR4c5m7tnh_S5sO4xDPVmc,1360
|
|
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=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
|
|
9
|
+
rpc_python_toolkit-0.2.1.dist-info/licenses/LICENSE,sha256=-JOXJbeUglpNN6FaH-5OkUacdU1e8cXsttGJxO8EAAY,1074
|
|
10
|
+
rpc_python_toolkit-0.2.1.dist-info/METADATA,sha256=9h1w-55QH0IpP4Tv2s2ZU1ryriyyqj7rxPZBrlPk5rI,8133
|
|
11
|
+
rpc_python_toolkit-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
rpc_python_toolkit-0.2.1.dist-info/top_level.txt,sha256=KeHhMmqjDgfOlP13qPd9_FhW3vXFe2xTL3HzuDAqiv4,19
|
|
13
|
+
rpc_python_toolkit-0.2.1.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|