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