1# A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751
2from webob import Request, Response
3from webob import exc
4from simplejson import loads, dumps
5import traceback
6import sys
7
8class 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
76class 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
97class _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
129class 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
137class 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
152class 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
163def 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
170def 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
192if __name__ == '__main__':
193    main()
194