Browse Source

make async compatible for input_body and output_body only

Bastien Sevajol 5 years ago
parent
commit
fc33b221ee
5 changed files with 205 additions and 134 deletions
  1. 31 0
      aiopoc.py
  2. 24 0
      example/example_a_aiohttp.py
  3. 63 29
      hapic/decorator.py
  4. 54 38
      hapic/ext/aiohttp/context.py
  5. 33 67
      hapic/hapic.py

+ 31 - 0
aiopoc.py View File

@@ -0,0 +1,31 @@
1
+from sh import tail
2
+from asyncio import sleep
3
+from aiohttp import web
4
+
5
+
6
+async def handle(request):
7
+    response = web.StreamResponse(
8
+        status=200,
9
+        reason='OK',
10
+        headers={
11
+            'Content-Type': 'text/plain; charset=utf-8',
12
+        },
13
+    )
14
+
15
+    await response.prepare(request)
16
+    response.enable_chunked_encoding()
17
+
18
+    for line in tail("-f", "aiopocdata.txt", _iter=True):
19
+        await response.write(line.encode('utf-8'))
20
+        await sleep(0.1)
21
+
22
+    return response
23
+
24
+
25
+app = web.Application()
26
+app.add_routes([
27
+    web.get('/', handle)
28
+])
29
+
30
+
31
+web.run_app(app)

+ 24 - 0
example/example_a_aiohttp.py View File

@@ -4,6 +4,7 @@ import yaml
4 4
 
5 5
 from aiohttp import web
6 6
 from hapic import async as hapic
7
+from hapic import async as HapicData
7 8
 import marshmallow
8 9
 
9 10
 from hapic.ext.aiohttp.context import AiohttpContext
@@ -16,6 +17,18 @@ class HandleInputPath(marshmallow.Schema):
16 17
     )
17 18
 
18 19
 
20
+class HandleInputBody(marshmallow.Schema):
21
+    foo = marshmallow.fields.String(
22
+        required=True,
23
+    )
24
+
25
+
26
+class Handle2OutputBody(marshmallow.Schema):
27
+    data = marshmallow.fields.Dict(
28
+        required=True,
29
+    )
30
+
31
+
19 32
 class HandleOutputBody(marshmallow.Schema):
20 33
     sentence = marshmallow.fields.String(
21 34
         required=True,
@@ -33,6 +46,16 @@ async def handle(request, hapic_data):
33 46
     })
34 47
 
35 48
 
49
+@hapic.with_api_doc()
50
+@hapic.input_body(HandleInputBody())
51
+@hapic.output_body(Handle2OutputBody())
52
+async def handle2(request, hapic_data: HapicData):
53
+    data = hapic_data.body
54
+    return {
55
+        'data': data,
56
+    }
57
+
58
+
36 59
 async def do_login(request):
37 60
     data = await request.json()
38 61
     login = data['login']
@@ -47,6 +70,7 @@ app.add_routes([
47 70
     web.get('/n/', handle),
48 71
     web.get('/n/{name}', handle),
49 72
     web.post('/n/{name}', handle),
73
+    web.post('/b/', handle2),
50 74
     web.post('/login', do_login),
51 75
 ])
52 76
 

+ 63 - 29
hapic/decorator.py View File

@@ -73,14 +73,15 @@ class ControllerWrapper(object):
73 73
         self,
74 74
         func: 'typing.Callable[..., typing.Any]',
75 75
     ) -> 'typing.Callable[..., typing.Any]':
76
-        async def wrapper(*args, **kwargs) -> typing.Any:
76
+        # async def wrapper(*args, **kwargs) -> typing.Any:
77
+        def wrapper(*args, **kwargs) -> typing.Any:
77 78
             # Note: Design of before_wrapped_func can be to update kwargs
78 79
             # by reference here
79
-            replacement_response = await self.before_wrapped_func(args, kwargs)
80
+            replacement_response = self.before_wrapped_func(args, kwargs)
80 81
             if replacement_response:
81 82
                 return replacement_response
82 83
 
83
-            response = await self._execute_wrapped_function(func, args, kwargs)
84
+            response = self._execute_wrapped_function(func, args, kwargs)
84 85
             new_response = self.after_wrapped_function(response)
85 86
             return new_response
86 87
         return functools.update_wrapper(wrapper, func)
@@ -191,9 +192,25 @@ class InputControllerWrapper(InputOutputControllerWrapper):
191 192
 
192 193
 
193 194
 # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
194
-# to permit async compatibility. Please re-think about code refact
195
-# TAG: REFACT_ASYNC
195
+# (and ControllerWrapper.get_wrapper rewrite) to permit async compatibility.
196
+# Please re-think about code refact. TAG: REFACT_ASYNC
196 197
 class AsyncInputControllerWrapper(InputControllerWrapper):
198
+    def get_wrapper(
199
+        self,
200
+        func: 'typing.Callable[..., typing.Any]',
201
+    ) -> 'typing.Callable[..., typing.Any]':
202
+        async def wrapper(*args, **kwargs) -> typing.Any:
203
+            # Note: Design of before_wrapped_func can be to update kwargs
204
+            # by reference here
205
+            replacement_response = await self.before_wrapped_func(args, kwargs)
206
+            if replacement_response:
207
+                return replacement_response
208
+
209
+            response = await self._execute_wrapped_function(func, args, kwargs)
210
+            new_response = self.after_wrapped_function(response)
211
+            return new_response
212
+        return functools.update_wrapper(wrapper, func)
213
+
197 214
     async def before_wrapped_func(
198 215
         self,
199 216
         func_args: typing.Tuple[typing.Any, ...],
@@ -203,27 +220,25 @@ class AsyncInputControllerWrapper(InputControllerWrapper):
203 220
         # hapic_data is given though decorators
204 221
         # Important note here: func_kwargs is update by reference !
205 222
         hapic_data = self.ensure_hapic_data(func_kwargs)
206
-        request_parameters = await self.get_request_parameters(
223
+        request_parameters = self.get_request_parameters(
207 224
             func_args,
208 225
             func_kwargs,
209 226
         )
210 227
 
211 228
         try:
212
-            processed_data = self.get_processed_data(request_parameters)
229
+            processed_data = await self.get_processed_data(request_parameters)
213 230
             self.update_hapic_data(hapic_data, processed_data)
214 231
         except ProcessException:
215 232
             error_response = self.get_error_response(request_parameters)
216 233
             return error_response
217 234
 
218
-    async def get_request_parameters(
235
+    async def get_processed_data(
219 236
         self,
220
-        func_args: typing.Tuple[typing.Any, ...],
221
-        func_kwargs: typing.Dict[str, typing.Any],
222
-    ) -> RequestParameters:
223
-        return await self.context.get_request_parameters(
224
-            *func_args,
225
-            **func_kwargs
226
-        )
237
+        request_parameters: RequestParameters,
238
+    ) -> typing.Any:
239
+        parameters_data = await self.get_parameters_data(request_parameters)
240
+        processed_data = self.processor.process(parameters_data)
241
+        return processed_data
227 242
 
228 243
 
229 244
 class OutputControllerWrapper(InputOutputControllerWrapper):
@@ -298,6 +313,25 @@ class OutputBodyControllerWrapper(OutputControllerWrapper):
298 313
     pass
299 314
 
300 315
 
316
+class AsyncOutputBodyControllerWrapper(OutputControllerWrapper):
317
+    def get_wrapper(
318
+        self,
319
+        func: 'typing.Callable[..., typing.Any]',
320
+    ) -> 'typing.Callable[..., typing.Any]':
321
+        # async def wrapper(*args, **kwargs) -> typing.Any:
322
+        async def wrapper(*args, **kwargs) -> typing.Any:
323
+            # Note: Design of before_wrapped_func can be to update kwargs
324
+            # by reference here
325
+            replacement_response = self.before_wrapped_func(args, kwargs)
326
+            if replacement_response:
327
+                return replacement_response
328
+
329
+            response = await self._execute_wrapped_function(func, args, kwargs)
330
+            new_response = self.after_wrapped_function(response)
331
+            return new_response
332
+        return functools.update_wrapper(wrapper, func)
333
+
334
+
301 335
 class OutputHeadersControllerWrapper(OutputControllerWrapper):
302 336
     pass
303 337
 
@@ -323,20 +357,6 @@ class InputPathControllerWrapper(InputControllerWrapper):
323 357
         return request_parameters.path_parameters
324 358
 
325 359
 
326
-# TODO BS 2018-07-23: This class is an copy-patse of InputPathControllerWrapper
327
-# to permit async compatibility. Please re-think about code refact
328
-# TAG: REFACT_ASYNC
329
-class AsyncInputPathControllerWrapper(AsyncInputControllerWrapper):
330
-    def update_hapic_data(
331
-        self, hapic_data: HapicData,
332
-        processed_data: typing.Any,
333
-    ) -> None:
334
-        hapic_data.path = processed_data
335
-
336
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
337
-        return request_parameters.path_parameters
338
-
339
-
340 360
 class InputQueryControllerWrapper(InputControllerWrapper):
341 361
     def __init__(
342 362
         self,
@@ -394,6 +414,20 @@ class InputBodyControllerWrapper(InputControllerWrapper):
394 414
         return request_parameters.body_parameters
395 415
 
396 416
 
417
+# TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
418
+# to permit async compatibility. Please re-think about code refact
419
+# TAG: REFACT_ASYNC
420
+class AsyncInputBodyControllerWrapper(AsyncInputControllerWrapper):
421
+    def update_hapic_data(
422
+        self, hapic_data: HapicData,
423
+        processed_data: typing.Any,
424
+    ) -> None:
425
+        hapic_data.body = processed_data
426
+
427
+    async def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
428
+        return await request_parameters.body_parameters
429
+
430
+
397 431
 class InputHeadersControllerWrapper(InputControllerWrapper):
398 432
     def update_hapic_data(
399 433
         self, hapic_data: HapicData,

+ 54 - 38
hapic/ext/aiohttp/context.py View File

@@ -5,6 +5,7 @@ from http import HTTPStatus
5 5
 from json import JSONDecodeError
6 6
 
7 7
 from aiohttp.web_request import Request
8
+from aiohttp.web_response import Response
8 9
 from multidict import MultiDict
9 10
 
10 11
 from hapic.context import BaseContext
@@ -16,6 +17,50 @@ from hapic.processor import RequestParameters
16 17
 from aiohttp import web
17 18
 
18 19
 
20
+class AiohttpRequestParameters(object):
21
+    def __init__(
22
+        self,
23
+        request: Request,
24
+    ) -> None:
25
+        self._request = request
26
+        self._parsed_body = None
27
+
28
+    @property
29
+    async def body_parameters(self) -> dict:
30
+        if self._parsed_body is None:
31
+            content_type = self.header_parameters.get('Content-Type')
32
+            is_json = content_type == 'application/json'
33
+
34
+            if is_json:
35
+                self._parsed_body = await self._request.json()
36
+            else:
37
+                self._parsed_body = await self._request.post()
38
+
39
+        return self._parsed_body
40
+
41
+    @property
42
+    def path_parameters(self):
43
+        return dict(self._request.match_info)
44
+
45
+    @property
46
+    def query_parameters(self):
47
+        return MultiDict(self._request.query.items())
48
+
49
+    @property
50
+    def form_parameters(self):
51
+        # TODO BS 2018-07-24: There is misunderstanding around body/form/json
52
+        return self.body_parameters
53
+
54
+    @property
55
+    def header_parameters(self):
56
+        return dict(self._request.headers.items())
57
+
58
+    @property
59
+    def files_parameters(self):
60
+        # TODO BS 2018-07-24: To do
61
+        raise NotImplementedError('todo')
62
+
63
+
19 64
 class AiohttpContext(BaseContext):
20 65
     def __init__(
21 66
         self,
@@ -27,7 +72,7 @@ class AiohttpContext(BaseContext):
27 72
     def app(self) -> web.Application:
28 73
         return self._app
29 74
 
30
-    async def get_request_parameters(
75
+    def get_request_parameters(
31 76
         self,
32 77
         *args,
33 78
         **kwargs
@@ -39,41 +84,7 @@ class AiohttpContext(BaseContext):
39 84
                 'Unable to get aiohttp request object',
40 85
             )
41 86
         request = typing.cast(Request, request)
42
-
43
-        path_parameters = dict(request.match_info)
44
-        query_parameters = MultiDict(request.query.items())
45
-
46
-        if request.can_read_body:
47
-            try:
48
-                # FIXME NOW: request.json() make
49
-                # request.content empty, do it ONLY if json headers
50
-                # body_parameters = await request.json()
51
-                # body_parameters = await request.content.read()
52
-                body_parameters = {}
53
-                pass
54
-            except JSONDecodeError:
55
-                # FIXME BS 2018-07-13: Raise an 400 error if header contain
56
-                # json type
57
-                body_parameters = {}
58
-        else:
59
-            body_parameters = {}
60
-
61
-        form_parameters_multi_dict_proxy = await request.post()
62
-        form_parameters = MultiDict(form_parameters_multi_dict_proxy.items())
63
-        header_parameters = dict(request.headers.items())
64
-
65
-        # TODO BS 2018-07-13: Manage files (see
66
-        # https://docs.aiohttp.org/en/stable/multipart.html)
67
-        files_parameters = dict()
68
-
69
-        return RequestParameters(
70
-            path_parameters=path_parameters,
71
-            query_parameters=query_parameters,
72
-            body_parameters=body_parameters,
73
-            form_parameters=form_parameters,
74
-            header_parameters=header_parameters,
75
-            files_parameters=files_parameters,
76
-        )
87
+        return AiohttpRequestParameters(request)
77 88
 
78 89
     def get_response(
79 90
         self,
@@ -81,14 +92,19 @@ class AiohttpContext(BaseContext):
81 92
         http_code: int,
82 93
         mimetype: str = 'application/json',
83 94
     ) -> typing.Any:
84
-        pass
95
+        return Response(
96
+            body=response,
97
+            status=http_code,
98
+            content_type=mimetype,
99
+        )
85 100
 
86 101
     def get_validation_error_response(
87 102
         self,
88 103
         error: ProcessValidationError,
89 104
         http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
90 105
     ) -> typing.Any:
91
-        pass
106
+        # TODO BS 2018-07-24: To do
107
+        raise NotImplementedError('todo')
92 108
 
93 109
     def find_route(
94 110
         self,

+ 33 - 67
hapic/hapic.py View File

@@ -15,13 +15,13 @@ from hapic.decorator import DECORATION_ATTRIBUTE_NAME
15 15
 from hapic.decorator import ControllerReference
16 16
 from hapic.decorator import ExceptionHandlerControllerWrapper
17 17
 from hapic.decorator import InputBodyControllerWrapper
18
+from hapic.decorator import AsyncInputBodyControllerWrapper
18 19
 from hapic.decorator import InputHeadersControllerWrapper
19 20
 from hapic.decorator import InputPathControllerWrapper
20
-from hapic.decorator import AsyncInputPathControllerWrapper
21 21
 from hapic.decorator import InputQueryControllerWrapper
22
-from hapic.decorator import AsyncInputQueryControllerWrapper
23 22
 from hapic.decorator import InputFilesControllerWrapper
24 23
 from hapic.decorator import OutputBodyControllerWrapper
24
+from hapic.decorator import AsyncOutputBodyControllerWrapper
25 25
 from hapic.decorator import OutputHeadersControllerWrapper
26 26
 from hapic.decorator import OutputFileControllerWrapper
27 27
 from hapic.description import InputBodyDescription
@@ -149,12 +149,21 @@ class Hapic(object):
149 149
         processor = processor or MarshmallowOutputProcessor()
150 150
         processor.schema = schema
151 151
         context = context or self._context_getter
152
-        decoration = OutputBodyControllerWrapper(
153
-            context=context,
154
-            processor=processor,
155
-            error_http_code=error_http_code,
156
-            default_http_code=default_http_code,
157
-        )
152
+
153
+        if self._async:
154
+            decoration = AsyncOutputBodyControllerWrapper(
155
+                context=context,
156
+                processor=processor,
157
+                error_http_code=error_http_code,
158
+                default_http_code=default_http_code,
159
+            )
160
+        else:
161
+            decoration = OutputBodyControllerWrapper(
162
+                context=context,
163
+                processor=processor,
164
+                error_http_code=error_http_code,
165
+                default_http_code=default_http_code,
166
+            )
158 167
 
159 168
         def decorator(func):
160 169
             self._buffer.output_body = OutputBodyDescription(decoration)
@@ -238,7 +247,7 @@ class Hapic(object):
238 247
         processor.schema = schema
239 248
         context = context or self._context_getter
240 249
 
241
-        decoration = self._get_input_path_controller_wrapper(
250
+        decoration = InputPathControllerWrapper(
242 251
             context=context,
243 252
             processor=processor,
244 253
             error_http_code=error_http_code,
@@ -263,7 +272,7 @@ class Hapic(object):
263 272
         processor.schema = schema
264 273
         context = context or self._context_getter
265 274
 
266
-        decoration = self._get_input_query_controller_wrapper(
275
+        decoration = InputQueryControllerWrapper(
267 276
             context=context,
268 277
             processor=processor,
269 278
             error_http_code=error_http_code,
@@ -288,12 +297,20 @@ class Hapic(object):
288 297
         processor.schema = schema
289 298
         context = context or self._context_getter
290 299
 
291
-        decoration = InputBodyControllerWrapper(
292
-            context=context,
293
-            processor=processor,
294
-            error_http_code=error_http_code,
295
-            default_http_code=default_http_code,
296
-        )
300
+        if self._async:
301
+            decoration = AsyncInputBodyControllerWrapper(
302
+                context=context,
303
+                processor=processor,
304
+                error_http_code=error_http_code,
305
+                default_http_code=default_http_code,
306
+            )
307
+        else:
308
+            decoration = InputBodyControllerWrapper(
309
+                context=context,
310
+                processor=processor,
311
+                error_http_code=error_http_code,
312
+                default_http_code=default_http_code,
313
+            )
297 314
 
298 315
         def decorator(func):
299 316
             self._buffer.input_body = InputBodyDescription(decoration)
@@ -488,54 +505,3 @@ class Hapic(object):
488 505
             route,
489 506
             swaggerui_path,
490 507
         )
491
-
492
-    def _get_input_path_controller_wrapper(
493
-        self,
494
-        processor: ProcessorInterface,
495
-        context: ContextInterface,
496
-        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
497
-        default_http_code: HTTPStatus = HTTPStatus.OK,
498
-    ) -> typing.Union[
499
-        InputPathControllerWrapper,
500
-        AsyncInputPathControllerWrapper,
501
-    ]:
502
-        if not self._async:
503
-            return InputPathControllerWrapper(
504
-                context=context,
505
-                processor=processor,
506
-                error_http_code=error_http_code,
507
-                default_http_code=default_http_code,
508
-            )
509
-        return AsyncInputPathControllerWrapper(
510
-            context=context,
511
-            processor=processor,
512
-            error_http_code=error_http_code,
513
-            default_http_code=default_http_code,
514
-        )
515
-
516
-    def _get_input_query_controller_wrapper(
517
-        self,
518
-        processor: ProcessorInterface,
519
-        context: ContextInterface,
520
-        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
521
-        default_http_code: HTTPStatus = HTTPStatus.OK,
522
-        as_list: typing.List[str]=None,
523
-    ) -> typing.Union[
524
-        InputQueryControllerWrapper,
525
-        AsyncInputQueryControllerWrapper,
526
-    ]:
527
-        if not self._async:
528
-            return InputQueryControllerWrapper(
529
-                context=context,
530
-                processor=processor,
531
-                error_http_code=error_http_code,
532
-                default_http_code=default_http_code,
533
-                as_list=as_list,
534
-            )
535
-        return AsyncInputQueryControllerWrapper(
536
-            context=context,
537
-            processor=processor,
538
-            error_http_code=error_http_code,
539
-            default_http_code=default_http_code,
540
-            as_list=as_list,
541
-        )