aboutsummaryrefslogtreecommitdiff
path: root/budget/rest.py
diff options
context:
space:
mode:
authorArnaud Bos <arnaud.tlse@gmail.com>2011-09-18 23:38:12 +0200
committerArnaud Bos <arnaud.tlse@gmail.com>2011-09-18 23:39:10 +0200
commit681f22f3e47c3fb75fdb1d858b179e945c952596 (patch)
treee83246b13d33f30083488b6913e96e261605f5e3 /budget/rest.py
parent89e1bbe134bc770d4a3f999a1329bd07522b07cf (diff)
parent20ab40690d74befcd8fc75f24f301759840bf43a (diff)
downloadihatemoney-mirror-681f22f3e47c3fb75fdb1d858b179e945c952596.zip
ihatemoney-mirror-681f22f3e47c3fb75fdb1d858b179e945c952596.tar.gz
ihatemoney-mirror-681f22f3e47c3fb75fdb1d858b179e945c952596.tar.bz2
Merge branch 'master' into auth-forms-usability
Diffstat (limited to 'budget/rest.py')
-rw-r--r--budget/rest.py158
1 files changed, 158 insertions, 0 deletions
diff --git a/budget/rest.py b/budget/rest.py
new file mode 100644
index 0000000..f237217
--- /dev/null
+++ b/budget/rest.py
@@ -0,0 +1,158 @@
+import json
+from flask import request
+import werkzeug
+
+class RESTResource(object):
+ """Represents a REST resource, with the different HTTP verbs"""
+ _NEED_ID = ["get", "update", "delete"]
+ _VERBS = {"get": "GET",
+ "update": "PUT",
+ "delete": "DELETE",
+ "list": "GET",
+ "add": "POST",}
+
+ def __init__(self, name, route, app, handler, authentifier=None,
+ actions=None, inject_name=None):
+ """
+ :name:
+ name of the resource. This is being used when registering
+ the route, for its name and for the name of the id parameter
+ that will be passed to the views
+
+ :route:
+ Default route for this resource
+
+ :app:
+ Application to register the routes onto
+
+ :actions:
+ Authorized actions. Optional. None means all.
+
+ :handler:
+ The handler instance which will handle the requests
+
+ :authentifier:
+ callable checking the authentication. If specified, all the
+ methods will be checked against it.
+ """
+ if not actions:
+ actions = self._VERBS.keys()
+
+ self._route = route
+ self._handler = handler
+ self._name = name
+ self._identifier = "%s_id" % name
+ self._authentifier = authentifier
+ self._inject_name = inject_name # FIXME
+
+ for action in actions:
+ self.add_url_rule(app, action)
+
+ def _get_route_for(self, action):
+ """Return the complete URL for this action.
+
+ Basically:
+
+ - get, update and delete need an id
+ - add and list does not
+ """
+ route = self._route
+
+ if action in self._NEED_ID:
+ route += "/<%s>" % self._identifier
+
+ return route
+
+ def add_url_rule(self, app, action):
+ """Registers a new url to the given application, regarding
+ the action.
+ """
+ method = getattr(self._handler, action)
+
+ # decorate the view
+ if self._authentifier:
+ method = need_auth(self._authentifier,
+ self._inject_name or self._name)(method)
+
+ method = serialize(method)
+
+ app.add_url_rule(
+ self._get_route_for(action),
+ "%s_%s" % (self._name, action),
+ method,
+ methods=[self._VERBS.get(action, "GET")])
+
+
+def need_auth(authentifier, name=None, remove_attr=True):
+ """Decorator checking that the authentifier does not returns false in
+ the current context.
+
+ If the request is authorized, the object returned by the authentifier
+ is added to the kwargs of the method.
+
+ If not, issue a 403 Forbidden error
+
+ :authentifier:
+ The callable to check the context onto.
+
+ :name:
+ **Optional**, name of the argument to put the object into.
+ If it is not provided, nothing will be added to the kwargs
+ of the decorated function
+
+ :remove_attr:
+ Remove or not the `*name*_id` from the kwargs before calling the
+ function
+ """
+ def wrapper(func):
+ def wrapped(*args, **kwargs):
+ result = authentifier(*args, **kwargs)
+ if result:
+ if name:
+ kwargs[name] = result
+ if remove_attr:
+ del kwargs["%s_id" % name]
+ return func(*args, **kwargs)
+ else:
+ return 403, "Forbidden"
+ return wrapped
+ return wrapper
+
+# serializers
+
+def serialize(func):
+ """If the object returned by the view is not already a Response, serialize
+ it using the ACCEPT header and return it.
+ """
+ def wrapped(*args, **kwargs):
+ # get the mimetype
+ mime = request.accept_mimetypes.best_match(SERIALIZERS.keys())
+ data = func(*args, **kwargs)
+ serializer = SERIALIZERS[mime]
+
+ status = 200
+ if len(data) == 2:
+ status, data = data
+
+ # serialize it
+ return werkzeug.Response(serializer.encode(data),
+ status=status, mimetype=mime)
+
+ return wrapped
+
+
+class JSONEncoder(json.JSONEncoder):
+ """Subclass of the default encoder to support custom objects"""
+ def default(self, o):
+ if hasattr(o, "_to_serialize"):
+ # build up the object
+ data = {}
+ for attr in o._to_serialize:
+ data[attr] = getattr(o, attr)
+ return data
+ elif hasattr(o, "isoformat"):
+ return o.isoformat()
+ else:
+ return json.JSONEncoder.default(self, o)
+
+SERIALIZERS = {"text/json": JSONEncoder()}