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

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
 
4
 
5
 from aiohttp import web
5
 from aiohttp import web
6
 from hapic import async as hapic
6
 from hapic import async as hapic
7
+from hapic import async as HapicData
7
 import marshmallow
8
 import marshmallow
8
 
9
 
9
 from hapic.ext.aiohttp.context import AiohttpContext
10
 from hapic.ext.aiohttp.context import AiohttpContext
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
 class HandleOutputBody(marshmallow.Schema):
32
 class HandleOutputBody(marshmallow.Schema):
20
     sentence = marshmallow.fields.String(
33
     sentence = marshmallow.fields.String(
21
         required=True,
34
         required=True,
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
 async def do_login(request):
59
 async def do_login(request):
37
     data = await request.json()
60
     data = await request.json()
38
     login = data['login']
61
     login = data['login']
47
     web.get('/n/', handle),
70
     web.get('/n/', handle),
48
     web.get('/n/{name}', handle),
71
     web.get('/n/{name}', handle),
49
     web.post('/n/{name}', handle),
72
     web.post('/n/{name}', handle),
73
+    web.post('/b/', handle2),
50
     web.post('/login', do_login),
74
     web.post('/login', do_login),
51
 ])
75
 ])
52
 
76
 

+ 63 - 29
hapic/decorator.py View File

73
         self,
73
         self,
74
         func: 'typing.Callable[..., typing.Any]',
74
         func: 'typing.Callable[..., typing.Any]',
75
     ) -> 'typing.Callable[..., typing.Any]':
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
             # Note: Design of before_wrapped_func can be to update kwargs
78
             # Note: Design of before_wrapped_func can be to update kwargs
78
             # by reference here
79
             # by reference here
79
-            replacement_response = await self.before_wrapped_func(args, kwargs)
80
+            replacement_response = self.before_wrapped_func(args, kwargs)
80
             if replacement_response:
81
             if replacement_response:
81
                 return replacement_response
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
             new_response = self.after_wrapped_function(response)
85
             new_response = self.after_wrapped_function(response)
85
             return new_response
86
             return new_response
86
         return functools.update_wrapper(wrapper, func)
87
         return functools.update_wrapper(wrapper, func)
191
 
192
 
192
 
193
 
193
 # TODO BS 2018-07-23: This class is an async version of InputControllerWrapper
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
 class AsyncInputControllerWrapper(InputControllerWrapper):
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
     async def before_wrapped_func(
214
     async def before_wrapped_func(
198
         self,
215
         self,
199
         func_args: typing.Tuple[typing.Any, ...],
216
         func_args: typing.Tuple[typing.Any, ...],
203
         # hapic_data is given though decorators
220
         # hapic_data is given though decorators
204
         # Important note here: func_kwargs is update by reference !
221
         # Important note here: func_kwargs is update by reference !
205
         hapic_data = self.ensure_hapic_data(func_kwargs)
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
             func_args,
224
             func_args,
208
             func_kwargs,
225
             func_kwargs,
209
         )
226
         )
210
 
227
 
211
         try:
228
         try:
212
-            processed_data = self.get_processed_data(request_parameters)
229
+            processed_data = await self.get_processed_data(request_parameters)
213
             self.update_hapic_data(hapic_data, processed_data)
230
             self.update_hapic_data(hapic_data, processed_data)
214
         except ProcessException:
231
         except ProcessException:
215
             error_response = self.get_error_response(request_parameters)
232
             error_response = self.get_error_response(request_parameters)
216
             return error_response
233
             return error_response
217
 
234
 
218
-    async def get_request_parameters(
235
+    async def get_processed_data(
219
         self,
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
 class OutputControllerWrapper(InputOutputControllerWrapper):
244
 class OutputControllerWrapper(InputOutputControllerWrapper):
298
     pass
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
 class OutputHeadersControllerWrapper(OutputControllerWrapper):
335
 class OutputHeadersControllerWrapper(OutputControllerWrapper):
302
     pass
336
     pass
303
 
337
 
323
         return request_parameters.path_parameters
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
 class InputQueryControllerWrapper(InputControllerWrapper):
360
 class InputQueryControllerWrapper(InputControllerWrapper):
341
     def __init__(
361
     def __init__(
342
         self,
362
         self,
394
         return request_parameters.body_parameters
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
 class InputHeadersControllerWrapper(InputControllerWrapper):
431
 class InputHeadersControllerWrapper(InputControllerWrapper):
398
     def update_hapic_data(
432
     def update_hapic_data(
399
         self, hapic_data: HapicData,
433
         self, hapic_data: HapicData,

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

5
 from json import JSONDecodeError
5
 from json import JSONDecodeError
6
 
6
 
7
 from aiohttp.web_request import Request
7
 from aiohttp.web_request import Request
8
+from aiohttp.web_response import Response
8
 from multidict import MultiDict
9
 from multidict import MultiDict
9
 
10
 
10
 from hapic.context import BaseContext
11
 from hapic.context import BaseContext
16
 from aiohttp import web
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
 class AiohttpContext(BaseContext):
64
 class AiohttpContext(BaseContext):
20
     def __init__(
65
     def __init__(
21
         self,
66
         self,
27
     def app(self) -> web.Application:
72
     def app(self) -> web.Application:
28
         return self._app
73
         return self._app
29
 
74
 
30
-    async def get_request_parameters(
75
+    def get_request_parameters(
31
         self,
76
         self,
32
         *args,
77
         *args,
33
         **kwargs
78
         **kwargs
39
                 'Unable to get aiohttp request object',
84
                 'Unable to get aiohttp request object',
40
             )
85
             )
41
         request = typing.cast(Request, request)
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
     def get_response(
89
     def get_response(
79
         self,
90
         self,
81
         http_code: int,
92
         http_code: int,
82
         mimetype: str = 'application/json',
93
         mimetype: str = 'application/json',
83
     ) -> typing.Any:
94
     ) -> typing.Any:
84
-        pass
95
+        return Response(
96
+            body=response,
97
+            status=http_code,
98
+            content_type=mimetype,
99
+        )
85
 
100
 
86
     def get_validation_error_response(
101
     def get_validation_error_response(
87
         self,
102
         self,
88
         error: ProcessValidationError,
103
         error: ProcessValidationError,
89
         http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
104
         http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
90
     ) -> typing.Any:
105
     ) -> typing.Any:
91
-        pass
106
+        # TODO BS 2018-07-24: To do
107
+        raise NotImplementedError('todo')
92
 
108
 
93
     def find_route(
109
     def find_route(
94
         self,
110
         self,

+ 33 - 67
hapic/hapic.py View File

15
 from hapic.decorator import ControllerReference
15
 from hapic.decorator import ControllerReference
16
 from hapic.decorator import ExceptionHandlerControllerWrapper
16
 from hapic.decorator import ExceptionHandlerControllerWrapper
17
 from hapic.decorator import InputBodyControllerWrapper
17
 from hapic.decorator import InputBodyControllerWrapper
18
+from hapic.decorator import AsyncInputBodyControllerWrapper
18
 from hapic.decorator import InputHeadersControllerWrapper
19
 from hapic.decorator import InputHeadersControllerWrapper
19
 from hapic.decorator import InputPathControllerWrapper
20
 from hapic.decorator import InputPathControllerWrapper
20
-from hapic.decorator import AsyncInputPathControllerWrapper
21
 from hapic.decorator import InputQueryControllerWrapper
21
 from hapic.decorator import InputQueryControllerWrapper
22
-from hapic.decorator import AsyncInputQueryControllerWrapper
23
 from hapic.decorator import InputFilesControllerWrapper
22
 from hapic.decorator import InputFilesControllerWrapper
24
 from hapic.decorator import OutputBodyControllerWrapper
23
 from hapic.decorator import OutputBodyControllerWrapper
24
+from hapic.decorator import AsyncOutputBodyControllerWrapper
25
 from hapic.decorator import OutputHeadersControllerWrapper
25
 from hapic.decorator import OutputHeadersControllerWrapper
26
 from hapic.decorator import OutputFileControllerWrapper
26
 from hapic.decorator import OutputFileControllerWrapper
27
 from hapic.description import InputBodyDescription
27
 from hapic.description import InputBodyDescription
149
         processor = processor or MarshmallowOutputProcessor()
149
         processor = processor or MarshmallowOutputProcessor()
150
         processor.schema = schema
150
         processor.schema = schema
151
         context = context or self._context_getter
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
         def decorator(func):
168
         def decorator(func):
160
             self._buffer.output_body = OutputBodyDescription(decoration)
169
             self._buffer.output_body = OutputBodyDescription(decoration)
238
         processor.schema = schema
247
         processor.schema = schema
239
         context = context or self._context_getter
248
         context = context or self._context_getter
240
 
249
 
241
-        decoration = self._get_input_path_controller_wrapper(
250
+        decoration = InputPathControllerWrapper(
242
             context=context,
251
             context=context,
243
             processor=processor,
252
             processor=processor,
244
             error_http_code=error_http_code,
253
             error_http_code=error_http_code,
263
         processor.schema = schema
272
         processor.schema = schema
264
         context = context or self._context_getter
273
         context = context or self._context_getter
265
 
274
 
266
-        decoration = self._get_input_query_controller_wrapper(
275
+        decoration = InputQueryControllerWrapper(
267
             context=context,
276
             context=context,
268
             processor=processor,
277
             processor=processor,
269
             error_http_code=error_http_code,
278
             error_http_code=error_http_code,
288
         processor.schema = schema
297
         processor.schema = schema
289
         context = context or self._context_getter
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
         def decorator(func):
315
         def decorator(func):
299
             self._buffer.input_body = InputBodyDescription(decoration)
316
             self._buffer.input_body = InputBodyDescription(decoration)
488
             route,
505
             route,
489
             swaggerui_path,
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
-        )