Adding the library.
[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         return_obj = loads(response_body)
110         return return_obj
111
112 class SafeTransport(XMLSafeTransport):
113     """ Just extends for HTTPS calls """
114     user_agent = Transport.user_agent
115     send_content = Transport.send_content
116     _parse_response = Transport._parse_response
117
118 class ServerProxy(XMLServerProxy):
119     """
120     Unfortunately, much more of this class has to be copied since
121     so much of it does the serialization.
122     """
123
124     def __init__(self, uri, transport=None, encoding=None, 
125                  verbose=0, version=None):
126         import urllib
127         global _version
128         if not version:
129             version = _version
130         self.__version = version
131         schema, uri = urllib.splittype(uri)
132         if schema not in ('http', 'https'):
133             raise IOError('Unsupported JSON-RPC protocol.')
134         self.__host, self.__handler = urllib.splithost(uri)
135         if not self.__handler:
136             # Not sure if this is in the JSON spec?
137             self.__handler = '/RPC2'
138         if transport is None:
139             if schema == 'https':
140                 transport = SafeTransport()
141             else:
142                 transport = Transport()
143         self.__transport = transport
144         self.__encoding = encoding
145         self.__verbose = verbose
146
147     def __request(self, methodname, params, rpcid=None):
148         request = dumps(params, methodname, encoding=self.__encoding,
149                         rpcid=rpcid, version=self.__version)
150         response = self.__run_request(request)
151         return response['result']
152     
153     def __notify(self, methodname, params, rpcid=None):
154         request = dumps(params, methodname, encoding=self.__encoding,
155                         rpcid=rpcid, version=self.__version, notify=True)
156         response = self.__run_request(request, notify=True)
157         return
158
159     def __run_request(self, request, notify=None):
160         global _last_request
161         global _last_response
162         _last_request = request
163         
164         if notify is True:
165             _last_response = None
166             return None
167
168         response = self.__transport.request(
169             self.__host,
170             self.__handler,
171             request,
172             verbose=self.__verbose
173         )
174         
175         # Here, the XMLRPC library translates a single list
176         # response to the single value -- should we do the
177         # same, and require a tuple / list to be passed to
178         # the response object, or expect the Server to be 
179         # outputting the response appropriately?
180         
181         _last_response = response
182         return response
183
184     def __getattr__(self, name):
185         # Same as original, just with new _Method and wrapper 
186         # for __notify
187         if name in ('__notify', '__run_request'):
188             wrapped_name = '_%s%s' % (self.__class__.__name__, name)
189             return getattr(self, wrapped_name)
190         return _Method(self.__request, name)
191
192 class _Method(XML_Method):
193     def __call__(self, *args, **kwargs):
194         if len(args) > 0 and len(kwargs) > 0:
195             raise ProtocolError('Cannot use both positional ' +
196                 'and keyword arguments (according to JSON-RPC spec.)')
197         if len(args) > 0:
198             return self.__send(self.__name, args)
199         else:
200             return self.__send(self.__name, kwargs)
201
202 # Batch implementation
203
204 class Job(object):
205     
206     def __init__(self, method, notify=False):
207         self.method = method
208         self.params = []
209         self.notify = notify
210
211     def __call__(self, *args, **kwargs):
212         if len(kwargs) > 0 and len(args) > 0:
213             raise ProtocolError('A Job cannot have both positional ' +
214                                 'and keyword arguments.')
215         if len(kwargs) > 0:
216             self.params = kwargs
217         else:
218             self.params = args
219
220     def request(self, encoding=None, rpcid=None):
221         return dumps(self.params, self.method, version=2.0,
222                      encoding=encoding, rpcid=rpcid, notify=self.notify)
223
224     def __repr__(self):
225         return '%s' % self.request()
226
227 class BatchServerProxy(ServerProxy):
228     
229     def __init__(self, uri, *args, **kwargs):
230         self.__job_list = []
231         ServerProxy.__init__(self, uri, *args, **kwargs)
232
233     def __run_request(self, request_body):
234         run_request = getattr(ServerProxy, '_ServerProxy__run_request')
235         return run_request(self, request_body)
236
237     def __request(self):
238         if len(self.__job_list) < 1:
239             # Should we alert? This /is/ pretty obvious.
240             return
241         request_body = '[ %s ]' % ','.join([job.request() for
242                                           job in self.__job_list])
243         responses = self.__run_request(request_body)
244         del self.__job_list[:]
245         return [ response['result'] for response in responses ]
246
247     def __notify(self, method, params):
248         new_job = Job(method, notify=True)
249         self.__job_list.append(new_job)
250
251     def __getattr__(self, name):
252         if name in ('__run', '__notify'):
253             wrapped_name = '_%s%s' % (self.__class__.__name__, name)
254             return getattr(self, wrapped_name)
255         new_job = Job(name)
256         self.__job_list.append(new_job)
257         return new_job
258
259     __run = __request
260
261 # These lines conform to xmlrpclib's "compatibility" line. 
262 # Not really sure if we should include these, but oh well.
263 Server = ServerProxy
264 BatchServer = BatchServerProxy
265
266 def run(batch):
267     """
268     This method is just a caller for the __run() on the actual
269     BatchServer itself. Useful only for those who don't like
270     calling __ methods. :)
271     """
272     batch.__run()
273
274
275
276 class Fault(dict):
277     # JSON-RPC error class
278     def __init__(self, code=-32000, message='Server error'):
279         self.code = code
280         self.message = message
281
282     def error(self):
283         return {'code':self.code, 'message':self.message}
284
285     def response(self, rpcid=None, version=None):
286         global _version
287         if not version:
288             version = _version
289         return dumps(self, rpcid=None, methodresponse=True,
290                      version=version)
291
292 def random_id(length=8):
293     import string
294     import random
295     random.seed()
296     choices = string.lowercase+string.digits
297     return_id = ''
298     for i in range(length):
299         return_id += random.choice(choices)
300     return return_id
301
302 class Payload(dict):
303     def __init__(self, rpcid=None, version=None):
304         global _version
305         if not version:
306             version = _version
307         self.id = rpcid
308         self.version = float(version)
309     
310     def request(self, method, params=[]):
311         if type(method) not in types.StringTypes:
312             raise ValueError('Method name must be a string.')
313         if not self.id:
314             self.id = random_id()
315         request = {'id':self.id, 'method':method, 'params':params}
316         if self.version >= 2:
317             request['jsonrpc'] = str(self.version)
318         return request
319
320     def notify(self, method, params=[]):
321         request = self.request(method, params)
322         if self.version >= 2:
323             del request['id']
324         else:
325             request['id'] = None
326         return request
327
328     def response(self, result=None):
329         response = {'result':result, 'id':self.id}
330         if self.version >= 2:
331             response['jsonrpc'] = str(self.version)
332         else:
333             response['error'] = None
334         return response
335
336     def error(self, code=-32000, message='Server error.'):
337         error = self.response()
338         if self.version >= 2:
339             del error['result']
340         else:
341             error['result'] = None
342         error['error'] = {'code':code, 'message':message}
343         return error
344
345 def dumps(params=[], methodname=None, methodresponse=None, 
346         encoding=None, rpcid=None, version=None, notify=None):
347     """
348     This differs from the Python implementation in that it implements 
349     the rpcid argument since the 2.0 spec requires it for responses.
350     """
351     global _version
352     if not version:
353         verion = _version
354     valid_params = (types.TupleType, types.ListType, types.DictType)
355     if methodname in types.StringTypes and \
356             type(params) not in valid_params and \
357             not isinstance(params, Fault):
358         """ 
359         If a method, and params are not in a listish or a Fault,
360         error out.
361         """
362         raise TypeError('Params must be a dict, list, tuple or Fault ' +
363                         'instance.')
364     if type(methodname) not in types.StringTypes and methodresponse != True:
365         raise ValueError('Method name must be a string, or methodresponse '+
366                          'must be set to True.')
367     if isinstance(params, Fault) and not methodresponse:
368         raise TypeError('You can only use a Fault for responses.')
369     # Begin parsing object
370     payload = Payload(rpcid=rpcid, version=version)
371     if not encoding:
372         encoding = 'utf-8'
373     if type(params) is Fault:
374         response = payload.error(params.code, params.message)
375         return jdumps(response, encoding=encoding)
376     if methodresponse is True:
377         if rpcid is None:
378             raise ValueError('A method response must have an rpcid.')
379         response = payload.response(params)
380         return jdumps(response, encoding=encoding)
381     request = None
382     if notify == True:
383         request = payload.notify(methodname, params)
384     else:
385         request = payload.request(methodname, params)
386     return jdumps(request, encoding=encoding)
387
388 def loads(data):
389     """
390     This differs from the Python implementation, in that it returns
391     the request structure in Dict format instead of the method, params.
392     It will return a list in the case of a batch request / response.
393     """
394     result = jloads(data)
395     # if the above raises an error, the implementing server code 
396     # should return something like the following:
397     # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
398     
399     result_list = []
400     if not isbatch(result):
401         result_list.append(result)
402     else:
403         result_list = 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))
411     del result_list
412     return result
413
414 def isbatch(result):
415     if type(result) not in (types.ListType, types.TupleType):
416         return False
417     if len(result) < 1:
418         return False
419     if type(result[0]) is not types.DictType:
420         return False
421     if 'jsonrpc' not in result[0].keys():
422         return False
423     try:
424         version = float(result[0]['jsonrpc'])
425     except ValueError:
426         raise ProtocolError('"jsonrpc" key must be a float(able) value.')
427     if version < 2:
428         return False
429     return True
430
431