Adding the library.
authorcatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>
Sun, 18 Oct 2009 06:52:46 +0000 (06:52 +0000)
committercatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>
Sun, 18 Oct 2009 06:52:46 +0000 (06:52 +0000)
git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@3 ae587032-bbab-11de-869a-473eb4776397

jsonrpclib.py [new file with mode: 0644]

diff --git a/jsonrpclib.py b/jsonrpclib.py
new file mode 100644 (file)
index 0000000..7fc6177
--- /dev/null
@@ -0,0 +1,431 @@
+"""
+JSONRPCLIB -- started by Josh Marshall
+
+This library is a JSON-RPC v.2 (proposed) implementation which
+follows the xmlrpclib API for portability between clients. It
+uses the same Server / ServerProxy, loads, dumps, etc. syntax,
+while providing features not present in XML-RPC like:
+
+* Keyword arguments
+* Notifications
+* Versioning
+* Batches and batch notifications
+
+Eventually, I'll add a SimpleXMLRPCServer compatible library,
+and other things to tie the thing off nicely. :)
+
+For a quick-start, just open a console and type the following,
+replacing the server address, method, and parameters 
+appropriately.
+>>> import jsonrpclib
+>>> server = jsonrpclib.Server('http://localhost:8181')
+>>> server.add(5, 6)
+11
+>>> jsonrpclib.__notify('add', (5, 6))
+
+See http://code.google.com/p/jsonrpclib/ for more info.
+"""
+
+import types
+import sys
+from xmlrpclib import Transport as XMLTransport
+from xmlrpclib import SafeTransport as XMLSafeTransport
+from xmlrpclib import ServerProxy as XMLServerProxy
+from xmlrpclib import _Method as XML_Method
+import time
+
+# JSON library importing
+cjson = None
+json = None
+try:
+    import cjson
+except ImportError:
+    pass
+if not cjson:
+    try:
+        import json
+    except ImportError:
+        pass
+if not cjson and not json: 
+    try:
+        import simplejson as json
+    except ImportError:
+        raise ImportError('You must have the cjson, json, or simplejson ' +
+                          'module(s) available.')
+
+# Library attributes
+_version = 2.0
+_last_response = None
+_last_request = None
+_user_agent = 'jsonrpclib/0.1 (Python %s)' % \
+    '.'.join([str(ver) for ver in sys.version_info[0:3]])
+
+#JSON Abstractions
+
+def jdumps(obj, encoding='utf-8'):
+    # Do 'serialize' test at some point for other classes
+    global cjson
+    if cjson:
+        return cjson.encode(obj)
+    else:
+        return json.dumps(obj, encoding=encoding)
+
+def jloads(json_string):
+    global cjson
+    if cjson:
+        return cjson.decode(json_string)
+    else:
+        return json.loads(json_string)
+
+
+# XMLRPClib re-implemntations
+
+class ProtocolError(Exception):
+    pass
+
+class Transport(XMLTransport):
+    """ Just extends the XMLRPC transport where necessary. """
+    user_agent = _user_agent
+
+    def send_content(self, connection, request_body):
+        connection.putheader("Content-Type", "text/json")
+        connection.putheader("Content-Length", str(len(request_body)))
+        connection.endheaders()
+        if request_body:
+            connection.send(request_body)
+
+    def _parse_response(self, file_h, sock):
+        response_body = ''
+        while 1:
+            if sock:
+                response = sock.recv(1024)
+            else:
+                response = file_h.read(1024)
+            if not response:
+                break
+            if self.verbose:
+                print 'body: %s' % response
+            response_body += response
+        return_obj = loads(response_body)
+        return return_obj
+
+class SafeTransport(XMLSafeTransport):
+    """ Just extends for HTTPS calls """
+    user_agent = Transport.user_agent
+    send_content = Transport.send_content
+    _parse_response = Transport._parse_response
+
+class ServerProxy(XMLServerProxy):
+    """
+    Unfortunately, much more of this class has to be copied since
+    so much of it does the serialization.
+    """
+
+    def __init__(self, uri, transport=None, encoding=None, 
+                 verbose=0, version=None):
+        import urllib
+        global _version
+        if not version:
+            version = _version
+        self.__version = version
+        schema, uri = urllib.splittype(uri)
+        if schema not in ('http', 'https'):
+            raise IOError('Unsupported JSON-RPC protocol.')
+        self.__host, self.__handler = urllib.splithost(uri)
+        if not self.__handler:
+            # Not sure if this is in the JSON spec?
+            self.__handler = '/RPC2'
+        if transport is None:
+            if schema == 'https':
+                transport = SafeTransport()
+            else:
+                transport = Transport()
+        self.__transport = transport
+        self.__encoding = encoding
+        self.__verbose = verbose
+
+    def __request(self, methodname, params, rpcid=None):
+        request = dumps(params, methodname, encoding=self.__encoding,
+                        rpcid=rpcid, version=self.__version)
+        response = self.__run_request(request)
+        return response['result']
+    
+    def __notify(self, methodname, params, rpcid=None):
+        request = dumps(params, methodname, encoding=self.__encoding,
+                        rpcid=rpcid, version=self.__version, notify=True)
+        response = self.__run_request(request, notify=True)
+        return
+
+    def __run_request(self, request, notify=None):
+        global _last_request
+        global _last_response
+        _last_request = request
+        
+        if notify is True:
+            _last_response = None
+            return None
+
+        response = self.__transport.request(
+            self.__host,
+            self.__handler,
+            request,
+            verbose=self.__verbose
+        )
+        
+        # Here, the XMLRPC library translates a single list
+        # response to the single value -- should we do the
+        # same, and require a tuple / list to be passed to
+        # the response object, or expect the Server to be 
+        # outputting the response appropriately?
+        
+        _last_response = response
+        return response
+
+    def __getattr__(self, name):
+        # Same as original, just with new _Method and wrapper 
+        # for __notify
+        if name in ('__notify', '__run_request'):
+            wrapped_name = '_%s%s' % (self.__class__.__name__, name)
+            return getattr(self, wrapped_name)
+        return _Method(self.__request, name)
+
+class _Method(XML_Method):
+    def __call__(self, *args, **kwargs):
+        if len(args) > 0 and len(kwargs) > 0:
+            raise ProtocolError('Cannot use both positional ' +
+                'and keyword arguments (according to JSON-RPC spec.)')
+        if len(args) > 0:
+            return self.__send(self.__name, args)
+        else:
+            return self.__send(self.__name, kwargs)
+
+# Batch implementation
+
+class Job(object):
+    
+    def __init__(self, method, notify=False):
+        self.method = method
+        self.params = []
+        self.notify = notify
+
+    def __call__(self, *args, **kwargs):
+        if len(kwargs) > 0 and len(args) > 0:
+            raise ProtocolError('A Job cannot have both positional ' +
+                                'and keyword arguments.')
+        if len(kwargs) > 0:
+            self.params = kwargs
+        else:
+            self.params = args
+
+    def request(self, encoding=None, rpcid=None):
+        return dumps(self.params, self.method, version=2.0,
+                     encoding=encoding, rpcid=rpcid, notify=self.notify)
+
+    def __repr__(self):
+        return '%s' % self.request()
+
+class BatchServerProxy(ServerProxy):
+    
+    def __init__(self, uri, *args, **kwargs):
+        self.__job_list = []
+        ServerProxy.__init__(self, uri, *args, **kwargs)
+
+    def __run_request(self, request_body):
+        run_request = getattr(ServerProxy, '_ServerProxy__run_request')
+        return run_request(self, request_body)
+
+    def __request(self):
+        if len(self.__job_list) < 1:
+            # Should we alert? This /is/ pretty obvious.
+            return
+        request_body = '[ %s ]' % ','.join([job.request() for
+                                          job in self.__job_list])
+        responses = self.__run_request(request_body)
+        del self.__job_list[:]
+        return [ response['result'] for response in responses ]
+
+    def __notify(self, method, params):
+        new_job = Job(method, notify=True)
+        self.__job_list.append(new_job)
+
+    def __getattr__(self, name):
+        if name in ('__run', '__notify'):
+            wrapped_name = '_%s%s' % (self.__class__.__name__, name)
+            return getattr(self, wrapped_name)
+        new_job = Job(name)
+        self.__job_list.append(new_job)
+        return new_job
+
+    __run = __request
+
+# These lines conform to xmlrpclib's "compatibility" line. 
+# Not really sure if we should include these, but oh well.
+Server = ServerProxy
+BatchServer = BatchServerProxy
+
+def run(batch):
+    """
+    This method is just a caller for the __run() on the actual
+    BatchServer itself. Useful only for those who don't like
+    calling __ methods. :)
+    """
+    batch.__run()
+
+
+
+class Fault(dict):
+    # JSON-RPC error class
+    def __init__(self, code=-32000, message='Server error'):
+        self.code = code
+        self.message = message
+
+    def error(self):
+        return {'code':self.code, 'message':self.message}
+
+    def response(self, rpcid=None, version=None):
+        global _version
+        if not version:
+            version = _version
+        return dumps(self, rpcid=None, methodresponse=True,
+                     version=version)
+
+def random_id(length=8):
+    import string
+    import random
+    random.seed()
+    choices = string.lowercase+string.digits
+    return_id = ''
+    for i in range(length):
+        return_id += random.choice(choices)
+    return return_id
+
+class Payload(dict):
+    def __init__(self, rpcid=None, version=None):
+        global _version
+        if not version:
+            version = _version
+        self.id = rpcid
+        self.version = float(version)
+    
+    def request(self, method, params=[]):
+        if type(method) not in types.StringTypes:
+            raise ValueError('Method name must be a string.')
+        if not self.id:
+            self.id = random_id()
+        request = {'id':self.id, 'method':method, 'params':params}
+        if self.version >= 2:
+            request['jsonrpc'] = str(self.version)
+        return request
+
+    def notify(self, method, params=[]):
+        request = self.request(method, params)
+        if self.version >= 2:
+            del request['id']
+        else:
+            request['id'] = None
+        return request
+
+    def response(self, result=None):
+        response = {'result':result, 'id':self.id}
+        if self.version >= 2:
+            response['jsonrpc'] = str(self.version)
+        else:
+            response['error'] = None
+        return response
+
+    def error(self, code=-32000, message='Server error.'):
+        error = self.response()
+        if self.version >= 2:
+            del error['result']
+        else:
+            error['result'] = None
+        error['error'] = {'code':code, 'message':message}
+        return error
+
+def dumps(params=[], methodname=None, methodresponse=None, 
+        encoding=None, rpcid=None, version=None, notify=None):
+    """
+    This differs from the Python implementation in that it implements 
+    the rpcid argument since the 2.0 spec requires it for responses.
+    """
+    global _version
+    if not version:
+        verion = _version
+    valid_params = (types.TupleType, types.ListType, types.DictType)
+    if methodname in types.StringTypes and \
+            type(params) not in valid_params and \
+            not isinstance(params, Fault):
+        """ 
+        If a method, and params are not in a listish or a Fault,
+        error out.
+        """
+        raise TypeError('Params must be a dict, list, tuple or Fault ' +
+                        'instance.')
+    if type(methodname) not in types.StringTypes and methodresponse != True:
+        raise ValueError('Method name must be a string, or methodresponse '+
+                         'must be set to True.')
+    if isinstance(params, Fault) and not methodresponse:
+        raise TypeError('You can only use a Fault for responses.')
+    # Begin parsing object
+    payload = Payload(rpcid=rpcid, version=version)
+    if not encoding:
+        encoding = 'utf-8'
+    if type(params) is Fault:
+        response = payload.error(params.code, params.message)
+        return jdumps(response, encoding=encoding)
+    if methodresponse is True:
+        if rpcid is None:
+            raise ValueError('A method response must have an rpcid.')
+        response = payload.response(params)
+        return jdumps(response, encoding=encoding)
+    request = None
+    if notify == True:
+        request = payload.notify(methodname, params)
+    else:
+        request = payload.request(methodname, params)
+    return jdumps(request, encoding=encoding)
+
+def loads(data):
+    """
+    This differs from the Python implementation, in that it returns
+    the request structure in Dict format instead of the method, params.
+    It will return a list in the case of a batch request / response.
+    """
+    result = jloads(data)
+    # if the above raises an error, the implementing server code 
+    # should return something like the following:
+    # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
+    
+    result_list = []
+    if not isbatch(result):
+        result_list.append(result)
+    else:
+        result_list = result
+    for entry in result_list:
+        if 'jsonrpc' in entry.keys() and float(entry['jsonrpc']) > 2.0:
+            raise NotImplementedError('JSON-RPC version not yet supported.')
+        if 'error' in entry.keys() and entry['error'] != None:
+            code = entry['error']['code']
+            message = entry['error']['message']
+            raise ProtocolError('ERROR %s: %s' % (code, message))
+    del result_list
+    return result
+
+def isbatch(result):
+    if type(result) not in (types.ListType, types.TupleType):
+        return False
+    if len(result) < 1:
+        return False
+    if type(result[0]) is not types.DictType:
+        return False
+    if 'jsonrpc' not in result[0].keys():
+        return False
+    try:
+        version = float(result[0]['jsonrpc'])
+    except ValueError:
+        raise ProtocolError('"jsonrpc" key must be a float(able) value.')
+    if version < 2:
+        return False
+    return True
+
+