""" Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ============================ JSONRPC Library (jsonrpclib) ============================ 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 >>> server._notify.add(5, 6) >>> batch = jsonrpclib.MultiCall(server) >>> batch.add(3, 50) >>> batch.add(2, 3) >>> batch._notify.add(3, 5) >>> batch() [53, 5] 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 import string import random # Library includes import jsonrpclib from jsonrpclib import config from jsonrpclib import history # JSON library importing cjson = None json = None try: import cjson except ImportError: try: import json except ImportError: try: import simplejson as json except ImportError: raise ImportError( 'You must have the cjson, json, or simplejson ' + 'module(s) available.' ) IDCHARS = string.ascii_lowercase+string.digits class UnixSocketMissing(Exception): """ Just a properly named Exception if Unix Sockets usage is attempted on a platform that doesn't support them (Windows) """ pass #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-implementations class ProtocolError(Exception): pass class TransportMixIn(object): """ Just extends the XMLRPC transport where necessary. """ user_agent = config.user_agent # for Python 2.7 support _connection = None def send_content(self, connection, request_body): connection.putheader("Content-Type", "application/json-rpc") connection.putheader("Content-Length", str(len(request_body))) connection.endheaders() if request_body: connection.send(request_body) def getparser(self): target = JSONTarget() return JSONParser(target), target class JSONParser(object): def __init__(self, target): self.target = target def feed(self, data): self.target.feed(data) def close(self): pass class JSONTarget(object): def __init__(self): self.data = [] def feed(self, data): self.data.append(data) def close(self): return ''.join(self.data) class Transport(TransportMixIn, XMLTransport): pass class SafeTransport(TransportMixIn, XMLSafeTransport): pass from httplib import HTTP, HTTPConnection from socket import socket USE_UNIX_SOCKETS = False try: from socket import AF_UNIX, SOCK_STREAM USE_UNIX_SOCKETS = True except ImportError: pass if (USE_UNIX_SOCKETS): class UnixHTTPConnection(HTTPConnection): def connect(self): self.sock = socket(AF_UNIX, SOCK_STREAM) self.sock.connect(self.host) class UnixHTTP(HTTP): _connection_class = UnixHTTPConnection class UnixTransport(TransportMixIn, XMLTransport): def make_connection(self, host): import httplib host, extra_headers, x509 = self.get_host_info(host) return UnixHTTP(host) 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 if not version: version = config.version self.__version = version schema, uri = urllib.splittype(uri) if schema not in ('http', 'https', 'unix'): raise IOError('Unsupported JSON-RPC protocol.') if schema == 'unix': if not USE_UNIX_SOCKETS: # Don't like the "generic" Exception... raise UnixSocketMissing("Unix sockets not available.") self.__host = uri self.__handler = '/' else: self.__host, self.__handler = urllib.splithost(uri) if not self.__handler: # Not sure if this is in the JSON spec? #self.__handler = '/' self.__handler == '/' if transport is None: if schema == 'unix': transport = UnixTransport() elif 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) check_for_errors(response) return response['result'] def _request_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) check_for_errors(response) return def _run_request(self, request, notify=None): history.add_request(request) 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? history.add_response(response) if not response: return None return_obj = loads(response) return return_obj def __getattr__(self, name): # Same as original, just with new _Method reference return _Method(self._request, name) @property def _notify(self): # Just like __getattr__, but with notify namespace. return _Notify(self._request_notify) 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) def __getattr__(self, name): self.__name = '%s.%s' % (self.__name, name) return self # The old method returned a new instance, but this seemed wasteful. # The only thing that changes is the name. #return _Method(self.__send, "%s.%s" % (self.__name, name)) class _Notify(object): def __init__(self, request): self._request = request def __getattr__(self, name): return _Method(self._request, name) # Batch implementation class MultiCallMethod(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('JSON-RPC does not support 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() def __getattr__(self, method): new_method = '%s.%s' % (self.method, method) self.method = new_method return self class MultiCallNotify(object): def __init__(self, multicall): self.multicall = multicall def __getattr__(self, name): new_job = MultiCallMethod(name, notify=True) self.multicall._job_list.append(new_job) return new_job class MultiCallIterator(object): def __init__(self, results): self.results = results def __iter__(self): for i in range(0, len(self.results)): yield self[i] raise StopIteration def __getitem__(self, i): item = self.results[i] check_for_errors(item) return item['result'] def __len__(self): return len(self.results) class MultiCall(object): def __init__(self, server): self._server = server self._job_list = [] 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._server._run_request(request_body) del self._job_list[:] if not responses: responses = [] return MultiCallIterator(responses) @property def _notify(self): return MultiCallNotify(self) def __getattr__(self, name): new_job = MultiCallMethod(name) self._job_list.append(new_job) return new_job __call__ = _request # These lines conform to xmlrpclib's "compatibility" line. # Not really sure if we should include these, but oh well. Server = ServerProxy class Fault(object): # JSON-RPC error class def __init__(self, code=-32000, message='Server error', rpcid=None): self.faultCode = code self.faultString = message self.rpcid = rpcid def error(self): return {'code':self.faultCode, 'message':self.faultString} def response(self, rpcid=None, version=None): if not version: version = config.version if rpcid: self.rpcid = rpcid return dumps( self, methodresponse=True, rpcid=self.rpcid, version=version ) def __repr__(self): return '' % (self.faultCode, self.faultString) def random_id(length=8): return_id = '' for i in range(length): return_id += random.choice(IDCHARS) return return_id class Payload(dict): def __init__(self, rpcid=None, version=None): if not version: version = config.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 } if params: request['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. """ if not version: version = config.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.') # Begin parsing object payload = Payload(rpcid=rpcid, version=version) if not encoding: encoding = 'utf-8' if type(params) is Fault: response = payload.error(params.faultCode, params.faultString) return jdumps(response, encoding=encoding) 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 config.use_jsonclass == True: from jsonrpclib import jsonclass params = jsonclass.dump(params) 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. """ if data == '': # notification return None 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 } if config.use_jsonclass == True: from jsonrpclib import jsonclass result = jsonclass.load(result) return result def check_for_errors(result): if not result: # Notification return result if type(result) is not types.DictType: raise TypeError('Response is not a dict.') if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0: raise NotImplementedError('JSON-RPC version not yet supported.') if 'result' not in result.keys() and 'error' not in result.keys(): raise ValueError('Response does not have a result or error key.') if 'error' in result.keys() and result['error'] != None: code = result['error']['code'] message = result['error']['message'] raise ProtocolError((code, message)) 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 def isnotification(request): if 'id' not in request.keys(): # 2.0 notification return True if request['id'] == None: # 1.0 notification return True return False