From 4bb96b28dee16e78d68f16f5bf158f1e879a0523 Mon Sep 17 00:00:00 2001
From: Alexis Metaireau <alexis@notmyidea.org>
Date: Sun, 11 Sep 2011 22:11:36 +0200
Subject: [PATCH] API first draft: utils. (related to #27)

Introduces the "rest" module, with reusable utils for flask applications (will be packaged as a flask extension later on).
---
 budget/api.py   |  64 ++++++++++++++++++++++++++
 budget/rest.py  | 120 ++++++++++++++++++++++++++++++++++++++++++++++++
 budget/run.py   |   3 +-
 budget/utils.py |  11 +++++
 4 files changed, 197 insertions(+), 1 deletion(-)
 create mode 100644 budget/api.py
 create mode 100644 budget/rest.py

diff --git a/budget/api.py b/budget/api.py
new file mode 100644
index 0000000..70864a6
--- /dev/null
+++ b/budget/api.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+from flask import *
+import werkzeug
+
+from models import db, Project, Person, Bill
+from utils import for_all_methods
+
+from rest import RESTResource, DefaultHandler, need_auth # FIXME make it an ext
+
+
+api = Blueprint("api", __name__, url_prefix="/api")
+
+def check_project(*args, **kwargs):
+    """Check the request for basic authentication for a given project.
+
+    Return the project if the authorization is good, False otherwise
+    """
+    auth = request.authorization
+
+    # project_id should be contained in kwargs and equal to the username
+    if auth and "project_id" in kwargs and \
+            auth.username == kwargs["project_id"]:
+        project = Project.query.get(auth.username)
+        if project.password == auth.password:
+            return project
+    return False
+
+
+class ProjectHandler(DefaultHandler):
+
+    def get(self, *args, **kwargs):
+        return "get"
+
+    def delete(self, *args, **kwargs):
+        return "delete"
+
+project_resource = RESTResource(
+    name="project",
+    route="/project", 
+    app=api, 
+    actions=["add", "update", "delete", "get"],
+    authentifier=check_project,
+    handler=ProjectHandler())
+
+# projects: add, delete, edit, get
+# GET /project/<id> → get
+# PUT /project/<id> → add & edit
+# DELETE /project/<id> → delete
+
+# project members: list, add, delete
+# GET /project/<id>/members → list
+# POST /project/<id>/members/ → add
+# PUT /project/<id>/members/<user_id> → edit
+# DELETE /project/<id>/members/<user_id> → delete
+
+# project bills: list, add, delete, edit, get
+# GET /project/<id>/bills → list
+# GET /project/<id>/bills/<bill_id> → get
+# DELETE /project/<id>/bills/<bill_id> → delete
+# POST /project/<id>/bills/ → add
+
+
+# GET, PUT, DELETE: /<id> : Get, update and delete
+# GET, POST: / Add & List
diff --git a/budget/rest.py b/budget/rest.py
new file mode 100644
index 0000000..f1de42f
--- /dev/null
+++ b/budget/rest.py
@@ -0,0 +1,120 @@
+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, actions, handler, authentifier):
+        """
+        :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.
+
+        :handler:
+            The handler instance which will handle the requests
+
+        :authentifier:
+            callable checking the authentication. If specified, all the 
+            methods will be checked against it.
+        """
+
+        self._route = route
+        self._handler = handler
+        self._name = name
+        self._identifier = "%s_id" % name
+        self._authentifier = authentifier
+
+        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._name)(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):
+    """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
+    """
+    def wrapper(func):
+        def wrapped(*args, **kwargs):
+            result = authentifier(*args, **kwargs)
+            if result:
+                if name:
+                    kwargs[name] = result
+                return func(*args, **kwargs)
+            else:
+                raise werkzeug.exceptions.Forbidden()
+        return wrapped
+    return wrapper
+
+
+class DefaultHandler(object):
+
+    def add(self, *args, **kwargs):
+        pass
+
+    def update(self, *args, **kwargs):
+        pass
+
+    def delete(self, *args, **kwargs):
+        pass
+
+    def list(self, *args, **kwargs):
+        pass
+
+    def get(self, *args, **kwargs):
+        pass
diff --git a/budget/run.py b/budget/run.py
index b1fad19..65c6591 100644
--- a/budget/run.py
+++ b/budget/run.py
@@ -1,11 +1,12 @@
 from web import main, db, mail
-import api
+from api import api
 
 from flask import *
 
 app = Flask(__name__)
 app.config.from_object("default_settings")
 app.register_blueprint(main)
+app.register_blueprint(api)
 
 # db
 db.init_app(app)
diff --git a/budget/utils.py b/budget/utils.py
index 262ebfe..8d67410 100644
--- a/budget/utils.py
+++ b/budget/utils.py
@@ -1,4 +1,6 @@
 from functools import wraps
+import inspect
+
 from flask import redirect, url_for, session, request
 from werkzeug.routing import HTTPException, RoutingException
 
@@ -34,3 +36,12 @@ class Redirect303(HTTPException, RoutingException):
 
     def get_response(self, environ):
         return redirect(self.new_url, 303)
+
+def for_all_methods(decorator):
+    """Apply a decorator to all the methods of a class"""
+    def decorate(cls):
+        for name, method in inspect.getmembers(cls, inspect.ismethod):
+            setattr(cls, name, decorator(method))
+        return cls
+    return decorate
+