From 8ce7c31e5d151b8376e3d06a291a2cd25466dbc5 Mon Sep 17 00:00:00 2001 From: catchjosh Date: Sun, 18 Oct 2009 06:52:46 +0000 Subject: [PATCH] Adding the library. git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@3 ae587032-bbab-11de-869a-473eb4776397 --- jsonrpclib.py | 431 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 jsonrpclib.py diff --git a/jsonrpclib.py b/jsonrpclib.py new file mode 100644 index 0000000..7fc6177 --- /dev/null +++ b/jsonrpclib.py @@ -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 + + -- 1.7.9.5