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 +