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