Changes between Initial Version and Version 1 of Jsonrpc


Ignore:
Timestamp:
Jun 3, 2009, 12:00:43 PM (15 years ago)
Author:
benw
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Jsonrpc

    v1 v1  
     1
     2== '''JsonRpc''' ==
     3
     4This is an easy-to-use implementation of a JSON-RPC handler for Django.
     5
     6'''Features:'''
     7
     8- Easy to use[[BR]]
     9- Extensible[[BR]]
     10- Automatic SMD generation[[BR]]
     11- safe method resolution (ie, none of this stuff: [http://secunia.com/advisories/14128/])[[BR]]
     12- No special urlconf or middleware magic[[BR]]
     13[[BR]]
     14[[BR]]
     15'''Example Django Usage'''
     16{{{
     17myproject/myapp/views.py:
     18
     19def my_rpc_view(request):
     20
     21    from jsonrpc import JsonRpc
     22    class MyRpcMethods(object):
     23
     24        url = reverse("myapp-rpc")
     25
     26        public__add(self, x, y):
     27            return x+y
     28
     29        public__subtract(self, x, y):
     30            return x-y
     31
     32        @staticmethod
     33        public__multiply(x, y):
     34            return x*y
     35
     36        public__find_person(self, attribs):
     37
     38            filters = {}
     39            for key in attribs.keys():
     40                if key in ("first_name", "last_name"):
     41                    filters[key] = attribs.get(key)
     42
     43            return Person.objects.filter(**filters).values()
     44
     45    rpc = JsonRpc( MyRpcMethods() )
     46    result = rpc.handle_request(request)
     47
     48    return HttpResponse(result)
     49
     50}}}
     51
     52'''Example Javascript client usage'''
     53{{{
     54
     55var rpc = new dojo.rpc.JsonService("/myapp/rpc/");
     56
     57rpc.add(3,4).addCallback(function(result) {
     58    console.log(result);
     59});
     60>>> 7
     61
     62rpc.callRemote("subtract", [9,5]).addCallback(function(result) {
     63    console.log(result);
     64});
     65>>> 4
     66
     67rpc.multiply(5, 4).addCallback(function(result) {
     68    console.log(result);
     69});
     70>>> 20
     71
     72rpc.find_person({last_name: "Baggins"}).addCallback(function(result) {
     73    dojo.forEach(result, function(person) {
     74      console.log("Found: " + person.first_name, person.last_name);
     75    });
     76});
     77>>> Found: Bilbo Baggins
     78>>> Found: Frodo Baggins
     79
     80}}}
     81
     82'''JsonRpc'''
     83{{{
     84jsonrpc.py
     85
     86class JsonRpc(object):
     87
     88    def __init__(self, instance=None, url=None, allow_errors=True, auto_smd=True):
     89        """
     90            'instance' is required and is an object containing public
     91            RPC methods (like what you would pass to register_instance in SimpleXMLRPCServer)
     92            Passing arbitrary functions (register_function) isn't supported yet
     93
     94            'url' is optional if given as an attibute of the instance.  It's where the JSON-RPC
     95            client posts its RPC requests
     96
     97            'allow_errors' tells the serializer to report exception values under the key 'error' in
     98            the result dict back to the client.  If set to False and an exception occurs, the client
     99            will just get: {'error': 'error'}
     100
     101            'auto_smd' lets us introspect the instance to find the RPC methods and generate the
     102            the appropriate method/signature descriptions.
     103
     104            If the instance contains a dict attribute 'methods', then that will be used instead
     105
     106            class MyRpcMethods(object):
     107                methods = {'add': ['x','y']}
     108
     109                # Will be reported
     110                public__add(self, x,y): return x+y
     111
     112                # Wont be reported
     113                public__subtract(self, x,y): return x-y
     114
     115            This is useful if you want to filter the reported methods based on some other condition
     116            than the 'public__' prefix  --- be advised that a clever client can still call non-reported
     117            methods via a stringified method name ie with Dojo's 'callRemote' method.  So this shouldn't
     118            be used for securty.  See the 'dispatch' method below if you want to secure your public RPC methods.
     119
     120        """
     121
     122        if instance is None:
     123            raise Exception("'instance' needed")
     124        self.instance = instance
     125
     126        if url is None:
     127            if hasattr(self.instance, "url"):
     128                url = str(self.instance.url)
     129            else:
     130                raise Exception("'url' needed")
     131        self.url = url
     132
     133        self.allow_errors = allow_errors
     134
     135        self.smd = {
     136            "serviceType": "JSON-RPC",
     137            "serviceURL": self.url,
     138            "methods": []
     139        }
     140
     141        if hasattr(self.instance, "methods") and \
     142            isinstance(self.instance.methods, dict):
     143            for key in self.instance.methods.keys():
     144                params = [ {"name": val} for val in self.instance.methods[key] ]
     145                self.smd["methods"].append({
     146                    "name": key,
     147                    "parameters": params
     148                })
     149        elif auto_smd:
     150            public_methods = [ m for m in dir(instance) \
     151                if ( m.startswith("public__") and callable(getattr(instance, m)) ) ]
     152            if public_methods:
     153                try:
     154                    import inspect
     155                    for method in public_methods:
     156                        sig = inspect.getargspec(getattr(instance, method))
     157                        self.smd["methods"].append({
     158                            "name": method.replace("public__", ""),
     159                            "parameters": [ {"name": val} for val in sig.args if (val != "self") ]
     160                        })
     161                except:
     162                    pass
     163                   
     164
     165    def dispatch(self, method, params):
     166        """
     167            The default dispatcher method.
     168
     169            It looks for a method in the supplied instance
     170            of the form 'public__method' and calls it
     171            with the params.  If a method 'dispatch' exists
     172            in the supplied instance, then it will be used
     173            instead.
     174
     175            Example:
     176
     177                class MyRPCMethods(object):
     178
     179                    def dispatch(self, method, params):
     180
     181                        if method == 'get_soup':
     182                            return 'No soup for you!'
     183                        elif method in ('get_cake', 'get_pie'):
     184                            return getattr(self, method)(*params)
     185                        else:
     186                            return 'Not on the menu!'
     187                    ...
     188        """
     189
     190        if hasattr(self.instance, "dispatch") and \
     191            callable(self.instance.dispatch):
     192            return self.instance.dispatch(method, params)
     193        else:
     194            if hasattr(self.instance, "public__" + method):
     195                return getattr(self.instance, "public__" + method)(*params)
     196            else:
     197                return "method not supported"
     198
     199    def serialize(self, raw_post_data):
     200        """
     201            Serializes the POST data from request.raw_post_data into JSON,
     202            calls the dispatcher to route the method call, then serializes
     203            and returns the response.
     204
     205            TODO: Better error handling!
     206        """
     207
     208        raw_request        = simplejson.loads(raw_post_data)
     209        request_id        = raw_request.get("id", 0)
     210        request_method    = raw_request.get("method")
     211        request_params    = raw_request.get("params", [])
     212
     213        response        = {"id": request_id}
     214
     215        try:
     216            response["result"] = self.dispatch(request_method, request_params)
     217        except:
     218            if self.allow_errors:
     219                from sys import exc_type, exc_value
     220                response["error"] = "%s: %s" % (exc_type, exc_value)
     221            else:
     222                response["error"] = "error"
     223
     224        return simplejson.dumps(response)
     225
     226    def handle_request(self, request):
     227        """
     228            Takes an HttpRequest object and returns a JSON data structure
     229            either of the results of an RPC method call or
     230            the SMD if it was a GET request, or the POST was empty.
     231
     232            In most cases, this can be returned directly to the client:
     233
     234            return HttpResponse(rpc.handle_request(request))
     235        """
     236
     237        if request.method == "POST" and \
     238            len(request.POST) > 0:
     239            return self.serialize(request.raw_post_data)
     240        else:
     241            return simplejson.dumps(self.smd)
     242}}}
     243       
Back to Top