2 JSONRPCLIB -- started by Josh Marshall
4 This library is a JSON-RPC v.2 (proposed) implementation which
5 follows the xmlrpclib API for portability between clients. It
6 uses the same Server / ServerProxy, loads, dumps, etc. syntax,
7 while providing features not present in XML-RPC like:
12 * Batches and batch notifications
14 Eventually, I'll add a SimpleXMLRPCServer compatible library,
15 and other things to tie the thing off nicely. :)
17 For a quick-start, just open a console and type the following,
18 replacing the server address, method, and parameters
21 >>> server = jsonrpclib.Server('http://localhost:8181')
24 >>> server._notify.add(5, 6)
25 >>> batch = jsonrpclib.MultiCall(server)
28 >>> batch._notify.add(3, 5)
32 See http://code.google.com/p/jsonrpclib/ for more info.
37 from xmlrpclib import Transport as XMLTransport
38 from xmlrpclib import SafeTransport as XMLSafeTransport
39 from xmlrpclib import ServerProxy as XMLServerProxy
40 from xmlrpclib import _Method as XML_Method
43 # JSON library importing
55 if not cjson and not json:
57 import simplejson as json
59 raise ImportError('You must have the cjson, json, or simplejson ' +
60 'module(s) available.')
66 _user_agent = 'jsonrpclib/0.1 (Python %s)' % \
67 '.'.join([str(ver) for ver in sys.version_info[0:3]])
71 def jdumps(obj, encoding='utf-8'):
72 # Do 'serialize' test at some point for other classes
75 return cjson.encode(obj)
77 return json.dumps(obj, encoding=encoding)
79 def jloads(json_string):
82 return cjson.decode(json_string)
84 return json.loads(json_string)
87 # XMLRPClib re-implemntations
89 class ProtocolError(Exception):
92 class Transport(XMLTransport):
93 """ Just extends the XMLRPC transport where necessary. """
94 user_agent = _user_agent
96 def send_content(self, connection, request_body):
97 connection.putheader("Content-Type", "text/json")
98 connection.putheader("Content-Length", str(len(request_body)))
99 connection.endheaders()
101 connection.send(request_body)
103 def _parse_response(self, file_h, sock):
107 response = sock.recv(1024)
109 response = file_h.read(1024)
112 response_body += response
114 print 'body: %s' % response
115 if response_body == '':
118 return_obj = loads(response_body)
121 class SafeTransport(XMLSafeTransport):
122 """ Just extends for HTTPS calls """
123 user_agent = Transport.user_agent
124 send_content = Transport.send_content
125 _parse_response = Transport._parse_response
127 class ServerProxy(XMLServerProxy):
129 Unfortunately, much more of this class has to be copied since
130 so much of it does the serialization.
133 def __init__(self, uri, transport=None, encoding=None,
134 verbose=0, version=None):
139 self.__version = version
140 schema, uri = urllib.splittype(uri)
141 if schema not in ('http', 'https'):
142 raise IOError('Unsupported JSON-RPC protocol.')
143 self.__host, self.__handler = urllib.splithost(uri)
144 if not self.__handler:
145 # Not sure if this is in the JSON spec?
146 self.__handler = '/RPC2'
147 if transport is None:
148 if schema == 'https':
149 transport = SafeTransport()
151 transport = Transport()
152 self.__transport = transport
153 self.__encoding = encoding
154 self.__verbose = verbose
156 def _request(self, methodname, params, rpcid=None):
157 request = dumps(params, methodname, encoding=self.__encoding,
158 rpcid=rpcid, version=self.__version)
159 response = self._run_request(request)
160 check_for_errors(response)
161 return response['result']
163 def _request_notify(self, methodname, params, rpcid=None):
164 request = dumps(params, methodname, encoding=self.__encoding,
165 rpcid=rpcid, version=self.__version, notify=True)
166 response = self._run_request(request, notify=True)
167 check_for_errors(response)
170 def _run_request(self, request, notify=None):
172 global _last_response
173 _last_request = request
175 response = self.__transport.request(
179 verbose=self.__verbose
182 # Here, the XMLRPC library translates a single list
183 # response to the single value -- should we do the
184 # same, and require a tuple / list to be passed to
185 # the response object, or expect the Server to be
186 # outputting the response appropriately?
188 _last_response = response
191 def __getattr__(self, name):
192 # Same as original, just with new _Method reference
193 return _Method(self._request, name)
197 # Just like __getattr__, but with notify namespace.
198 return _Notify(self._request_notify)
201 class _Method(XML_Method):
203 def __call__(self, *args, **kwargs):
204 if len(args) > 0 and len(kwargs) > 0:
205 raise ProtocolError('Cannot use both positional ' +
206 'and keyword arguments (according to JSON-RPC spec.)')
208 return self.__send(self.__name, args)
210 return self.__send(self.__name, kwargs)
212 class _Notify(object):
213 def __init__(self, request):
214 self._request = request
216 def __getattr__(self, name):
217 return _Method(self._request, name)
219 # Batch implementation
221 class MultiCallMethod(object):
223 def __init__(self, method, notify=False):
228 def __call__(self, *args, **kwargs):
229 if len(kwargs) > 0 and len(args) > 0:
230 raise ProtocolError('JSON-RPC does not support both ' +
231 'positional and keyword arguments.')
237 def request(self, encoding=None, rpcid=None):
238 return dumps(self.params, self.method, version=2.0,
239 encoding=encoding, rpcid=rpcid, notify=self.notify)
242 return '%s' % self.request()
244 class MultiCallNotify(object):
245 def __getattr__(self, name):
246 return MultiCallMethod(name, notify=True)
248 class MultiCallIterator(object):
250 def __init__(self, results):
251 self.results = results
254 for i in range(0, len(self.results)):
258 def __getitem__(self, i):
259 item = self.results[i]
260 check_for_errors(item)
261 return item['result']
264 return len(self.results)
266 class MultiCall(object):
268 def __init__(self, server):
269 self.__server = server
273 if len(self.__job_list) < 1:
274 # Should we alert? This /is/ pretty obvious.
276 request_body = '[ %s ]' % ','.join([job.request() for
277 job in self.__job_list])
278 responses = self.__server._run_request(request_body)
279 del self.__job_list[:]
280 return MultiCallIterator(responses)
284 return MultiCallNotify()
286 def __getattr__(self, name):
287 new_job = MultiCallMethod(name)
288 self.__job_list.append(new_job)
293 # These lines conform to xmlrpclib's "compatibility" line.
294 # Not really sure if we should include these, but oh well.
298 # JSON-RPC error class
299 def __init__(self, code=-32000, message='Server error'):
300 self.faultCode = code
301 self.faultString = message
304 return {'code':self.faultCode, 'message':self.faultString}
306 def response(self, rpcid=None, version=None):
310 return dumps(self, rpcid=rpcid, version=version)
313 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
315 def random_id(length=8):
319 choices = string.lowercase+string.digits
321 for i in range(length):
322 return_id += random.choice(choices)
326 def __init__(self, rpcid=None, version=None):
331 self.version = float(version)
333 def request(self, method, params=[]):
334 if type(method) not in types.StringTypes:
335 raise ValueError('Method name must be a string.')
337 self.id = random_id()
338 request = {'id':self.id, 'method':method, 'params':params}
339 if self.version >= 2:
340 request['jsonrpc'] = str(self.version)
343 def notify(self, method, params=[]):
344 request = self.request(method, params)
345 if self.version >= 2:
351 def response(self, result=None):
352 response = {'result':result, 'id':self.id}
353 if self.version >= 2:
354 response['jsonrpc'] = str(self.version)
356 response['error'] = None
359 def error(self, code=-32000, message='Server error.'):
360 error = self.response()
361 if self.version >= 2:
364 error['result'] = None
365 error['error'] = {'code':code, 'message':message}
368 def dumps(params=[], methodname=None, methodresponse=None,
369 encoding=None, rpcid=None, version=None, notify=None):
371 This differs from the Python implementation in that it implements
372 the rpcid argument since the 2.0 spec requires it for responses.
377 valid_params = (types.TupleType, types.ListType, types.DictType)
378 if methodname in types.StringTypes and \
379 type(params) not in valid_params and \
380 not isinstance(params, Fault):
382 If a method, and params are not in a listish or a Fault,
385 raise TypeError('Params must be a dict, list, tuple or Fault ' +
387 # Begin parsing object
388 payload = Payload(rpcid=rpcid, version=version)
391 if type(params) is Fault:
392 response = payload.error(params.faultCode, params.faultString)
393 return jdumps(response, encoding=encoding)
394 if type(methodname) not in types.StringTypes and methodresponse != True:
395 raise ValueError('Method name must be a string, or methodresponse '+
396 'must be set to True.')
397 if methodresponse is True:
399 raise ValueError('A method response must have an rpcid.')
400 response = payload.response(params)
401 return jdumps(response, encoding=encoding)
404 request = payload.notify(methodname, params)
406 request = payload.request(methodname, params)
407 return jdumps(request, encoding=encoding)
411 This differs from the Python implementation, in that it returns
412 the request structure in Dict format instead of the method, params.
413 It will return a list in the case of a batch request / response.
418 result = jloads(data)
419 # if the above raises an error, the implementing server code
420 # should return something like the following:
421 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
424 def check_for_errors(result):
428 if type(result) is not types.DictType:
429 raise TypeError('Response is not a dict.')
430 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
431 raise NotImplementedError('JSON-RPC version not yet supported.')
432 if 'result' not in result.keys() and 'error' not in result.keys():
433 raise ValueError('Response does not have a result or error key.')
434 if 'error' in result.keys() and result['error'] != None:
435 code = result['error']['code']
436 message = result['error']['message']
437 raise ProtocolError('ERROR %s: %s' % (code, message))
441 if type(result) not in (types.ListType, types.TupleType):
445 if type(result[0]) is not types.DictType:
447 if 'jsonrpc' not in result[0].keys():
450 version = float(result[0]['jsonrpc'])
452 raise ProtocolError('"jsonrpc" key must be a float(able) value.')