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

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

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
         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
-        def wrapper(*args, **kwargs) -> typing.Any:
76
+        async def wrapper(*args, **kwargs) -> typing.Any:
77
             # Note: Design of before_wrapped_func can be to update kwargs
77
             # Note: Design of before_wrapped_func can be to update kwargs
78
             # by reference here
78
             # by reference here
79
-            replacement_response = self.before_wrapped_func(args, kwargs)
79
+            replacement_response = await self.before_wrapped_func(args, kwargs)
80
             if replacement_response:
80
             if replacement_response:
81
                 return replacement_response
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
             new_response = self.after_wrapped_function(response)
84
             new_response = self.after_wrapped_function(response)
85
             return new_response
85
             return new_response
86
         return functools.update_wrapper(wrapper, func)
86
         return functools.update_wrapper(wrapper, func)
190
         return error_response
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
 class OutputControllerWrapper(InputOutputControllerWrapper):
229
 class OutputControllerWrapper(InputOutputControllerWrapper):
194
     def __init__(
230
     def __init__(
195
         self,
231
         self,
287
         return request_parameters.path_parameters
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
 class InputQueryControllerWrapper(InputControllerWrapper):
340
 class InputQueryControllerWrapper(InputControllerWrapper):
291
     def __init__(
341
     def __init__(
292
         self,
342
         self,

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

1
+# coding: utf-8

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

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
 from hapic.decorator import InputBodyControllerWrapper
17
 from hapic.decorator import InputBodyControllerWrapper
18
 from hapic.decorator import InputHeadersControllerWrapper
18
 from hapic.decorator import InputHeadersControllerWrapper
19
 from hapic.decorator import InputPathControllerWrapper
19
 from hapic.decorator import InputPathControllerWrapper
20
+from hapic.decorator import AsyncInputPathControllerWrapper
20
 from hapic.decorator import InputQueryControllerWrapper
21
 from hapic.decorator import InputQueryControllerWrapper
21
 from hapic.decorator import InputFilesControllerWrapper
22
 from hapic.decorator import InputFilesControllerWrapper
22
 from hapic.decorator import OutputBodyControllerWrapper
23
 from hapic.decorator import OutputBodyControllerWrapper
45
 
46
 
46
 
47
 
47
 class Hapic(object):
48
 class Hapic(object):
48
-    def __init__(self):
49
+    def __init__(
50
+        self,
51
+        async: bool = False,
52
+    ):
49
         self._buffer = DecorationBuffer()
53
         self._buffer = DecorationBuffer()
50
         self._controllers = []  # type: typing.List[DecoratedController]
54
         self._controllers = []  # type: typing.List[DecoratedController]
51
         self._context = None  # type: ContextInterface
55
         self._context = None  # type: ContextInterface
52
         self._error_builder = None  # type: ErrorBuilderInterface
56
         self._error_builder = None  # type: ErrorBuilderInterface
57
+        self._async = async
53
         self.doc_generator = DocGenerator()
58
         self.doc_generator = DocGenerator()
54
 
59
 
55
         # This local function will be pass to different components
60
         # This local function will be pass to different components
232
         processor.schema = schema
237
         processor.schema = schema
233
         context = context or self._context_getter
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
         def decorator(func):
247
         def decorator(func):
482
             route,
487
             route,
483
             swaggerui_path,
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
+        )