aboutsummaryrefslogtreecommitdiff
path: root/budget/rest.py
blob: 992a61e979e5d3f06e090de1343c0f213797a1c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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 401 Unauthorized 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 401, "Unauthorized"
        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()) or "text/json"
        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()}