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