1 # A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751 2 from webob import Request, Response 3 from webob import exc 4 from simplejson import loads, dumps 5 import traceback 6 import sys 7 8 class JsonRpcApp(object): 9 """ 10 Serve the given object via json-rpc (http://json-rpc.org/) 11 """ 12 13 def __init__(self, obj): 14 self.obj = obj 15 16 def __call__(self, environ, start_response): 17 req = Request(environ) 18 try: 19 resp = self.process(req) 20 except ValueError, e: 21 resp = exc.HTTPBadRequest(str(e)) 22 except exc.HTTPException, e: 23 resp = e 24 return resp(environ, start_response) 25 26 def process(self, req): 27 if not req.method == 'POST': 28 raise exc.HTTPMethodNotAllowed( 29 "Only POST allowed", 30 allowed='POST') 31 try: 32 json = loads(req.body) 33 except ValueError, e: 34 raise ValueError('Bad JSON: %s' % e) 35 try: 36 method = json['method'] 37 params = json['params'] 38 id = json['id'] 39 except KeyError, e: 40 raise ValueError( 41 "JSON body missing parameter: %s" % e) 42 if method.startswith('_'): 43 raise exc.HTTPForbidden( 44 "Bad method name %s: must not start with _" % method) 45 if not isinstance(params, list): 46 raise ValueError( 47 "Bad params %r: must be a list" % params) 48 try: 49 method = getattr(self.obj, method) 50 except AttributeError: 51 raise ValueError( 52 "No such method %s" % method) 53 try: 54 result = method(*params) 55 except: 56 text = traceback.format_exc() 57 exc_value = sys.exc_info()[1] 58 error_value = dict( 59 name='JSONRPCError', 60 code=100, 61 message=str(exc_value), 62 error=text) 63 return Response( 64 status=500, 65 content_type='application/json', 66 body=dumps(dict(result=None, 67 error=error_value, 68 id=id))) 69 return Response( 70 content_type='application/json', 71 body=dumps(dict(result=result, 72 error=None, 73 id=id))) 74 75 76 class ServerProxy(object): 77 """ 78 JSON proxy to a remote service. 79 """ 80 81 def __init__(self, url, proxy=None): 82 self._url = url 83 if proxy is None: 84 from wsgiproxy.exactproxy import proxy_exact_request 85 proxy = proxy_exact_request 86 self.proxy = proxy 87 88 def __getattr__(self, name): 89 if name.startswith('_'): 90 raise AttributeError(name) 91 return _Method(self, name) 92 93 def __repr__(self): 94 return '<%s for %s>' % ( 95 self.__class__.__name__, self._url) 96 97 class _Method(object): 98 99 def __init__(self, parent, name): 100 self.parent = parent 101 self.name = name 102 103 def __call__(self, *args): 104 json = dict(method=self.name, 105 id=None, 106 params=list(args)) 107 req = Request.blank(self.parent._url) 108 req.method = 'POST' 109 req.content_type = 'application/json' 110 req.body = dumps(json) 111 resp = req.get_response(self.parent.proxy) 112 if resp.status_code != 200 and not ( 113 resp.status_code == 500 114 and resp.content_type == 'application/json'): 115 raise ProxyError( 116 "Error from JSON-RPC client %s: %s" 117 % (self.parent._url, resp.status), 118 resp) 119 json = loads(resp.body) 120 if json.get('error') is not None: 121 e = Fault( 122 json['error'].get('message'), 123 json['error'].get('code'), 124 json['error'].get('error'), 125 resp) 126 raise e 127 return json['result'] 128 129 class ProxyError(Exception): 130 """ 131 Raised when a request via ServerProxy breaks 132 """ 133 def __init__(self, message, response): 134 Exception.__init__(self, message) 135 self.response = response 136 137 class Fault(Exception): 138 """ 139 Raised when there is a remote error 140 """ 141 def __init__(self, message, code, error, response): 142 Exception.__init__(self, message) 143 self.code = code 144 self.error = error 145 self.response = response 146 def __str__(self): 147 return 'Method error calling %s: %s\n%s' % ( 148 self.response.request.url, 149 self.args[0], 150 self.error) 151 152 class DemoObject(object): 153 """ 154 Something interesting to attach to 155 """ 156 def add(self, *args): 157 return sum(args) 158 def average(self, *args): 159 return sum(args) / float(len(args)) 160 def divide(self, a, b): 161 return a / b 162 163 def make_app(expr): 164 module, expression = expr.split(':', 1) 165 __import__(module) 166 module = sys.modules[module] 167 obj = eval(expression, module.__dict__) 168 return JsonRpcApp(obj) 169 170 def main(args=None): 171 import optparse 172 from wsgiref import simple_server 173 parser = optparse.OptionParser( 174 usage='%prog [OPTIONS] MODULE:EXPRESSION') 175 parser.add_option( 176 '-p', '--port', default='8080', 177 help='Port to serve on (default 8080)') 178 parser.add_option( 179 '-H', '--host', default='127.0.0.1', 180 help='Host to serve on (default localhost; 0.0.0.0 to make public)') 181 options, args = parser.parse_args() 182 if not args or len(args) > 1: 183 print 'You must give a single object reference' 184 parser.print_help() 185 sys.exit(2) 186 app = make_app(args[0]) 187 server = simple_server.make_server(options.host, int(options.port), app) 188 print 'Serving on http://%s:%s' % (options.host, options.port) 189 server.serve_forever() 190 # Try python jsonrpc.py 'jsonrpc:DemoObject()' 191 192 if __name__ == '__main__': 193 main() 194