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