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
59 from config import config
60 from history import history
62 # JSON library importing
74 if not cjson and not json:
76 import simplejson as json
78 raise ImportError('You must have the cjson, json, or simplejson ' +
79 'module(s) available.')
83 def jdumps(obj, encoding='utf-8'):
84 # Do 'serialize' test at some point for other classes
87 return cjson.encode(obj)
89 return json.dumps(obj, encoding=encoding)
91 def jloads(json_string):
94 return cjson.decode(json_string)
96 return json.loads(json_string)
99 # XMLRPClib re-implemntations
101 class ProtocolError(Exception):
104 class Transport(XMLTransport):
105 """ Just extends the XMLRPC transport where necessary. """
106 user_agent = config.user_agent
108 def send_content(self, connection, request_body):
109 connection.putheader("Content-Type", "application/json-rpc")
110 connection.putheader("Content-Length", str(len(request_body)))
111 connection.endheaders()
113 connection.send(request_body)
115 def _parse_response(self, file_h, sock):
119 response = sock.recv(1024)
121 response = file_h.read(1024)
124 response_body += response
126 print 'body: %s' % response
127 if response_body == '':
130 return_obj = loads(response_body)
133 class SafeTransport(XMLSafeTransport):
134 """ Just extends for HTTPS calls """
135 user_agent = Transport.user_agent
136 send_content = Transport.send_content
137 _parse_response = Transport._parse_response
139 class ServerProxy(XMLServerProxy):
141 Unfortunately, much more of this class has to be copied since
142 so much of it does the serialization.
145 def __init__(self, uri, transport=None, encoding=None,
146 verbose=0, version=None):
149 version = config.version
150 self.__version = version
151 schema, uri = urllib.splittype(uri)
152 if schema not in ('http', 'https'):
153 raise IOError('Unsupported JSON-RPC protocol.')
154 self.__host, self.__handler = urllib.splithost(uri)
155 if not self.__handler:
156 # Not sure if this is in the JSON spec?
157 self.__handler = '/RPC2'
158 if transport is None:
159 if schema == 'https':
160 transport = SafeTransport()
162 transport = Transport()
163 self.__transport = transport
164 self.__encoding = encoding
165 self.__verbose = verbose
167 def _request(self, methodname, params, rpcid=None):
168 request = dumps(params, methodname, encoding=self.__encoding,
169 rpcid=rpcid, version=self.__version)
170 response = self._run_request(request)
171 check_for_errors(response)
172 return response['result']
174 def _request_notify(self, methodname, params, rpcid=None):
175 request = dumps(params, methodname, encoding=self.__encoding,
176 rpcid=rpcid, version=self.__version, notify=True)
177 response = self._run_request(request, notify=True)
178 check_for_errors(response)
181 def _run_request(self, request, notify=None):
182 history.add_request(request)
184 response = self.__transport.request(
188 verbose=self.__verbose
191 # Here, the XMLRPC library translates a single list
192 # response to the single value -- should we do the
193 # same, and require a tuple / list to be passed to
194 # the response object, or expect the Server to be
195 # outputting the response appropriately?
197 history.add_response(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 # Even though this is verbatim, it doesn't support
223 # keyword arguments unless we rewrite it.
224 return _Method(self.__send, "%s.%s" % (self.__name, name))
226 class _Notify(object):
227 def __init__(self, request):
228 self._request = request
230 def __getattr__(self, name):
231 return _Method(self._request, name)
233 # Batch implementation
235 class MultiCallMethod(object):
237 def __init__(self, method, notify=False):
242 def __call__(self, *args, **kwargs):
243 if len(kwargs) > 0 and len(args) > 0:
244 raise ProtocolError('JSON-RPC does not support both ' +
245 'positional and keyword arguments.')
251 def request(self, encoding=None, rpcid=None):
252 return dumps(self.params, self.method, version=2.0,
253 encoding=encoding, rpcid=rpcid, notify=self.notify)
256 return '%s' % self.request()
258 class MultiCallNotify(object):
260 def __init__(self, multicall):
261 self.multicall = multicall
263 def __getattr__(self, name):
264 new_job = MultiCallMethod(name, notify=True)
265 self.multicall._job_list.append(new_job)
268 class MultiCallIterator(object):
270 def __init__(self, results):
271 self.results = results
274 for i in range(0, len(self.results)):
278 def __getitem__(self, i):
279 item = self.results[i]
280 check_for_errors(item)
281 return item['result']
284 return len(self.results)
286 class MultiCall(object):
288 def __init__(self, server):
289 self._server = server
293 if len(self._job_list) < 1:
294 # Should we alert? This /is/ pretty obvious.
296 request_body = '[ %s ]' % ','.join([job.request() for
297 job in self._job_list])
298 responses = self._server._run_request(request_body)
299 del self._job_list[:]
300 return MultiCallIterator(responses)
304 return MultiCallNotify(self)
306 def __getattr__(self, name):
307 new_job = MultiCallMethod(name)
308 self._job_list.append(new_job)
313 # These lines conform to xmlrpclib's "compatibility" line.
314 # Not really sure if we should include these, but oh well.
318 # JSON-RPC error class
319 def __init__(self, code=-32000, message='Server error'):
320 self.faultCode = code
321 self.faultString = message
324 return {'code':self.faultCode, 'message':self.faultString}
326 def response(self, rpcid=None, version=None):
328 version = config.version
329 return dumps(self, methodresponse=True, rpcid=rpcid, version=version)
332 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
334 def random_id(length=8):
338 choices = string.lowercase+string.digits
340 for i in range(length):
341 return_id += random.choice(choices)
345 def __init__(self, rpcid=None, version=None):
347 version = config.version
349 self.version = float(version)
351 def request(self, method, params=[]):
352 if type(method) not in types.StringTypes:
353 raise ValueError('Method name must be a string.')
355 self.id = random_id()
356 request = {'id':self.id, 'method':method, 'params':params}
357 if self.version >= 2:
358 request['jsonrpc'] = str(self.version)
361 def notify(self, method, params=[]):
362 request = self.request(method, params)
363 if self.version >= 2:
369 def response(self, result=None):
370 response = {'result':result, 'id':self.id}
371 if self.version >= 2:
372 response['jsonrpc'] = str(self.version)
374 response['error'] = None
377 def error(self, code=-32000, message='Server error.'):
378 error = self.response()
379 if self.version >= 2:
382 error['result'] = None
383 error['error'] = {'code':code, 'message':message}
386 def dumps(params=[], methodname=None, methodresponse=None,
387 encoding=None, rpcid=None, version=None, notify=None):
389 This differs from the Python implementation in that it implements
390 the rpcid argument since the 2.0 spec requires it for responses.
393 version = config.version
394 valid_params = (types.TupleType, types.ListType, types.DictType)
395 if methodname in types.StringTypes and \
396 type(params) not in valid_params and \
397 not isinstance(params, Fault):
399 If a method, and params are not in a listish or a Fault,
402 raise TypeError('Params must be a dict, list, tuple or Fault ' +
404 # Begin parsing object
405 payload = Payload(rpcid=rpcid, version=version)
408 if type(params) is Fault:
409 response = payload.error(params.faultCode, params.faultString)
410 return jdumps(response, encoding=encoding)
411 if type(methodname) not in types.StringTypes and methodresponse != True:
412 raise ValueError('Method name must be a string, or methodresponse '+
413 'must be set to True.')
414 if config.use_jsonclass == True:
416 params = jsonclass.dump(params)
417 if methodresponse is True:
419 raise ValueError('A method response must have an rpcid.')
420 response = payload.response(params)
421 return jdumps(response, encoding=encoding)
424 request = payload.notify(methodname, params)
426 request = payload.request(methodname, params)
427 return jdumps(request, encoding=encoding)
431 This differs from the Python implementation, in that it returns
432 the request structure in Dict format instead of the method, params.
433 It will return a list in the case of a batch request / response.
438 result = jloads(data)
439 # if the above raises an error, the implementing server code
440 # should return something like the following:
441 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
442 if config.use_jsonclass == True:
444 result = jsonclass.load(result)
447 def check_for_errors(result):
451 if type(result) is not types.DictType:
452 raise TypeError('Response is not a dict.')
453 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
454 raise NotImplementedError('JSON-RPC version not yet supported.')
455 if 'result' not in result.keys() and 'error' not in result.keys():
456 raise ValueError('Response does not have a result or error key.')
457 if 'error' in result.keys() and result['error'] != None:
458 code = result['error']['code']
459 message = result['error']['message']
460 raise ProtocolError((code, message))
464 if type(result) not in (types.ListType, types.TupleType):
468 if type(result[0]) is not types.DictType:
470 if 'jsonrpc' not in result[0].keys():
473 version = float(result[0]['jsonrpc'])
475 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
480 def isnotification(request):
481 if 'id' not in request.keys():
484 if request['id'] == None: