2 JSONRPCLIB -- started by Josh Marshall
4 This library is a JSON-RPC v.2 (proposed) implementation which
5 follows the xmlrpclib API for portability between clients. It
6 uses the same Server / ServerProxy, loads, dumps, etc. syntax,
7 while providing features not present in XML-RPC like:
12 * Batches and batch notifications
14 Eventually, I'll add a SimpleXMLRPCServer compatible library,
15 and other things to tie the thing off nicely. :)
17 For a quick-start, just open a console and type the following,
18 replacing the server address, method, and parameters
21 >>> server = jsonrpclib.Server('http://localhost:8181')
24 >>> jsonrpclib.__notify('add', (5, 6))
26 See http://code.google.com/p/jsonrpclib/ for more info.
31 from xmlrpclib import Transport as XMLTransport
32 from xmlrpclib import SafeTransport as XMLSafeTransport
33 from xmlrpclib import ServerProxy as XMLServerProxy
34 from xmlrpclib import _Method as XML_Method
37 # JSON library importing
49 if not cjson and not json:
51 import simplejson as json
53 raise ImportError('You must have the cjson, json, or simplejson ' +
54 'module(s) available.')
60 _user_agent = 'jsonrpclib/0.1 (Python %s)' % \
61 '.'.join([str(ver) for ver in sys.version_info[0:3]])
65 def jdumps(obj, encoding='utf-8'):
66 # Do 'serialize' test at some point for other classes
69 return cjson.encode(obj)
71 return json.dumps(obj, encoding=encoding)
73 def jloads(json_string):
76 return cjson.decode(json_string)
78 return json.loads(json_string)
81 # XMLRPClib re-implemntations
83 class ProtocolError(Exception):
86 class Transport(XMLTransport):
87 """ Just extends the XMLRPC transport where necessary. """
88 user_agent = _user_agent
90 def send_content(self, connection, request_body):
91 connection.putheader("Content-Type", "text/json")
92 connection.putheader("Content-Length", str(len(request_body)))
93 connection.endheaders()
95 connection.send(request_body)
97 def _parse_response(self, file_h, sock):
101 response = sock.recv(1024)
103 response = file_h.read(1024)
107 print 'body: %s' % response
108 response_body += response
109 if response_body == '':
112 return_obj = loads(response_body)
115 class SafeTransport(XMLSafeTransport):
116 """ Just extends for HTTPS calls """
117 user_agent = Transport.user_agent
118 send_content = Transport.send_content
119 _parse_response = Transport._parse_response
121 class ServerProxy(XMLServerProxy):
123 Unfortunately, much more of this class has to be copied since
124 so much of it does the serialization.
127 def __init__(self, uri, transport=None, encoding=None,
128 verbose=0, version=None):
133 self.__version = version
134 schema, uri = urllib.splittype(uri)
135 if schema not in ('http', 'https'):
136 raise IOError('Unsupported JSON-RPC protocol.')
137 self.__host, self.__handler = urllib.splithost(uri)
138 if not self.__handler:
139 # Not sure if this is in the JSON spec?
140 self.__handler = '/RPC2'
141 if transport is None:
142 if schema == 'https':
143 transport = SafeTransport()
145 transport = Transport()
146 self.__transport = transport
147 self.__encoding = encoding
148 self.__verbose = verbose
150 def __request(self, methodname, params, rpcid=None):
151 request = dumps(params, methodname, encoding=self.__encoding,
152 rpcid=rpcid, version=self.__version)
153 response = self.__run_request(request)
154 return response['result']
156 def __notify(self, methodname, params, rpcid=None):
157 request = dumps(params, methodname, encoding=self.__encoding,
158 rpcid=rpcid, version=self.__version, notify=True)
160 response = self.__run_request(request, notify=True)
163 def __run_request(self, request, notify=None):
165 global _last_response
166 _last_request = request
170 response = self.__transport.request(
174 verbose=self.__verbose
177 # Here, the XMLRPC library translates a single list
178 # response to the single value -- should we do the
179 # same, and require a tuple / list to be passed to
180 # the response object, or expect the Server to be
181 # outputting the response appropriately?
183 _last_response = response
185 # notification, no result
187 return check_for_errors(response)
189 def __getattr__(self, name):
190 # Same as original, just with new _Method and wrapper
192 if name in ('__notify', '__run_request'):
193 wrapped_name = '_%s%s' % (self.__class__.__name__, name)
194 return getattr(self, wrapped_name)
195 return _Method(self.__request, name)
197 class _Method(XML_Method):
198 def __call__(self, *args, **kwargs):
199 if len(args) > 0 and len(kwargs) > 0:
200 raise ProtocolError('Cannot use both positional ' +
201 'and keyword arguments (according to JSON-RPC spec.)')
203 return self.__send(self.__name, args)
205 return self.__send(self.__name, kwargs)
207 # Batch implementation
209 class MultiCallMethod(object):
211 def __init__(self, method, notify=False):
216 def __call__(self, *args, **kwargs):
217 if len(kwargs) > 0 and len(args) > 0:
218 raise ProtocolError('A Job cannot have both positional ' +
219 'and keyword arguments.')
225 def request(self, encoding=None, rpcid=None):
226 return dumps(self.params, self.method, version=2.0,
227 encoding=encoding, rpcid=rpcid, notify=self.notify)
230 return '%s' % self.request()
232 class MultiCall(object):
234 def __init__(self, server):
235 self.__server = server
238 def __run_request(self, request_body):
239 run_request = getattr(self.__server, '_ServerProxy__run_request')
240 return run_request(request_body)
243 if len(self.__job_list) < 1:
244 # Should we alert? This /is/ pretty obvious.
246 request_body = '[ %s ]' % ','.join([job.request() for
247 job in self.__job_list])
248 responses = self.__run_request(request_body)
249 del self.__job_list[:]
250 return [ response['result'] for response in responses ]
252 def __notify(self, method, params=[]):
253 new_job = MultiCallMethod(method, notify=True)
254 new_job.params = params
255 self.__job_list.append(new_job)
257 def __getattr__(self, name):
258 if name in ('__run', '__notify'):
259 wrapped_name = '_%s%s' % (self.__class__.__name__, name)
260 return getattr(self, wrapped_name)
261 new_job = MultiCallMethod(name)
262 self.__job_list.append(new_job)
267 # These lines conform to xmlrpclib's "compatibility" line.
268 # Not really sure if we should include these, but oh well.
272 # JSON-RPC error class
273 def __init__(self, code=-32000, message='Server error'):
274 self.faultCode = code
275 self.faultString = message
278 return {'code':self.faultCode, 'message':self.faultString}
280 def response(self, rpcid=None, version=None):
284 return dumps(self, rpcid=rpcid, version=version)
287 return '<Fault %s: %s>' % (self.faultCode, self.faultString)
289 def random_id(length=8):
293 choices = string.lowercase+string.digits
295 for i in range(length):
296 return_id += random.choice(choices)
300 def __init__(self, rpcid=None, version=None):
305 self.version = float(version)
307 def request(self, method, params=[]):
308 if type(method) not in types.StringTypes:
309 raise ValueError('Method name must be a string.')
311 self.id = random_id()
312 request = {'id':self.id, 'method':method, 'params':params}
313 if self.version >= 2:
314 request['jsonrpc'] = str(self.version)
317 def notify(self, method, params=[]):
318 request = self.request(method, params)
319 if self.version >= 2:
325 def response(self, result=None):
326 response = {'result':result, 'id':self.id}
327 if self.version >= 2:
328 response['jsonrpc'] = str(self.version)
330 response['error'] = None
333 def error(self, code=-32000, message='Server error.'):
334 error = self.response()
335 if self.version >= 2:
338 error['result'] = None
339 error['error'] = {'code':code, 'message':message}
342 def dumps(params=[], methodname=None, methodresponse=None,
343 encoding=None, rpcid=None, version=None, notify=None):
345 This differs from the Python implementation in that it implements
346 the rpcid argument since the 2.0 spec requires it for responses.
351 valid_params = (types.TupleType, types.ListType, types.DictType)
352 if methodname in types.StringTypes and \
353 type(params) not in valid_params and \
354 not isinstance(params, Fault):
356 If a method, and params are not in a listish or a Fault,
359 raise TypeError('Params must be a dict, list, tuple or Fault ' +
361 # Begin parsing object
362 payload = Payload(rpcid=rpcid, version=version)
365 if type(params) is Fault:
366 response = payload.error(params.faultCode, params.faultString)
367 return jdumps(response, encoding=encoding)
368 if type(methodname) not in types.StringTypes and methodresponse != True:
369 raise ValueError('Method name must be a string, or methodresponse '+
370 'must be set to True.')
371 if methodresponse is True:
373 raise ValueError('A method response must have an rpcid.')
374 response = payload.response(params)
375 return jdumps(response, encoding=encoding)
378 request = payload.notify(methodname, params)
380 request = payload.request(methodname, params)
381 return jdumps(request, encoding=encoding)
385 This differs from the Python implementation, in that it returns
386 the request structure in Dict format instead of the method, params.
387 It will return a list in the case of a batch request / response.
392 result = jloads(data)
393 # if the above raises an error, the implementing server code
394 # should return something like the following:
395 # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
398 def check_for_errors(result):
400 if not isbatch(result):
401 result_list.append(result)
404 for entry in result_list:
405 if 'jsonrpc' in entry.keys() and float(entry['jsonrpc']) > 2.0:
406 raise NotImplementedError('JSON-RPC version not yet supported.')
407 if 'error' in entry.keys() and entry['error'] != None:
408 code = entry['error']['code']
409 message = entry['error']['message']
410 raise ProtocolError('ERROR %s: %s' % (code, message))
415 if type(result) not in (types.ListType, types.TupleType):
419 if type(result[0]) is not types.DictType:
421 if 'jsonrpc' not in result[0].keys():
424 version = float(result[0]['jsonrpc'])
426 raise ProtocolError('"jsonrpc" key must be a float(able) value.')