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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Process incoming JSON-RPC requests in Python.
|
|
2
|
+
|
|
3
|
+
Write methods with @method, then pass each request string to dispatch, or to
|
|
4
|
+
async_dispatch in an asyncio server. Documentation:
|
|
5
|
+
https://bensynapse.github.io/jsonrpcserver/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# __all__ also tells type checkers that these names are re-exported.
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Error",
|
|
11
|
+
"InvalidParams",
|
|
12
|
+
"JsonRpcError",
|
|
13
|
+
"Result",
|
|
14
|
+
"Success",
|
|
15
|
+
"async_dispatch",
|
|
16
|
+
"async_dispatch_to_response",
|
|
17
|
+
"async_dispatch_to_serializable",
|
|
18
|
+
"dispatch",
|
|
19
|
+
"dispatch_to_response",
|
|
20
|
+
"dispatch_to_serializable",
|
|
21
|
+
"method",
|
|
22
|
+
"serve",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from .async_main import (
|
|
27
|
+
dispatch as async_dispatch,
|
|
28
|
+
)
|
|
29
|
+
from .async_main import (
|
|
30
|
+
dispatch_to_response as async_dispatch_to_response,
|
|
31
|
+
)
|
|
32
|
+
from .async_main import (
|
|
33
|
+
dispatch_to_serializable as async_dispatch_to_serializable,
|
|
34
|
+
)
|
|
35
|
+
from .exceptions import JsonRpcError
|
|
36
|
+
from .main import dispatch, dispatch_to_response, dispatch_to_serializable
|
|
37
|
+
from .methods import method
|
|
38
|
+
from .result import Error, InvalidParams, Result, Success
|
|
39
|
+
from .server import serve
|
|
40
|
+
|
|
41
|
+
__version__ = "5.0.10"
|
|
42
|
+
"""The version of jsonrpcserver, as a string. Added in 5.0.10."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Async version of dispatcher.py"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from functools import partial
|
|
6
|
+
from inspect import isawaitable
|
|
7
|
+
from itertools import starmap
|
|
8
|
+
from typing import Any, Callable, Optional, Tuple, cast
|
|
9
|
+
|
|
10
|
+
from oslash.either import Left
|
|
11
|
+
|
|
12
|
+
from .dispatcher import (
|
|
13
|
+
NORESPONSE,
|
|
14
|
+
BatchTooLargeResponse,
|
|
15
|
+
Deserialized,
|
|
16
|
+
InvalidResultError,
|
|
17
|
+
create_request,
|
|
18
|
+
deserialize_request,
|
|
19
|
+
exception_data,
|
|
20
|
+
extract_args,
|
|
21
|
+
extract_kwargs,
|
|
22
|
+
extract_list,
|
|
23
|
+
get_method,
|
|
24
|
+
log_invalid_result,
|
|
25
|
+
member_id,
|
|
26
|
+
not_notification,
|
|
27
|
+
to_response,
|
|
28
|
+
validate_args,
|
|
29
|
+
validate_request,
|
|
30
|
+
validate_result,
|
|
31
|
+
)
|
|
32
|
+
from .exceptions import JsonRpcError
|
|
33
|
+
from .methods import AnyMethod, MethodsArgument
|
|
34
|
+
|
|
35
|
+
# Importable from here in 5.0.9. The "as" form marks a re-export.
|
|
36
|
+
from .methods import Method as Method
|
|
37
|
+
from .methods import Methods as Methods
|
|
38
|
+
from .request import Request
|
|
39
|
+
from .response import InvalidRequestResponse, Response, ServerErrorResponse
|
|
40
|
+
from .result import ErrorResult, InternalErrorResult, Result
|
|
41
|
+
from .utils import identity, make_list, unwrap
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def call(
|
|
47
|
+
request: Request, context: Any, method: AnyMethod, debug: bool = False
|
|
48
|
+
) -> Result:
|
|
49
|
+
try:
|
|
50
|
+
result = method(*extract_args(request, context), **extract_kwargs(request))
|
|
51
|
+
# Plain (sync) methods work too. Their result is used as it is.
|
|
52
|
+
if isawaitable(result):
|
|
53
|
+
result = await result
|
|
54
|
+
validate_result(result)
|
|
55
|
+
except JsonRpcError as exc:
|
|
56
|
+
return Left(ErrorResult(code=exc.code, message=exc.message, data=exc.data))
|
|
57
|
+
except InvalidResultError as exc:
|
|
58
|
+
log_invalid_result(request.method, exc, logger)
|
|
59
|
+
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
# Other error inside method - Internal error
|
|
62
|
+
logger.exception("Method %r raised an exception", request.method)
|
|
63
|
+
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
64
|
+
# validate_result has checked it.
|
|
65
|
+
return cast(Result, result)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def dispatch_request(
|
|
69
|
+
methods: MethodsArgument, context: Any, request: Request, debug: bool = False
|
|
70
|
+
) -> Tuple[Request, Result]:
|
|
71
|
+
method = get_method(methods, request.method).bind(
|
|
72
|
+
partial(validate_args, request, context)
|
|
73
|
+
)
|
|
74
|
+
if isinstance(method, Left):
|
|
75
|
+
return request, cast(Result, method)
|
|
76
|
+
return request, await call(request, context, unwrap(method), debug=debug)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def dispatch_deserialized(
|
|
80
|
+
methods: MethodsArgument,
|
|
81
|
+
context: Any,
|
|
82
|
+
post_process: Callable[[Response], Any],
|
|
83
|
+
deserialized: Deserialized,
|
|
84
|
+
debug: bool = False,
|
|
85
|
+
) -> Any:
|
|
86
|
+
results = await asyncio.gather(
|
|
87
|
+
*(
|
|
88
|
+
dispatch_request(methods, context, r, debug=debug)
|
|
89
|
+
for r in map(create_request, make_list(deserialized))
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return extract_list(
|
|
93
|
+
isinstance(deserialized, list),
|
|
94
|
+
map(
|
|
95
|
+
post_process,
|
|
96
|
+
starmap(to_response, filter(not_notification, results)),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def dispatch_single(
|
|
102
|
+
validator: Callable[[Deserialized], object],
|
|
103
|
+
methods: MethodsArgument,
|
|
104
|
+
context: Any,
|
|
105
|
+
deserialized: Deserialized,
|
|
106
|
+
debug: bool = False,
|
|
107
|
+
) -> Optional[Response]:
|
|
108
|
+
"""Validate and dispatch one member of a batch."""
|
|
109
|
+
if isinstance(deserialized, list):
|
|
110
|
+
return Left(InvalidRequestResponse("The request failed schema validation"))
|
|
111
|
+
result = validate_request(validator, deserialized)
|
|
112
|
+
if isinstance(result, Left):
|
|
113
|
+
return Left(result._error)
|
|
114
|
+
return cast(
|
|
115
|
+
Optional[Response],
|
|
116
|
+
await dispatch_deserialized(
|
|
117
|
+
methods, context, identity, unwrap(result), debug=debug
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def dispatch_member(
|
|
123
|
+
validator: Callable[[Deserialized], object],
|
|
124
|
+
methods: MethodsArgument,
|
|
125
|
+
context: Any,
|
|
126
|
+
post_process: Callable[[Response], Any],
|
|
127
|
+
member: Any,
|
|
128
|
+
debug: bool = False,
|
|
129
|
+
) -> Any:
|
|
130
|
+
"""Dispatch one member of a batch, keeping any failure inside that member.
|
|
131
|
+
|
|
132
|
+
Returns: The post-processed response, or NORESPONSE for a notification.
|
|
133
|
+
"""
|
|
134
|
+
response: Optional[Response]
|
|
135
|
+
try:
|
|
136
|
+
response = await dispatch_single(
|
|
137
|
+
validator, methods, context, member, debug=debug
|
|
138
|
+
)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
logger.exception("Error while dispatching a member of a batch")
|
|
141
|
+
response = Left(
|
|
142
|
+
ServerErrorResponse(exception_data(exc, debug), member_id(member))
|
|
143
|
+
)
|
|
144
|
+
return NORESPONSE if response is None else post_process(response)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def dispatch_to_response_pure(
|
|
148
|
+
*,
|
|
149
|
+
deserializer: Callable[[str], Deserialized],
|
|
150
|
+
validator: Callable[[Deserialized], object],
|
|
151
|
+
methods: MethodsArgument,
|
|
152
|
+
context: Any,
|
|
153
|
+
post_process: Callable[[Response], Any],
|
|
154
|
+
request: str,
|
|
155
|
+
debug: bool = False,
|
|
156
|
+
max_batch_size: Optional[int] = None,
|
|
157
|
+
) -> Any:
|
|
158
|
+
try:
|
|
159
|
+
parsed = deserialize_request(deserializer, request)
|
|
160
|
+
if isinstance(parsed, Left):
|
|
161
|
+
return post_process(Left(parsed._error))
|
|
162
|
+
deserialized = unwrap(parsed)
|
|
163
|
+
if isinstance(deserialized, list) and deserialized:
|
|
164
|
+
if max_batch_size is not None and len(deserialized) > max_batch_size:
|
|
165
|
+
return post_process(
|
|
166
|
+
Left(BatchTooLargeResponse(len(deserialized), max_batch_size))
|
|
167
|
+
)
|
|
168
|
+
responses = await asyncio.gather(
|
|
169
|
+
*(
|
|
170
|
+
dispatch_member(
|
|
171
|
+
validator, methods, context, post_process, member, debug=debug
|
|
172
|
+
)
|
|
173
|
+
for member in deserialized
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return extract_list(
|
|
177
|
+
True, [response for response in responses if response is not NORESPONSE]
|
|
178
|
+
)
|
|
179
|
+
validated = validate_request(validator, deserialized)
|
|
180
|
+
if isinstance(validated, Left):
|
|
181
|
+
return post_process(Left(validated._error))
|
|
182
|
+
return await dispatch_deserialized(
|
|
183
|
+
methods, context, post_process, unwrap(validated), debug=debug
|
|
184
|
+
)
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
logger.exception("Error while dispatching the request")
|
|
187
|
+
return post_process(Left(ServerErrorResponse(exception_data(exc, debug), None)))
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""The async dispatch functions, for asyncio servers.
|
|
2
|
+
|
|
3
|
+
They take the same arguments as the functions in main.py, and are imported from
|
|
4
|
+
the package as async_dispatch, async_dispatch_to_serializable and
|
|
5
|
+
async_dispatch_to_response. Methods can be async functions or plain ones. The
|
|
6
|
+
requests in a batch run concurrently.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast
|
|
10
|
+
|
|
11
|
+
from .async_dispatcher import dispatch_to_response_pure
|
|
12
|
+
from .dispatcher import Deserialized, check_max_batch_size
|
|
13
|
+
from .main import (
|
|
14
|
+
default_deserializer,
|
|
15
|
+
default_serializer,
|
|
16
|
+
default_validator,
|
|
17
|
+
serialize,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Importable from here in 5.0.9. The "as" form marks a re-export.
|
|
21
|
+
from .methods import Methods as Methods
|
|
22
|
+
from .methods import MethodsArgument, global_methods
|
|
23
|
+
from .response import Response, to_serializable
|
|
24
|
+
from .sentinels import NOCONTEXT
|
|
25
|
+
from .utils import identity
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def dispatch_to_response(
|
|
29
|
+
request: str,
|
|
30
|
+
methods: Optional[MethodsArgument] = None,
|
|
31
|
+
*,
|
|
32
|
+
context: Any = NOCONTEXT,
|
|
33
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
34
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
35
|
+
post_process: Callable[[Response], Any] = identity,
|
|
36
|
+
debug: bool = False,
|
|
37
|
+
max_batch_size: Optional[int] = None,
|
|
38
|
+
) -> Union[Response, Iterable[Response], None]:
|
|
39
|
+
"""Dispatch a request and give the response as Response objects. Async.
|
|
40
|
+
|
|
41
|
+
The async version of `dispatch_to_response`, imported from the package as
|
|
42
|
+
`async_dispatch_to_response`. It takes the same arguments.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
request: The JSON-RPC request string.
|
|
46
|
+
methods: The same as for `dispatch_to_response`.
|
|
47
|
+
context: The same as for `dispatch_to_response`.
|
|
48
|
+
deserializer: The same as for `dispatch_to_response`.
|
|
49
|
+
validator: The same as for `dispatch_to_response`.
|
|
50
|
+
post_process: The same as for `dispatch_to_response`.
|
|
51
|
+
debug: The same as for `dispatch_to_response`.
|
|
52
|
+
max_batch_size: The same as for `dispatch_to_response`. Every request in a
|
|
53
|
+
batch runs at the same time, so a limit matters even more here.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A Response for a single request, a list of Responses for a batch, or None
|
|
57
|
+
if there's nothing to send back. The type hint says Iterable for a
|
|
58
|
+
batch, but it's a list.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If `max_batch_size` isn't None or a positive int.
|
|
62
|
+
"""
|
|
63
|
+
check_max_batch_size(max_batch_size)
|
|
64
|
+
response = await dispatch_to_response_pure(
|
|
65
|
+
deserializer=deserializer,
|
|
66
|
+
validator=validator,
|
|
67
|
+
post_process=post_process,
|
|
68
|
+
context=context,
|
|
69
|
+
methods=global_methods if methods is None else methods,
|
|
70
|
+
request=request,
|
|
71
|
+
debug=debug,
|
|
72
|
+
max_batch_size=max_batch_size,
|
|
73
|
+
)
|
|
74
|
+
return cast(Union[Response, Iterable[Response], None], response)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def dispatch_to_serializable(
|
|
78
|
+
request: str,
|
|
79
|
+
methods: Optional[MethodsArgument] = None,
|
|
80
|
+
*,
|
|
81
|
+
context: Any = NOCONTEXT,
|
|
82
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
83
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
84
|
+
debug: bool = False,
|
|
85
|
+
max_batch_size: Optional[int] = None,
|
|
86
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]], None]:
|
|
87
|
+
"""Dispatch a request and give the response as a dict. Async.
|
|
88
|
+
|
|
89
|
+
The async version of `dispatch_to_serializable`, imported from the package as
|
|
90
|
+
`async_dispatch_to_serializable`.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
request: The JSON-RPC request string.
|
|
94
|
+
methods: The same as for `dispatch_to_response`.
|
|
95
|
+
context: The same as for `dispatch_to_response`.
|
|
96
|
+
deserializer: The same as for `dispatch_to_response`.
|
|
97
|
+
validator: The same as for `dispatch_to_response`.
|
|
98
|
+
debug: The same as for `dispatch_to_response`.
|
|
99
|
+
max_batch_size: The same as for `dispatch_to_response`.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The response as a dict, a list of dicts for a batch, or None if there's
|
|
103
|
+
nothing to send back.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If `max_batch_size` isn't None or a positive int.
|
|
107
|
+
"""
|
|
108
|
+
return cast(
|
|
109
|
+
Union[Dict[str, Any], List[Dict[str, Any]], None],
|
|
110
|
+
await dispatch_to_response(
|
|
111
|
+
request,
|
|
112
|
+
methods,
|
|
113
|
+
context=context,
|
|
114
|
+
deserializer=deserializer,
|
|
115
|
+
validator=validator,
|
|
116
|
+
debug=debug,
|
|
117
|
+
max_batch_size=max_batch_size,
|
|
118
|
+
post_process=to_serializable,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def dispatch_to_json(
|
|
124
|
+
request: str,
|
|
125
|
+
methods: Optional[MethodsArgument] = None,
|
|
126
|
+
*,
|
|
127
|
+
context: Any = NOCONTEXT,
|
|
128
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
129
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
130
|
+
debug: bool = False,
|
|
131
|
+
max_batch_size: Optional[int] = None,
|
|
132
|
+
serializer: Callable[
|
|
133
|
+
[Union[Dict[str, Any], List[Dict[str, Any]], None]], str
|
|
134
|
+
] = default_serializer,
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Dispatch a request and give the response as a JSON string. Async.
|
|
137
|
+
|
|
138
|
+
This is `async_dispatch`, the async version of `dispatch`. Methods can be async
|
|
139
|
+
functions or plain ones (plain ones work from 5.0.10). A plain method runs on
|
|
140
|
+
the event loop, so a slow one holds up every other request.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
request: The JSON-RPC request string.
|
|
144
|
+
methods: The same as for `dispatch_to_response`.
|
|
145
|
+
context: The same as for `dispatch_to_response`.
|
|
146
|
+
deserializer: The same as for `dispatch_to_response`.
|
|
147
|
+
validator: The same as for `dispatch_to_response`.
|
|
148
|
+
debug: The same as for `dispatch_to_response`.
|
|
149
|
+
max_batch_size: The same as for `dispatch_to_response`.
|
|
150
|
+
serializer: The same as for `dispatch`.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The response as a JSON string, or "" if there's nothing to send back.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If `max_batch_size` isn't None or a positive int.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> import asyncio
|
|
160
|
+
>>> from jsonrpcserver import Result, Success, async_dispatch
|
|
161
|
+
>>> async def ping() -> Result:
|
|
162
|
+
... return Success("pong")
|
|
163
|
+
>>> request = '{"jsonrpc": "2.0", "method": "ping", "id": 1}'
|
|
164
|
+
>>> asyncio.run(async_dispatch(request, {"ping": ping}))
|
|
165
|
+
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
|
|
166
|
+
"""
|
|
167
|
+
response = await dispatch_to_serializable(
|
|
168
|
+
request,
|
|
169
|
+
methods,
|
|
170
|
+
context=context,
|
|
171
|
+
deserializer=deserializer,
|
|
172
|
+
validator=validator,
|
|
173
|
+
debug=debug,
|
|
174
|
+
max_batch_size=max_batch_size,
|
|
175
|
+
)
|
|
176
|
+
return "" if response is None else serialize(serializer, response, debug)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
dispatch = dispatch_to_json
|
|
180
|
+
"""Another name for `dispatch_to_json`. The package exports it as `async_dispatch`."""
|
jsonrpcserver/codes.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The error codes jsonrpcserver sends.
|
|
2
|
+
|
|
3
|
+
The first five are defined by the spec:
|
|
4
|
+
https://www.jsonrpc.org/specification#error_object
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
ERROR_PARSE_ERROR = -32700
|
|
8
|
+
"""The request isn't valid JSON (or the deserializer raised)."""
|
|
9
|
+
ERROR_INVALID_REQUEST = -32600
|
|
10
|
+
"""The JSON isn't a valid request object, or a batch is empty or too big."""
|
|
11
|
+
ERROR_METHOD_NOT_FOUND = -32601
|
|
12
|
+
"""No method has that name."""
|
|
13
|
+
ERROR_INVALID_PARAMS = -32602
|
|
14
|
+
"""The params don't fit the method's signature, or the method said they're invalid."""
|
|
15
|
+
ERROR_INTERNAL_ERROR = -32603
|
|
16
|
+
"""The method raised, returned something other than a Result, or its result
|
|
17
|
+
could not be serialized.
|
|
18
|
+
"""
|
|
19
|
+
ERROR_SERVER_ERROR = -32000
|
|
20
|
+
"""Something failed in jsonrpcserver itself, outside any method."""
|