blob: c038992f3c6e3eb800a873a2ba59c43aabd97c1c [file] [log] [blame]
# A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751
from webob import Request, Response
from webob import exc
from simplejson import loads, dumps
import traceback
import sys
class JsonRpcApp(object):
"""
Serve the given object via json-rpc (http://json-rpc.org/)
"""
def __init__(self, obj):
self.obj = obj
def __call__(self, environ, start_response):
req = Request(environ)
try:
resp = self.process(req)
except ValueError, e:
resp = exc.HTTPBadRequest(str(e))
except exc.HTTPException, e:
resp = e
return resp(environ, start_response)
def process(self, req):
if not req.method == 'POST':
raise exc.HTTPMethodNotAllowed(
"Only POST allowed",
allowed='POST')
try:
json = loads(req.body)
except ValueError, e:
raise ValueError('Bad JSON: %s' % e)
try:
method = json['method']
params = json['params']
id = json['id']
except KeyError, e:
raise ValueError(
"JSON body missing parameter: %s" % e)
if method.startswith('_'):
raise exc.HTTPForbidden(
"Bad method name %s: must not start with _" % method)
if not isinstance(params, list):
raise ValueError(
"Bad params %r: must be a list" % params)
try:
method = getattr(self.obj, method)
except AttributeError:
raise ValueError(
"No such method %s" % method)
try:
result = method(*params)
except:
text = traceback.format_exc()
exc_value = sys.exc_info()[1]
error_value = dict(
name='JSONRPCError',
code=100,
message=str(exc_value),
error=text)
return Response(
status=500,
content_type='application/json',
body=dumps(dict(result=None,
error=error_value,
id=id)))
return Response(
content_type='application/json',
body=dumps(dict(result=result,
error=None,
id=id)))
class ServerProxy(object):
"""
JSON proxy to a remote service.
"""
def __init__(self, url, proxy=None):
self._url = url
if proxy is None:
from wsgiproxy.exactproxy import proxy_exact_request
proxy = proxy_exact_request
self.proxy = proxy
def __getattr__(self, name):
if name.startswith('_'):
raise AttributeError(name)
return _Method(self, name)
def __repr__(self):
return '<%s for %s>' % (
self.__class__.__name__, self._url)
class _Method(object):
def __init__(self, parent, name):
self.parent = parent
self.name = name
def __call__(self, *args):
json = dict(method=self.name,
id=None,
params=list(args))
req = Request.blank(self.parent._url)
req.method = 'POST'
req.content_type = 'application/json'
req.body = dumps(json)
resp = req.get_response(self.parent.proxy)
if resp.status_code != 200 and not (
resp.status_code == 500
and resp.content_type == 'application/json'):
raise ProxyError(
"Error from JSON-RPC client %s: %s"
% (self.parent._url, resp.status),
resp)
json = loads(resp.body)
if json.get('error') is not None:
e = Fault(
json['error'].get('message'),
json['error'].get('code'),
json['error'].get('error'),
resp)
raise e
return json['result']
class ProxyError(Exception):
"""
Raised when a request via ServerProxy breaks
"""
def __init__(self, message, response):
Exception.__init__(self, message)
self.response = response
class Fault(Exception):
"""
Raised when there is a remote error
"""
def __init__(self, message, code, error, response):
Exception.__init__(self, message)
self.code = code
self.error = error
self.response = response
def __str__(self):
return 'Method error calling %s: %s\n%s' % (
self.response.request.url,
self.args[0],
self.error)
class DemoObject(object):
"""
Something interesting to attach to
"""
def add(self, *args):
return sum(args)
def average(self, *args):
return sum(args) / float(len(args))
def divide(self, a, b):
return a / b
def make_app(expr):
module, expression = expr.split(':', 1)
__import__(module)
module = sys.modules[module]
obj = eval(expression, module.__dict__)
return JsonRpcApp(obj)
def main(args=None):
import optparse
from wsgiref import simple_server
parser = optparse.OptionParser(
usage='%prog [OPTIONS] MODULE:EXPRESSION')
parser.add_option(
'-p', '--port', default='8080',
help='Port to serve on (default 8080)')
parser.add_option(
'-H', '--host', default='127.0.0.1',
help='Host to serve on (default localhost; 0.0.0.0 to make public)')
options, args = parser.parse_args()
if not args or len(args) > 1:
print 'You must give a single object reference'
parser.print_help()
sys.exit(2)
app = make_app(args[0])
server = simple_server.make_server(options.host, int(options.port), app)
print 'Serving on http://%s:%s' % (options.host, options.port)
server.serve_forever()
# Try python jsonrpc.py 'jsonrpc:DemoObject()'
if __name__ == '__main__':
main()