Browse Source

aiohttp support for input_path

Bastien Sevajol 5 years ago
parent
commit
f8f2792927
6 changed files with 313 additions and 9 deletions
  1. 61 0
      example/example_a_aiohttp.py
  2. 20 0
      hapic/async.py
  3. 53 3
      hapic/decorator.py
  4. 1 0
      hapic/ext/aiohttp/__init__.py
  5. 143 0
      hapic/ext/aiohttp/context.py
  6. 35 6
      hapic/hapic.py

+ 61 - 0
example/example_a_aiohttp.py View File

@@ -0,0 +1,61 @@
1
+# coding: utf-8
2
+import json
3
+import yaml
4
+
5
+from aiohttp import web
6
+from hapic import async as hapic
7
+import marshmallow
8
+
9
+from hapic.ext.aiohttp.context import AiohttpContext
10
+
11
+
12
+class HandleInputPath(marshmallow.Schema):
13
+    name = marshmallow.fields.String(
14
+        required=False,
15
+        allow_none=True,
16
+    )
17
+
18
+
19
+class HandleOutputBody(marshmallow.Schema):
20
+    sentence = marshmallow.fields.String(
21
+        required=True,
22
+    )
23
+
24
+
25
+@hapic.with_api_doc()
26
+@hapic.input_path(HandleInputPath())
27
+# @hapic.output_body(HandleOutputBody())
28
+async def handle(request, hapic_data):
29
+    name = request.match_info.get('name', "Anonymous")
30
+    text = "Hello, " + name
31
+    return web.json_response({
32
+        'sentence': text,
33
+    })
34
+
35
+
36
+async def do_login(request):
37
+    data = await request.json()
38
+    login = data['login']
39
+    password = data['password']
40
+
41
+    return web.json_response({
42
+        'login': login,
43
+    })
44
+
45
+app = web.Application(debug=True)
46
+app.add_routes([
47
+    web.get('/n/', handle),
48
+    web.get('/n/{name}', handle),
49
+    web.post('/n/{name}', handle),
50
+    web.post('/login', do_login),
51
+])
52
+
53
+
54
+hapic.set_context(AiohttpContext(app))
55
+
56
+# print(yaml.dump(
57
+#     json.loads(json.dumps(hapic.generate_doc())),
58
+#     default_flow_style=False,
59
+# ))
60
+
61
+web.run_app(app)

+ 20 - 0
hapic/async.py View File

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+from hapic.hapic import Hapic
3
+
4
+_hapic_default = Hapic(async=True)
5
+
6
+with_api_doc = _hapic_default.with_api_doc
7
+input_headers = _hapic_default.input_headers
8
+input_body = _hapic_default.input_body
9
+input_path = _hapic_default.input_path
10
+input_query = _hapic_default.input_query
11
+input_forms = _hapic_default.input_forms
12
+input_files = _hapic_default.input_files
13
+output_headers = _hapic_default.output_headers
14
+output_body = _hapic_default.output_body
15
+output_file = _hapic_default.output_file
16
+generate_doc = _hapic_default.generate_doc
17
+set_context = _hapic_default.set_context
18
+reset_context = _hapic_default.reset_context
19
+add_documentation_view = _hapic_default.add_documentation_view
20
+handle_exception = _hapic_default.handle_exception

+ 53 - 3
hapic/decorator.py View File

@@ -73,14 +73,14 @@ class ControllerWrapper(object):
73 73
         self,
74 74
         func: 'typing.Callable[..., typing.Any]',
75 75
     ) -> 'typing.Callable[..., typing.Any]':
76
-        def wrapper(*args, **kwargs) -> typing.Any:
76
+        async def wrapper(*args, **kwargs) -> typing.Any:
77 77
             # Note: Design of before_wrapped_func can be to update kwargs
78 78
             # by reference here
79
-            replacement_response = self.before_wrapped_func(args, kwargs)
79
+            replacement_response = await self.before_wrapped_func(args, kwargs)
80 80
             if replacement_response:
81 81
                 return replacement_response
82 82
 
83
-            response = self._execute_wrapped_function(func, args, kwargs)
83
+            response = await self._execute_wrapped_function(func, args, kwargs)
84 84
             new_response = self.after_wrapped_function(response)
85 85
             return new_response
86 86
         return functools.update_wrapper(wrapper, func)
@@ -190,6 +190,42 @@ class InputControllerWrapper(InputOutputControllerWrapper):
190 190
         return error_response
191 191
 
192 192
 
193
+# 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
196
+class AsyncInputControllerWrapper(InputControllerWrapper):
197
+    async def before_wrapped_func(
198
+        self,
199
+        func_args: typing.Tuple[typing.Any, ...],
200
+        func_kwargs: typing.Dict[str, typing.Any],
201
+    ) -> typing.Any:
202
+        # Retrieve hapic_data instance or create new one
203
+        # hapic_data is given though decorators
204
+        # Important note here: func_kwargs is update by reference !
205
+        hapic_data = self.ensure_hapic_data(func_kwargs)
206
+        request_parameters = await self.get_request_parameters(
207
+            func_args,
208
+            func_kwargs,
209
+        )
210
+
211
+        try:
212
+            processed_data = self.get_processed_data(request_parameters)
213
+            self.update_hapic_data(hapic_data, processed_data)
214
+        except ProcessException:
215
+            error_response = self.get_error_response(request_parameters)
216
+            return error_response
217
+
218
+    async def get_request_parameters(
219
+        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
+        )
227
+
228
+
193 229
 class OutputControllerWrapper(InputOutputControllerWrapper):
194 230
     def __init__(
195 231
         self,
@@ -287,6 +323,20 @@ class InputPathControllerWrapper(InputControllerWrapper):
287 323
         return request_parameters.path_parameters
288 324
 
289 325
 
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
+
290 340
 class InputQueryControllerWrapper(InputControllerWrapper):
291 341
     def __init__(
292 342
         self,

+ 1 - 0
hapic/ext/aiohttp/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 143 - 0
hapic/ext/aiohttp/context.py View File

@@ -0,0 +1,143 @@
1
+# coding: utf-8
2
+import asyncio
3
+import typing
4
+from http import HTTPStatus
5
+from json import JSONDecodeError
6
+
7
+from aiohttp.web_request import Request
8
+from multidict import MultiDict
9
+
10
+from hapic.context import BaseContext
11
+from hapic.context import RouteRepresentation
12
+from hapic.decorator import DecoratedController
13
+from hapic.exception import WorkflowException
14
+from hapic.processor import ProcessValidationError
15
+from hapic.processor import RequestParameters
16
+from aiohttp import web
17
+
18
+
19
+class AiohttpContext(BaseContext):
20
+    def __init__(
21
+        self,
22
+        app: web.Application,
23
+    ) -> None:
24
+        self._app = app
25
+
26
+    @property
27
+    def app(self) -> web.Application:
28
+        return self._app
29
+
30
+    async def get_request_parameters(
31
+        self,
32
+        *args,
33
+        **kwargs
34
+    ) -> RequestParameters:
35
+        try:
36
+            request = args[0]
37
+        except IndexError:
38
+            raise WorkflowException(
39
+                'Unable to get aiohttp request object',
40
+            )
41
+        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
+        )
77
+
78
+    def get_response(
79
+        self,
80
+        response: str,
81
+        http_code: int,
82
+        mimetype: str = 'application/json',
83
+    ) -> typing.Any:
84
+        pass
85
+
86
+    def get_validation_error_response(
87
+        self,
88
+        error: ProcessValidationError,
89
+        http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
90
+    ) -> typing.Any:
91
+        pass
92
+
93
+    def find_route(
94
+        self,
95
+        decorated_controller: DecoratedController,
96
+    ) -> RouteRepresentation:
97
+        pass
98
+
99
+    def get_swagger_path(
100
+        self,
101
+        contextualised_rule: str,
102
+    ) -> str:
103
+        pass
104
+
105
+    def by_pass_output_wrapping(
106
+        self,
107
+        response: typing.Any,
108
+    ) -> bool:
109
+        pass
110
+
111
+    def add_view(
112
+        self,
113
+        route: str,
114
+        http_method: str,
115
+        view_func: typing.Callable[..., typing.Any],
116
+    ) -> None:
117
+        pass
118
+
119
+    def serve_directory(
120
+        self,
121
+        route_prefix: str,
122
+        directory_path: str,
123
+    ) -> None:
124
+        pass
125
+
126
+    def is_debug(
127
+        self,
128
+    ) -> bool:
129
+        pass
130
+
131
+    def handle_exception(
132
+        self,
133
+        exception_class: typing.Type[Exception],
134
+        http_code: int,
135
+    ) -> None:
136
+        pass
137
+
138
+    def handle_exceptions(
139
+        self,
140
+        exception_classes: typing.List[typing.Type[Exception]],
141
+        http_code: int,
142
+    ) -> None:
143
+        pass

+ 35 - 6
hapic/hapic.py View File

@@ -17,6 +17,7 @@ from hapic.decorator import ExceptionHandlerControllerWrapper
17 17
 from hapic.decorator import InputBodyControllerWrapper
18 18
 from hapic.decorator import InputHeadersControllerWrapper
19 19
 from hapic.decorator import InputPathControllerWrapper
20
+from hapic.decorator import AsyncInputPathControllerWrapper
20 21
 from hapic.decorator import InputQueryControllerWrapper
21 22
 from hapic.decorator import InputFilesControllerWrapper
22 23
 from hapic.decorator import OutputBodyControllerWrapper
@@ -45,11 +46,15 @@ from hapic.error import ErrorBuilderInterface
45 46
 
46 47
 
47 48
 class Hapic(object):
48
-    def __init__(self):
49
+    def __init__(
50
+        self,
51
+        async: bool = False,
52
+    ):
49 53
         self._buffer = DecorationBuffer()
50 54
         self._controllers = []  # type: typing.List[DecoratedController]
51 55
         self._context = None  # type: ContextInterface
52 56
         self._error_builder = None  # type: ErrorBuilderInterface
57
+        self._async = async
53 58
         self.doc_generator = DocGenerator()
54 59
 
55 60
         # This local function will be pass to different components
@@ -232,11 +237,11 @@ class Hapic(object):
232 237
         processor.schema = schema
233 238
         context = context or self._context_getter
234 239
 
235
-        decoration = InputPathControllerWrapper(
236
-            context=context,
237
-            processor=processor,
238
-            error_http_code=error_http_code,
239
-            default_http_code=default_http_code,
240
+        decoration = self._get_input_path_controller_wrapper(
241
+            processor,
242
+            context,
243
+            error_http_code,
244
+            default_http_code,
240 245
         )
241 246
 
242 247
         def decorator(func):
@@ -482,3 +487,27 @@ class Hapic(object):
482 487
             route,
483 488
             swaggerui_path,
484 489
         )
490
+
491
+    def _get_input_path_controller_wrapper(
492
+        self,
493
+        processor: ProcessorInterface,
494
+        context: ContextInterface,
495
+        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
496
+        default_http_code: HTTPStatus = HTTPStatus.OK,
497
+    ) -> typing.Union[
498
+        InputPathControllerWrapper,
499
+        AsyncInputPathControllerWrapper,
500
+    ]:
501
+        if not self._async:
502
+            return InputPathControllerWrapper(
503
+                context=context,
504
+                processor=processor,
505
+                error_http_code=error_http_code,
506
+                default_http_code=default_http_code,
507
+            )
508
+        return AsyncInputPathControllerWrapper(
509
+            context=context,
510
+            processor=processor,
511
+            error_http_code=error_http_code,
512
+            default_http_code=default_http_code,
513
+        )