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
58 # JSON library importing
70 if not cjson and not json:
72 import simplejson as json
74 raise ImportError('You must have the cjson, json, or simplejson ' +
75 'module(s) available.')
81 _user_agent = 'jsonrpclib/0.1 (Python %s)' % \
82 '.'.join([str(ver) for ver in sys.version_info[0:3]])
86 def jdumps(obj, encoding='utf-8'):
87 # Do 'serialize' test at some point for other classes
90 return cjson.encode(obj)
92 return json.dumps(obj, encoding=encoding)
94 def jloads(json_string):
97 return cjson.decode(json_string)
99 return json.loads(json_string)
102 # XMLRPClib re-implemntations
104 class ProtocolError(Exception):
107 class Transport(XMLTransport):
108 """ Just extends the XMLRPC transport where necessary. """
109 user_agent = _user_agent
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)
118 def _parse_response(self, file_h, sock):
122 response = sock.recv(1024)
124 response = file_h.read(1024)
127 response_body += response
129 print 'body: %s' % response
130 if response_body == '':
133 return_obj = loads(response_body)
136 class SafeTransport(XMLSafeTransport):
137 """ Just extends for HTTPS calls """
138 user_agent = Transport.user_agent
139 send_content = Transport.send_content
140 _parse_response = Transport._parse_response
142 class ServerProxy(XMLServerProxy):
144 Unfortunately, much more of this class has to be copied since
145 so much of it does the serialization.
148 def __init__(self, uri, transport=None, encoding=None,
149 verbose=0, version=None):
154 self.__version = version
155 schema, uri = urllib.splittype(uri)
156 if schema not in ('http', 'https'):
157 raise IOError('Unsupported JSON-RPC protocol.')
158 self.__host, self.__handler = urllib.splithost(uri)
159 if not self.__handler:
160 # Not sure if this is in the JSON spec?
161 self.__handler = '/RPC2'
162 if transport is None:
163 if schema == 'https':
164 transport = SafeTransport()
166 transport = Transport()
167 self.__transport = transport
168 self.__encoding = encoding
169 self.__verbose = verbose
171 def _request(self, methodname, params, rpcid=None):
172 request = dumps(params, methodname, encoding=self.__encoding,
173 rpcid=rpcid, version=self.__version)
174 response = self._run_request(request)
175 check_for_errors(response)
176 return response['result']
178 def _request_notify(self, methodname, params, rpcid=None):
179 request = dumps(params, methodname, encoding=self.__encoding,
180 rpcid=rpcid, version=self.__version, notify=True)
181 response = self._run_request(request, notify=True)
182 check_for_errors(response)
185 def _run_request(self, request, notify=None):
187 global _last_response
188 _last_request = request
190 response = self.__transport.request(
194 verbose=self.__verbose
197 # Here, the XMLRPC library translates a single list
198 # response to the single value -- should we do the
199 # same, and require a tuple / list to be passed to
200 # the response object, or expect the Server to be
201 # outputting the response appropriately?
203 _last_response = response
206 def __getattr__(self, name):
207 # Same as original, just with new _Method reference
208 return _Method(self._request, name)
212 # Just like __getattr__, but with notify namespace.
213 return _Notify(self._request_notify)
216 class _Method(XML_Method):
218 def __call__(self, *args, **kwargs):
219 if len(args) > 0 and len(kwargs) > 0:
220 raise ProtocolError('Cannot use both positional ' +
221 'and keyword arguments (according to JSON-RPC spec.)')
223 return self.__send(self.__name, args)
225 return self.__send(self.__name, kwargs)
227 def __getattr__(self, name):
228 # Even though this is verbatim, it doesn't support
229 # keyword arguments unless we rewrite it.
230 return _Method(self.__send, "%s.%s" % (self.__name, name))
232 class _Notify(object):
233 def __init__(self, request):
234 self._request = request
236 def __getattr__(self, name):
237 return _Method(self._request, name)
239 # Batch implementation
241 class MultiCallMethod(object):
243 def __init__(self, method, notify=False):
248 def __call__(self, *args, **kwargs):
249 if len(kwargs) > 0 and len(args) > 0:
250 raise ProtocolError('JSON-RPC does not support both ' +
251 'positional and keyword arguments.')
257 def request(self, encoding=None, rpcid=None):
258 return dumps(self.params, self.method, version=2.0,
259 encoding=encoding, rpcid=rpcid, notify=self.notify)
262 return '%s' % self.request()
264 class MultiCallNotify(object):
266 def __init__(self, multicall):
267 self.multicall = multicall
269 def __getattr__(self, name):
270 new_job = MultiCallMethod(name, notify=True)
271 self.multicall._job_list.append(new_job)
274 class MultiCallIterator(object):
276 def __init__(self, results):
277 self.results = results
280 for i in range(0, len(self.results)):
284 def __getitem__(self, i):
285 item = self.results[i]
286 check_for_errors(item)
287 return item['result']
290 return len(self.results)
292 class MultiCall(object):
294 def __init__(self, server):
295 self._server = server
299 if len(self._job_list) < 1:
300 # Should we alert? This /is/ pretty obvious.
302 request_body = '[ %s ]' % ','.join([job.request() for
303 job in self._job_list])
304 responses = self._server._run_request(request_body)
305 del self._job_list[:]
306 return MultiCallIterator(responses)
310 return MultiCallNotify(self)
312 def __getattr__(self, name):
313 new_job = MultiCallMethod(name)
314 self._job_list.append(new_job)
319 # These lines conform to xmlrpclib's "compatibility" line.
320 # Not really sure if we should include these, but oh well.
324 # JSON-RPC error class
325 def __init__(self, code=-32000, message='Server error'):
326 self.faultCode = code
327 self.faultString = message
330 return {'code':self.faultCode, 'message':self.faultString}
332 def response(self, rpcid=None, version=None):
336 return dumps(self, rpcid=rpcid, version=version)
339 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
341 def random_id(length=8):
345 choices = string.lowercase+string.digits
347 for i in range(length):
348 return_id += random.choice(choices)
352 def __init__(self, rpcid=None, version=None):
357 self.version = float(version)
359 def request(self, method, params=[]):
360 if type(method) not in types.StringTypes:
361 raise ValueError('Method name must be a string.')
363 self.id = random_id()
364 request = {'id':self.id, 'method':method, 'params':params}
365 if self.version >= 2:
366 request['jsonrpc'] = str(self.version)
369 def notify(self, method, params=[]):
370 request = self.request(method, params)
371 if self.version >= 2:
377 def response(self, result=None):
378 response = {'result':result, 'id':self.id}
379 if self.version >= 2:
380 response['jsonrpc'] = str(self.version)
382 response['error'] = None
385 def error(self, code=-32000, message='Server error.'):
386 error = self.response()
387 if self.version >= 2:
390 error['result'] = None
391 error['error'] = {'code':code, 'message':message}
394 def dumps(params=[], methodname=None, methodresponse=None,
395 encoding=None, rpcid=None, version=None, notify=None):
397 This differs from the Python implementation in that it implements
398 the rpcid argument since the 2.0 spec requires it for responses.
403 valid_params = (types.TupleType, types.ListType, types.DictType)
404 if methodname in types.StringTypes and \
405 type(params) not in valid_params and \
406 not isinstance(params, Fault):
408 If a method, and params are not in a listish or a Fault,
411 raise TypeError('Params must be a dict, list, tuple or Fault ' +
413 # Begin parsing object
414 payload = Payload(rpcid=rpcid, version=version)
417 if type(params) is Fault:
418 response = payload.error(params.faultCode, params.faultString)
419 return jdumps(response, encoding=encoding)
420 if type(methodname) not in types.StringTypes and methodresponse != True:
421 raise ValueError('Method name must be a string, or methodresponse '+
422 'must be set to True.')
423 if methodresponse is True:
425 raise ValueError('A method response must have an rpcid.')
426 response = payload.response(params)
427 return jdumps(response, encoding=encoding)
430 request = payload.notify(methodname, params)
432 request = payload.request(methodname, params)
433 return jdumps(request, encoding=encoding)
437 This differs from the Python implementation, in that it returns
438 the request structure in Dict format instead of the method, params.
439 It will return a list in the case of a batch request / response.
444 result = jloads(data)
445 # if the above raises an error, the implementing server code
446 # should return something like the following:
447 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
450 def check_for_errors(result):
454 if type(result) is not types.DictType:
455 raise TypeError('Response is not a dict.')
456 if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
457 raise NotImplementedError('JSON-RPC version not yet supported.')
458 if 'result' not in result.keys() and 'error' not in result.keys():
459 raise ValueError('Response does not have a result or error key.')
460 if 'error' in result.keys() and result['error'] != None:
461 code = result['error']['code']
462 message = result['error']['message']
463 raise ProtocolError('ERROR %s: %s' % (code, message))
467 if type(result) not in (types.ListType, types.TupleType):
471 if type(result[0]) is not types.DictType:
473 if 'jsonrpc' not in result[0].keys():
476 version = float(result[0]['jsonrpc'])
478 raise ProtocolError('"jsonrpc" key must be a float(able) value.')
483 def isnotification(request):
484 if 'id' not in request.keys():
487 if request['id'] == None: