summaryrefslogtreecommitdiff
path: root/ick2/apibase.py
blob: 46c17cfa6c75cbeb84f48842d625f8a9d0fa67c1 (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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Copyright (C) 2017-2019  Lars Wirzenius
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import ick2


import bottle


class APIbase:

    def __init__(self, state):
        assert (state is None or
                isinstance(state, ick2.MemoryStore) or
                isinstance(state, ick2.MuckStore))
        self._trans = ick2.TransactionalState(state)

    def get_routes(self, path):
        resource_path = '{}/<name:re:[^/+][^/]*?(/[^/+][^/]*?)*>'.format(path)
        return [
            {
                'method': 'POST',
                'path': path,
                'callback': self.POST(self.create),
            },
            {
                'method': 'GET',
                'path': path,
                'callback': self.GET(self.list),
            },
            {
                'method': 'GET',
                'path': resource_path,
                'callback': self.GET(self.show),
            },
            {
                'method': 'PUT',
                'path': resource_path,
                'callback': self.PUT(self.update),
            },
            {
                'method': 'DELETE',
                'path': resource_path,
                'callback': self.DELETE(self.delete),
            },
        ]

    # This is quite ugly. The apifw library we use as a wrapper around
    # Bottle should be doing this. But due to stupid reasons, it's
    # awkward to modify that library, for now, and anyway all of this
    # is going to be rewritten in a better programming language
    # eventually, so we take the past of least effort and increase
    # technical debt.
    def _get_token(self):
        v = bottle.request.get_header('Authorization', '')
        prefix = 'Bearer '
        if v.startswith(prefix):
            return v[len(prefix):]
        return None

    def GET(self, callback):
        def wrapper(content_type, body, **kwargs):
            ick2.log.log(
                'trace', msg_text='GET called', kwargs=kwargs,
                content_type=content_type, body=body)
            try:
                kwargs['token'] = self._get_token()
                if 'raw_uri_path' in kwargs:
                    del kwargs['raw_uri_path']
                body = callback(**kwargs)
            except ick2.ParametersMissing as e:
                ick2.log.log('error', msg_text=str(e), kwargs=kwargs)
                return ick2.conflict(str(e))
            except ick2.NotFound as e:
                ick2.log.log(
                    'error', msg_text='GET Not found', kwargs=kwargs,
                    exc_info=True)
                return ick2.not_found(e)
            if isinstance(body, dict):
                return ick2.OK(body)
            if isinstance(body, str):
                return ick2.text_plain(body)
            raise Exception('this must not happen')
        return wrapper

    def POST(self, callback):
        def wrapper(content_type, body, **kwargs):
            ick2.log.log(
                'trace', msg_text='POST called', kwargs=kwargs,
                content_type=content_type, body=body)
            try:
                kwargs['token'] = self._get_token()
                body = callback(body, **kwargs)
            except ick2.ExistsAlready as e:
                ick2.log.log('error', msg_text=str(e), kwargs=kwargs)
                return ick2.conflict(str(e))
            return ick2.created(body)
        return wrapper

    def PUT(self, callback):
        def wrapper(content_type, body, **kwargs):
            ick2.log.log(
                'trace', msg_text='PUT called', kwargs=kwargs,
                content_type=content_type, body=body)
            kwargs['token'] = self._get_token()
            if 'raw_uri_path' in kwargs:
                del kwargs['raw_uri_path']
            try:
                body = callback(body, **kwargs)
            except ick2.NotFound as e:
                ick2.log.log(
                    'warning', msg_text='PUT Not found', kwargs=kwargs)
                return ick2.not_found(e)
            ick2.log.log('trace', msg_text='returned body', body=repr(body))
            return ick2.OK(body)
        return wrapper

    def DELETE(self, callback):
        def wrapper(content_type, body, **kwargs):
            ick2.log.log(
                'trace', msg_text='DELETE called', kwargs=kwargs,
                content_type=content_type, body=body)
            try:
                kwargs['token'] = self._get_token()
                if 'raw_uri_path' in kwargs:
                    del kwargs['raw_uri_path']
                body = callback(**kwargs)
            except ick2.NotFound as e:
                ick2.log.log(
                    'warning', msg_text='DELETE Not found', kwargs=kwargs)
                return ick2.not_found(e)
            return ick2.OK(body)
        return wrapper

    def create(self, body, token=None, **kwargs):
        raise NotImplementedError()

    def update(self, body, name, token=None, **kwargs):
        raise NotImplementedError()

    def delete(self, name, token=None, **kwargs):
        raise NotImplementedError()

    def list(self, token=None, **kwargs):
        raise NotImplementedError()

    def show(self, name, token=None, **kwargs):
        raise NotImplementedError()


class ResourceApiBase(APIbase):

    def __init__(self, type_name, state):
        super().__init__(state)
        self._type_name = type_name

    def list(self, token=None, **kwargs):
        resources = self._trans.get_resources(token, self._type_name)
        return {
            self._type_name: [r.as_dict() for r in resources]
        }

    def show(self, name, token=None, **kwargs):
        return self._trans.get_resource(token, self._type_name, name).as_dict()

    def create(self, body, token=None, **kwargs):
        ick2.log.log(
            'trace', msg_text='create resource',
            resource_type=self._type_name,
            body=body, token=token, kwargs=kwargs)

        as_dict = self.mangle_new_resource(body)
        name = self.get_resource_name(as_dict)

        with self._trans.new(token, self._type_name, name) as resource:
            resource.from_dict(as_dict)

        return as_dict

    def mangle_new_resource(self, resource):  # pragma: no cover
        return resource

    def get_resource_name(self, resource):  # pragma: no cover
        raise NotImplementedError()

    def update(self, body, name, token=None, **kwargs):
        name = self.get_resource_name(body)

        with self._trans.modify(token, self._type_name, name) as resource:
            as_dict = self.mangle_updated_resource(resource.as_dict(), body)
            resource.from_dict(as_dict)

        return as_dict

    def mangle_updated_resource(self, old, new):  # pragma: no cover
        return new

    def delete(self, name, token=None, **kwargs):
        self._trans.remove_resource(token, self._type_name, name)