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)
116 def _parse_response(self, file_h, sock):
120 response = sock.recv(1024)
122 response = file_h.read(1024)
125 response_body += response
127 print 'body: %s' % response
130 class SafeTransport(XMLSafeTransport):
131 """ Just extends for HTTPS calls """
132 user_agent = Transport.user_agent
133 send_content = Transport.send_content
134 _parse_response = Transport._parse_response
136 class ServerProxy(XMLServerProxy):
138 Unfortunately, much more of this class has to be copied since
139 so much of it does the serialization.
142 def __init__(self, uri, transport=None, encoding=None,
143 verbose=0, version=None):
146 version = config.version
147 self.__version = version
148 schema, uri = urllib.splittype(uri)
149 if schema not in ('http', 'https'):
150 raise IOError('Unsupported JSON-RPC protocol.')
151 self.__host, self.__handler = urllib.splithost(uri)
152 if not self.__handler:
153 # Not sure if this is in the JSON spec?
154 self.__handler = '/RPC2'
155 if transport is None:
156 if schema == 'https':
157 transport = SafeTransport()
159 transport = Transport()
160 self.__transport = transport
161 self.__encoding = encoding
162 self.__verbose = verbose
164 def _request(self, methodname, params, rpcid=None):
165 request = dumps(params, methodname, encoding=self.__encoding,
166 rpcid=rpcid, version=self.__version)
167 response = self._run_request(request)
168 check_for_errors(response)
169 return response['result']
171 def _request_notify(self, methodname, params, rpcid=None):
172 request = dumps(params, methodname, encoding=self.__encoding,
173 rpcid=rpcid, version=self.__version, notify=True)
174 response = self._run_request(request, notify=True)
175 check_for_errors(response)
178 def _run_request(self, request, notify=None):
179 history.add_request(request)
181 response = self.__transport.request(
185 verbose=self.__verbose
188 # Here, the XMLRPC library translates a single list
189 # response to the single value -- should we do the
190 # same, and require a tuple / list to be passed to
191 # the response object, or expect the Server to be
192 # outputting the response appropriately?
194 history.add_response(response)
197 return_obj = loads(response)
200 def __getattr__(self, name):
201 # Same as original, just with new _Method reference
202 return _Method(self._request, name)
206 # Just like __getattr__, but with notify namespace.
207 return _Notify(self._request_notify)
210 class _Method(XML_Method):
212 def __call__(self, *args, **kwargs):
213 if len(args) > 0 and len(kwargs) > 0:
214 raise ProtocolError('Cannot use both positional ' +
215 'and keyword arguments (according to JSON-RPC spec.)')
217 return self.__send(self.__name, args)
219 return self.__send(self.__name, kwargs)
221 def __getattr__(self, name):
222 self.__name = '%s.%s' % (self.__name, name)
224 # The old method returned a new instance, but this seemed wasteful.
225 # The only thing that changes is the name.
226 #return _Method(self.__send, "%s.%s" % (self.__name, name))
228 class _Notify(object):
229 def __init__(self, request):
230 self._request = request
232 def __getattr__(self, name):
233 return _Method(self._request, name)
235 # Batch implementation
237 class MultiCallMethod(object):
239 def __init__(self, method, notify=False):
244 def __call__(self, *args, **kwargs):
245 if len(kwargs) > 0 and len(args) > 0:
246 raise ProtocolError('JSON-RPC does not support both ' +
247 'positional and keyword arguments.')
253 def request(self, encoding=None, rpcid=None):
254 return dumps(self.params, self.method, version=2.0,
255 encoding=encoding, rpcid=rpcid, notify=self.notify)
258 return '%s' % self.request()
260 def __getattr__(self, method):
261 new_method = '%s.%s' % (self.method, method)
262 self.method = new_method
265 class MultiCallNotify(object):
267 def __init__(self, multicall):
268 self.multicall = multicall
270 def __getattr__(self, name):
271 new_job = MultiCallMethod(name, notify=True)
272 self.multicall._job_list.append(new_job)
275 class MultiCallIterator(object):
277 def __init__(self, results):
278 self.results = results
281 for i in range(0, len(self.results)):
285 def __getitem__(self, i):
286 item = self.results[i]
287 check_for_errors(item)
288 return item['result']
291 return len(self.results)
293 class MultiCall(object):
295 def __init__(self, server):
296 self._server = server
300 if len(self._job_list) < 1:
301 # Should we alert? This /is/ pretty obvious.
303 request_body = '[ %s ]' % ','.join([job.request() for
304 job in self._job_list])
305 responses = self._server._run_request(request_body)
306 del self._job_list[:]
309 return MultiCallIterator(responses)
313 return MultiCallNotify(self)
315 def __getattr__(self, name):
316 new_job = MultiCallMethod(name)
317 self._job_list.append(new_job)
322 # These lines conform to xmlrpclib's "compatibility" line.
323 # Not really sure if we should include these, but oh well.
327 # JSON-RPC error class
328 def __init__(self, code=-32000, message='Server error', rpcid=None):
329 self.faultCode = code
330 self.faultString = message
334 return {'code':self.faultCode, 'message':self.faultString}
336 def response(self, rpcid=None, version=None):
338 version = config.version
342 self, methodresponse=True, rpcid=self.rpcid, version=version
346 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
348 def random_id(length=8):
352 choices = string.lowercase+string.digits
354 for i in range(length):
355 return_id += random.choice(choices)
359 def __init__(self, rpcid=None, version=None):
361 version = config.version
363 self.version = float(version)
365 def request(self, method, params=[]):
366 if type(method) not in types.StringTypes:
367 raise ValueError('Method name must be a string.')
369 self.id = random_id()
370 request = { 'id':self.id, 'method':method }
372 request['params'] = params
373 if self.version >= 2:
374 request['jsonrpc'] = str(self.version)
377 def notify(self, method, params=[]):
378 request = self.request(method, params)
379 if self.version >= 2:
385 def response(self, result=None):
386 response = {'result':result, 'id':self.id}
387 if self.version >= 2:
388 response['jsonrpc'] = str(self.version)
390 response['error'] = None
393 def error(self, code=-32000, message='Server error.'):
394 error = self.response()
395 if self.version >= 2:
398 error['result'] = None
399 error['error'] = {'code':code, 'message':message}
402 def dumps(params=[], methodname=None, methodresponse=None,
403 encoding=None, rpcid=None, version=None, notify=None):
405 This differs from the Python implementation in that it implements
406 the rpcid argument since the 2.0 spec requires it for responses.
409 version = config.version
410 valid_params = (types.TupleType, types.ListType, types.DictType)
411 if methodname in types.StringTypes and \
412 type(params) not in valid_params and \
413 not isinstance(params, Fault):
415 If a method, and params are not in a listish or a Fault,
418 raise TypeError('Params must be a dict, list, tuple or Fault ' +
420 # Begin parsing object
421 payload = Payload(rpcid=rpcid, version=version)
424 if type(params) is Fault:
425 response = payload.error(params.faultCode, params.faultString)
426 return jdumps(response, encoding=encoding)
427 if type(methodname) not in types.StringTypes and methodresponse != True:
428 raise ValueError('Method name must be a string, or methodresponse '+
429 'must be set to True.')
430 if config.use_jsonclass == True:
431 from jsonrpclib import jsonclass
432 params = jsonclass.dump(params)
433 if methodresponse is True:
435 raise ValueError('A method response must have an rpcid.')
436 response = payload.response(params)
437 return jdumps(response, encoding=encoding)
440 request = payload.notify(methodname, params)
442 request = payload.request(methodname, params)
443 return jdumps(request, encoding=encoding)
447 This differs from the Python implementation, in that it returns
448 the request structure in Dict format instead of the method, params.
449 It will return a list in the case of a batch request / response.
454 result = jloads(data)
455 # if the above raises an error, the implementing server code
456 # should return something like the following:
457 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
458 if config.use_jsonclass == True:
459 from jsonrpclib import jsonclass
460 result = jsonclass.load(result)
463 def check_for_errors(result):
467 if type(result) is not types.DictType:
468 raise TypeError('Response is not a dict.')
469 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
470 raise NotImplementedError('JSON-RPC version not yet supported.')
471 if 'result' not in result.keys() and 'error' not in result.keys():
472 raise ValueError('Response does not have a result or error key.')
473 if 'error' in result.keys() and result['error'] != None:
474 code = result['error']['code']
475 message = result['error']['message']
476 raise ProtocolError((code, message))
480 if type(result) not in (types.ListType, types.TupleType):
484 if type(result[0]) is not types.DictType:
486 if 'jsonrpc' not in result[0].keys():
489 version = float(result[0]['jsonrpc'])
491 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
496 def isnotification(request):
497 if 'id' not in request.keys():
500 if request['id'] == None: