# 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()