-
-
Notifications
You must be signed in to change notification settings - Fork 100
/
jsonrpc2.py
267 lines (196 loc) · 8.24 KB
/
jsonrpc2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
from . import six
import json
from .exceptions import JSONRPCError, JSONRPCInvalidRequestException
from .base import JSONRPCBaseRequest, JSONRPCBaseResponse
class JSONRPC20Request(JSONRPCBaseRequest):
""" A rpc call is represented by sending a Request object to a Server.
:param str method: A String containing the name of the method to be
invoked. Method names that begin with the word rpc followed by a
period character (U+002E or ASCII 46) are reserved for rpc-internal
methods and extensions and MUST NOT be used for anything else.
:param params: A Structured value that holds the parameter values to be
used during the invocation of the method. This member MAY be omitted.
:type params: iterable or dict
:param _id: An identifier established by the Client that MUST contain a
String, Number, or NULL value if included. If it is not included it is
assumed to be a notification. The value SHOULD normally not be Null
[1] and Numbers SHOULD NOT contain fractional parts [2].
:type _id: str or int or None
:param bool is_notification: Whether request is notification or not. If
value is True, _id is not included to request. It allows to create
requests with id = null.
The Server MUST reply with the same value in the Response object if
included. This member is used to correlate the context between the two
objects.
[1] The use of Null as a value for the id member in a Request object is
discouraged, because this specification uses a value of Null for Responses
with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null
for Notifications this could cause confusion in handling.
[2] Fractional parts may be problematic, since many decimal fractions
cannot be represented exactly as binary fractions.
"""
JSONRPC_VERSION = "2.0"
REQUIRED_FIELDS = set(["jsonrpc", "method"])
POSSIBLE_FIELDS = set(["jsonrpc", "method", "params", "id"])
@property
def data(self):
data = dict(
(k, v) for k, v in self._data.items()
if not (k == "id" and self.is_notification)
)
data["jsonrpc"] = self.JSONRPC_VERSION
return data
@data.setter
def data(self, value):
if not isinstance(value, dict):
raise ValueError("data should be dict")
self._data = value
@property
def method(self):
return self._data.get("method")
@method.setter
def method(self, value):
if not isinstance(value, six.string_types):
raise ValueError("Method should be string")
if value.startswith("rpc."):
raise ValueError(
"Method names that begin with the word rpc followed by a " +
"period character (U+002E or ASCII 46) are reserved for " +
"rpc-internal methods and extensions and MUST NOT be used " +
"for anything else.")
self._data["method"] = str(value)
@property
def params(self):
return self._data.get("params")
@params.setter
def params(self, value):
if value is not None and not isinstance(value, (list, tuple, dict)):
raise ValueError("Incorrect params {0}".format(value))
value = list(value) if isinstance(value, tuple) else value
if value is not None:
self._data["params"] = value
@property
def _id(self):
return self._data.get("id")
@_id.setter
def _id(self, value):
if value is not None and \
not isinstance(value, six.string_types + six.integer_types):
raise ValueError("id should be string or integer")
self._data["id"] = value
@classmethod
def from_json(cls, json_str):
data = cls.deserialize(json_str)
return cls.from_data(data)
@classmethod
def from_data(cls, data):
is_batch = isinstance(data, list)
data = data if is_batch else [data]
if not data:
raise JSONRPCInvalidRequestException("[] value is not accepted")
if not all(isinstance(d, dict) for d in data):
raise JSONRPCInvalidRequestException(
"Each request should be an object (dict)")
result = []
for d in data:
if not cls.REQUIRED_FIELDS <= set(d.keys()) <= cls.POSSIBLE_FIELDS:
extra = set(d.keys()) - cls.POSSIBLE_FIELDS
missed = cls.REQUIRED_FIELDS - set(d.keys())
msg = "Invalid request. Extra fields: {0}, Missed fields: {1}"
raise JSONRPCInvalidRequestException(msg.format(extra, missed))
try:
result.append(JSONRPC20Request(
method=d["method"], params=d.get("params"),
_id=d.get("id"), is_notification="id" not in d,
))
except ValueError as e:
raise JSONRPCInvalidRequestException(str(e))
return JSONRPC20BatchRequest(*result) if is_batch else result[0]
class JSONRPC20BatchRequest(object):
""" Batch JSON-RPC 2.0 Request.
:param JSONRPC20Request *requests: requests
"""
JSONRPC_VERSION = "2.0"
def __init__(self, *requests):
self.requests = requests
@classmethod
def from_json(cls, json_str):
return JSONRPC20Request.from_json(json_str)
@property
def json(self):
return json.dumps([r.data for r in self.requests])
def __iter__(self):
return iter(self.requests)
class JSONRPC20Response(JSONRPCBaseResponse):
""" JSON-RPC response object to JSONRPC20Request.
When a rpc call is made, the Server MUST reply with a Response, except for
in the case of Notifications. The Response is expressed as a single JSON
Object, with the following members:
:param str jsonrpc: A String specifying the version of the JSON-RPC
protocol. MUST be exactly "2.0".
:param result: This member is REQUIRED on success.
This member MUST NOT exist if there was an error invoking the method.
The value of this member is determined by the method invoked on the
Server.
:param dict error: This member is REQUIRED on error.
This member MUST NOT exist if there was no error triggered during
invocation. The value for this member MUST be an Object.
:param id: This member is REQUIRED.
It MUST be the same as the value of the id member in the Request
Object. If there was an error in detecting the id in the Request
object (e.g. Parse error/Invalid Request), it MUST be Null.
:type id: str or int or None
Either the result member or error member MUST be included, but both
members MUST NOT be included.
"""
JSONRPC_VERSION = "2.0"
@property
def data(self):
data = dict((k, v) for k, v in self._data.items())
data["jsonrpc"] = self.JSONRPC_VERSION
return data
@data.setter
def data(self, value):
if not isinstance(value, dict):
raise ValueError("data should be dict")
self._data = value
@property
def result(self):
return self._data.get("result")
@result.setter
def result(self, value):
if self.error:
raise ValueError("Either result or error should be used")
self._data["result"] = value
@property
def error(self):
return self._data.get("error")
@error.setter
def error(self, value):
self._data.pop('value', None)
if value:
self._data["error"] = value
# Test error
JSONRPCError(**value)
@property
def _id(self):
return self._data.get("id")
@_id.setter
def _id(self, value):
if value is not None and \
not isinstance(value, six.string_types + six.integer_types):
raise ValueError("id should be string or integer")
self._data["id"] = value
class JSONRPC20BatchResponse(object):
JSONRPC_VERSION = "2.0"
def __init__(self, *responses):
self.responses = responses
self.request = None # type: JSONRPC20BatchRequest
@property
def data(self):
return [r.data for r in self.responses]
@property
def json(self):
return json.dumps(self.data)
def __iter__(self):
return iter(self.responses)