2 Copyright 2009 Josh Marshall
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
7 http://www.apache.org/licenses/LICENSE-2.0
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
15 ============================
16 JSONRPC Library (jsonrpclib)
17 ============================
19 This library is a JSON-RPC v.2 (proposed) implementation which
20 follows the xmlrpclib API for portability between clients. It
21 uses the same Server / ServerProxy, loads, dumps, etc. syntax,
22 while providing features not present in XML-RPC like:
27 * Batches and batch notifications
29 Eventually, I'll add a SimpleXMLRPCServer compatible library,
30 and other things to tie the thing off nicely. :)
32 For a quick-start, just open a console and type the following,
33 replacing the server address, method, and parameters
36 >>> server = jsonrpclib.Server('http://localhost:8181')
39 >>> server._notify.add(5, 6)
40 >>> batch = jsonrpclib.MultiCall(server)
43 >>> batch._notify.add(3, 5)
47 See http://code.google.com/p/jsonrpclib/ for more info.
52 from xmlrpclib import Transport as XMLTransport
53 from xmlrpclib import SafeTransport as XMLSafeTransport
54 from xmlrpclib import ServerProxy as XMLServerProxy
55 from xmlrpclib import _Method as XML_Method
60 from jsonrpclib import config
61 from jsonrpclib import history
63 # JSON library importing
75 if not cjson and not json:
77 import simplejson as json
79 raise ImportError('You must have the cjson, json, or simplejson ' +
80 'module(s) available.')
84 def jdumps(obj, encoding='utf-8'):
85 # Do 'serialize' test at some point for other classes
88 return cjson.encode(obj)
90 return json.dumps(obj, encoding=encoding)
92 def jloads(json_string):
95 return cjson.decode(json_string)
97 return json.loads(json_string)
100 # XMLRPClib re-implemntations
102 class ProtocolError(Exception):
105 class Transport(XMLTransport):
106 """ Just extends the XMLRPC transport where necessary. """
107 user_agent = config.user_agent
109 def send_content(self, connection, request_body):
110 connection.putheader("Content-Type", "application/json-rpc")
111 connection.putheader("Content-Length", str(len(request_body)))
112 connection.endheaders()
114 connection.send(request_body)
117 target = JSONTarget()
118 return JSONParser(target), target
120 class JSONParser(object):
121 def __init__(self, target):
124 def feed(self, data):
125 self.target.feed(data)
130 class JSONTarget(object):
134 def feed(self, data):
135 self.data.append(data)
138 return ''.join(self.data)
140 class SafeTransport(XMLSafeTransport):
141 """ Just extends for HTTPS calls """
142 user_agent = Transport.user_agent
143 send_content = Transport.send_content
144 getparser = Transport.getparser
146 class ServerProxy(XMLServerProxy):
148 Unfortunately, much more of this class has to be copied since
149 so much of it does the serialization.
152 def __init__(self, uri, transport=None, encoding=None,
153 verbose=0, version=None):
156 version = config.version
157 self.__version = version
158 schema, uri = urllib.splittype(uri)
159 if schema not in ('http', 'https'):
160 raise IOError('Unsupported JSON-RPC protocol.')
161 self.__host, self.__handler = urllib.splithost(uri)
162 if not self.__handler:
163 # Not sure if this is in the JSON spec?
164 #self.__handler = '/'
165 self.__handler == '/'
166 if transport is None:
167 if schema == 'https':
168 transport = SafeTransport()
170 transport = Transport()
171 self.__transport = transport
172 self.__encoding = encoding
173 self.__verbose = verbose
175 def _request(self, methodname, params, rpcid=None):
176 request = dumps(params, methodname, encoding=self.__encoding,
177 rpcid=rpcid, version=self.__version)
178 response = self._run_request(request)
179 check_for_errors(response)
180 return response['result']
182 def _request_notify(self, methodname, params, rpcid=None):
183 request = dumps(params, methodname, encoding=self.__encoding,
184 rpcid=rpcid, version=self.__version, notify=True)
185 response = self._run_request(request, notify=True)
186 check_for_errors(response)
189 def _run_request(self, request, notify=None):
190 history.add_request(request)
192 response = self.__transport.request(
196 verbose=self.__verbose
199 # Here, the XMLRPC library translates a single list
200 # response to the single value -- should we do the
201 # same, and require a tuple / list to be passed to
202 # the response object, or expect the Server to be
203 # outputting the response appropriately?
205 history.add_response(response)
208 return_obj = loads(response)
211 def __getattr__(self, name):
212 # Same as original, just with new _Method reference
213 return _Method(self._request, name)
217 # Just like __getattr__, but with notify namespace.
218 return _Notify(self._request_notify)
221 class _Method(XML_Method):
223 def __call__(self, *args, **kwargs):
224 if len(args) > 0 and len(kwargs) > 0:
225 raise ProtocolError('Cannot use both positional ' +
226 'and keyword arguments (according to JSON-RPC spec.)')
228 return self.__send(self.__name, args)
230 return self.__send(self.__name, kwargs)
232 def __getattr__(self, name):
233 self.__name = '%s.%s' % (self.__name, name)
235 # The old method returned a new instance, but this seemed wasteful.
236 # The only thing that changes is the name.
237 #return _Method(self.__send, "%s.%s" % (self.__name, name))
239 class _Notify(object):
240 def __init__(self, request):
241 self._request = request
243 def __getattr__(self, name):
244 return _Method(self._request, name)
246 # Batch implementation
248 class MultiCallMethod(object):
250 def __init__(self, method, notify=False):
255 def __call__(self, *args, **kwargs):
256 if len(kwargs) > 0 and len(args) > 0:
257 raise ProtocolError('JSON-RPC does not support both ' +
258 'positional and keyword arguments.')
264 def request(self, encoding=None, rpcid=None):
265 return dumps(self.params, self.method, version=2.0,
266 encoding=encoding, rpcid=rpcid, notify=self.notify)
269 return '%s' % self.request()
271 def __getattr__(self, method):
272 new_method = '%s.%s' % (self.method, method)
273 self.method = new_method
276 class MultiCallNotify(object):
278 def __init__(self, multicall):
279 self.multicall = multicall
281 def __getattr__(self, name):
282 new_job = MultiCallMethod(name, notify=True)
283 self.multicall._job_list.append(new_job)
286 class MultiCallIterator(object):
288 def __init__(self, results):
289 self.results = results
292 for i in range(0, len(self.results)):
296 def __getitem__(self, i):
297 item = self.results[i]
298 check_for_errors(item)
299 return item['result']
302 return len(self.results)
304 class MultiCall(object):
306 def __init__(self, server):
307 self._server = server
311 if len(self._job_list) < 1:
312 # Should we alert? This /is/ pretty obvious.
314 request_body = '[ %s ]' % ','.join([job.request() for
315 job in self._job_list])
316 responses = self._server._run_request(request_body)
317 del self._job_list[:]
320 return MultiCallIterator(responses)
324 return MultiCallNotify(self)
326 def __getattr__(self, name):
327 new_job = MultiCallMethod(name)
328 self._job_list.append(new_job)
333 # These lines conform to xmlrpclib's "compatibility" line.
334 # Not really sure if we should include these, but oh well.
338 # JSON-RPC error class
339 def __init__(self, code=-32000, message='Server error', rpcid=None):
340 self.faultCode = code
341 self.faultString = message
345 return {'code':self.faultCode, 'message':self.faultString}
347 def response(self, rpcid=None, version=None):
349 version = config.version
353 self, methodresponse=True, rpcid=self.rpcid, version=version
357 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
359 def random_id(length=8):
363 choices = string.lowercase+string.digits
365 for i in range(length):
366 return_id += random.choice(choices)
370 def __init__(self, rpcid=None, version=None):
372 version = config.version
374 self.version = float(version)
376 def request(self, method, params=[]):
377 if type(method) not in types.StringTypes:
378 raise ValueError('Method name must be a string.')
380 self.id = random_id()
381 request = { 'id':self.id, 'method':method }
383 request['params'] = params
384 if self.version >= 2:
385 request['jsonrpc'] = str(self.version)
388 def notify(self, method, params=[]):
389 request = self.request(method, params)
390 if self.version >= 2:
396 def response(self, result=None):
397 response = {'result':result, 'id':self.id}
398 if self.version >= 2:
399 response['jsonrpc'] = str(self.version)
401 response['error'] = None
404 def error(self, code=-32000, message='Server error.'):
405 error = self.response()
406 if self.version >= 2:
409 error['result'] = None
410 error['error'] = {'code':code, 'message':message}
413 def dumps(params=[], methodname=None, methodresponse=None,
414 encoding=None, rpcid=None, version=None, notify=None):
416 This differs from the Python implementation in that it implements
417 the rpcid argument since the 2.0 spec requires it for responses.
420 version = config.version
421 valid_params = (types.TupleType, types.ListType, types.DictType)
422 if methodname in types.StringTypes and \
423 type(params) not in valid_params and \
424 not isinstance(params, Fault):
426 If a method, and params are not in a listish or a Fault,
429 raise TypeError('Params must be a dict, list, tuple or Fault ' +
431 # Begin parsing object
432 payload = Payload(rpcid=rpcid, version=version)
435 if type(params) is Fault:
436 response = payload.error(params.faultCode, params.faultString)
437 return jdumps(response, encoding=encoding)
438 if type(methodname) not in types.StringTypes and methodresponse != True:
439 raise ValueError('Method name must be a string, or methodresponse '+
440 'must be set to True.')
441 if config.use_jsonclass == True:
442 from jsonrpclib import jsonclass
443 params = jsonclass.dump(params)
444 if methodresponse is True:
446 raise ValueError('A method response must have an rpcid.')
447 response = payload.response(params)
448 return jdumps(response, encoding=encoding)
451 request = payload.notify(methodname, params)
453 request = payload.request(methodname, params)
454 return jdumps(request, encoding=encoding)
458 This differs from the Python implementation, in that it returns
459 the request structure in Dict format instead of the method, params.
460 It will return a list in the case of a batch request / response.
465 result = jloads(data)
466 # if the above raises an error, the implementing server code
467 # should return something like the following:
468 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
469 if config.use_jsonclass == True:
470 from jsonrpclib import jsonclass
471 result = jsonclass.load(result)
474 def check_for_errors(result):
478 if type(result) is not types.DictType:
479 raise TypeError('Response is not a dict.')
480 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
481 raise NotImplementedError('JSON-RPC version not yet supported.')
482 if 'result' not in result.keys() and 'error' not in result.keys():
483 raise ValueError('Response does not have a result or error key.')
484 if 'error' in result.keys() and result['error'] != None:
485 code = result['error']['code']
486 message = result['error']['message']
487 raise ProtocolError((code, message))
491 if type(result) not in (types.ListType, types.TupleType):
495 if type(result[0]) is not types.DictType:
497 if 'jsonrpc' not in result[0].keys():
500 version = float(result[0]['jsonrpc'])
502 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
507 def isnotification(request):
508 if 'id' not in request.keys():
511 if request['id'] == None: