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 from httplib import HTTP, HTTPConnection
149 from socket import socket, AF_UNIX, SOCK_STREAM
150 class UnixHTTPConnection(HTTPConnection):
152 self.sock = socket(AF_UNIX, SOCK_STREAM)
153 self.sock.connect(self.host)
155 class UnixHTTP(HTTP):
156 _connection_class = UnixHTTPConnection
158 class UnixTransport(TransportMixIn, XMLTransport):
159 def make_connection(self, host):
161 host, extra_headers, x509 = self.get_host_info(host)
162 return UnixHTTP(host)
165 class ServerProxy(XMLServerProxy):
167 Unfortunately, much more of this class has to be copied since
168 so much of it does the serialization.
171 def __init__(self, uri, transport=None, encoding=None,
172 verbose=0, version=None):
175 version = config.version
176 self.__version = version
177 schema, uri = urllib.splittype(uri)
178 if schema not in ('http', 'https', 'unix'):
179 raise IOError('Unsupported JSON-RPC protocol.')
184 self.__host, self.__handler = urllib.splithost(uri)
185 if not self.__handler:
186 # Not sure if this is in the JSON spec?
187 #self.__handler = '/'
188 self.__handler == '/'
189 if transport is None:
191 transport = UnixTransport()
192 elif schema == 'https':
193 transport = SafeTransport()
195 transport = Transport()
196 self.__transport = transport
197 self.__encoding = encoding
198 self.__verbose = verbose
200 def _request(self, methodname, params, rpcid=None):
201 request = dumps(params, methodname, encoding=self.__encoding,
202 rpcid=rpcid, version=self.__version)
203 response = self._run_request(request)
204 check_for_errors(response)
205 return response['result']
207 def _request_notify(self, methodname, params, rpcid=None):
208 request = dumps(params, methodname, encoding=self.__encoding,
209 rpcid=rpcid, version=self.__version, notify=True)
210 response = self._run_request(request, notify=True)
211 check_for_errors(response)
214 def _run_request(self, request, notify=None):
215 history.add_request(request)
217 response = self.__transport.request(
221 verbose=self.__verbose
224 # Here, the XMLRPC library translates a single list
225 # response to the single value -- should we do the
226 # same, and require a tuple / list to be passed to
227 # the response object, or expect the Server to be
228 # outputting the response appropriately?
230 history.add_response(response)
233 return_obj = loads(response)
236 def __getattr__(self, name):
237 # Same as original, just with new _Method reference
238 return _Method(self._request, name)
242 # Just like __getattr__, but with notify namespace.
243 return _Notify(self._request_notify)
246 class _Method(XML_Method):
248 def __call__(self, *args, **kwargs):
249 if len(args) > 0 and len(kwargs) > 0:
250 raise ProtocolError('Cannot use both positional ' +
251 'and keyword arguments (according to JSON-RPC spec.)')
253 return self.__send(self.__name, args)
255 return self.__send(self.__name, kwargs)
257 def __getattr__(self, name):
258 self.__name = '%s.%s' % (self.__name, name)
260 # The old method returned a new instance, but this seemed wasteful.
261 # The only thing that changes is the name.
262 #return _Method(self.__send, "%s.%s" % (self.__name, name))
264 class _Notify(object):
265 def __init__(self, request):
266 self._request = request
268 def __getattr__(self, name):
269 return _Method(self._request, name)
271 # Batch implementation
273 class MultiCallMethod(object):
275 def __init__(self, method, notify=False):
280 def __call__(self, *args, **kwargs):
281 if len(kwargs) > 0 and len(args) > 0:
282 raise ProtocolError('JSON-RPC does not support both ' +
283 'positional and keyword arguments.')
289 def request(self, encoding=None, rpcid=None):
290 return dumps(self.params, self.method, version=2.0,
291 encoding=encoding, rpcid=rpcid, notify=self.notify)
294 return '%s' % self.request()
296 def __getattr__(self, method):
297 new_method = '%s.%s' % (self.method, method)
298 self.method = new_method
301 class MultiCallNotify(object):
303 def __init__(self, multicall):
304 self.multicall = multicall
306 def __getattr__(self, name):
307 new_job = MultiCallMethod(name, notify=True)
308 self.multicall._job_list.append(new_job)
311 class MultiCallIterator(object):
313 def __init__(self, results):
314 self.results = results
317 for i in range(0, len(self.results)):
321 def __getitem__(self, i):
322 item = self.results[i]
323 check_for_errors(item)
324 return item['result']
327 return len(self.results)
329 class MultiCall(object):
331 def __init__(self, server):
332 self._server = server
336 if len(self._job_list) < 1:
337 # Should we alert? This /is/ pretty obvious.
339 request_body = '[ %s ]' % ','.join([job.request() for
340 job in self._job_list])
341 responses = self._server._run_request(request_body)
342 del self._job_list[:]
345 return MultiCallIterator(responses)
349 return MultiCallNotify(self)
351 def __getattr__(self, name):
352 new_job = MultiCallMethod(name)
353 self._job_list.append(new_job)
358 # These lines conform to xmlrpclib's "compatibility" line.
359 # Not really sure if we should include these, but oh well.
363 # JSON-RPC error class
364 def __init__(self, code=-32000, message='Server error', rpcid=None):
365 self.faultCode = code
366 self.faultString = message
370 return {'code':self.faultCode, 'message':self.faultString}
372 def response(self, rpcid=None, version=None):
374 version = config.version
378 self, methodresponse=True, rpcid=self.rpcid, version=version
382 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
384 def random_id(length=8):
388 choices = string.lowercase+string.digits
390 for i in range(length):
391 return_id += random.choice(choices)
395 def __init__(self, rpcid=None, version=None):
397 version = config.version
399 self.version = float(version)
401 def request(self, method, params=[]):
402 if type(method) not in types.StringTypes:
403 raise ValueError('Method name must be a string.')
405 self.id = random_id()
406 request = { 'id':self.id, 'method':method }
408 request['params'] = params
409 if self.version >= 2:
410 request['jsonrpc'] = str(self.version)
413 def notify(self, method, params=[]):
414 request = self.request(method, params)
415 if self.version >= 2:
421 def response(self, result=None):
422 response = {'result':result, 'id':self.id}
423 if self.version >= 2:
424 response['jsonrpc'] = str(self.version)
426 response['error'] = None
429 def error(self, code=-32000, message='Server error.'):
430 error = self.response()
431 if self.version >= 2:
434 error['result'] = None
435 error['error'] = {'code':code, 'message':message}
438 def dumps(params=[], methodname=None, methodresponse=None,
439 encoding=None, rpcid=None, version=None, notify=None):
441 This differs from the Python implementation in that it implements
442 the rpcid argument since the 2.0 spec requires it for responses.
445 version = config.version
446 valid_params = (types.TupleType, types.ListType, types.DictType)
447 if methodname in types.StringTypes and \
448 type(params) not in valid_params and \
449 not isinstance(params, Fault):
451 If a method, and params are not in a listish or a Fault,
454 raise TypeError('Params must be a dict, list, tuple or Fault ' +
456 # Begin parsing object
457 payload = Payload(rpcid=rpcid, version=version)
460 if type(params) is Fault:
461 response = payload.error(params.faultCode, params.faultString)
462 return jdumps(response, encoding=encoding)
463 if type(methodname) not in types.StringTypes and methodresponse != True:
464 raise ValueError('Method name must be a string, or methodresponse '+
465 'must be set to True.')
466 if config.use_jsonclass == True:
467 from jsonrpclib import jsonclass
468 params = jsonclass.dump(params)
469 if methodresponse is True:
471 raise ValueError('A method response must have an rpcid.')
472 response = payload.response(params)
473 return jdumps(response, encoding=encoding)
476 request = payload.notify(methodname, params)
478 request = payload.request(methodname, params)
479 return jdumps(request, encoding=encoding)
483 This differs from the Python implementation, in that it returns
484 the request structure in Dict format instead of the method, params.
485 It will return a list in the case of a batch request / response.
490 result = jloads(data)
491 # if the above raises an error, the implementing server code
492 # should return something like the following:
493 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
494 if config.use_jsonclass == True:
495 from jsonrpclib import jsonclass
496 result = jsonclass.load(result)
499 def check_for_errors(result):
503 if type(result) is not types.DictType:
504 raise TypeError('Response is not a dict.')
505 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
506 raise NotImplementedError('JSON-RPC version not yet supported.')
507 if 'result' not in result.keys() and 'error' not in result.keys():
508 raise ValueError('Response does not have a result or error key.')
509 if 'error' in result.keys() and result['error'] != None:
510 code = result['error']['code']
511 message = result['error']['message']
512 raise ProtocolError((code, message))
516 if type(result) not in (types.ListType, types.TupleType):
520 if type(result[0]) is not types.DictType:
522 if 'jsonrpc' not in result[0].keys():
525 version = float(result[0]['jsonrpc'])
527 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
532 def isnotification(request):
533 if 'id' not in request.keys():
536 if request['id'] == None: