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