== '''JsonRpc''' == This is an easy-to-use implementation of a JSON-RPC handler for Django. '''Features:''' - Easy to use[[BR]] - Extensible[[BR]] - Automatic SMD generation[[BR]] - safe method resolution (ie, none of this stuff: [http://secunia.com/advisories/14128/])[[BR]] - No special urlconf or middleware magic[[BR]] [[BR]] [[BR]] '''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 # # Copyright (c) 2009, Ben Wilber (benwilber@gmail.com) # All rights reserved # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License. You should have # received a copy of the GPL license along with this program; if you # did not, you can find it at http://www.gnu.org/ # 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) }}}