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/main.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""The dispatch functions.
|
|
2
|
+
|
|
3
|
+
All three take a JSON-RPC request string, call the methods and give the response.
|
|
4
|
+
They differ only in the form of the response:
|
|
5
|
+
|
|
6
|
+
- dispatch_to_response gives Response objects, or None for a notification.
|
|
7
|
+
- dispatch_to_serializable gives a dict or a list of dicts, or None for a
|
|
8
|
+
notification.
|
|
9
|
+
- dispatch_to_json, also called dispatch, gives a JSON string, or an empty string
|
|
10
|
+
for a notification.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import pkgutil
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
17
|
+
|
|
18
|
+
from jsonschema.validators import validator_for
|
|
19
|
+
|
|
20
|
+
from .codes import ERROR_INTERNAL_ERROR
|
|
21
|
+
from .dispatcher import (
|
|
22
|
+
Deserialized,
|
|
23
|
+
check_max_batch_size,
|
|
24
|
+
dispatch_to_response_pure,
|
|
25
|
+
exception_data,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Importable from here in 5.0.9. The "as" form marks a re-export.
|
|
29
|
+
from .methods import Methods as Methods
|
|
30
|
+
from .methods import MethodsArgument, global_methods
|
|
31
|
+
from .response import (
|
|
32
|
+
Response,
|
|
33
|
+
to_dict,
|
|
34
|
+
)
|
|
35
|
+
from .response import (
|
|
36
|
+
# Importable from here in 5.0.9. The "as" form marks it as a re-export.
|
|
37
|
+
to_serializable_one as to_serializable_one,
|
|
38
|
+
)
|
|
39
|
+
from .sentinels import NOCONTEXT, NODATA
|
|
40
|
+
from .utils import identity
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
default_deserializer = json.loads
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def default_serializer(response: Any) -> str:
|
|
48
|
+
"""json.dumps, but NaN and Infinity raise ValueError instead of producing output
|
|
49
|
+
that isn't valid JSON.
|
|
50
|
+
"""
|
|
51
|
+
return json.dumps(response, allow_nan=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def serialize_one(
|
|
55
|
+
serializer: Callable[[Any], str], response: Dict[str, Any], debug: bool
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Serialize one response. If that fails, serialize an Internal error for the same
|
|
58
|
+
request instead, so the client still gets a valid JSON-RPC response.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return serializer(response)
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
logger.exception(
|
|
64
|
+
"Could not serialize the response for request id %r", response.get("id")
|
|
65
|
+
)
|
|
66
|
+
data = exception_data(exc, debug)
|
|
67
|
+
error: Dict[str, Any] = {
|
|
68
|
+
"jsonrpc": "2.0",
|
|
69
|
+
"error": {
|
|
70
|
+
"code": ERROR_INTERNAL_ERROR,
|
|
71
|
+
"message": "Internal error",
|
|
72
|
+
**({} if data is NODATA else {"data": data}),
|
|
73
|
+
},
|
|
74
|
+
"id": response.get("id"),
|
|
75
|
+
}
|
|
76
|
+
try:
|
|
77
|
+
return serializer(error)
|
|
78
|
+
except Exception:
|
|
79
|
+
# The id itself can't be serialized (for example it was 1e400, which
|
|
80
|
+
# json.loads reads as infinity).
|
|
81
|
+
error["id"] = None
|
|
82
|
+
return json.dumps(error)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def serialize(
|
|
86
|
+
serializer: Callable[[Any], str],
|
|
87
|
+
response: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
88
|
+
debug: bool,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Serialize a response or batch of responses.
|
|
91
|
+
|
|
92
|
+
A response that can't be serialized becomes an Internal error response. In a batch,
|
|
93
|
+
the other responses are kept.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
return serializer(response)
|
|
97
|
+
except Exception:
|
|
98
|
+
if isinstance(response, list):
|
|
99
|
+
return (
|
|
100
|
+
"["
|
|
101
|
+
+ ", ".join(serialize_one(serializer, r, debug) for r in response)
|
|
102
|
+
+ "]"
|
|
103
|
+
)
|
|
104
|
+
return serialize_one(serializer, response, debug)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Prepare the jsonschema validator. This is global so it loads only once, not every
|
|
108
|
+
# time dispatch is called.
|
|
109
|
+
# pkgutil.get_data works on every supported Python, and inside zip files and frozen
|
|
110
|
+
# apps. importlib.resources.read_text is deprecated on 3.11 and 3.12.
|
|
111
|
+
schema = json.loads(
|
|
112
|
+
pkgutil.get_data(__name__.rpartition(".")[0], "request-schema.json") or b""
|
|
113
|
+
)
|
|
114
|
+
klass = validator_for(schema)
|
|
115
|
+
klass.check_schema(schema)
|
|
116
|
+
default_validator = klass(schema).validate
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def dispatch_to_response(
|
|
120
|
+
request: str,
|
|
121
|
+
methods: Optional[MethodsArgument] = None,
|
|
122
|
+
*,
|
|
123
|
+
context: Any = NOCONTEXT,
|
|
124
|
+
deserializer: Callable[[str], Deserialized] = json.loads,
|
|
125
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
126
|
+
post_process: Callable[[Response], Any] = identity,
|
|
127
|
+
debug: bool = False,
|
|
128
|
+
max_batch_size: Optional[int] = None,
|
|
129
|
+
) -> Union[Response, List[Response], None]:
|
|
130
|
+
"""Dispatch a request and give the response as Response objects.
|
|
131
|
+
|
|
132
|
+
Most code wants `dispatch` (a JSON string) or `dispatch_to_serializable` (dicts)
|
|
133
|
+
instead. Use this one to inspect or change responses before they're serialized.
|
|
134
|
+
|
|
135
|
+
Each Response is an oslash `Right` holding a `SuccessResponse`, or a `Left`
|
|
136
|
+
holding an `ErrorResponse`. oslash has no public way to read them, so check
|
|
137
|
+
`isinstance(response, Left)` and read `response._error` or `response._value`.
|
|
138
|
+
These attributes are stable for all of 5.x. Printing a Response raises
|
|
139
|
+
`TypeError`, because of a bug in oslash; print `to_dict(response)` instead.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
request: The JSON-RPC request string.
|
|
143
|
+
methods: The methods requests can call, as a dict (or any mapping) of names
|
|
144
|
+
to functions. The default is the dict that `@method` fills in,
|
|
145
|
+
`jsonrpcserver.methods.global_methods`.
|
|
146
|
+
context: If given, it's passed as the first argument to every method. The
|
|
147
|
+
client can't see or set it.
|
|
148
|
+
deserializer: The function that parses the request string. The default is
|
|
149
|
+
`json.loads`. If it raises, the client gets a -32700 Parse error whose
|
|
150
|
+
`data` is the exception message.
|
|
151
|
+
validator: The function that checks a parsed request against the JSON-RPC
|
|
152
|
+
spec. It should raise an exception, of any kind, if the request is
|
|
153
|
+
invalid. In a batch it's called once for each request (new in 5.0.10).
|
|
154
|
+
The default checks against a JSON schema. `lambda _: None` turns
|
|
155
|
+
validation off.
|
|
156
|
+
post_process: A function applied to each Response before it's returned.
|
|
157
|
+
debug: If True, the error response for an exception a method doesn't catch
|
|
158
|
+
includes the exception message in `data`. The default leaves `data`
|
|
159
|
+
out, because exception messages can contain passwords, file paths and
|
|
160
|
+
other details a client shouldn't see. The exception is logged either
|
|
161
|
+
way. New in 5.0.10.
|
|
162
|
+
max_batch_size: The most requests a batch may hold. A bigger batch gets a
|
|
163
|
+
single -32600 Invalid request response, and none of it is run. The
|
|
164
|
+
default, None, means no limit. A server open to the internet should set
|
|
165
|
+
one, such as 100. New in 5.0.10.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A Response for a single request, a list of Responses for a batch, or None
|
|
169
|
+
if there's nothing to send back (a notification, or a batch of only
|
|
170
|
+
notifications). With `post_process`, whatever it returns for each.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: If `max_batch_size` isn't None or a positive int. Requests never
|
|
174
|
+
raise: a bad request or a failing method gives an error response.
|
|
175
|
+
"""
|
|
176
|
+
check_max_batch_size(max_batch_size)
|
|
177
|
+
response = dispatch_to_response_pure(
|
|
178
|
+
deserializer=deserializer,
|
|
179
|
+
validator=validator,
|
|
180
|
+
post_process=post_process,
|
|
181
|
+
context=context,
|
|
182
|
+
methods=global_methods if methods is None else methods,
|
|
183
|
+
request=request,
|
|
184
|
+
debug=debug,
|
|
185
|
+
max_batch_size=max_batch_size,
|
|
186
|
+
)
|
|
187
|
+
return cast(Union[Response, List[Response], None], response)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def dispatch_to_serializable(
|
|
191
|
+
request: str,
|
|
192
|
+
methods: Optional[MethodsArgument] = None,
|
|
193
|
+
*,
|
|
194
|
+
context: Any = NOCONTEXT,
|
|
195
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
196
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
197
|
+
debug: bool = False,
|
|
198
|
+
max_batch_size: Optional[int] = None,
|
|
199
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]], None]:
|
|
200
|
+
"""Dispatch a request and give the response as a dict.
|
|
201
|
+
|
|
202
|
+
Use it when your framework serializes the response itself, such as a Django
|
|
203
|
+
`JsonResponse`, or when you want to inspect it.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
request: The JSON-RPC request string.
|
|
207
|
+
methods: The same as for `dispatch_to_response`.
|
|
208
|
+
context: The same as for `dispatch_to_response`.
|
|
209
|
+
deserializer: The same as for `dispatch_to_response`.
|
|
210
|
+
validator: The same as for `dispatch_to_response`.
|
|
211
|
+
debug: The same as for `dispatch_to_response`.
|
|
212
|
+
max_batch_size: The same as for `dispatch_to_response`.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The response as a dict, a list of dicts for a batch, or None if there's
|
|
216
|
+
nothing to send back.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If `max_batch_size` isn't None or a positive int.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> from jsonrpcserver import Result, Success, dispatch_to_serializable
|
|
223
|
+
>>> def ping() -> Result:
|
|
224
|
+
... return Success("pong")
|
|
225
|
+
>>> dispatch_to_serializable(
|
|
226
|
+
... '{"jsonrpc": "2.0", "method": "ping", "id": 1}', methods={"ping": ping}
|
|
227
|
+
... )
|
|
228
|
+
{'jsonrpc': '2.0', 'result': 'pong', 'id': 1}
|
|
229
|
+
"""
|
|
230
|
+
return cast(
|
|
231
|
+
Union[Dict[str, Any], List[Dict[str, Any]], None],
|
|
232
|
+
dispatch_to_response(
|
|
233
|
+
request,
|
|
234
|
+
methods,
|
|
235
|
+
context=context,
|
|
236
|
+
deserializer=deserializer,
|
|
237
|
+
validator=validator,
|
|
238
|
+
debug=debug,
|
|
239
|
+
max_batch_size=max_batch_size,
|
|
240
|
+
post_process=to_dict,
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def dispatch_to_json(
|
|
246
|
+
request: str,
|
|
247
|
+
methods: Optional[MethodsArgument] = None,
|
|
248
|
+
*,
|
|
249
|
+
context: Any = NOCONTEXT,
|
|
250
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
251
|
+
validator: Callable[[Deserialized], object] = default_validator,
|
|
252
|
+
debug: bool = False,
|
|
253
|
+
max_batch_size: Optional[int] = None,
|
|
254
|
+
serializer: Callable[
|
|
255
|
+
[Union[Dict[str, Any], List[Dict[str, Any]], str]], str
|
|
256
|
+
] = default_serializer,
|
|
257
|
+
) -> str:
|
|
258
|
+
"""Dispatch a request and give the response as a JSON string.
|
|
259
|
+
|
|
260
|
+
This is `dispatch`, the function most code uses. Send the string back to the
|
|
261
|
+
client. An empty string means there's nothing to send back: the request was a
|
|
262
|
+
notification, or a batch of only notifications. Over HTTP, send status 204 with
|
|
263
|
+
no body for that.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
request: The JSON-RPC request string.
|
|
267
|
+
methods: The same as for `dispatch_to_response`.
|
|
268
|
+
context: The same as for `dispatch_to_response`.
|
|
269
|
+
deserializer: The same as for `dispatch_to_response`.
|
|
270
|
+
validator: The same as for `dispatch_to_response`.
|
|
271
|
+
debug: The same as for `dispatch_to_response`.
|
|
272
|
+
max_batch_size: The same as for `dispatch_to_response`.
|
|
273
|
+
serializer: The function that turns the response into a string. The
|
|
274
|
+
default is `json.dumps` with `allow_nan=False`, so a result holding NaN
|
|
275
|
+
or Infinity gives an Internal error instead of invalid JSON (new in
|
|
276
|
+
5.0.10). If the serializer raises for a response, say because the
|
|
277
|
+
method returned a `datetime`, that response becomes an Internal error
|
|
278
|
+
and the rest of a batch is sent as usual.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The response as a JSON string, or "" if there's nothing to send back.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
ValueError: If `max_batch_size` isn't None or a positive int.
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
>>> from jsonrpcserver import Result, Success, dispatch
|
|
288
|
+
>>> def ping() -> Result:
|
|
289
|
+
... return Success("pong")
|
|
290
|
+
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping})
|
|
291
|
+
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
|
|
292
|
+
"""
|
|
293
|
+
response = dispatch_to_serializable(
|
|
294
|
+
request,
|
|
295
|
+
methods,
|
|
296
|
+
context=context,
|
|
297
|
+
deserializer=deserializer,
|
|
298
|
+
validator=validator,
|
|
299
|
+
debug=debug,
|
|
300
|
+
max_batch_size=max_batch_size,
|
|
301
|
+
)
|
|
302
|
+
# Better to respond with the empty string instead of json "null", because "null" is
|
|
303
|
+
# an invalid JSON-RPC response.
|
|
304
|
+
return "" if response is None else serialize(serializer, response, debug)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
dispatch = dispatch_to_json
|
|
308
|
+
"""Another name for `dispatch_to_json`, and the one most code uses."""
|
jsonrpcserver/methods.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Methods: the functions a JSON-RPC request can call.
|
|
2
|
+
|
|
3
|
+
The dispatch functions look methods up in a dict of names to functions. The @method
|
|
4
|
+
decorator adds a function to global_methods, the dict they use by default. To use
|
|
5
|
+
your own dict instead, pass it as the methods argument:
|
|
6
|
+
|
|
7
|
+
dispatch(request) # the functions registered with @method
|
|
8
|
+
dispatch(request, methods={"ping": ping}) # only the functions in this dict
|
|
9
|
+
|
|
10
|
+
Either way, a method returns Success(...) or Error(...), not a plain value.
|
|
11
|
+
|
|
12
|
+
A request's params are either a list (positional arguments) or an object (named
|
|
13
|
+
arguments), not both. That's a JSON-RPC rule.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
from typing import Any, Callable, Dict, Mapping, Optional, TypeVar, overload
|
|
18
|
+
|
|
19
|
+
from .result import Result
|
|
20
|
+
|
|
21
|
+
Method = Callable[..., Result]
|
|
22
|
+
Methods = Dict[str, Method]
|
|
23
|
+
# Async methods return a coroutine rather than a Result, so the dispatch functions
|
|
24
|
+
# accept any callable. They check what comes back at run time.
|
|
25
|
+
AnyMethod = Callable[..., Any]
|
|
26
|
+
# What the dispatch functions accept for their methods argument. Any mapping will do.
|
|
27
|
+
MethodsArgument = Mapping[str, AnyMethod]
|
|
28
|
+
|
|
29
|
+
global_methods: Dict[str, AnyMethod] = {}
|
|
30
|
+
"""The methods registered with `@method`, as a dict of names to functions.
|
|
31
|
+
|
|
32
|
+
The dispatch functions use it when they're called without `methods`. It's shared by
|
|
33
|
+
the whole process, so every module that uses `@method` adds to it.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def method(f: F, name: Optional[str] = None) -> F: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def method(f: None = None, name: Optional[str] = None) -> Callable[[F], F]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def method(f: Optional[F] = None, name: Optional[str] = None) -> Any:
|
|
48
|
+
"""Register a function as a JSON-RPC method.
|
|
49
|
+
|
|
50
|
+
The function is added to `global_methods`, which the dispatch functions use when
|
|
51
|
+
they're called without `methods`. It's returned unchanged, so you can still call
|
|
52
|
+
it yourself, and type checkers see its real signature (from 5.0.10). A method
|
|
53
|
+
with the same name as an earlier one replaces it, without a warning.
|
|
54
|
+
|
|
55
|
+
Use it with or without arguments:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@method
|
|
59
|
+
def ping() -> Result:
|
|
60
|
+
return Success("pong")
|
|
61
|
+
|
|
62
|
+
@method(name="sum")
|
|
63
|
+
def add(a: int, b: int) -> Result:
|
|
64
|
+
return Success(a + b)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
f: The function. Leave it out to pass `name`.
|
|
69
|
+
name: The name requests use to call the method. The default is the
|
|
70
|
+
function's name.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The function itself, or, when called with only `name`, a decorator.
|
|
74
|
+
|
|
75
|
+
Warns:
|
|
76
|
+
UserWarning: If the name starts with "rpc.". The JSON-RPC spec reserves those
|
|
77
|
+
names. New in 5.0.10.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def decorator(func: F) -> F:
|
|
81
|
+
method_name = func.__name__ if name is None else name
|
|
82
|
+
if method_name.startswith("rpc."):
|
|
83
|
+
warnings.warn(
|
|
84
|
+
f"Method names starting with 'rpc.' are reserved by the JSON-RPC spec "
|
|
85
|
+
f"({method_name!r})",
|
|
86
|
+
stacklevel=3 if callable(f) else 2,
|
|
87
|
+
)
|
|
88
|
+
global_methods[method_name] = func
|
|
89
|
+
return func
|
|
90
|
+
|
|
91
|
+
return decorator(f) if callable(f) else decorator
|
jsonrpcserver/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
3
|
+
"description": "A JSON RPC 2.0 request",
|
|
4
|
+
"oneOf": [
|
|
5
|
+
{
|
|
6
|
+
"description": "An individual request",
|
|
7
|
+
"$ref": "#/definitions/request"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"description": "An array of requests",
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "$ref": "#/definitions/request" },
|
|
13
|
+
"minItems": 1
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"definitions": {
|
|
17
|
+
"request": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": [ "jsonrpc", "method" ],
|
|
20
|
+
"properties": {
|
|
21
|
+
"jsonrpc": { "enum": [ "2.0" ] },
|
|
22
|
+
"method": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"id": {
|
|
26
|
+
"type": [ "string", "number", "null" ],
|
|
27
|
+
"note": [
|
|
28
|
+
"While allowed, null should be avoided: http://www.jsonrpc.org/specification#id1",
|
|
29
|
+
"While allowed, a number with a fractional part should be avoided: http://www.jsonrpc.org/specification#id2"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"params": {
|
|
33
|
+
"type": [ "array", "object" ]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
jsonrpcserver/request.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""A simple namedtuple to hold a request.
|
|
2
|
+
|
|
3
|
+
After parsing a request string, we put the (dict) requests into these Request
|
|
4
|
+
namedtuples, simply because they're nicer to work with.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, NamedTuple, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Request(NamedTuple):
|
|
11
|
+
"""JSON-RPC Request"""
|
|
12
|
+
|
|
13
|
+
method: str
|
|
14
|
+
params: Union[List[Any], Dict[str, Any]]
|
|
15
|
+
id: Any # Use NOID for a Notification.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Responses: what dispatch_to_response gives, and how to turn them into dicts.
|
|
2
|
+
|
|
3
|
+
A Response is a Result plus the request's id
|
|
4
|
+
(https://www.jsonrpc.org/specification#response_object).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Any, Dict, List, NamedTuple, Type, Union, cast
|
|
9
|
+
|
|
10
|
+
from oslash.either import Either, Left, Right
|
|
11
|
+
|
|
12
|
+
from .codes import (
|
|
13
|
+
ERROR_INVALID_REQUEST,
|
|
14
|
+
ERROR_METHOD_NOT_FOUND,
|
|
15
|
+
ERROR_PARSE_ERROR,
|
|
16
|
+
ERROR_SERVER_ERROR,
|
|
17
|
+
)
|
|
18
|
+
from .sentinels import NODATA
|
|
19
|
+
|
|
20
|
+
Deserialized = Union[Dict[str, Any], List[Dict[str, Any]]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SuccessResponse(NamedTuple):
|
|
24
|
+
"""A successful response: the method's result and the request's id."""
|
|
25
|
+
|
|
26
|
+
result: Any
|
|
27
|
+
id: Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ErrorResponse(NamedTuple):
|
|
31
|
+
"""An error response: the error's code, message and data, and the request's id.
|
|
32
|
+
|
|
33
|
+
`data` is `NODATA` when there's no data, so it's left out of the response. `id`
|
|
34
|
+
is None when the request couldn't be read, as the spec requires.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
code: int
|
|
38
|
+
message: str
|
|
39
|
+
data: Any
|
|
40
|
+
id: Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# oslash's Either takes the success type first, then the error type.
|
|
44
|
+
Response = Either[SuccessResponse, ErrorResponse]
|
|
45
|
+
"""What `dispatch_to_response` gives for each request.
|
|
46
|
+
|
|
47
|
+
An oslash `Either`: a `Right` holding a `SuccessResponse`, or a `Left` holding an
|
|
48
|
+
`ErrorResponse`. To read one, check `isinstance(response, Left)`, then read
|
|
49
|
+
`response._error` (an `ErrorResponse`) or `response._value` (a `SuccessResponse`).
|
|
50
|
+
oslash has no public accessor, but these attributes are stable for all of 5.x. Or turn
|
|
51
|
+
it into a dict with `to_dict`.
|
|
52
|
+
"""
|
|
53
|
+
ResponseType = Type[Response]
|
|
54
|
+
"""Deprecated: use `Response`. Kept so code written for 5.0.9 keeps working."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ParseErrorResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name
|
|
58
|
+
"""An ErrorResponse with most attributes already populated.
|
|
59
|
+
|
|
60
|
+
From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of
|
|
61
|
+
the id member in the Request Object. If there was an error in detecting the id in
|
|
62
|
+
the Request object (e.g. Parse error/Invalid Request), it MUST be Null."
|
|
63
|
+
"""
|
|
64
|
+
return ErrorResponse(ERROR_PARSE_ERROR, "Parse error", data, None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def InvalidRequestResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name
|
|
68
|
+
"""An ErrorResponse with most attributes already populated.
|
|
69
|
+
|
|
70
|
+
From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of
|
|
71
|
+
the id member in the Request Object. If there was an error in detecting the id in
|
|
72
|
+
the Request object (e.g. Parse error/Invalid Request), it MUST be Null."
|
|
73
|
+
"""
|
|
74
|
+
return ErrorResponse(ERROR_INVALID_REQUEST, "Invalid request", data, None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def MethodNotFoundResponse(data: Any, id: Any) -> ErrorResponse:
|
|
78
|
+
"""An ErrorResponse with some attributes already populated."""
|
|
79
|
+
# pylint: disable=invalid-name,redefined-builtin
|
|
80
|
+
return ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", data, id)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def ServerErrorResponse(data: Any, id: Any) -> ErrorResponse:
|
|
84
|
+
"""An ErrorResponse with some attributes already populated."""
|
|
85
|
+
# pylint: disable=invalid-name,redefined-builtin
|
|
86
|
+
return ErrorResponse(ERROR_SERVER_ERROR, "Server error", data, id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def to_error_dict(response: ErrorResponse) -> Dict[str, Any]:
|
|
90
|
+
"""Turn an ErrorResponse into a JSON-RPC response dict, leaving out missing data."""
|
|
91
|
+
return {
|
|
92
|
+
"jsonrpc": "2.0",
|
|
93
|
+
"error": {
|
|
94
|
+
"code": response.code,
|
|
95
|
+
"message": response.message,
|
|
96
|
+
# "data" may be omitted.
|
|
97
|
+
**({"data": response.data} if response.data is not NODATA else {}),
|
|
98
|
+
},
|
|
99
|
+
"id": response.id,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def to_success_dict(response: SuccessResponse) -> Dict[str, Any]:
|
|
104
|
+
"""Turn a SuccessResponse into a JSON-RPC response dict."""
|
|
105
|
+
return {"jsonrpc": "2.0", "result": response.result, "id": response.id}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def to_dict(response: Response) -> Dict[str, Any]:
|
|
109
|
+
"""Turn a Response into a JSON-RPC response dict.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
response: A Response from `dispatch_to_response`.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The response as a dict, ready for `json.dumps`.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> from jsonrpcserver import Result, Success, dispatch_to_response
|
|
119
|
+
>>> def ping() -> Result:
|
|
120
|
+
... return Success("pong")
|
|
121
|
+
>>> request = '{"jsonrpc": "2.0", "method": "ping", "id": 1}'
|
|
122
|
+
>>> to_dict(dispatch_to_response(request, {"ping": ping}))
|
|
123
|
+
{'jsonrpc': '2.0', 'result': 'pong', 'id': 1}
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(response, Left):
|
|
126
|
+
return to_error_dict(response._error)
|
|
127
|
+
success = cast("Right[SuccessResponse, ErrorResponse]", response)
|
|
128
|
+
return to_success_dict(success._value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def to_serializable(
|
|
132
|
+
response: Union[Response, List[Response], None],
|
|
133
|
+
) -> Union[Deserialized, None]:
|
|
134
|
+
"""Turn a Response, a list of them or None into a dict, a list of dicts or None."""
|
|
135
|
+
if response is None:
|
|
136
|
+
return None
|
|
137
|
+
if isinstance(response, List):
|
|
138
|
+
return [to_dict(r) for r in response]
|
|
139
|
+
return to_dict(response)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# The names used in 5.0.9. They were renamed before 5.0.10, and are kept so code that
|
|
143
|
+
# imports them keeps working. They will be removed in 6.0.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _deprecated(old: str, new: str) -> None:
|
|
147
|
+
warnings.warn(
|
|
148
|
+
f"jsonrpcserver.response.{old} is deprecated and will be removed in 6.0. "
|
|
149
|
+
f"Use {new} instead.",
|
|
150
|
+
DeprecationWarning,
|
|
151
|
+
stacklevel=3,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def serialize_error(response: ErrorResponse) -> Dict[str, Any]:
|
|
156
|
+
"""Deprecated: use `to_error_dict`. Warns with DeprecationWarning (5.0.10)."""
|
|
157
|
+
_deprecated("serialize_error", "to_error_dict")
|
|
158
|
+
return to_error_dict(response)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def serialize_success(response: SuccessResponse) -> Dict[str, Any]:
|
|
162
|
+
"""Deprecated: use `to_success_dict`. Warns with DeprecationWarning (5.0.10)."""
|
|
163
|
+
_deprecated("serialize_success", "to_success_dict")
|
|
164
|
+
return to_success_dict(response)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def to_serializable_one(response: Response) -> Dict[str, Any]:
|
|
168
|
+
"""Deprecated: use `to_dict`. Warns with DeprecationWarning (5.0.10)."""
|
|
169
|
+
_deprecated("to_serializable_one", "to_dict")
|
|
170
|
+
return to_dict(response)
|