jsonrpcserver 5.0.10__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.
- jsonrpcserver/__init__.py +42 -0
- jsonrpcserver/async_dispatcher.py +187 -0
- jsonrpcserver/async_main.py +180 -0
- jsonrpcserver/codes.py +20 -0
- jsonrpcserver/dispatcher.py +460 -0
- jsonrpcserver/exceptions.py +35 -0
- jsonrpcserver/main.py +308 -0
- jsonrpcserver/methods.py +91 -0
- jsonrpcserver/py.typed +0 -0
- jsonrpcserver/request-schema.json +39 -0
- jsonrpcserver/request.py +15 -0
- jsonrpcserver/response.py +170 -0
- jsonrpcserver/result.py +150 -0
- jsonrpcserver/sentinels.py +28 -0
- jsonrpcserver/server.py +115 -0
- jsonrpcserver/utils.py +51 -0
- jsonrpcserver-5.0.10.dist-info/METADATA +163 -0
- jsonrpcserver-5.0.10.dist-info/RECORD +20 -0
- jsonrpcserver-5.0.10.dist-info/WHEEL +4 -0
- jsonrpcserver-5.0.10.dist-info/licenses/LICENSE +21 -0
jsonrpcserver/result.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""What a method returns: Success, Error or InvalidParams.
|
|
2
|
+
|
|
3
|
+
A Result is the "result" or "error" part of a JSON-RPC response object
|
|
4
|
+
(https://www.jsonrpc.org/specification#response_object). The library adds the
|
|
5
|
+
"jsonrpc" and "id" parts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, NamedTuple
|
|
9
|
+
|
|
10
|
+
from oslash.either import Either, Left, Right
|
|
11
|
+
|
|
12
|
+
from .codes import ERROR_INTERNAL_ERROR, ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND
|
|
13
|
+
from .sentinels import NODATA
|
|
14
|
+
from .utils import warn_if_invalid_error
|
|
15
|
+
|
|
16
|
+
# pylint: disable=missing-class-docstring,missing-function-docstring,invalid-name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SuccessResult(NamedTuple):
|
|
20
|
+
"""The value inside the Result that `Success` returns."""
|
|
21
|
+
|
|
22
|
+
result: Any = None
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"SuccessResult({self.result!r})"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ErrorResult(NamedTuple):
|
|
29
|
+
"""The value inside the Result that `Error` and `InvalidParams` return.
|
|
30
|
+
|
|
31
|
+
`data` is `NODATA` when there's no data, so it's left out of the response.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
code: int
|
|
35
|
+
message: str
|
|
36
|
+
data: Any = NODATA # The spec says this value may be omitted
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return (
|
|
40
|
+
f"ErrorResult(code={self.code!r}, message={self.message!r}, "
|
|
41
|
+
f"data={self.data!r})"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Union of the two valid result types. oslash's Either takes the success type
|
|
46
|
+
# first, then the error type.
|
|
47
|
+
Result = Either[SuccessResult, ErrorResult]
|
|
48
|
+
"""The return type of a method: what `Success`, `Error` and `InvalidParams` give.
|
|
49
|
+
|
|
50
|
+
It's an oslash `Either`: a `Right` holding a `SuccessResult`, or a `Left` holding an
|
|
51
|
+
`ErrorResult`. Use it as the return annotation of your methods. You don't need to
|
|
52
|
+
look inside it.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Helpers
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def MethodNotFoundResult(data: Any) -> ErrorResult:
|
|
60
|
+
return ErrorResult(ERROR_METHOD_NOT_FOUND, "Method not found", data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def InternalErrorResult(data: Any) -> ErrorResult:
|
|
64
|
+
return ErrorResult(ERROR_INTERNAL_ERROR, "Internal error", data)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def InvalidParamsResult(data: Any = NODATA) -> ErrorResult:
|
|
68
|
+
return ErrorResult(ERROR_INVALID_PARAMS, "Invalid params", data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Helpers (the public functions)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def Success(result: Any = None) -> Result:
|
|
75
|
+
"""A successful result. Return it from a method.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
result: The value for the response's `result` member. It can be anything
|
|
79
|
+
the serializer handles. The default, None, is sent as `null`.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A Result holding the value.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> from jsonrpcserver import Result, Success, dispatch
|
|
86
|
+
>>> def ping() -> Result:
|
|
87
|
+
... return Success("pong")
|
|
88
|
+
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping})
|
|
89
|
+
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
|
|
90
|
+
"""
|
|
91
|
+
return Right(SuccessResult(result))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def Error(code: int, message: str, data: Any = NODATA) -> Result:
|
|
95
|
+
"""An error result. Return it from a method to send an error response.
|
|
96
|
+
|
|
97
|
+
It's sent as it is, whatever `debug` is, so don't put secrets in it.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
code: The error code, an integer. The spec reserves -32768 to -32000 for
|
|
101
|
+
its own errors, so pick other numbers for yours.
|
|
102
|
+
message: A short description of the error, as a string.
|
|
103
|
+
data: Extra information for the client, such as details of what went
|
|
104
|
+
wrong. If it isn't given, the response has no `data` member.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A Result holding the error.
|
|
108
|
+
|
|
109
|
+
Warns:
|
|
110
|
+
UserWarning: If `code` isn't an int or `message` isn't a str. The error is
|
|
111
|
+
still sent as given. New in 5.0.10.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> from jsonrpcserver import Error, Result, dispatch_to_serializable
|
|
115
|
+
>>> def fail() -> Result:
|
|
116
|
+
... return Error(1, "It failed", {"reason": "example"})
|
|
117
|
+
>>> request = '{"jsonrpc": "2.0", "method": "fail", "id": 1}'
|
|
118
|
+
>>> dispatch_to_serializable(request, {"fail": fail})["error"]
|
|
119
|
+
{'code': 1, 'message': 'It failed', 'data': {'reason': 'example'}}
|
|
120
|
+
"""
|
|
121
|
+
warn_if_invalid_error(code, message, stacklevel=2)
|
|
122
|
+
return Left(ErrorResult(code, message, data))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def InvalidParams(data: Any = NODATA) -> Result:
|
|
126
|
+
"""An Invalid params error: `Error(-32602, "Invalid params", data)`.
|
|
127
|
+
|
|
128
|
+
Return it when the arguments have the right shape but a bad value.
|
|
129
|
+
jsonrpcserver already sends this error when the arguments don't fit the
|
|
130
|
+
method's signature.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
data: What was wrong, for the client. If it isn't given, the response has no
|
|
134
|
+
`data` member.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A Result holding the error.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
>>> from jsonrpcserver import InvalidParams, Result, Success
|
|
141
|
+
>>> from jsonrpcserver import dispatch_to_serializable
|
|
142
|
+
>>> def rate(stars: int) -> Result:
|
|
143
|
+
... if stars not in range(1, 6):
|
|
144
|
+
... return InvalidParams("Stars must be 1 to 5")
|
|
145
|
+
... return Success()
|
|
146
|
+
>>> request = '{"jsonrpc": "2.0", "method": "rate", "params": [6], "id": 1}'
|
|
147
|
+
>>> dispatch_to_serializable(request, {"rate": rate})["error"]
|
|
148
|
+
{'code': -32602, 'message': 'Invalid params', 'data': 'Stars must be 1 to 5'}
|
|
149
|
+
"""
|
|
150
|
+
return Left(InvalidParamsResult(data))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Sentinels - these are used to indicate no data is present.
|
|
2
|
+
|
|
3
|
+
We can't use None, because None may be a valid piece of data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Sentinel:
|
|
10
|
+
"""Use this class to create a unique object.
|
|
11
|
+
|
|
12
|
+
Has a nicer repr than `object()`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# pylint: disable=too-few-public-methods
|
|
16
|
+
def __init__(self, name: str):
|
|
17
|
+
self.name = name
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"<{sys.intern(str(self.name)).rsplit('.', 1)[-1]}>"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
NOCONTEXT = Sentinel("NoContext")
|
|
24
|
+
"""The default for `context`: don't pass a context to methods."""
|
|
25
|
+
NODATA = Sentinel("NoData")
|
|
26
|
+
"""The default for `data` in `Error` and `JsonRpcError`: leave `data` out."""
|
|
27
|
+
NOID = Sentinel("NoId")
|
|
28
|
+
"""The id of a notification, which has none."""
|
jsonrpcserver/server.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""A development server for trying out methods, built on Python's http.server.
|
|
2
|
+
|
|
3
|
+
It's meant for trying things out. For production, put dispatch behind a real web
|
|
4
|
+
server or framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .main import dispatch
|
|
14
|
+
from .response import ParseErrorResponse, to_error_dict
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RequestHandler(BaseHTTPRequestHandler):
|
|
20
|
+
"""Handle HTTP requests"""
|
|
21
|
+
|
|
22
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
23
|
+
"""Log through the logging module instead of writing to sys.stderr.
|
|
24
|
+
|
|
25
|
+
sys.stderr is None in some environments, such as a PyInstaller app built with
|
|
26
|
+
--noconsole. Writing to it there raised an exception that dropped every
|
|
27
|
+
connection without a response (#269).
|
|
28
|
+
"""
|
|
29
|
+
logger.info("%s - %s", self.address_string(), format % args)
|
|
30
|
+
|
|
31
|
+
def do_POST(self) -> None:
|
|
32
|
+
"""Handle POST request"""
|
|
33
|
+
try:
|
|
34
|
+
length = int(self.headers["Content-Length"])
|
|
35
|
+
except TypeError:
|
|
36
|
+
self.send_error(411, "Content-Length required")
|
|
37
|
+
return
|
|
38
|
+
except ValueError:
|
|
39
|
+
self.send_error(400, "Invalid Content-Length")
|
|
40
|
+
return
|
|
41
|
+
if length < 0:
|
|
42
|
+
self.send_error(400, "Invalid Content-Length")
|
|
43
|
+
return
|
|
44
|
+
body = self.rfile.read(length)
|
|
45
|
+
try:
|
|
46
|
+
request = body.decode("utf-8")
|
|
47
|
+
except UnicodeDecodeError as exc:
|
|
48
|
+
response = json.dumps(to_error_dict(ParseErrorResponse(str(exc))))
|
|
49
|
+
else:
|
|
50
|
+
response = dispatch(request)
|
|
51
|
+
if response:
|
|
52
|
+
self.send_response(200)
|
|
53
|
+
self.send_header("Content-Type", "application/json")
|
|
54
|
+
self.send_header("Content-Length", str(len(response.encode())))
|
|
55
|
+
self.end_headers()
|
|
56
|
+
self.wfile.write(response.encode())
|
|
57
|
+
else:
|
|
58
|
+
# A notification, or a batch of only notifications. There's nothing to send.
|
|
59
|
+
self.send_response(204)
|
|
60
|
+
self.end_headers()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def listening_message(name: str, port: int) -> str:
|
|
64
|
+
"""The line serve() shows when it starts."""
|
|
65
|
+
if name in ("", "0.0.0.0"):
|
|
66
|
+
return (
|
|
67
|
+
f" * Listening on port {port} on every network interface. This is a "
|
|
68
|
+
'development server. Use serve("localhost", ...) to accept only local '
|
|
69
|
+
"connections."
|
|
70
|
+
)
|
|
71
|
+
return f" * Listening on http://{name}:{port}/. This is a development server."
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def serve(name: str = "", port: int = 5000) -> None:
|
|
75
|
+
"""Serve the methods registered with `@method` over HTTP. For development only.
|
|
76
|
+
|
|
77
|
+
It answers POST requests on any path with `dispatch`, sends 204 No Content for a
|
|
78
|
+
notification, and runs each request in its own thread. It runs until the process
|
|
79
|
+
is stopped, for example with Ctrl+C.
|
|
80
|
+
|
|
81
|
+
When it starts, it logs where it's listening on the `jsonrpcserver.server`
|
|
82
|
+
logger. If logging isn't configured, it writes that line to stderr instead
|
|
83
|
+
(new in 5.0.10). Each request is logged at INFO level.
|
|
84
|
+
|
|
85
|
+
It has no TLS, no authentication and no request size limit, and it doesn't pass
|
|
86
|
+
`max_batch_size`. Put `dispatch` behind a real web server or framework in
|
|
87
|
+
production.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name: The host name or address to listen on. The default, "", listens on
|
|
91
|
+
every network interface, so other machines can connect. Pass "localhost"
|
|
92
|
+
to accept only local connections.
|
|
93
|
+
port: The port to listen on.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
```python
|
|
97
|
+
from jsonrpcserver import Result, Success, method, serve
|
|
98
|
+
|
|
99
|
+
@method
|
|
100
|
+
def ping() -> Result:
|
|
101
|
+
return Success("pong")
|
|
102
|
+
|
|
103
|
+
serve("localhost", 8000)
|
|
104
|
+
```
|
|
105
|
+
"""
|
|
106
|
+
httpd = ThreadingHTTPServer((name, port), RequestHandler)
|
|
107
|
+
try:
|
|
108
|
+
message = listening_message(name, port)
|
|
109
|
+
logger.info("%s", message)
|
|
110
|
+
if not logger.hasHandlers() and sys.stderr is not None:
|
|
111
|
+
# logging isn't configured, so the line above went nowhere.
|
|
112
|
+
print(message, file=sys.stderr, flush=True)
|
|
113
|
+
httpd.serve_forever()
|
|
114
|
+
finally:
|
|
115
|
+
httpd.server_close()
|
jsonrpcserver/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Utility functions"""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Any, Callable, List, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from oslash.either import Either, Right
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
# pylint: disable=invalid-name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def identity(x: Any) -> Any:
|
|
15
|
+
"""Returns the argument."""
|
|
16
|
+
return x
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]:
|
|
20
|
+
"""Compose two or more functions producing a single composite function."""
|
|
21
|
+
return reduce(lambda f, g: lambda *a, **kw: f(g(*a, **kw)), funcs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_list(x: Any) -> List[Any]:
|
|
25
|
+
"""Puts a value into a list if it's not already."""
|
|
26
|
+
# pyright can't tell the element type of a list narrowed from Any.
|
|
27
|
+
return x if isinstance(x, list) else [x] # pyright: ignore[reportUnknownVariableType]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def warn_if_invalid_error(code: Any, message: Any, stacklevel: int) -> None:
|
|
31
|
+
"""Warn if an error's code or message breaks the JSON-RPC spec.
|
|
32
|
+
|
|
33
|
+
The spec says code MUST be an integer and message SHOULD be a string. These used to
|
|
34
|
+
be sent as given, which some clients can't parse. It's a warning, not an error,
|
|
35
|
+
so existing code keeps working.
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(code, bool) or not isinstance(code, int):
|
|
38
|
+
warnings.warn(
|
|
39
|
+
f"JSON-RPC error codes must be integers, not {code!r}",
|
|
40
|
+
stacklevel=stacklevel + 1,
|
|
41
|
+
)
|
|
42
|
+
if not isinstance(message, str):
|
|
43
|
+
warnings.warn(
|
|
44
|
+
f"JSON-RPC error messages should be strings, not {message!r}",
|
|
45
|
+
stacklevel=stacklevel + 1,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def unwrap(either: Either[T, Any]) -> T:
|
|
50
|
+
"""The value inside a Right. Only call this once you know it isn't a Left."""
|
|
51
|
+
return cast("Right[T, Any]", either)._value
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jsonrpcserver
|
|
3
|
+
Version: 5.0.10
|
|
4
|
+
Summary: Process incoming JSON-RPC 2.0 requests in Python
|
|
5
|
+
Keywords: json-rpc,jsonrpc,json-rpc-2.0,rpc,server
|
|
6
|
+
Author-email: Beau Barker <beau@explodinglabs.com>
|
|
7
|
+
Maintainer-email: Synapse Research <ben@synapsereality.io>
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
|
+
Classifier: Topic :: Internet
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: jsonschema<5
|
|
30
|
+
Requires-Dist: oslash<1
|
|
31
|
+
Requires-Dist: hypothesis ; extra == "test"
|
|
32
|
+
Requires-Dist: pytest ; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-asyncio ; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-cov ; extra == "test"
|
|
35
|
+
Project-URL: Changelog, https://bensynapse.github.io/jsonrpcserver/changelog/
|
|
36
|
+
Project-URL: Documentation, https://bensynapse.github.io/jsonrpcserver/
|
|
37
|
+
Project-URL: Homepage, https://bensynapse.github.io/jsonrpcserver/
|
|
38
|
+
Project-URL: Issues, https://github.com/bensynapse/jsonrpcserver/issues
|
|
39
|
+
Project-URL: Repository, https://github.com/bensynapse/jsonrpcserver
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Import-Name: jsonrpcserver
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<a href="https://pypi.org/project/jsonrpcserver/"><img src="https://img.shields.io/pypi/v/jsonrpcserver.svg" alt="PyPI version" /></a>
|
|
45
|
+
<a href="https://github.com/bensynapse/jsonrpcserver/actions/workflows/ci.yml"><img src="https://github.com/bensynapse/jsonrpcserver/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
46
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/#install"><img src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue" alt="Python 3.8 to 3.14" /></a>
|
|
47
|
+
<a href="https://pypistats.org/packages/jsonrpcserver"><img src="https://img.shields.io/pypi/dm/jsonrpcserver" alt="Downloads per month" /></a>
|
|
48
|
+
<a href="https://github.com/bensynapse/jsonrpcserver/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bensynapse/jsonrpcserver" alt="License: MIT" /></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<img alt="jsonrpcserver" src="https://raw.githubusercontent.com/bensynapse/jsonrpcserver/main/logo.png" />
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<i>Process incoming JSON-RPC 2.0 requests in Python</i>
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<p align="center">
|
|
60
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/">Documentation</a> |
|
|
61
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/reference/">API reference</a> |
|
|
62
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/examples/">Examples</a> |
|
|
63
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/changelog/">Changelog</a> |
|
|
64
|
+
<a href="https://bensynapse.github.io/jsonrpcserver/migration/">Migration</a>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
jsonrpcserver takes a [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
|
|
68
|
+
request, calls your Python function and gives you the response to send back.
|
|
69
|
+
It leaves the networking to you, so it fits into whatever server you already
|
|
70
|
+
have.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- Works with any framework or transport. The docs have tested examples for
|
|
75
|
+
http.server, Flask, Werkzeug, Django, FastAPI, aiohttp, Sanic, Tornado,
|
|
76
|
+
websockets, ZeroMQ and Socket.IO.
|
|
77
|
+
- Sync and async: `dispatch` and `async_dispatch`. With `async_dispatch`, the
|
|
78
|
+
requests in a batch run concurrently.
|
|
79
|
+
- Follows the spec for batches, notifications and errors, and checks each
|
|
80
|
+
request's params against your function's signature.
|
|
81
|
+
- Keeps exception messages out of responses and in your logs, and limits
|
|
82
|
+
batch size with `max_batch_size` (from 5.0.10).
|
|
83
|
+
- Typed: ships `py.typed`, and `@method` keeps your functions' signatures for
|
|
84
|
+
mypy and pyright.
|
|
85
|
+
- Safe to call from several threads, and tested on Python 3.8 to 3.14,
|
|
86
|
+
including free-threaded 3.14t.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pip install jsonrpcserver
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If you're on 5.0.9 or older, upgrade: those versions send exception messages
|
|
95
|
+
to clients. The
|
|
96
|
+
[changelog](https://bensynapse.github.io/jsonrpcserver/changelog/) lists what
|
|
97
|
+
changed in 5.0.10.
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
Save this as `server.py` and run it with `python server.py`:
|
|
102
|
+
|
|
103
|
+
<!-- skip: it runs forever. docs/examples/check_examples.py runs the same file. -->
|
|
104
|
+
```python
|
|
105
|
+
from jsonrpcserver import Result, Success, method, serve
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@method
|
|
109
|
+
def ping() -> Result:
|
|
110
|
+
return Success("pong")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
serve("localhost", 8000)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then send it a request from another terminal:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
curl -s -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' http://localhost:8000/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Output:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
{"jsonrpc": "2.0", "result": "pong", "id": 1}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`serve()` is a small server for trying things out. In your own framework,
|
|
130
|
+
pass the request body to `dispatch` and send back the string it returns. The
|
|
131
|
+
[examples](https://bensynapse.github.io/jsonrpcserver/examples/) show how.
|
|
132
|
+
|
|
133
|
+
## Security
|
|
134
|
+
|
|
135
|
+
Before you put a server on the internet, pass `max_batch_size` to `dispatch`
|
|
136
|
+
and limit the request body size in your framework. Run 5.0.10 or later. The
|
|
137
|
+
[Security page](https://bensynapse.github.io/jsonrpcserver/security/)
|
|
138
|
+
explains why.
|
|
139
|
+
|
|
140
|
+
## Documentation
|
|
141
|
+
|
|
142
|
+
- [Documentation](https://bensynapse.github.io/jsonrpcserver/): guides,
|
|
143
|
+
framework examples and the
|
|
144
|
+
[API reference](https://bensynapse.github.io/jsonrpcserver/reference/).
|
|
145
|
+
- [Migrating from 4.x](https://bensynapse.github.io/jsonrpcserver/migration/):
|
|
146
|
+
methods must now return `Success(value)`. A 4.x method that returns a plain
|
|
147
|
+
value still runs, but the client gets an Internal error.
|
|
148
|
+
- [Contributing](https://github.com/bensynapse/jsonrpcserver/blob/main/CONTRIBUTING.md)
|
|
149
|
+
and the
|
|
150
|
+
[security policy](https://github.com/bensynapse/jsonrpcserver/blob/main/SECURITY.md).
|
|
151
|
+
- [License](https://github.com/bensynapse/jsonrpcserver/blob/main/LICENSE): MIT.
|
|
152
|
+
|
|
153
|
+
## See also
|
|
154
|
+
|
|
155
|
+
[jsonrpcclient](https://bensynapse.github.io/jsonrpcclient/)
|
|
156
|
+
([GitHub](https://github.com/bensynapse/jsonrpcclient)) creates JSON-RPC
|
|
157
|
+
requests and parses the responses in Python. It's the client-side companion
|
|
158
|
+
to this library, and its quickstart talks to the server above.
|
|
159
|
+
|
|
160
|
+
## Credits
|
|
161
|
+
|
|
162
|
+
Created by Beau Barker. Maintained by [Synapse Research](https://synapsereality.io).
|
|
163
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
jsonrpcserver/__init__.py,sha256=I0vyGlCzULRQfDmNYgSTEq75mh5d74raO6ulAEcPpE0,1143
|
|
2
|
+
jsonrpcserver/async_dispatcher.py,sha256=fGm58YijUQ4De1FJHSvVQz7pcKhBbQRTRL3CmZxZ-qA,6254
|
|
3
|
+
jsonrpcserver/async_main.py,sha256=l91jnWHcxR7pCLsOkOaEtRrSSEcR8Wn360JnNK_7K1U,6698
|
|
4
|
+
jsonrpcserver/codes.py,sha256=chdfOcuTHbv-rVRe27aehsr-y2x4Opax0p7wSDJlCiw,748
|
|
5
|
+
jsonrpcserver/dispatcher.py,sha256=-9JCmFjMS6_TZOf9Mgob-P6a2J5GhOh0LfWTGvwCYTA,16947
|
|
6
|
+
jsonrpcserver/exceptions.py,sha256=7I2krzLJOrifighXVXxl9exwgajLgy6dJqAyrCkZZcM,1439
|
|
7
|
+
jsonrpcserver/main.py,sha256=HjeQChWFqw4RLd0NjurUCEtjJq8VqFdO1mro1EgvNTw,11907
|
|
8
|
+
jsonrpcserver/methods.py,sha256=hiEzP5x5Liw8VhemqFz9lRuJoJk9Xq9vHKkhYT-bzxU,3142
|
|
9
|
+
jsonrpcserver/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
jsonrpcserver/request-schema.json,sha256=JY5j2q8hU6hbJBdyv7I932YDw5AjuMmw-v3KUDVyvTc,1277
|
|
11
|
+
jsonrpcserver/request.py,sha256=R2bChmGSDH3gYD2JdegQbzpZjdop_wOqLWSQzZqm9nE,398
|
|
12
|
+
jsonrpcserver/response.py,sha256=IWIxpObs4l07IdMezlMNHFX0S0lF4EwTaS3H8UK8tpo,6053
|
|
13
|
+
jsonrpcserver/result.py,sha256=wp3iwsB1Jnq0mCs0seghm4SJnQ9Y3aomuAGbHpSux3k,5129
|
|
14
|
+
jsonrpcserver/sentinels.py,sha256=fwL9_C57DhuQkxp2pW9T4fi0IcH6nCHHyfaaNcjBXSU,741
|
|
15
|
+
jsonrpcserver/server.py,sha256=gnpEY1YhCyiJh6EwooSdlEWq8vfyHkjXz-WFcjqfVw4,4210
|
|
16
|
+
jsonrpcserver/utils.py,sha256=aGXOeAmNZNSertL3d87mgWAt4vX48EfTL-E636mWR5o,1683
|
|
17
|
+
jsonrpcserver-5.0.10.dist-info/licenses/LICENSE,sha256=_FwZBYAN8tqi6GIMUwE3E9UUG06-g-gEjYUrCr7c2Gg,1106
|
|
18
|
+
jsonrpcserver-5.0.10.dist-info/WHEEL,sha256=rLJE8OLPBZbz7tqxs1yD-wA-ADa1Ec5PuYU-Y_PNWDI,81
|
|
19
|
+
jsonrpcserver-5.0.10.dist-info/METADATA,sha256=u9ZXOJ5upoDzx9MMRjHt_RjzsTEmuk1Ytzrp4uLebyo,6830
|
|
20
|
+
jsonrpcserver-5.0.10.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Beau Barker (beau at explodinglabs.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
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.
|