summaryrefslogtreecommitdiff
path: root/jsonrpclib
diff options
context:
space:
mode:
authorcatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>2010-09-04 21:44:53 +0000
committercatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>2010-09-04 21:44:53 +0000
commit1cb797f048ae5131a01086a93d87952c03e8abe7 (patch)
treec72fa6cb0dc82a2d2d4940f1cb77e5fb92d4f5cd /jsonrpclib
parent9cb9396c901daeeb9bf47675bbce707671b5e6af (diff)
downloadjsonrpclib-1cb797f048ae5131a01086a93d87952c03e8abe7.tar.gz
Added unit tests, renamed to follow absolute importing guidelines, and made minor corrections introduced by unit tests.
git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@18 ae587032-bbab-11de-869a-473eb4776397
Diffstat (limited to 'jsonrpclib')
-rw-r--r--jsonrpclib/SimpleJSONRPCServer.py61
-rw-r--r--jsonrpclib/__init__.py9
-rw-r--r--jsonrpclib/config.py9
-rw-r--r--jsonrpclib/history.py9
-rw-r--r--jsonrpclib/jsonrpclib.py487
5 files changed, 62 insertions, 513 deletions
diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py
index f049f85..75193b2 100644
--- a/jsonrpclib/SimpleJSONRPCServer.py
+++ b/jsonrpclib/SimpleJSONRPCServer.py
@@ -8,20 +8,35 @@ 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
+
+def validate_request(request):
+ if type(request) is not types.DictType:
+ fault = Fault(
+ -32600, 'Request must be {}, not %s.' % type(request)
+ )
+ return fault
+ rpcid = request.get('id', None)
+ version = get_version(request)
+ if not version:
+ fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid)
+ return fault
+ request.setdefault('params', [])
+ method = request.get('method', None)
+ params = request.get('params')
+ param_types = (types.ListType, types.DictType, types.TupleType)
+ if not method or type(method) not in types.StringTypes or \
+ type(params) not in param_types:
+ fault = Fault(
+ -32600, 'Invalid request parameters or method.', rpcid=rpcid
+ )
+ return fault
+ return True
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
@@ -34,34 +49,42 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
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)
+ except Exception, e:
+ fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e))
response = fault.response()
return response
+ if not request:
+ fault = Fault(-32600, 'Request invalid -- no request data.')
+ return fault.response()
if type(request) is types.ListType:
# This SHOULD be a batch, by spec
responses = []
for req_entry in request:
+ result = validate_request(req_entry)
+ if type(result) is Fault:
+ responses.append(result.response())
+ continue
resp_entry = self._marshaled_single_dispatch(req_entry)
if resp_entry is not None:
responses.append(resp_entry)
- response = '[%s]' % ','.join(responses)
- else:
+ if len(responses) > 0:
+ response = '[%s]' % ','.join(responses)
+ else:
+ response = ''
+ else:
+ result = validate_request(request)
+ if type(result) is Fault:
+ return result.response()
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)
+ method = request.get('method')
+ params = request.get('params')
try:
response = self._dispatch(method, params)
except:
diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py
index 1fd4dc0..6e884b8 100644
--- a/jsonrpclib/__init__.py
+++ b/jsonrpclib/__init__.py
@@ -1,3 +1,6 @@
-from jsonrpclib import *
-from config import config
-from history import history
+from jsonrpclib.config import Config
+config = Config.instance()
+from jsonrpclib.history import History
+history = History.instance()
+from jsonrpclib.jsonrpc import Server, MultiCall, Fault
+from jsonrpclib.jsonrpc import ProtocolError, loads, dumps
diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py
index 1f9d7d8..4d28f1b 100644
--- a/jsonrpclib/config.py
+++ b/jsonrpclib/config.py
@@ -29,5 +29,10 @@ class Config(object):
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
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ if not cls._instance:
+ cls._instance = cls()
+ return cls._instance
diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py
index ec53235..e6a01cf 100644
--- a/jsonrpclib/history.py
+++ b/jsonrpclib/history.py
@@ -8,6 +8,13 @@ class History(object):
"""
requests = []
responses = []
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ if not cls._instance:
+ cls._instance = cls()
+ return cls._instance
def add_response(self, response_obj):
self.responses.append(response_obj)
@@ -32,5 +39,3 @@ class History(object):
def clear(self):
del self.requests[:]
del self.responses[:]
-
-history = History()
diff --git a/jsonrpclib/jsonrpclib.py b/jsonrpclib/jsonrpclib.py
deleted file mode 100644
index 01d61b9..0000000
--- a/jsonrpclib/jsonrpclib.py
+++ /dev/null
@@ -1,487 +0,0 @@
-"""
-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 '<Fault %s: %s>' % (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((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