From: catchjosh Date: Mon, 2 Nov 2009 05:02:32 +0000 (+0000) Subject: Oops -- NOW adding the new jsonrpclib/ files (jsonclass, config, history, __init__) X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/python-jsonrpclib.git/commitdiff_plain/669c1369c82b5dec6fd7952dce27a41ac1690b11 Oops -- NOW adding the new jsonrpclib/ files (jsonclass, config, history, __init__) git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@13 ae587032-bbab-11de-869a-473eb4776397 --- diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py new file mode 100644 index 0000000..ae41f93 --- /dev/null +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -0,0 +1,188 @@ +import jsonrpclib +from jsonrpclib import Fault +import SimpleXMLRPCServer +import SocketServer +import types +import traceback +import fcntl +import sys + +def get_version(request): + if type(request) not in (types.ListType, types.DictType): + return None + if type(request) is types.ListType: + if len(request) == 0: + return None + if 'jsonrpc' not in request[0].keys(): + return None + return '2.0' + # must be a dict + if 'jsonrpc' in request.keys(): + return 2.0 + if 'id' in request.keys(): + return 1.0 + return None + +class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): + + def __init__(self, encoding=None): + SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, + allow_none=True, + encoding=encoding) + + def _marshaled_dispatch(self, data, dispatch_method = None): + response = None + try: + request = jsonrpclib.loads(data) + except: + fault = Fault(-32600, 'Request %s invalid.' % data) + response = fault.response() + return response + version = get_version(request) + if not version: + fault = Fault(-32600, 'Request %s invalid.' % data) + response = fault.response() + return response + if type(request) is types.ListType: + # This SHOULD be a batch, by spec + responses = [] + for req_entry in request: + resp_entry = self._marshaled_single_dispatch(req_entry) + if resp_entry is not None: + responses.append(resp_entry) + response = '[%s]' % ','.join(responses) + else: + response = self._marshaled_single_dispatch(request) + return response + + def _marshaled_single_dispatch(self, request): + # TODO - Use the multiprocessing and skip the response if + # it is a notification + method = request['method'] + params = request['params'] + # Put in support for custom dispatcher here + # (See SimpleXMLRPCServer._marshaled_dispatch) + try: + response = self._dispatch(method, params) + except: + exc_type, exc_value, exc_tb = sys.exc_info() + fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) + return fault.response() + if 'id' not in request.keys() or request['id'] == None: + # It's a notification + return None + try: + response = jsonrpclib.dumps(response, + methodresponse=True, + rpcid=request['id'] + ) + return response + except: + exc_type, exc_value, exc_tb = sys.exc_info() + fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) + return fault.response() + + def _dispatch(self, method, params): + func = None + try: + func = self.funcs[method] + except KeyError: + if self.instance is not None: + if hasattr(self.instance, '_dispatch'): + return self.instance._dispatch(method, params) + else: + try: + func = resolve_dotted_attribute( + self.instance, + method, + True + ) + except AttributeError: + pass + if func is not None: + try: + if type(params) is types.ListType: + response = func(*params) + else: + response = func(**params) + return response + except TypeError: + return Fault(-32602, 'Invalid parameters.') + except: + err_lines = traceback.format_exc().splitlines() + trace_string = '%s | %s' % (err_lines[-3], err_lines[-1]) + fault = jsonrpclib.Fault(-32603, 'Server error: %s' % + trace_string) + return fault + else: + return Fault(-32601, 'Method %s not supported.' % method) + +class SimpleJSONRPCRequestHandler( + SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + + def do_POST(self): + if not self.is_rpc_path_valid(): + self.report_404() + return + try: + max_chunk_size = 10*1024*1024 + size_remaining = int(self.headers["content-length"]) + L = [] + while size_remaining: + chunk_size = min(size_remaining, max_chunk_size) + L.append(self.rfile.read(chunk_size)) + size_remaining -= len(L[-1]) + data = ''.join(L) + response = self.server._marshaled_dispatch(data) + self.send_response(200) + except Exception, e: + self.send_response(500) + err_lines = traceback.format_exc().splitlines() + trace_string = '%s | %s' % (err_lines[-3], err_lines[-1]) + fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string) + response = fault.response() + if response == None: + response = '' + self.send_header("Content-type", "application/json-rpc") + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + self.wfile.flush() + self.connection.shutdown(1) + +class SimpleJSONRPCServer(SocketServer.TCPServer, + SimpleJSONRPCDispatcher): + + allow_reuse_address = True + + def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, + logRequests=True, encoding=None, bind_and_activate=True): + self.logRequests = logRequests + SimpleJSONRPCDispatcher.__init__(self, encoding) + SocketServer.TCPServer.__init__(self, addr, requestHandler, + bind_and_activate) + if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): + flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) + flags |= fcntl.FD_CLOEXEC + fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) + +class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): + + def __init__(self, encoding=None): + SimpleJSONRPCDispatcher.__init__(self, encoding) + + def handle_jsonrpc(self, request_text): + response = self._marshaled_dispatch(request_text) + print 'Content-Type: application/json-rpc' + print 'Content-Length: %d' % len(response) + print + sys.stdout.write(response) + + handle_xmlrpc = handle_jsonrpc + +if __name__ == '__main__': + print 'Running JSON-RPC server on port 8000' + server = SimpleJSONRPCServer(("localhost", 8000)) + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.serve_forever() diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py new file mode 100644 index 0000000..1fd4dc0 --- /dev/null +++ b/jsonrpclib/__init__.py @@ -0,0 +1,3 @@ +from jsonrpclib import * +from config import config +from history import history diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py new file mode 100644 index 0000000..1f9d7d8 --- /dev/null +++ b/jsonrpclib/config.py @@ -0,0 +1,33 @@ +import sys + +class LocalClasses(dict): + def add(self, cls): + self[cls.__name__] = cls + +class Config(object): + """ + This is pretty much used exclusively for the 'jsonclass' + functionality... set use_jsonclass to False to turn it off. + You can change serialize_method and ignore_attribute, or use + the local_classes.add(class) to include "local" classes. + """ + use_jsonclass = True + # Change to False to keep __jsonclass__ entries raw. + serialize_method = '_serialize' + # The serialize_method should be a string that references the + # method on a custom class object which is responsible for + # returning a tuple of the constructor arguments and a dict of + # attributes. + ignore_attribute = '_ignore' + # The ignore attribute should be a string that references the + # attribute on a custom class object which holds strings and / or + # references of the attributes the class translator should ignore. + classes = LocalClasses() + # The list of classes to use for jsonclass translation. + version = 2.0 + # Version of the JSON-RPC spec to support + user_agent = 'jsonrpclib/0.1 (Python %s)' % \ + '.'.join([str(ver) for ver in sys.version_info[0:3]]) + # User agent to use for calls. + +config = Config diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py new file mode 100644 index 0000000..ec53235 --- /dev/null +++ b/jsonrpclib/history.py @@ -0,0 +1,36 @@ + +class History(object): + """ + This holds all the response and request objects for a + session. A server using this should call "clear" after + each request cycle in order to keep it from clogging + memory. + """ + requests = [] + responses = [] + + def add_response(self, response_obj): + self.responses.append(response_obj) + + def add_request(self, request_obj): + self.requests.append(request_obj) + + @property + def request(self): + if len(self.requests) == 0: + return None + else: + return self.requests[-1] + + @property + def response(self): + if len(self.responses) == 0: + return None + else: + return self.responses[-1] + + def clear(self): + del self.requests[:] + del self.responses[:] + +history = History() diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py new file mode 100644 index 0000000..298c3da --- /dev/null +++ b/jsonrpclib/jsonclass.py @@ -0,0 +1,145 @@ +import types +import inspect +import re +import traceback + +from jsonrpclib import config + +iter_types = [ + types.DictType, + types.ListType, + types.TupleType +] + +string_types = [ + types.StringType, + types.UnicodeType +] + +numeric_types = [ + types.IntType, + types.LongType, + types.FloatType +] + +value_types = [ + types.BooleanType, + types.NoneType +] + +supported_types = iter_types+string_types+numeric_types+value_types +invalid_module_chars = r'[^a-zA-Z0-9\_\.]' + +class TranslationError(Exception): + pass + +def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): + if not serialize_method: + serialize_method = config.serialize_method + if not ignore_attribute: + ignore_attribute = config.ignore_attribute + obj_type = type(obj) + # Parse / return default "types"... + if obj_type in numeric_types+string_types+value_types: + return obj + if obj_type in iter_types: + if obj_type in (types.ListType, types.TupleType): + new_obj = [] + for item in obj: + new_obj.append(dump(item, serialize_method, + ignore_attribute, ignore)) + if obj_type is types.TupleType: + new_obj = tuple(new_obj) + return new_obj + # It's a dict... + else: + new_obj = {} + for key, value in obj.iteritems(): + new_obj[key] = dump(value, serialize_method, + ignore_attribute, ignore) + return new_obj + # It's not a standard type, so it needs __jsonclass__ + module_name = inspect.getmodule(obj).__name__ + class_name = obj.__class__.__name__ + json_class = class_name + if module_name not in ['', '__main__']: + json_class = '%s.%s' % (module_name, json_class) + return_obj = {"__jsonclass__":[json_class,]} + # If a serialization method is defined.. + if serialize_method in dir(obj): + # Params can be a dict (keyword) or list (positional) + # Attrs MUST be a dict. + serialize = getattr(obj, serialize_method) + params, attrs = serialize() + return_obj['__jsonclass__'].append(params) + return_obj.update(attrs) + return return_obj + # Otherwise, try to figure it out + # Obviously, we can't assume to know anything about the + # parameters passed to __init__ + return_obj['__jsonclass__'].append([]) + attrs = {} + ignore_list = getattr(obj, ignore_attribute, [])+ignore + for attr_name, attr_value in obj.__dict__.iteritems(): + if type(attr_value) in supported_types and \ + attr_name not in ignore_list and \ + attr_value not in ignore_list: + attrs[attr_name] = dump(attr_value, serialize_method, + ignore_attribute, ignore) + return_obj.update(attrs) + return return_obj + +def load(obj): + if type(obj) in string_types+numeric_types+value_types: + return obj + if type(obj) is types.ListType: + return_list = [] + for entry in obj: + return_list.append(load(entry)) + return return_list + # Othewise, it's a dict type + if '__jsonclass__' not in obj.keys(): + return_dict = {} + for key, value in obj.iteritems(): + new_value = load(value) + return_dict[key] = new_value + return return_dict + # It's a dict, and it's a __jsonclass__ + orig_module_name = obj['__jsonclass__'][0] + params = obj['__jsonclass__'][1] + if orig_module_name == '': + raise TranslationError('Module name empty.') + json_module_clean = re.sub(invalid_module_chars, '', orig_module_name) + if json_module_clean != orig_module_name: + raise TranslationError('Module name %s has invalid characters.' % + orig_module_name) + json_module_parts = json_module_clean.split('.') + json_class = None + if len(json_module_parts) == 1: + # Local class name -- probably means it won't work + if json_module_parts[0] not in config.classes.keys(): + raise TranslationError('Unknown class or module %s.' % + json_module_parts[0]) + json_class = config.classes[json_module_parts[0]] + else: + json_class_name = json_module_parts.pop() + json_module_tree = '.'.join(json_module_parts) + try: + temp_module = __import__(json_module_tree) + except ImportError: + raise TranslationError('Could not import %s from module %s.' % + (json_class_name, json_module_tree)) + json_class = getattr(temp_module, json_class_name) + # Creating the object... + new_obj = None + if type(params) is types.ListType: + new_obj = json_class(*params) + elif type(params) is types.DictType: + new_obj = json_class(**params) + else: + raise TranslationError('Constructor args must be a dict or list.') + for key, value in obj.iteritems(): + if key == '__jsonclass__': + continue + setattr(new_obj, key, value) + return new_obj diff --git a/jsonrpclib/jsonrpclib.py b/jsonrpclib/jsonrpclib.py new file mode 100644 index 0000000..c423ab9 --- /dev/null +++ b/jsonrpclib/jsonrpclib.py @@ -0,0 +1,487 @@ +""" +Copyright 2009 Josh Marshall +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 + +# Library includes +from config import config +from history import history + +# 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.') + +#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 = config.user_agent + + 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 _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 + response_body += response + if self.verbose: + print 'body: %s' % response + if response_body == '': + # Notification + return None + 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 + if not version: + version = config.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) + 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) + return response + + 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): + # Even though this is verbatim, it doesn't support + # keyword arguments unless we rewrite it. + 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() + +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[:] + 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'): + self.faultCode = code + self.faultString = message + + def error(self): + return {'code':self.faultCode, 'message':self.faultString} + + def response(self, rpcid=None, version=None): + if not version: + version = config.version + return dumps(self, methodresponse=True, rpcid=rpcid, version=version) + + def __repr__(self): + return '' % (self.faultCode, self.faultString) + +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): + 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, '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: + 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: + 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('ERROR %s: %s' % (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