aboutsummaryrefslogtreecommitdiff
path: root/budget
diff options
context:
space:
mode:
authorAlexis Metaireau <alexis@notmyidea.org>2011-09-11 22:11:36 +0200
committerAlexis Metaireau <alexis@notmyidea.org>2011-09-11 22:11:36 +0200
commit4bb96b28dee16e78d68f16f5bf158f1e879a0523 (patch)
treeefabe3db1bdf7b885c2d4f196d490d935105dc67 /budget
parent20f6f204cfc704b7486cfd9f36a643073fdc57fa (diff)
downloadihatemoney-mirror-4bb96b28dee16e78d68f16f5bf158f1e879a0523.zip
ihatemoney-mirror-4bb96b28dee16e78d68f16f5bf158f1e879a0523.tar.gz
ihatemoney-mirror-4bb96b28dee16e78d68f16f5bf158f1e879a0523.tar.bz2
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).
Diffstat (limited to 'budget')
-rw-r--r--budget/api.py64
-rw-r--r--budget/rest.py120
-rw-r--r--budget/run.py3
-rw-r--r--budget/utils.py11
4 files changed, 197 insertions, 1 deletions
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
+