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 = '/'
155 self.__handler == '/'
156 if transport is None:
157 if schema == 'https':
158 transport = SafeTransport()
160 transport = Transport()
161 self.__transport = transport
162 self.__encoding = encoding
163 self.__verbose = verbose
165 def _request(self, methodname, params, rpcid=None):
166 request = dumps(params, methodname, encoding=self.__encoding,
167 rpcid=rpcid, version=self.__version)
168 response = self._run_request(request)
169 check_for_errors(response)
170 return response['result']
172 def _request_notify(self, methodname, params, rpcid=None):
173 request = dumps(params, methodname, encoding=self.__encoding,
174 rpcid=rpcid, version=self.__version, notify=True)
175 response = self._run_request(request, notify=True)
176 check_for_errors(response)
179 def _run_request(self, request, notify=None):
180 history.add_request(request)
182 response = self.__transport.request(
186 verbose=self.__verbose
189 # Here, the XMLRPC library translates a single list
190 # response to the single value -- should we do the
191 # same, and require a tuple / list to be passed to
192 # the response object, or expect the Server to be
193 # outputting the response appropriately?
195 history.add_response(response)
198 return_obj = loads(response)
201 def __getattr__(self, name):
202 # Same as original, just with new _Method reference
203 return _Method(self._request, name)
207 # Just like __getattr__, but with notify namespace.
208 return _Notify(self._request_notify)
211 class _Method(XML_Method):
213 def __call__(self, *args, **kwargs):
214 if len(args) > 0 and len(kwargs) > 0:
215 raise ProtocolError('Cannot use both positional ' +
216 'and keyword arguments (according to JSON-RPC spec.)')
218 return self.__send(self.__name, args)
220 return self.__send(self.__name, kwargs)
222 def __getattr__(self, name):
223 self.__name = '%s.%s' % (self.__name, name)
225 # The old method returned a new instance, but this seemed wasteful.
226 # The only thing that changes is the name.
227 #return _Method(self.__send, "%s.%s" % (self.__name, name))
229 class _Notify(object):
230 def __init__(self, request):
231 self._request = request
233 def __getattr__(self, name):
234 return _Method(self._request, name)
236 # Batch implementation
238 class MultiCallMethod(object):
240 def __init__(self, method, notify=False):
245 def __call__(self, *args, **kwargs):
246 if len(kwargs) > 0 and len(args) > 0:
247 raise ProtocolError('JSON-RPC does not support both ' +
248 'positional and keyword arguments.')
254 def request(self, encoding=None, rpcid=None):
255 return dumps(self.params, self.method, version=2.0,
256 encoding=encoding, rpcid=rpcid, notify=self.notify)
259 return '%s' % self.request()
261 def __getattr__(self, method):
262 new_method = '%s.%s' % (self.method, method)
263 self.method = new_method
266 class MultiCallNotify(object):
268 def __init__(self, multicall):
269 self.multicall = multicall
271 def __getattr__(self, name):
272 new_job = MultiCallMethod(name, notify=True)
273 self.multicall._job_list.append(new_job)
276 class MultiCallIterator(object):
278 def __init__(self, results):
279 self.results = results
282 for i in range(0, len(self.results)):
286 def __getitem__(self, i):
287 item = self.results[i]
288 check_for_errors(item)
289 return item['result']
292 return len(self.results)
294 class MultiCall(object):
296 def __init__(self, server):
297 self._server = server
301 if len(self._job_list) < 1:
302 # Should we alert? This /is/ pretty obvious.
304 request_body = '[ %s ]' % ','.join([job.request() for
305 job in self._job_list])
306 responses = self._server._run_request(request_body)
307 del self._job_list[:]
310 return MultiCallIterator(responses)
314 return MultiCallNotify(self)
316 def __getattr__(self, name):
317 new_job = MultiCallMethod(name)
318 self._job_list.append(new_job)
323 # These lines conform to xmlrpclib's "compatibility" line.
324 # Not really sure if we should include these, but oh well.
328 # JSON-RPC error class
329 def __init__(self, code=-32000, message='Server error', rpcid=None):
330 self.faultCode = code
331 self.faultString = message
335 return {'code':self.faultCode, 'message':self.faultString}
337 def response(self, rpcid=None, version=None):
339 version = config.version
343 self, methodresponse=True, rpcid=self.rpcid, version=version
347 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
349 def random_id(length=8):
353 choices = string.lowercase+string.digits
355 for i in range(length):
356 return_id += random.choice(choices)
360 def __init__(self, rpcid=None, version=None):
362 version = config.version
364 self.version = float(version)
366 def request(self, method, params=[]):
367 if type(method) not in types.StringTypes:
368 raise ValueError('Method name must be a string.')
370 self.id = random_id()
371 request = { 'id':self.id, 'method':method }
373 request['params'] = params
374 if self.version >= 2:
375 request['jsonrpc'] = str(self.version)
378 def notify(self, method, params=[]):
379 request = self.request(method, params)
380 if self.version >= 2:
386 def response(self, result=None):
387 response = {'result':result, 'id':self.id}
388 if self.version >= 2:
389 response['jsonrpc'] = str(self.version)
391 response['error'] = None
394 def error(self, code=-32000, message='Server error.'):
395 error = self.response()
396 if self.version >= 2:
399 error['result'] = None
400 error['error'] = {'code':code, 'message':message}
403 def dumps(params=[], methodname=None, methodresponse=None,
404 encoding=None, rpcid=None, version=None, notify=None):
406 This differs from the Python implementation in that it implements
407 the rpcid argument since the 2.0 spec requires it for responses.
410 version = config.version
411 valid_params = (types.TupleType, types.ListType, types.DictType)
412 if methodname in types.StringTypes and \
413 type(params) not in valid_params and \
414 not isinstance(params, Fault):
416 If a method, and params are not in a listish or a Fault,
419 raise TypeError('Params must be a dict, list, tuple or Fault ' +
421 # Begin parsing object
422 payload = Payload(rpcid=rpcid, version=version)
425 if type(params) is Fault:
426 response = payload.error(params.faultCode, params.faultString)
427 return jdumps(response, encoding=encoding)
428 if type(methodname) not in types.StringTypes and methodresponse != True:
429 raise ValueError('Method name must be a string, or methodresponse '+
430 'must be set to True.')
431 if config.use_jsonclass == True:
432 from jsonrpclib import jsonclass
433 params = jsonclass.dump(params)
434 if methodresponse is True:
436 raise ValueError('A method response must have an rpcid.')
437 response = payload.response(params)
438 return jdumps(response, encoding=encoding)
441 request = payload.notify(methodname, params)
443 request = payload.request(methodname, params)
444 return jdumps(request, encoding=encoding)
448 This differs from the Python implementation, in that it returns
449 the request structure in Dict format instead of the method, params.
450 It will return a list in the case of a batch request / response.
455 result = jloads(data)
456 # if the above raises an error, the implementing server code
457 # should return something like the following:
458 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
459 if config.use_jsonclass == True:
460 from jsonrpclib import jsonclass
461 result = jsonclass.load(result)
464 def check_for_errors(result):
468 if type(result) is not types.DictType:
469 raise TypeError('Response is not a dict.')
470 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
471 raise NotImplementedError('JSON-RPC version not yet supported.')
472 if 'result' not in result.keys() and 'error' not in result.keys():
473 raise ValueError('Response does not have a result or error key.')
474 if 'error' in result.keys() and result['error'] != None:
475 code = result['error']['code']
476 message = result['error']['message']
477 raise ProtocolError((code, message))
481 if type(result) not in (types.ListType, types.TupleType):
485 if type(result[0]) is not types.DictType:
487 if 'jsonrpc' not in result[0].keys():
490 version = float(result[0]['jsonrpc'])
492 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
497 def isnotification(request):
498 if 'id' not in request.keys():
501 if request['id'] == None: