Code


Version 1 (modified by benw, 5 years ago) (diff)

--

JsonRpc

This is an easy-to-use implementation of a JSON-RPC handler for Django.

Features:



Example Django Usage

myproject/myapp/views.py:

def my_rpc_view(request):

    from jsonrpc import JsonRpc
    class MyRpcMethods(object):

        url = reverse("myapp-rpc")

        public__add(self, x, y):
            return x+y

        public__subtract(self, x, y):
            return x-y

        @staticmethod
        public__multiply(x, y):
            return x*y

        public__find_person(self, attribs):

            filters = {}
            for key in attribs.keys():
                if key in ("first_name", "last_name"):
                    filters[key] = attribs.get(key)

            return Person.objects.filter(**filters).values()

    rpc = JsonRpc( MyRpcMethods() )
    result = rpc.handle_request(request)

    return HttpResponse(result)

Example Javascript client usage

var rpc = new dojo.rpc.JsonService("/myapp/rpc/");

rpc.add(3,4).addCallback(function(result) {
    console.log(result);
});
>>> 7

rpc.callRemote("subtract", [9,5]).addCallback(function(result) {
    console.log(result);
});
>>> 4

rpc.multiply(5, 4).addCallback(function(result) {
    console.log(result);
});
>>> 20

rpc.find_person({last_name: "Baggins"}).addCallback(function(result) {
    dojo.forEach(result, function(person) {
      console.log("Found: " + person.first_name, person.last_name);
    });
});
>>> Found: Bilbo Baggins
>>> Found: Frodo Baggins

JsonRpc

jsonrpc.py

class JsonRpc(object):

    def __init__(self, instance=None, url=None, allow_errors=True, auto_smd=True):
        """
            'instance' is required and is an object containing public
            RPC methods (like what you would pass to register_instance in SimpleXMLRPCServer)
            Passing arbitrary functions (register_function) isn't supported yet

            'url' is optional if given as an attibute of the instance.  It's where the JSON-RPC
            client posts its RPC requests

            'allow_errors' tells the serializer to report exception values under the key 'error' in
            the result dict back to the client.  If set to False and an exception occurs, the client
            will just get: {'error': 'error'}

            'auto_smd' lets us introspect the instance to find the RPC methods and generate the
            the appropriate method/signature descriptions.

            If the instance contains a dict attribute 'methods', then that will be used instead

            class MyRpcMethods(object):
                methods = {'add': ['x','y']}

                # Will be reported
                public__add(self, x,y): return x+y

                # Wont be reported
                public__subtract(self, x,y): return x-y

            This is useful if you want to filter the reported methods based on some other condition
            than the 'public__' prefix  --- be advised that a clever client can still call non-reported
            methods via a stringified method name ie with Dojo's 'callRemote' method.  So this shouldn't
            be used for securty.  See the 'dispatch' method below if you want to secure your public RPC methods.

        """

        if instance is None:
            raise Exception("'instance' needed")
        self.instance = instance

        if url is None:
            if hasattr(self.instance, "url"):
                url = str(self.instance.url)
            else:
                raise Exception("'url' needed")
        self.url = url

        self.allow_errors = allow_errors

        self.smd = {
            "serviceType": "JSON-RPC",
            "serviceURL": self.url,
            "methods": []
        }

        if hasattr(self.instance, "methods") and \
            isinstance(self.instance.methods, dict):
            for key in self.instance.methods.keys():
                params = [ {"name": val} for val in self.instance.methods[key] ]
                self.smd["methods"].append({
                    "name": key,
                    "parameters": params
                })
        elif auto_smd:
            public_methods = [ m for m in dir(instance) \
                if ( m.startswith("public__") and callable(getattr(instance, m)) ) ]
            if public_methods:
                try:
                    import inspect
                    for method in public_methods:
                        sig = inspect.getargspec(getattr(instance, method))
                        self.smd["methods"].append({
                            "name": method.replace("public__", ""),
                            "parameters": [ {"name": val} for val in sig.args if (val != "self") ]
                        })
                except:
                    pass
                    

    def dispatch(self, method, params):
        """
            The default dispatcher method.

            It looks for a method in the supplied instance
            of the form 'public__method' and calls it
            with the params.  If a method 'dispatch' exists
            in the supplied instance, then it will be used
            instead.

            Example:

                class MyRPCMethods(object):

                    def dispatch(self, method, params):

                        if method == 'get_soup':
                            return 'No soup for you!'
                        elif method in ('get_cake', 'get_pie'):
                            return getattr(self, method)(*params)
                        else:
                            return 'Not on the menu!'
                    ...
        """

        if hasattr(self.instance, "dispatch") and \
            callable(self.instance.dispatch):
            return self.instance.dispatch(method, params)
        else:
            if hasattr(self.instance, "public__" + method):
                return getattr(self.instance, "public__" + method)(*params)
            else:
                return "method not supported"

    def serialize(self, raw_post_data):
        """
            Serializes the POST data from request.raw_post_data into JSON,
            calls the dispatcher to route the method call, then serializes
            and returns the response.

            TODO: Better error handling!
        """

        raw_request        = simplejson.loads(raw_post_data)
        request_id        = raw_request.get("id", 0)
        request_method    = raw_request.get("method")
        request_params    = raw_request.get("params", [])

        response        = {"id": request_id}

        try:
            response["result"] = self.dispatch(request_method, request_params)
        except:
            if self.allow_errors:
                from sys import exc_type, exc_value
                response["error"] = "%s: %s" % (exc_type, exc_value)
            else:
                response["error"] = "error"

        return simplejson.dumps(response)

    def handle_request(self, request):
        """
            Takes an HttpRequest object and returns a JSON data structure
            either of the results of an RPC method call or
            the SMD if it was a GET request, or the POST was empty.

            In most cases, this can be returned directly to the client:

            return HttpResponse(rpc.handle_request(request))
        """

        if request.method == "POST" and \
            len(request.POST) > 0:
            return self.serialize(request.raw_post_data)
        else:
            return simplejson.dumps(self.smd)