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 TransportMixIn(object):
106 """ Just extends the XMLRPC transport where necessary. """
107 user_agent = config.user_agent
108 # for Python 2.7 support
111 def send_content(self, connection, request_body):
112 connection.putheader("Content-Type", "application/json-rpc")
113 connection.putheader("Content-Length", str(len(request_body)))
114 connection.endheaders()
116 connection.send(request_body)
119 target = JSONTarget()
120 return JSONParser(target), target
122 class JSONParser(object):
123 def __init__(self, target):
126 def feed(self, data):
127 self.target.feed(data)
132 class JSONTarget(object):
136 def feed(self, data):
137 self.data.append(data)
140 return ''.join(self.data)
142 class Transport(TransportMixIn, XMLTransport):
145 class SafeTransport(TransportMixIn, XMLSafeTransport):
148 class ServerProxy(XMLServerProxy):
150 Unfortunately, much more of this class has to be copied since
151 so much of it does the serialization.
154 def __init__(self, uri, transport=None, encoding=None,
155 verbose=0, version=None):
158 version = config.version
159 self.__version = version
160 schema, uri = urllib.splittype(uri)
161 if schema not in ('http', 'https'):
162 raise IOError('Unsupported JSON-RPC protocol.')
163 self.__host, self.__handler = urllib.splithost(uri)
164 if not self.__handler:
165 # Not sure if this is in the JSON spec?
166 #self.__handler = '/'
167 self.__handler == '/'
168 if transport is None:
169 if schema == 'https':
170 transport = SafeTransport()
172 transport = Transport()
173 self.__transport = transport
174 self.__encoding = encoding
175 self.__verbose = verbose
177 def _request(self, methodname, params, rpcid=None):
178 request = dumps(params, methodname, encoding=self.__encoding,
179 rpcid=rpcid, version=self.__version)
180 response = self._run_request(request)
181 check_for_errors(response)
182 return response['result']
184 def _request_notify(self, methodname, params, rpcid=None):
185 request = dumps(params, methodname, encoding=self.__encoding,
186 rpcid=rpcid, version=self.__version, notify=True)
187 response = self._run_request(request, notify=True)
188 check_for_errors(response)
191 def _run_request(self, request, notify=None):
192 history.add_request(request)
194 response = self.__transport.request(
198 verbose=self.__verbose
201 # Here, the XMLRPC library translates a single list
202 # response to the single value -- should we do the
203 # same, and require a tuple / list to be passed to
204 # the response object, or expect the Server to be
205 # outputting the response appropriately?
207 history.add_response(response)
210 return_obj = loads(response)
213 def __getattr__(self, name):
214 # Same as original, just with new _Method reference
215 return _Method(self._request, name)
219 # Just like __getattr__, but with notify namespace.
220 return _Notify(self._request_notify)
223 class _Method(XML_Method):
225 def __call__(self, *args, **kwargs):
226 if len(args) > 0 and len(kwargs) > 0:
227 raise ProtocolError('Cannot use both positional ' +
228 'and keyword arguments (according to JSON-RPC spec.)')
230 return self.__send(self.__name, args)
232 return self.__send(self.__name, kwargs)
234 def __getattr__(self, name):
235 self.__name = '%s.%s' % (self.__name, name)
237 # The old method returned a new instance, but this seemed wasteful.
238 # The only thing that changes is the name.
239 #return _Method(self.__send, "%s.%s" % (self.__name, name))
241 class _Notify(object):
242 def __init__(self, request):
243 self._request = request
245 def __getattr__(self, name):
246 return _Method(self._request, name)
248 # Batch implementation
250 class MultiCallMethod(object):
252 def __init__(self, method, notify=False):
257 def __call__(self, *args, **kwargs):
258 if len(kwargs) > 0 and len(args) > 0:
259 raise ProtocolError('JSON-RPC does not support both ' +
260 'positional and keyword arguments.')
266 def request(self, encoding=None, rpcid=None):
267 return dumps(self.params, self.method, version=2.0,
268 encoding=encoding, rpcid=rpcid, notify=self.notify)
271 return '%s' % self.request()
273 def __getattr__(self, method):
274 new_method = '%s.%s' % (self.method, method)
275 self.method = new_method
278 class MultiCallNotify(object):
280 def __init__(self, multicall):
281 self.multicall = multicall
283 def __getattr__(self, name):
284 new_job = MultiCallMethod(name, notify=True)
285 self.multicall._job_list.append(new_job)
288 class MultiCallIterator(object):
290 def __init__(self, results):
291 self.results = results
294 for i in range(0, len(self.results)):
298 def __getitem__(self, i):
299 item = self.results[i]
300 check_for_errors(item)
301 return item['result']
304 return len(self.results)
306 class MultiCall(object):
308 def __init__(self, server):
309 self._server = server
313 if len(self._job_list) < 1:
314 # Should we alert? This /is/ pretty obvious.
316 request_body = '[ %s ]' % ','.join([job.request() for
317 job in self._job_list])
318 responses = self._server._run_request(request_body)
319 del self._job_list[:]
322 return MultiCallIterator(responses)
326 return MultiCallNotify(self)
328 def __getattr__(self, name):
329 new_job = MultiCallMethod(name)
330 self._job_list.append(new_job)
335 # These lines conform to xmlrpclib's "compatibility" line.
336 # Not really sure if we should include these, but oh well.
340 # JSON-RPC error class
341 def __init__(self, code=-32000, message='Server error', rpcid=None):
342 self.faultCode = code
343 self.faultString = message
347 return {'code':self.faultCode, 'message':self.faultString}
349 def response(self, rpcid=None, version=None):
351 version = config.version
355 self, methodresponse=True, rpcid=self.rpcid, version=version
359 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
361 def random_id(length=8):
365 choices = string.lowercase+string.digits
367 for i in range(length):
368 return_id += random.choice(choices)
372 def __init__(self, rpcid=None, version=None):
374 version = config.version
376 self.version = float(version)
378 def request(self, method, params=[]):
379 if type(method) not in types.StringTypes:
380 raise ValueError('Method name must be a string.')
382 self.id = random_id()
383 request = { 'id':self.id, 'method':method }
385 request['params'] = params
386 if self.version >= 2:
387 request['jsonrpc'] = str(self.version)
390 def notify(self, method, params=[]):
391 request = self.request(method, params)
392 if self.version >= 2:
398 def response(self, result=None):
399 response = {'result':result, 'id':self.id}
400 if self.version >= 2:
401 response['jsonrpc'] = str(self.version)
403 response['error'] = None
406 def error(self, code=-32000, message='Server error.'):
407 error = self.response()
408 if self.version >= 2:
411 error['result'] = None
412 error['error'] = {'code':code, 'message':message}
415 def dumps(params=[], methodname=None, methodresponse=None,
416 encoding=None, rpcid=None, version=None, notify=None):
418 This differs from the Python implementation in that it implements
419 the rpcid argument since the 2.0 spec requires it for responses.
422 version = config.version
423 valid_params = (types.TupleType, types.ListType, types.DictType)
424 if methodname in types.StringTypes and \
425 type(params) not in valid_params and \
426 not isinstance(params, Fault):
428 If a method, and params are not in a listish or a Fault,
431 raise TypeError('Params must be a dict, list, tuple or Fault ' +
433 # Begin parsing object
434 payload = Payload(rpcid=rpcid, version=version)
437 if type(params) is Fault:
438 response = payload.error(params.faultCode, params.faultString)
439 return jdumps(response, encoding=encoding)
440 if type(methodname) not in types.StringTypes and methodresponse != True:
441 raise ValueError('Method name must be a string, or methodresponse '+
442 'must be set to True.')
443 if config.use_jsonclass == True:
444 from jsonrpclib import jsonclass
445 params = jsonclass.dump(params)
446 if methodresponse is True:
448 raise ValueError('A method response must have an rpcid.')
449 response = payload.response(params)
450 return jdumps(response, encoding=encoding)
453 request = payload.notify(methodname, params)
455 request = payload.request(methodname, params)
456 return jdumps(request, encoding=encoding)
460 This differs from the Python implementation, in that it returns
461 the request structure in Dict format instead of the method, params.
462 It will return a list in the case of a batch request / response.
467 result = jloads(data)
468 # if the above raises an error, the implementing server code
469 # should return something like the following:
470 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
471 if config.use_jsonclass == True:
472 from jsonrpclib import jsonclass
473 result = jsonclass.load(result)
476 def check_for_errors(result):
480 if type(result) is not types.DictType:
481 raise TypeError('Response is not a dict.')
482 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
483 raise NotImplementedError('JSON-RPC version not yet supported.')
484 if 'result' not in result.keys() and 'error' not in result.keys():
485 raise ValueError('Response does not have a result or error key.')
486 if 'error' in result.keys() and result['error'] != None:
487 code = result['error']['code']
488 message = result['error']['message']
489 raise ProtocolError((code, message))
493 if type(result) not in (types.ListType, types.TupleType):
497 if type(result[0]) is not types.DictType:
499 if 'jsonrpc' not in result[0].keys():
502 version = float(result[0]['jsonrpc'])
504 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
509 def isnotification(request):
510 if 'id' not in request.keys():
513 if request['id'] == None: