Making corrections for 2.7 compatibility (which mostly means bypassing the xml parser...
[invirt/packages/python-jsonrpclib.git] / jsonrpclib / jsonrpc.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 import jsonrpclib
60 from jsonrpclib import config
61 from jsonrpclib import history
62
63 # JSON library importing
64 cjson = None
65 json = None
66 try:
67     import cjson
68 except ImportError:
69     pass
70 if not cjson:
71     try:
72         import json
73     except ImportError:
74         pass
75 if not cjson and not json: 
76     try:
77         import simplejson as json
78     except ImportError:
79         raise ImportError('You must have the cjson, json, or simplejson ' +
80                           'module(s) available.')
81
82 #JSON Abstractions
83
84 def jdumps(obj, encoding='utf-8'):
85     # Do 'serialize' test at some point for other classes
86     global cjson
87     if cjson:
88         return cjson.encode(obj)
89     else:
90         return json.dumps(obj, encoding=encoding)
91
92 def jloads(json_string):
93     global cjson
94     if cjson:
95         return cjson.decode(json_string)
96     else:
97         return json.loads(json_string)
98
99
100 # XMLRPClib re-implemntations
101
102 class ProtocolError(Exception):
103     pass
104
105 class Transport(XMLTransport):
106     """ Just extends the XMLRPC transport where necessary. """
107     user_agent = config.user_agent
108
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()
113         if request_body:
114             connection.send(request_body)
115
116     def getparser(self):
117         target = JSONTarget()
118         return JSONParser(target), target
119
120 class JSONParser(object):
121     def __init__(self, target):
122         self.target = target
123
124     def feed(self, data):
125         self.target.feed(data)
126
127     def close(self):
128         pass
129
130 class JSONTarget(object):
131     def __init__(self):
132         self.data = []
133
134     def feed(self, data):
135         self.data.append(data)
136
137     def close(self):
138         return ''.join(self.data)
139
140 class SafeTransport(XMLSafeTransport):
141     """ Just extends for HTTPS calls """
142     user_agent = Transport.user_agent
143     send_content = Transport.send_content
144     getparser = Transport.getparser
145
146 class ServerProxy(XMLServerProxy):
147     """
148     Unfortunately, much more of this class has to be copied since
149     so much of it does the serialization.
150     """
151
152     def __init__(self, uri, transport=None, encoding=None, 
153                  verbose=0, version=None):
154         import urllib
155         if not version:
156             version = config.version
157         self.__version = version
158         schema, uri = urllib.splittype(uri)
159         if schema not in ('http', 'https'):
160             raise IOError('Unsupported JSON-RPC protocol.')
161         self.__host, self.__handler = urllib.splithost(uri)
162         if not self.__handler:
163             # Not sure if this is in the JSON spec?
164             #self.__handler = '/'
165             self.__handler == '/'
166         if transport is None:
167             if schema == 'https':
168                 transport = SafeTransport()
169             else:
170                 transport = Transport()
171         self.__transport = transport
172         self.__encoding = encoding
173         self.__verbose = verbose
174
175     def _request(self, methodname, params, rpcid=None):
176         request = dumps(params, methodname, encoding=self.__encoding,
177                         rpcid=rpcid, version=self.__version)
178         response = self._run_request(request)
179         check_for_errors(response)
180         return response['result']
181
182     def _request_notify(self, methodname, params, rpcid=None):
183         request = dumps(params, methodname, encoding=self.__encoding,
184                         rpcid=rpcid, version=self.__version, notify=True)
185         response = self._run_request(request, notify=True)
186         check_for_errors(response)
187         return
188
189     def _run_request(self, request, notify=None):
190         history.add_request(request)
191
192         response = self.__transport.request(
193             self.__host,
194             self.__handler,
195             request,
196             verbose=self.__verbose
197         )
198         
199         # Here, the XMLRPC library translates a single list
200         # response to the single value -- should we do the
201         # same, and require a tuple / list to be passed to
202         # the response object, or expect the Server to be 
203         # outputting the response appropriately?
204         
205         history.add_response(response)
206         if not response:
207             return None
208         return_obj = loads(response)
209         return return_obj
210
211     def __getattr__(self, name):
212         # Same as original, just with new _Method reference
213         return _Method(self._request, name)
214
215     @property
216     def _notify(self):
217         # Just like __getattr__, but with notify namespace.
218         return _Notify(self._request_notify)
219
220
221 class _Method(XML_Method):
222     
223     def __call__(self, *args, **kwargs):
224         if len(args) > 0 and len(kwargs) > 0:
225             raise ProtocolError('Cannot use both positional ' +
226                 'and keyword arguments (according to JSON-RPC spec.)')
227         if len(args) > 0:
228             return self.__send(self.__name, args)
229         else:
230             return self.__send(self.__name, kwargs)
231
232     def __getattr__(self, name):
233         self.__name = '%s.%s' % (self.__name, name)
234         return self
235         # The old method returned a new instance, but this seemed wasteful.
236         # The only thing that changes is the name.
237         #return _Method(self.__send, "%s.%s" % (self.__name, name))
238
239 class _Notify(object):
240     def __init__(self, request):
241         self._request = request
242
243     def __getattr__(self, name):
244         return _Method(self._request, name)
245         
246 # Batch implementation
247
248 class MultiCallMethod(object):
249     
250     def __init__(self, method, notify=False):
251         self.method = method
252         self.params = []
253         self.notify = notify
254
255     def __call__(self, *args, **kwargs):
256         if len(kwargs) > 0 and len(args) > 0:
257             raise ProtocolError('JSON-RPC does not support both ' +
258                                 'positional and keyword arguments.')
259         if len(kwargs) > 0:
260             self.params = kwargs
261         else:
262             self.params = args
263
264     def request(self, encoding=None, rpcid=None):
265         return dumps(self.params, self.method, version=2.0,
266                      encoding=encoding, rpcid=rpcid, notify=self.notify)
267
268     def __repr__(self):
269         return '%s' % self.request()
270         
271     def __getattr__(self, method):
272         new_method = '%s.%s' % (self.method, method)
273         self.method = new_method
274         return self
275
276 class MultiCallNotify(object):
277     
278     def __init__(self, multicall):
279         self.multicall = multicall
280
281     def __getattr__(self, name):
282         new_job = MultiCallMethod(name, notify=True)
283         self.multicall._job_list.append(new_job)
284         return new_job
285
286 class MultiCallIterator(object):
287     
288     def __init__(self, results):
289         self.results = results
290
291     def __iter__(self):
292         for i in range(0, len(self.results)):
293             yield self[i]
294         raise StopIteration
295
296     def __getitem__(self, i):
297         item = self.results[i]
298         check_for_errors(item)
299         return item['result']
300
301     def __len__(self):
302         return len(self.results)
303
304 class MultiCall(object):
305     
306     def __init__(self, server):
307         self._server = server
308         self._job_list = []
309
310     def _request(self):
311         if len(self._job_list) < 1:
312             # Should we alert? This /is/ pretty obvious.
313             return
314         request_body = '[ %s ]' % ','.join([job.request() for
315                                           job in self._job_list])
316         responses = self._server._run_request(request_body)
317         del self._job_list[:]
318         if not responses:
319             responses = []
320         return MultiCallIterator(responses)
321
322     @property
323     def _notify(self):
324         return MultiCallNotify(self)
325
326     def __getattr__(self, name):
327         new_job = MultiCallMethod(name)
328         self._job_list.append(new_job)
329         return new_job
330
331     __call__ = _request
332
333 # These lines conform to xmlrpclib's "compatibility" line. 
334 # Not really sure if we should include these, but oh well.
335 Server = ServerProxy
336
337 class Fault(object):
338     # JSON-RPC error class
339     def __init__(self, code=-32000, message='Server error', rpcid=None):
340         self.faultCode = code
341         self.faultString = message
342         self.rpcid = rpcid
343
344     def error(self):
345         return {'code':self.faultCode, 'message':self.faultString}
346
347     def response(self, rpcid=None, version=None):
348         if not version:
349             version = config.version
350         if rpcid:
351             self.rpcid = rpcid
352         return dumps(
353             self, methodresponse=True, rpcid=self.rpcid, version=version
354         )
355
356     def __repr__(self):
357         return '<Fault %s: %s>' % (self.faultCode, self.faultString)
358
359 def random_id(length=8):
360     import string
361     import random
362     random.seed()
363     choices = string.lowercase+string.digits
364     return_id = ''
365     for i in range(length):
366         return_id += random.choice(choices)
367     return return_id
368
369 class Payload(dict):
370     def __init__(self, rpcid=None, version=None):
371         if not version:
372             version = config.version
373         self.id = rpcid
374         self.version = float(version)
375     
376     def request(self, method, params=[]):
377         if type(method) not in types.StringTypes:
378             raise ValueError('Method name must be a string.')
379         if not self.id:
380             self.id = random_id()
381         request = { 'id':self.id, 'method':method }
382         if params:
383             request['params'] = params
384         if self.version >= 2:
385             request['jsonrpc'] = str(self.version)
386         return request
387
388     def notify(self, method, params=[]):
389         request = self.request(method, params)
390         if self.version >= 2:
391             del request['id']
392         else:
393             request['id'] = None
394         return request
395
396     def response(self, result=None):
397         response = {'result':result, 'id':self.id}
398         if self.version >= 2:
399             response['jsonrpc'] = str(self.version)
400         else:
401             response['error'] = None
402         return response
403
404     def error(self, code=-32000, message='Server error.'):
405         error = self.response()
406         if self.version >= 2:
407             del error['result']
408         else:
409             error['result'] = None
410         error['error'] = {'code':code, 'message':message}
411         return error
412
413 def dumps(params=[], methodname=None, methodresponse=None, 
414         encoding=None, rpcid=None, version=None, notify=None):
415     """
416     This differs from the Python implementation in that it implements 
417     the rpcid argument since the 2.0 spec requires it for responses.
418     """
419     if not version:
420         version = config.version
421     valid_params = (types.TupleType, types.ListType, types.DictType)
422     if methodname in types.StringTypes and \
423             type(params) not in valid_params and \
424             not isinstance(params, Fault):
425         """ 
426         If a method, and params are not in a listish or a Fault,
427         error out.
428         """
429         raise TypeError('Params must be a dict, list, tuple or Fault ' +
430                         'instance.')
431     # Begin parsing object
432     payload = Payload(rpcid=rpcid, version=version)
433     if not encoding:
434         encoding = 'utf-8'
435     if type(params) is Fault:
436         response = payload.error(params.faultCode, params.faultString)
437         return jdumps(response, encoding=encoding)
438     if type(methodname) not in types.StringTypes and methodresponse != True:
439         raise ValueError('Method name must be a string, or methodresponse '+
440                          'must be set to True.')
441     if config.use_jsonclass == True:
442         from jsonrpclib import jsonclass
443         params = jsonclass.dump(params)
444     if methodresponse is True:
445         if rpcid is None:
446             raise ValueError('A method response must have an rpcid.')
447         response = payload.response(params)
448         return jdumps(response, encoding=encoding)
449     request = None
450     if notify == True:
451         request = payload.notify(methodname, params)
452     else:
453         request = payload.request(methodname, params)
454     return jdumps(request, encoding=encoding)
455
456 def loads(data):
457     """
458     This differs from the Python implementation, in that it returns
459     the request structure in Dict format instead of the method, params.
460     It will return a list in the case of a batch request / response.
461     """
462     if data == '':
463         # notification
464         return None
465     result = jloads(data)
466     # if the above raises an error, the implementing server code 
467     # should return something like the following:
468     # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
469     if config.use_jsonclass == True:
470         from jsonrpclib import jsonclass
471         result = jsonclass.load(result)
472     return result
473
474 def check_for_errors(result):
475     if not result:
476         # Notification
477         return result
478     if type(result) is not types.DictType:
479         raise TypeError('Response is not a dict.')
480     if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
481         raise NotImplementedError('JSON-RPC version not yet supported.')
482     if 'result' not in result.keys() and 'error' not in result.keys():
483         raise ValueError('Response does not have a result or error key.')
484     if 'error' in result.keys() and result['error'] != None:
485         code = result['error']['code']
486         message = result['error']['message']
487         raise ProtocolError((code, message))
488     return result
489
490 def isbatch(result):
491     if type(result) not in (types.ListType, types.TupleType):
492         return False
493     if len(result) < 1:
494         return False
495     if type(result[0]) is not types.DictType:
496         return False
497     if 'jsonrpc' not in result[0].keys():
498         return False
499     try:
500         version = float(result[0]['jsonrpc'])
501     except ValueError:
502         raise ProtocolError('"jsonrpc" key must be a float(able) value.')
503     if version < 2:
504         return False
505     return True
506
507 def isnotification(request):
508     if 'id' not in request.keys():
509         # 2.0 notification
510         return True
511     if request['id'] == None:
512         # 1.0 notification
513         return True
514     return False