Browse Source

initial code

Bastien Sevajol 6 years ago
commit
1f4e423d4f
7 changed files with 512 additions and 0 deletions
  1. 5 0
      .gitignore
  2. 16 0
      example.py
  3. 58 0
      example_a.py
  4. 63 0
      example_b.py
  5. 36 0
      hapic/__init__.py
  6. 332 0
      hapic/hapic.py
  7. 2 0
      requirements.txt

+ 5 - 0
.gitignore View File

@@ -0,0 +1,5 @@
1
+.idea
2
+*.pyc
3
+__pycache__
4
+*~
5
+/venv

+ 16 - 0
example.py View File

@@ -0,0 +1,16 @@
1
+# -*- coding: utf-8 -*-
2
+import marshmallow
3
+
4
+
5
+class HelloResponseSchema(marshmallow.Schema):
6
+    sentence = marshmallow.fields.String(required=True)
7
+    name = marshmallow.fields.String(required=True)
8
+    color = marshmallow.fields.String(required=False)
9
+
10
+
11
+class HelloPathSchema(marshmallow.Schema):
12
+    name = marshmallow.fields.String(required=True)
13
+
14
+
15
+class HelloJsonSchema(marshmallow.Schema):
16
+    color = marshmallow.fields.String(required=True)

+ 58 - 0
example_a.py View File

@@ -0,0 +1,58 @@
1
+# -*- coding: utf-8 -*-
2
+import bottle
3
+import hapic
4
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema
5
+from hapic.hapic import HapicData
6
+
7
+app = bottle.Bottle()
8
+
9
+
10
+def bob(f):
11
+    def boby(*args, **kwargs):
12
+        return f(*args, **kwargs)
13
+    return boby
14
+
15
+
16
+@hapic.with_api_doc()
17
+@hapic.ext.bottle.bottle_context()
18
+@hapic.input_path(HelloPathSchema())
19
+@hapic.output(HelloResponseSchema())
20
+def hello(name: str, hapic_data: HapicData):
21
+    return {
22
+        'sentence': 'Hello !',
23
+        'name': name,
24
+    }
25
+
26
+
27
+@hapic.with_api_doc()
28
+@hapic.ext.bottle.bottle_context()
29
+@hapic.input_path(HelloPathSchema())
30
+@hapic.input_body(HelloJsonSchema())
31
+@hapic.output(HelloResponseSchema())
32
+@bob
33
+def hello2(name: str, hapic_data: HapicData):
34
+    return {
35
+        'sentence': 'Hello !',
36
+        'name': name,
37
+        'color': hapic_data.body.get('color'),
38
+    }
39
+
40
+kwargs = {'validated_data': {'name': 'bob'}, 'name': 'bob'}
41
+
42
+
43
+@hapic.with_api_doc()
44
+@hapic.ext.bottle.bottle_context()
45
+@hapic.output(HelloResponseSchema())
46
+def hello3(name: str, hapic_data: HapicData):
47
+    return {
48
+        'sentence': 'Hello !',
49
+        'name': name,
50
+    }
51
+
52
+
53
+app.route('/hello/<name>', callback=hello)
54
+app.route('/hello2/<name>', callback=hello2, method='POST')
55
+app.route('/hello3/<name>', callback=hello3)
56
+
57
+hapic.generate_doc(app)
58
+app.run(host='localhost', port=8080, debug=True)

+ 63 - 0
example_b.py View File

@@ -0,0 +1,63 @@
1
+# -*- coding: utf-8 -*-
2
+import bottle
3
+import hapic
4
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema
5
+from hapic.hapic import MarshmallowOutputProcessor, BottleContext, \
6
+    MarshmallowPathInputProcessor, MarshmallowJsonInputProcessor
7
+
8
+
9
+def bob(f):
10
+    def boby(*args, **kwargs):
11
+        return f
12
+    return boby
13
+
14
+
15
+@hapic.with_api_doc_bis()
16
+@bottle.route('/hello/<name>')
17
+@hapic.input(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
18
+@hapic.output(HelloResponseSchema(), MarshmallowOutputProcessor())
19
+@bob
20
+def hello(name: str):
21
+    return "Hello {}!".format(name)
22
+
23
+
24
+@hapic.with_api_doc_bis()
25
+@bottle.route('/hello2/<name>')
26
+@hapic.input(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
27
+@hapic.input(HelloJsonSchema(), MarshmallowJsonInputProcessor(), context=BottleContext())  # nopep8
28
+@hapic.output(HelloResponseSchema())
29
+@bob
30
+def hello2(name: str):
31
+    return "Hello {}!".format(name)
32
+
33
+
34
+@hapic.with_api_doc_bis()
35
+@bottle.route('/hello3/<name>')
36
+@hapic.output(HelloResponseSchema())
37
+def hello3(name: str):
38
+    return "Hello {}!".format(name)
39
+
40
+hapic.generate_doc()
41
+bottle.run(host='localhost', port=8080, debug=True)
42
+
43
+
44
+
45
+
46
+
47
+@bottle.route('/hello/<name>')
48
+@hapic.input_body(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
49
+@hapic.input_header(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
50
+@hapic.input_query(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
51
+@hapic.input_path(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
52
+@hapic.output(HelloResponseSchema(), MarshmallowOutputProcessor())
53
+def hello(name: str, hapic_data):
54
+    return "Hello {}!".format(name)
55
+
56
+
57
+@hapic.with_api_doc_bis()
58
+@bottle.route('/hello/<name>')
59
+@hapic.input(HelloPathSchema(), MarshmallowPathInputProcessor(), context=BottleContext())  # nopep8
60
+@hapic.output(HelloResponseSchema(), MarshmallowOutputProcessor())
61
+@bob
62
+def hello(name: str):
63
+    return "Hello {}!".format(name)

+ 36 - 0
hapic/__init__.py View File

@@ -0,0 +1,36 @@
1
+# -*- coding: utf-8 -*-
2
+# from hapic.hapic import Hapic
3
+
4
+# _hapic_default = Hapic()
5
+#
6
+# with_api_doc = _hapic_default.with_api_doc
7
+# with_api_doc_bis = _hapic_default.with_api_doc_bis
8
+# generate_doc = _hapic_default.generate_doc
9
+
10
+from hapic.hapic import with_api_doc
11
+from hapic.hapic import with_api_doc_bis
12
+from hapic.hapic import generate_doc
13
+from hapic.hapic import output
14
+from hapic.hapic import input_body
15
+from hapic.hapic import input_query
16
+from hapic.hapic import input_path
17
+from hapic.hapic import input_headers
18
+from hapic.hapic import BottleContext
19
+from hapic.hapic import set_fake_default_context
20
+
21
+
22
+class FakeSetContext(object):
23
+    def bottle_context(self):
24
+        hapic.set_fake_default_context(BottleContext())
25
+        def decorator(func):
26
+            def wrapper(*args, **kwargs):
27
+                return func(*args, **kwargs)
28
+            return wrapper
29
+        return decorator
30
+
31
+
32
+class FakeExt(object):
33
+    bottle = FakeSetContext()
34
+
35
+
36
+ext = FakeExt()

+ 332 - 0
hapic/hapic.py View File

@@ -0,0 +1,332 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+import typing
4
+
5
+import functools
6
+
7
+import bottle
8
+
9
+# TODO: Gérer les erreurs de schema
10
+# TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb)
11
+
12
+
13
+# CHANGE
14
+flatten = lambda l: [item for sublist in l for item in sublist]
15
+
16
+
17
+_waiting = {}
18
+_endpoints = {}
19
+_default_global_context = None
20
+
21
+
22
+def set_fake_default_context(context):
23
+    global _default_global_context
24
+    _default_global_context = context
25
+
26
+
27
+def _register(func):
28
+    assert func not in _endpoints
29
+    global _waiting
30
+
31
+    _endpoints[func] = _waiting
32
+    _waiting = {}
33
+
34
+
35
+def with_api_doc():
36
+    def decorator(func):
37
+
38
+        @functools.wraps(func)
39
+        def wrapper(*args, **kwargs):
40
+            return func(*args, **kwargs)
41
+
42
+        _register(wrapper)
43
+        return wrapper
44
+
45
+    return decorator
46
+
47
+
48
+def with_api_doc_bis():
49
+    def decorator(func):
50
+
51
+        @functools.wraps(func)
52
+        def wrapper(*args, **kwargs):
53
+            return func(*args, **kwargs)
54
+
55
+        _register(func)
56
+        return wrapper
57
+
58
+    return decorator
59
+
60
+
61
+def generate_doc(app=None):
62
+    # TODO @Damien bottle specific code !
63
+    app = app or bottle.default_app()
64
+
65
+    route_by_callbacks = []
66
+    routes = flatten(app.router.dyna_routes.values())
67
+    for path, path_regex, route, func_ in routes:
68
+        route_by_callbacks.append(route.callback)
69
+
70
+    for func, descriptions in _endpoints.items():
71
+        routes = flatten(app.router.dyna_routes.values())
72
+        for path, path_regex, route, func_ in routes:
73
+            if route.callback == func:
74
+                print(route.method, path, descriptions)
75
+                continue
76
+
77
+
78
+class RequestParameters(object):
79
+    def __init__(
80
+        self,
81
+        path_parameters,
82
+        query_parameters,
83
+        body_parameters,
84
+        form_parameters,
85
+        header_parameters,
86
+    ):
87
+        self.path_parameters = path_parameters
88
+        self.query_parameters = query_parameters
89
+        self.body_parameters = body_parameters
90
+        self.form_parameters = form_parameters
91
+        self.header_parameters = header_parameters
92
+
93
+
94
+class ContextInterface(object):
95
+    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
96
+        raise NotImplementedError()
97
+
98
+    def get_response(
99
+        self,
100
+        response: dict,
101
+        http_code: int,
102
+    ):
103
+        raise NotImplementedError()
104
+
105
+
106
+class BottleContext(ContextInterface):
107
+    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
108
+        return RequestParameters(
109
+            path_parameters=bottle.request.url_args,
110
+            query_parameters=bottle.request.params,
111
+            body_parameters=bottle.request.json,
112
+            form_parameters={},  # TODO
113
+            header_parameters={},  # TODO
114
+        )
115
+
116
+    def get_response(
117
+        self,
118
+        response: dict,
119
+        http_code: int,
120
+    ) -> bottle.HTTPResponse:
121
+        return bottle.HTTPResponse(
122
+            body=json.dumps(response),
123
+            headers=[
124
+                ('Content-Type', 'application/json'),
125
+            ],
126
+            status=http_code,
127
+        )
128
+
129
+
130
+class OutputProcessorInterface(object):
131
+    def __init__(self):
132
+        self.schema = None
133
+
134
+    def process(self, value):
135
+        raise NotImplementedError
136
+
137
+
138
+class InputProcessorInterface(object):
139
+    def __init__(self):
140
+        self.schema = None
141
+
142
+    def process(self, request_context: RequestParameters):
143
+        raise NotImplementedError
144
+
145
+
146
+class MarshmallowOutputProcessor(OutputProcessorInterface):
147
+    def process(self, data: typing.Any):
148
+        return self.schema.dump(data).data
149
+
150
+
151
+class MarshmallowInputProcessor(OutputProcessorInterface):
152
+    def process(self, data: dict):
153
+        return self.schema.load(data).data
154
+
155
+
156
+# class MarshmallowPathInputProcessor(OutputProcessorInterface):
157
+#     def process(self, request_context: RequestParameters):
158
+#         return self.schema.load(request_context.path_parameters).data
159
+#
160
+#
161
+# class MarshmallowQueryInputProcessor(OutputProcessorInterface):
162
+#     def process(self, request_context: RequestParameters):
163
+#         return self.schema.load(request_context.query_parameters).data
164
+#
165
+#
166
+# class MarshmallowJsonInputProcessor(OutputProcessorInterface):
167
+#     def process(self, request_context: RequestParameters):
168
+#         return self.schema.load(request_context.json_parameters).data
169
+
170
+
171
+# class MarshmallowFormInputProcessor(OutputProcessorInterface):
172
+#     def process(self, request_context: RequestParameters):
173
+#         return self.schema.load(xxx).data
174
+#
175
+#
176
+# class MarshmallowHeaderInputProcessor(OutputProcessorInterface):
177
+#     def process(self, request_context: RequestParameters):
178
+#         return self.schema.load(xxx).data
179
+
180
+
181
+class HapicData(object):
182
+    def __init__(self):
183
+        self.body = {}
184
+        self.path = {}
185
+        self.query = {}
186
+        self.headers = {}
187
+
188
+
189
+# TODO: Il faut un output_body et un output_header
190
+def output(
191
+    schema,
192
+    processor: OutputProcessorInterface=None,
193
+    context: ContextInterface=None,
194
+    default_http_code=200,
195
+    default_error_code=500,
196
+):
197
+    processor = processor or MarshmallowOutputProcessor()
198
+    processor.schema = schema
199
+    context = context or _default_global_context
200
+
201
+    def decorator(func):
202
+        # @functools.wraps(func)
203
+        def wrapper(*args, **kwargs):
204
+            raw_response = func(*args, **kwargs)
205
+            processed_response = processor.process(raw_response)
206
+            prepared_response = context.get_response(
207
+                processed_response,
208
+                default_http_code,
209
+            )
210
+            return prepared_response
211
+
212
+        _waiting['output'] = schema
213
+
214
+        return wrapper
215
+    return decorator
216
+
217
+
218
+# TODO: raccourcis 'input' tout court ?
219
+def input_body(
220
+    schema,
221
+    processor: InputProcessorInterface=None,
222
+    context: ContextInterface=None,
223
+    error_http_code=400,
224
+):
225
+    processor = processor or MarshmallowInputProcessor()
226
+    processor.schema = schema
227
+    context = context or _default_global_context
228
+
229
+    def decorator(func):
230
+
231
+        # @functools.wraps(func)
232
+        def wrapper(*args, **kwargs):
233
+            updated_kwargs = {'hapic_data': HapicData()}
234
+            updated_kwargs.update(kwargs)
235
+            hapic_data = updated_kwargs['hapic_data']
236
+
237
+            request_parameters = context.get_request_parameters(*args, **updated_kwargs)
238
+            hapic_data.body = processor.process(request_parameters.body_parameters)
239
+
240
+            return func(*args, **updated_kwargs)
241
+
242
+        _waiting.setdefault('input', []).append(schema)
243
+
244
+        return wrapper
245
+    return decorator
246
+
247
+
248
+def input_path(
249
+    schema,
250
+    processor: InputProcessorInterface=None,
251
+    context: ContextInterface=None,
252
+    error_http_code=400,
253
+):
254
+    processor = processor or MarshmallowInputProcessor()
255
+    processor.schema = schema
256
+    context = context or _default_global_context
257
+
258
+    def decorator(func):
259
+
260
+        # @functools.wraps(func)
261
+        def wrapper(*args, **kwargs):
262
+            updated_kwargs = {'hapic_data': HapicData()}
263
+            updated_kwargs.update(kwargs)
264
+            hapic_data = updated_kwargs['hapic_data']
265
+
266
+            request_parameters = context.get_request_parameters(*args, **updated_kwargs)
267
+            hapic_data.path = processor.process(request_parameters.path_parameters)
268
+
269
+            return func(*args, **updated_kwargs)
270
+
271
+        _waiting.setdefault('input', []).append(schema)
272
+
273
+        return wrapper
274
+    return decorator
275
+
276
+
277
+def input_query(
278
+    schema,
279
+    processor: InputProcessorInterface=None,
280
+    context: ContextInterface=None,
281
+    error_http_code=400,
282
+):
283
+    processor = processor or MarshmallowInputProcessor()
284
+    processor.schema = schema
285
+    context = context or _default_global_context
286
+
287
+    def decorator(func):
288
+
289
+        # @functools.wraps(func)
290
+        def wrapper(*args, **kwargs):
291
+            updated_kwargs = {'hapic_data': HapicData()}
292
+            updated_kwargs.update(kwargs)
293
+            hapic_data = updated_kwargs['hapic_data']
294
+
295
+            request_parameters = context.get_request_parameters(*args, **updated_kwargs)
296
+            hapic_data.query = processor.process(request_parameters.query_parameters)
297
+
298
+            return func(*args, **updated_kwargs)
299
+
300
+        _waiting.setdefault('input', []).append(schema)
301
+
302
+        return wrapper
303
+    return decorator
304
+
305
+
306
+def input_headers(
307
+    schema,
308
+    processor: InputProcessorInterface,
309
+    context: ContextInterface=None,
310
+    error_http_code=400,
311
+):
312
+    processor = processor or MarshmallowInputProcessor()
313
+    processor.schema = schema
314
+    context = context or _default_global_context
315
+
316
+    def decorator(func):
317
+
318
+        # @functools.wraps(func)
319
+        def wrapper(*args, **kwargs):
320
+            updated_kwargs = {'hapic_data': HapicData()}
321
+            updated_kwargs.update(kwargs)
322
+            hapic_data = updated_kwargs['hapic_data']
323
+
324
+            request_parameters = context.get_request_parameters(*args, **updated_kwargs)
325
+            hapic_data.headers = processor.process(request_parameters.header_parameters)
326
+
327
+            return func(*args, **updated_kwargs)
328
+
329
+        _waiting.setdefault('input', []).append(schema)
330
+
331
+        return wrapper
332
+    return decorator

+ 2 - 0
requirements.txt View File

@@ -0,0 +1,2 @@
1
+bottle==0.12.13
2
+marshmallow==2.13.6