19cb47d3fa7f5db1ac3862ee9b3c54557ce52081
[invirt/packages/python-jsonrpclib.git] / jsonrpclib.py
1 """
2 JSONRPCLIB -- started by Josh Marshall
3
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:
8
9 * Keyword arguments
10 * Notifications
11 * Versioning
12 * Batches and batch notifications
13
14 Eventually, I'll add a SimpleXMLRPCServer compatible library,
15 and other things to tie the thing off nicely. :)
16
17 For a quick-start, just open a console and type the following,
18 replacing the server address, method, and parameters 
19 appropriately.
20 >>> import jsonrpclib
21 >>> server = jsonrpclib.Server('http://localhost:8181')
22 >>> server.add(5, 6)
23 11
24 >>> jsonrpclib.__notify('add', (5, 6))
25
26 See http://code.google.com/p/jsonrpclib/ for more info.
27 """
28
29 import types
30 import sys
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
35 import time
36
37 # JSON library importing
38 cjson = None
39 json = None
40 try:
41     import cjson
42 except ImportError:
43     pass
44 if not cjson:
45     try:
46         import json
47     except ImportError:
48         pass
49 if not cjson and not json: 
50     try:
51         import simplejson as json
52     except ImportError:
53         raise ImportError('You must have the cjson, json, or simplejson ' +
54                           'module(s) available.')
55
56 # Library attributes
57 _version = 2.0
58 _last_response = None
59 _last_request = None
60 _user_agent = 'jsonrpclib/0.1 (Python %s)' % \
61     '.'.join([str(ver) for ver in sys.version_info[0:3]])
62
63 #JSON Abstractions
64
65 def jdumps(obj, encoding='utf-8'):
66     # Do 'serialize' test at some point for other classes
67     global cjson
68     if cjson:
69         return cjson.encode(obj)
70     else:
71         return json.dumps(obj, encoding=encoding)
72
73 def jloads(json_string):
74     global cjson
75     if cjson:
76         return cjson.decode(json_string)
77     else:
78         return json.loads(json_string)
79
80
81 # XMLRPClib re-implemntations
82
83 class ProtocolError(Exception):
84     pass
85
86 class Transport(XMLTransport):
87     """ Just extends the XMLRPC transport where necessary. """
88     user_agent = _user_agent
89
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()
94         if request_body:
95             connection.send(request_body)
96
97     def _parse_response(self, file_h, sock):
98         response_body = ''
99         while 1:
100             if sock:
101                 response = sock.recv(1024)
102             else:
103                 response = file_h.read(1024)
104             if not response:
105                 break
106             if self.verbose:
107                 print 'body: %s' % response
108             response_body += response
109         if response_body == '':
110             # Notification
111             return None
112         return_obj = loads(response_body)
113         return return_obj
114
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
120
121 class ServerProxy(XMLServerProxy):
122     """
123     Unfortunately, much more of this class has to be copied since
124     so much of it does the serialization.
125     """
126
127     def __init__(self, uri, transport=None, encoding=None, 
128                  verbose=0, version=None):
129         import urllib
130         global _version
131         if not version:
132             version = _version
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()
144             else:
145                 transport = Transport()
146         self.__transport = transport
147         self.__encoding = encoding
148         self.__verbose = verbose
149
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']
155     
156     def __notify(self, methodname, params, rpcid=None):
157         request = dumps(params, methodname, encoding=self.__encoding,
158                         rpcid=rpcid, version=self.__version, notify=True)
159         response = self.__run_request(request, notify=True)
160         return
161
162     def __run_request(self, request, notify=None):
163         global _last_request
164         global _last_response
165         _last_request = request
166
167         response = self.__transport.request(
168             self.__host,
169             self.__handler,
170             request,
171             verbose=self.__verbose
172         )
173         
174         # Here, the XMLRPC library translates a single list
175         # response to the single value -- should we do the
176         # same, and require a tuple / list to be passed to
177         # the response object, or expect the Server to be 
178         # outputting the response appropriately?
179         
180         _last_response = response
181         if not response:
182             # notification, no result
183             return None
184         return check_for_errors(response)
185
186     def __getattr__(self, name):
187         # Same as original, just with new _Method and wrapper 
188         # for __notify
189         if name in ('__notify', '__run_request'):
190             wrapped_name = '_%s%s' % (self.__class__.__name__, name)
191             return getattr(self, wrapped_name)
192         return _Method(self.__request, name)
193
194 class _Method(XML_Method):
195     def __call__(self, *args, **kwargs):
196         if len(args) > 0 and len(kwargs) > 0:
197             raise ProtocolError('Cannot use both positional ' +
198                 'and keyword arguments (according to JSON-RPC spec.)')
199         if len(args) > 0:
200             return self.__send(self.__name, args)
201         else:
202             return self.__send(self.__name, kwargs)
203
204 # Batch implementation
205
206 class MultiCallMethod(object):
207     
208     def __init__(self, method, notify=False):
209         self.method = method
210         self.params = []
211         self.notify = notify
212
213     def __call__(self, *args, **kwargs):
214         if len(kwargs) > 0 and len(args) > 0:
215             raise ProtocolError('A Job cannot have both positional ' +
216                                 'and keyword arguments.')
217         if len(kwargs) > 0:
218             self.params = kwargs
219         else:
220             self.params = args
221
222     def request(self, encoding=None, rpcid=None):
223         return dumps(self.params, self.method, version=2.0,
224                      encoding=encoding, rpcid=rpcid, notify=self.notify)
225
226     def __repr__(self):
227         return '%s' % self.request()
228
229 class MultiCall(object):
230     
231     def __init__(self, server):
232         self.__server = server
233         self.__job_list = []
234
235     def __run_request(self, request_body):
236         run_request = getattr(self.__server, '_ServerProxy__run_request')
237         return run_request(request_body)
238
239     def __request(self):
240         if len(self.__job_list) < 1:
241             # Should we alert? This /is/ pretty obvious.
242             return
243         request_body = '[ %s ]' % ','.join([job.request() for
244                                           job in self.__job_list])
245         responses = self.__run_request(request_body)
246         del self.__job_list[:]
247         return [ response['result'] for response in responses ]
248
249     def __notify(self, method, params=[]):
250         new_job = MultiCallMethod(method, notify=True)
251         new_job.params = params
252         self.__job_list.append(new_job)
253         
254     def __getattr__(self, name):
255         if name in ('__run', '__notify'):
256             wrapped_name = '_%s%s' % (self.__class__.__name__, name)
257             return getattr(self, wrapped_name)
258         new_job = MultiCallMethod(name)
259         self.__job_list.append(new_job)
260         return new_job
261
262     __call__ = __request
263
264 # These lines conform to xmlrpclib's "compatibility" line. 
265 # Not really sure if we should include these, but oh well.
266 Server = ServerProxy
267
268 class Fault(object):
269     # JSON-RPC error class
270     def __init__(self, code=-32000, message='Server error'):
271         self.faultCode = code
272         self.faultString = message
273
274     def error(self):
275         return {'code':self.faultCode, 'message':self.faultString}
276
277     def response(self, rpcid=None, version=None):
278         global _version
279         if not version:
280             version = _version
281         return dumps(self, rpcid=rpcid, version=version)
282
283     def __repr__(self):
284         return '<Fault %s: %s>' % (self.faultCode, self.faultString)
285
286 def random_id(length=8):
287     import string
288     import random
289     random.seed()
290     choices = string.lowercase+string.digits
291     return_id = ''
292     for i in range(length):
293         return_id += random.choice(choices)
294     return return_id
295
296 class Payload(dict):
297     def __init__(self, rpcid=None, version=None):
298         global _version
299         if not version:
300             version = _version
301         self.id = rpcid
302         self.version = float(version)
303     
304     def request(self, method, params=[]):
305         if type(method) not in types.StringTypes:
306             raise ValueError('Method name must be a string.')
307         if not self.id:
308             self.id = random_id()
309         request = {'id':self.id, 'method':method, 'params':params}
310         if self.version >= 2:
311             request['jsonrpc'] = str(self.version)
312         return request
313
314     def notify(self, method, params=[]):
315         request = self.request(method, params)
316         if self.version >= 2:
317             del request['id']
318         else:
319             request['id'] = None
320         return request
321
322     def response(self, result=None):
323         response = {'result':result, 'id':self.id}
324         if self.version >= 2:
325             response['jsonrpc'] = str(self.version)
326         else:
327             response['error'] = None
328         return response
329
330     def error(self, code=-32000, message='Server error.'):
331         error = self.response()
332         if self.version >= 2:
333             del error['result']
334         else:
335             error['result'] = None
336         error['error'] = {'code':code, 'message':message}
337         return error
338
339 def dumps(params=[], methodname=None, methodresponse=None, 
340         encoding=None, rpcid=None, version=None, notify=None):
341     """
342     This differs from the Python implementation in that it implements 
343     the rpcid argument since the 2.0 spec requires it for responses.
344     """
345     global _version
346     if not version:
347         version = _version
348     valid_params = (types.TupleType, types.ListType, types.DictType)
349     if methodname in types.StringTypes and \
350             type(params) not in valid_params and \
351             not isinstance(params, Fault):
352         """ 
353         If a method, and params are not in a listish or a Fault,
354         error out.
355         """
356         raise TypeError('Params must be a dict, list, tuple or Fault ' +
357                         'instance.')
358     # Begin parsing object
359     payload = Payload(rpcid=rpcid, version=version)
360     if not encoding:
361         encoding = 'utf-8'
362     if type(params) is Fault:
363         response = payload.error(params.faultCode, params.faultString)
364         return jdumps(response, encoding=encoding)
365     if type(methodname) not in types.StringTypes and methodresponse != True:
366         raise ValueError('Method name must be a string, or methodresponse '+
367                          'must be set to True.')
368     if methodresponse is True:
369         if rpcid is None:
370             raise ValueError('A method response must have an rpcid.')
371         response = payload.response(params)
372         return jdumps(response, encoding=encoding)
373     request = None
374     if notify == True:
375         request = payload.notify(methodname, params)
376     else:
377         request = payload.request(methodname, params)
378     return jdumps(request, encoding=encoding)
379
380 def loads(data):
381     """
382     This differs from the Python implementation, in that it returns
383     the request structure in Dict format instead of the method, params.
384     It will return a list in the case of a batch request / response.
385     """
386     if data == '':
387         # notification
388         return None
389     result = jloads(data)
390     # if the above raises an error, the implementing server code 
391     # should return something like the following:
392     # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
393     return result
394
395 def check_for_errors(result):
396     result_list = []
397     if not isbatch(result):
398         result_list.append(result)
399     else:
400         result_list = result
401     for entry in result_list:
402         if 'jsonrpc' in entry.keys() and float(entry['jsonrpc']) > 2.0:
403             raise NotImplementedError('JSON-RPC version not yet supported.')
404         if 'error' in entry.keys() and entry['error'] != None:
405             code = entry['error']['code']
406             message = entry['error']['message']
407             raise ProtocolError('ERROR %s: %s' % (code, message))
408     del result_list
409     return result
410
411 def isbatch(result):
412     if type(result) not in (types.ListType, types.TupleType):
413         return False
414     if len(result) < 1:
415         return False
416     if type(result[0]) is not types.DictType:
417         return False
418     if 'jsonrpc' not in result[0].keys():
419         return False
420     try:
421         version = float(result[0]['jsonrpc'])
422     except ValueError:
423         raise ProtocolError('"jsonrpc" key must be a float(able) value.')
424     if version < 2:
425         return False
426     return True
427
428