ProtocolErrors raise a (code, message) tuple instead of a string as per request in...
[invirt/packages/python-jsonrpclib.git] / jsonrpclib / jsonrpclib.py
1 """
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 
6
7    http://www.apache.org/licenses/LICENSE-2.0 
8
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. 
14
15 ============================
16 JSONRPC Library (jsonrpclib)
17 ============================
18
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:
23
24 * Keyword arguments
25 * Notifications
26 * Versioning
27 * Batches and batch notifications
28
29 Eventually, I'll add a SimpleXMLRPCServer compatible library,
30 and other things to tie the thing off nicely. :)
31
32 For a quick-start, just open a console and type the following,
33 replacing the server address, method, and parameters 
34 appropriately.
35 >>> import jsonrpclib
36 >>> server = jsonrpclib.Server('http://localhost:8181')
37 >>> server.add(5, 6)
38 11
39 >>> server._notify.add(5, 6)
40 >>> batch = jsonrpclib.MultiCall(server)
41 >>> batch.add(3, 50)
42 >>> batch.add(2, 3)
43 >>> batch._notify.add(3, 5)
44 >>> batch()
45 [53, 5]
46
47 See http://code.google.com/p/jsonrpclib/ for more info.
48 """
49
50 import types
51 import sys
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
56 import time
57
58 # Library includes
59 from config import config
60 from history import history
61
62 # JSON library importing
63 cjson = None
64 json = None
65 try:
66     import cjson
67 except ImportError:
68     pass
69 if not cjson:
70     try:
71         import json
72     except ImportError:
73         pass
74 if not cjson and not json: 
75     try:
76         import simplejson as json
77     except ImportError:
78         raise ImportError('You must have the cjson, json, or simplejson ' +
79                           'module(s) available.')
80
81 #JSON Abstractions
82
83 def jdumps(obj, encoding='utf-8'):
84     # Do 'serialize' test at some point for other classes
85     global cjson
86     if cjson:
87         return cjson.encode(obj)
88     else:
89         return json.dumps(obj, encoding=encoding)
90
91 def jloads(json_string):
92     global cjson
93     if cjson:
94         return cjson.decode(json_string)
95     else:
96         return json.loads(json_string)
97
98
99 # XMLRPClib re-implemntations
100
101 class ProtocolError(Exception):
102     pass
103
104 class Transport(XMLTransport):
105     """ Just extends the XMLRPC transport where necessary. """
106     user_agent = config.user_agent
107
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()
112         if request_body:
113             connection.send(request_body)
114
115     def _parse_response(self, file_h, sock):
116         response_body = ''
117         while 1:
118             if sock:
119                 response = sock.recv(1024)
120             else:
121                 response = file_h.read(1024)
122             if not response:
123                 break
124             response_body += response
125             if self.verbose:
126                 print 'body: %s' % response
127         if response_body == '':
128             # Notification
129             return None
130         return_obj = loads(response_body)
131         return return_obj
132
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
138
139 class ServerProxy(XMLServerProxy):
140     """
141     Unfortunately, much more of this class has to be copied since
142     so much of it does the serialization.
143     """
144
145     def __init__(self, uri, transport=None, encoding=None, 
146                  verbose=0, version=None):
147         import urllib
148         if not version:
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()
161             else:
162                 transport = Transport()
163         self.__transport = transport
164         self.__encoding = encoding
165         self.__verbose = verbose
166
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']
173
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)
179         return
180
181     def _run_request(self, request, notify=None):
182         history.add_request(request)
183
184         response = self.__transport.request(
185             self.__host,
186             self.__handler,
187             request,
188             verbose=self.__verbose
189         )
190         
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?
196         
197         history.add_response(response)
198         return response
199
200     def __getattr__(self, name):
201         # Same as original, just with new _Method reference
202         return _Method(self._request, name)
203
204     @property
205     def _notify(self):
206         # Just like __getattr__, but with notify namespace.
207         return _Notify(self._request_notify)
208
209
210 class _Method(XML_Method):
211     
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.)')
216         if len(args) > 0:
217             return self.__send(self.__name, args)
218         else:
219             return self.__send(self.__name, kwargs)
220
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))
225
226 class _Notify(object):
227     def __init__(self, request):
228         self._request = request
229
230     def __getattr__(self, name):
231         return _Method(self._request, name)
232         
233 # Batch implementation
234
235 class MultiCallMethod(object):
236     
237     def __init__(self, method, notify=False):
238         self.method = method
239         self.params = []
240         self.notify = notify
241
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.')
246         if len(kwargs) > 0:
247             self.params = kwargs
248         else:
249             self.params = args
250
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)
254
255     def __repr__(self):
256         return '%s' % self.request()
257
258 class MultiCallNotify(object):
259     
260     def __init__(self, multicall):
261         self.multicall = multicall
262
263     def __getattr__(self, name):
264         new_job = MultiCallMethod(name, notify=True)
265         self.multicall._job_list.append(new_job)
266         return new_job
267
268 class MultiCallIterator(object):
269     
270     def __init__(self, results):
271         self.results = results
272
273     def __iter__(self):
274         for i in range(0, len(self.results)):
275             yield self[i]
276         raise StopIteration
277
278     def __getitem__(self, i):
279         item = self.results[i]
280         check_for_errors(item)
281         return item['result']
282
283     def __len__(self):
284         return len(self.results)
285
286 class MultiCall(object):
287     
288     def __init__(self, server):
289         self._server = server
290         self._job_list = []
291
292     def _request(self):
293         if len(self._job_list) < 1:
294             # Should we alert? This /is/ pretty obvious.
295             return
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)
301
302     @property
303     def _notify(self):
304         return MultiCallNotify(self)
305
306     def __getattr__(self, name):
307         new_job = MultiCallMethod(name)
308         self._job_list.append(new_job)
309         return new_job
310
311     __call__ = _request
312
313 # These lines conform to xmlrpclib's "compatibility" line. 
314 # Not really sure if we should include these, but oh well.
315 Server = ServerProxy
316
317 class Fault(object):
318     # JSON-RPC error class
319     def __init__(self, code=-32000, message='Server error'):
320         self.faultCode = code
321         self.faultString = message
322
323     def error(self):
324         return {'code':self.faultCode, 'message':self.faultString}
325
326     def response(self, rpcid=None, version=None):
327         if not version:
328             version = config.version
329         return dumps(self, methodresponse=True, rpcid=rpcid, version=version)
330
331     def __repr__(self):
332         return '<Fault %s: %s>' % (self.faultCode, self.faultString)
333
334 def random_id(length=8):
335     import string
336     import random
337     random.seed()
338     choices = string.lowercase+string.digits
339     return_id = ''
340     for i in range(length):
341         return_id += random.choice(choices)
342     return return_id
343
344 class Payload(dict):
345     def __init__(self, rpcid=None, version=None):
346         if not version:
347             version = config.version
348         self.id = rpcid
349         self.version = float(version)
350     
351     def request(self, method, params=[]):
352         if type(method) not in types.StringTypes:
353             raise ValueError('Method name must be a string.')
354         if not self.id:
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)
359         return request
360
361     def notify(self, method, params=[]):
362         request = self.request(method, params)
363         if self.version >= 2:
364             del request['id']
365         else:
366             request['id'] = None
367         return request
368
369     def response(self, result=None):
370         response = {'result':result, 'id':self.id}
371         if self.version >= 2:
372             response['jsonrpc'] = str(self.version)
373         else:
374             response['error'] = None
375         return response
376
377     def error(self, code=-32000, message='Server error.'):
378         error = self.response()
379         if self.version >= 2:
380             del error['result']
381         else:
382             error['result'] = None
383         error['error'] = {'code':code, 'message':message}
384         return error
385
386 def dumps(params=[], methodname=None, methodresponse=None, 
387         encoding=None, rpcid=None, version=None, notify=None):
388     """
389     This differs from the Python implementation in that it implements 
390     the rpcid argument since the 2.0 spec requires it for responses.
391     """
392     if not version:
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):
398         """ 
399         If a method, and params are not in a listish or a Fault,
400         error out.
401         """
402         raise TypeError('Params must be a dict, list, tuple or Fault ' +
403                         'instance.')
404     # Begin parsing object
405     payload = Payload(rpcid=rpcid, version=version)
406     if not encoding:
407         encoding = 'utf-8'
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:
415         import jsonclass
416         params = jsonclass.dump(params)
417     if methodresponse is True:
418         if rpcid is None:
419             raise ValueError('A method response must have an rpcid.')
420         response = payload.response(params)
421         return jdumps(response, encoding=encoding)
422     request = None
423     if notify == True:
424         request = payload.notify(methodname, params)
425     else:
426         request = payload.request(methodname, params)
427     return jdumps(request, encoding=encoding)
428
429 def loads(data):
430     """
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.
434     """
435     if data == '':
436         # notification
437         return None
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:
443         import jsonclass
444         result = jsonclass.load(result)
445     return result
446
447 def check_for_errors(result):
448     if not result:
449         # Notification
450         return 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))
461     return result
462
463 def isbatch(result):
464     if type(result) not in (types.ListType, types.TupleType):
465         return False
466     if len(result) < 1:
467         return False
468     if type(result[0]) is not types.DictType:
469         return False
470     if 'jsonrpc' not in result[0].keys():
471         return False
472     try:
473         version = float(result[0]['jsonrpc'])
474     except ValueError:
475         raise ProtocolError('"jsonrpc" key must be a float(able) value.')
476     if version < 2:
477         return False
478     return True
479
480 def isnotification(request):
481     if 'id' not in request.keys():
482         # 2.0 notification
483         return True
484     if request['id'] == None:
485         # 1.0 notification
486         return True
487     return False