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