Browse Source

finish basic support (without doc generation) of aiohttp and outuput stream in async

Bastien Sevajol 5 years ago
parent
commit
18afa078a9
6 changed files with 282 additions and 14 deletions
  1. 1 1
      hapic/async.py
  2. 18 6
      hapic/decorator.py
  3. 28 5
      hapic/ext/aiohttp/context.py
  4. 2 2
      hapic/hapic.py
  5. 2 0
      setup.py
  6. 231 0
      tests/ext/unit/test_aiohttp.py

+ 1 - 1
hapic/async.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from hapic.hapic import Hapic
2
 from hapic.hapic import Hapic
3
 
3
 
4
-_hapic_default = Hapic(async=True)
4
+_hapic_default = Hapic(async_=True)
5
 
5
 
6
 with_api_doc = _hapic_default.with_api_doc
6
 with_api_doc = _hapic_default.with_api_doc
7
 input_headers = _hapic_default.input_headers
7
 input_headers = _hapic_default.input_headers

+ 18 - 6
hapic/decorator.py View File

78
             # Note: Design of before_wrapped_func can be to update kwargs
78
             # Note: Design of before_wrapped_func can be to update kwargs
79
             # by reference here
79
             # by reference here
80
             replacement_response = self.before_wrapped_func(args, kwargs)
80
             replacement_response = self.before_wrapped_func(args, kwargs)
81
-            if replacement_response:
81
+            if replacement_response is not None:
82
                 return replacement_response
82
                 return replacement_response
83
 
83
 
84
             response = self._execute_wrapped_function(func, args, kwargs)
84
             response = self._execute_wrapped_function(func, args, kwargs)
203
             # Note: Design of before_wrapped_func can be to update kwargs
203
             # Note: Design of before_wrapped_func can be to update kwargs
204
             # by reference here
204
             # by reference here
205
             replacement_response = await self.before_wrapped_func(args, kwargs)
205
             replacement_response = await self.before_wrapped_func(args, kwargs)
206
-            if replacement_response:
206
+            if replacement_response is not None:
207
                 return replacement_response
207
                 return replacement_response
208
 
208
 
209
             response = await self._execute_wrapped_function(func, args, kwargs)
209
             response = await self._execute_wrapped_function(func, args, kwargs)
229
             processed_data = await self.get_processed_data(request_parameters)
229
             processed_data = await self.get_processed_data(request_parameters)
230
             self.update_hapic_data(hapic_data, processed_data)
230
             self.update_hapic_data(hapic_data, processed_data)
231
         except ProcessException:
231
         except ProcessException:
232
-            error_response = self.get_error_response(request_parameters)
232
+            error_response = await self.get_error_response(request_parameters)
233
             return error_response
233
             return error_response
234
 
234
 
235
     async def get_processed_data(
235
     async def get_processed_data(
327
             # Note: Design of before_wrapped_func can be to update kwargs
327
             # Note: Design of before_wrapped_func can be to update kwargs
328
             # by reference here
328
             # by reference here
329
             replacement_response = self.before_wrapped_func(args, kwargs)
329
             replacement_response = self.before_wrapped_func(args, kwargs)
330
-            if replacement_response:
330
+            if replacement_response is not None:
331
                 return replacement_response
331
                 return replacement_response
332
 
332
 
333
             response = await self._execute_wrapped_function(func, args, kwargs)
333
             response = await self._execute_wrapped_function(func, args, kwargs)
346
             # Note: Design of before_wrapped_func can be to update kwargs
346
             # Note: Design of before_wrapped_func can be to update kwargs
347
             # by reference here
347
             # by reference here
348
             replacement_response = self.before_wrapped_func(args, kwargs)
348
             replacement_response = self.before_wrapped_func(args, kwargs)
349
-            if replacement_response:
349
+            if replacement_response is not None:
350
                 return replacement_response
350
                 return replacement_response
351
 
351
 
352
             stream_response = await self.context.get_stream_response_object(
352
             stream_response = await self.context.get_stream_response_object(
353
                 args,
353
                 args,
354
                 kwargs,
354
                 kwargs,
355
             )
355
             )
356
-            async for stream_item in self._execute_wrapped_function(
356
+            async for stream_item in await self._execute_wrapped_function(
357
                 func,
357
                 func,
358
                 args,
358
                 args,
359
                 kwargs,
359
                 kwargs,
475
     async def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
475
     async def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
476
         return await request_parameters.body_parameters
476
         return await request_parameters.body_parameters
477
 
477
 
478
+    async def get_error_response(
479
+        self,
480
+        request_parameters: RequestParameters,
481
+    ) -> typing.Any:
482
+        parameters_data = await self.get_parameters_data(request_parameters)
483
+        error = self.processor.get_validation_error(parameters_data)
484
+        error_response = self.context.get_validation_error_response(
485
+            error,
486
+            http_code=self.error_http_code,
487
+        )
488
+        return error_response
489
+
478
 
490
 
479
 class InputHeadersControllerWrapper(InputControllerWrapper):
491
 class InputHeadersControllerWrapper(InputControllerWrapper):
480
     def update_hapic_data(
492
     def update_hapic_data(

+ 28 - 5
hapic/ext/aiohttp/context.py View File

12
 from hapic.context import BaseContext
12
 from hapic.context import BaseContext
13
 from hapic.context import RouteRepresentation
13
 from hapic.context import RouteRepresentation
14
 from hapic.decorator import DecoratedController
14
 from hapic.decorator import DecoratedController
15
-from hapic.exception import WorkflowException
15
+from hapic.error import ErrorBuilderInterface, DefaultErrorBuilder
16
+from hapic.exception import WorkflowException, OutputValidationException
16
 from hapic.processor import ProcessValidationError
17
 from hapic.processor import ProcessValidationError
17
 from hapic.processor import RequestParameters
18
 from hapic.processor import RequestParameters
18
 from aiohttp import web
19
 from aiohttp import web
66
     def __init__(
67
     def __init__(
67
         self,
68
         self,
68
         app: web.Application,
69
         app: web.Application,
70
+        default_error_builder: ErrorBuilderInterface=None,
69
         debug: bool = False,
71
         debug: bool = False,
70
     ) -> None:
72
     ) -> None:
71
         self._app = app
73
         self._app = app
72
         self._debug = debug
74
         self._debug = debug
75
+        self.default_error_builder = \
76
+            default_error_builder or DefaultErrorBuilder()  # FDV
73
 
77
 
74
     @property
78
     @property
75
     def app(self) -> web.Application:
79
     def app(self) -> web.Application:
106
         error: ProcessValidationError,
110
         error: ProcessValidationError,
107
         http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
111
         http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
108
     ) -> typing.Any:
112
     ) -> typing.Any:
109
-        # TODO BS 2018-07-24: To do
110
-        raise NotImplementedError('todo')
113
+        error_builder = self.get_default_error_builder()
114
+        error_content = error_builder.build_from_validation_error(
115
+            error,
116
+        )
117
+
118
+        # Check error
119
+        dumped = error_builder.dump(error_content).data
120
+        unmarshall = error_builder.load(dumped)
121
+        if unmarshall.errors:
122
+            raise OutputValidationException(
123
+                'Validation error during dump of error response: {}'.format(
124
+                    str(unmarshall.errors)
125
+                )
126
+            )
127
+
128
+        return web.Response(
129
+            text=json.dumps(dumped),
130
+            headers=[
131
+                ('Content-Type', 'application/json'),
132
+            ],
133
+            status=int(http_code),
134
+        )
111
 
135
 
112
     def find_route(
136
     def find_route(
113
         self,
137
         self,
127
         self,
151
         self,
128
         response: typing.Any,
152
         response: typing.Any,
129
     ) -> bool:
153
     ) -> bool:
130
-        # TODO BS 2018-07-15: to do
131
-        raise NotImplementedError('todo')
154
+        return isinstance(response, web.Response)
132
 
155
 
133
     def add_view(
156
     def add_view(
134
         self,
157
         self,

+ 2 - 2
hapic/hapic.py View File

51
 class Hapic(object):
51
 class Hapic(object):
52
     def __init__(
52
     def __init__(
53
         self,
53
         self,
54
-        async: bool = False,
54
+        async_: bool = False,
55
     ):
55
     ):
56
         self._buffer = DecorationBuffer()
56
         self._buffer = DecorationBuffer()
57
         self._controllers = []  # type: typing.List[DecoratedController]
57
         self._controllers = []  # type: typing.List[DecoratedController]
58
         self._context = None  # type: ContextInterface
58
         self._context = None  # type: ContextInterface
59
         self._error_builder = None  # type: ErrorBuilderInterface
59
         self._error_builder = None  # type: ErrorBuilderInterface
60
-        self._async = async
60
+        self._async = async_
61
         self.doc_generator = DocGenerator()
61
         self.doc_generator = DocGenerator()
62
 
62
 
63
         # This local function will be pass to different components
63
         # This local function will be pass to different components

+ 2 - 0
setup.py View File

29
     'flask',
29
     'flask',
30
     'pyramid',
30
     'pyramid',
31
     'webtest',
31
     'webtest',
32
+    'aiohttp',
33
+    'pytest-aiohttp',
32
 ]
34
 ]
33
 dev_require = [
35
 dev_require = [
34
     'requests',
36
     'requests',

+ 231 - 0
tests/ext/unit/test_aiohttp.py View File

1
+# coding: utf-8
2
+from aiohttp import web
3
+import marshmallow
4
+
5
+from hapic import Hapic
6
+from hapic import HapicData
7
+from hapic.ext.aiohttp.context import AiohttpContext
8
+
9
+
10
+class TestAiohttpExt(object):
11
+    async def test_aiohttp_only__ok__nominal_case(
12
+        self,
13
+        aiohttp_client,
14
+        loop,
15
+    ):
16
+        async def hello(request):
17
+            return web.Response(text='Hello, world')
18
+
19
+        app = web.Application(debug=True)
20
+        app.router.add_get('/', hello)
21
+        client = await aiohttp_client(app)
22
+        resp = await client.get('/')
23
+        assert resp.status == 200
24
+        text = await resp.text()
25
+        assert 'Hello, world' in text
26
+
27
+    async def test_aiohttp_input_path__ok__nominal_case(
28
+        self,
29
+        aiohttp_client,
30
+        loop,
31
+    ):
32
+        hapic = Hapic(async_=True)
33
+
34
+        class InputPathSchema(marshmallow.Schema):
35
+            name = marshmallow.fields.String()
36
+
37
+        @hapic.input_path(InputPathSchema())
38
+        async def hello(request, hapic_data: HapicData):
39
+            name = hapic_data.path.get('name')
40
+            return web.Response(text='Hello, {}'.format(name))
41
+
42
+        app = web.Application(debug=True)
43
+        app.router.add_get('/{name}', hello)
44
+        hapic.set_context(AiohttpContext(app))
45
+        client = await aiohttp_client(app)
46
+
47
+        resp = await client.get('/bob')
48
+        assert resp.status == 200
49
+
50
+        text = await resp.text()
51
+        assert 'Hello, bob' in text
52
+
53
+    async def test_aiohttp_input_path__error_wrong_input_parameter(
54
+        self,
55
+        aiohttp_client,
56
+        loop,
57
+    ):
58
+        hapic = Hapic(async_=True)
59
+
60
+        class InputPathSchema(marshmallow.Schema):
61
+            i = marshmallow.fields.Integer()
62
+
63
+        @hapic.input_path(InputPathSchema())
64
+        async def hello(request, hapic_data: HapicData):
65
+            i = hapic_data.path.get('i')
66
+            return web.Response(text='integer: {}'.format(str(i)))
67
+
68
+        app = web.Application(debug=True)
69
+        app.router.add_get('/{i}', hello)
70
+        hapic.set_context(AiohttpContext(app))
71
+        client = await aiohttp_client(app)
72
+
73
+        resp = await client.get('/bob')  # NOTE: should be integer here
74
+        assert resp.status == 400
75
+
76
+        error = await resp.json()
77
+        assert 'Validation error of input data' in error.get('message')
78
+        assert {'i': ['Not a valid integer.']} == error.get('details')
79
+
80
+    async def test_aiohttp_input_body__ok_nominal_case(
81
+        self,
82
+        aiohttp_client,
83
+        loop,
84
+    ):
85
+        hapic = Hapic(async_=True)
86
+
87
+        class InputBodySchema(marshmallow.Schema):
88
+            name = marshmallow.fields.String()
89
+
90
+        @hapic.input_body(InputBodySchema())
91
+        async def hello(request, hapic_data: HapicData):
92
+            name = hapic_data.body.get('name')
93
+            return web.Response(text='Hello, {}'.format(name))
94
+
95
+        app = web.Application(debug=True)
96
+        app.router.add_post('/', hello)
97
+        hapic.set_context(AiohttpContext(app))
98
+        client = await aiohttp_client(app)
99
+
100
+        resp = await client.post('/', data={'name': 'bob'})
101
+        assert resp.status == 200
102
+
103
+        text = await resp.text()
104
+        assert 'Hello, bob' in text
105
+
106
+    async def test_aiohttp_input_body__error__incorrect_input_body(
107
+        self,
108
+        aiohttp_client,
109
+        loop,
110
+    ):
111
+        hapic = Hapic(async_=True)
112
+
113
+        class InputBodySchema(marshmallow.Schema):
114
+            i = marshmallow.fields.Integer()
115
+
116
+        @hapic.input_body(InputBodySchema())
117
+        async def hello(request, hapic_data: HapicData):
118
+            i = hapic_data.body.get('i')
119
+            return web.Response(text='integer, {}'.format(i))
120
+
121
+        app = web.Application(debug=True)
122
+        app.router.add_post('/', hello)
123
+        hapic.set_context(AiohttpContext(app))
124
+        client = await aiohttp_client(app)
125
+
126
+        resp = await client.post('/', data={'i': 'bob'})  # NOTE: should be int
127
+        assert resp.status == 400
128
+
129
+        error = await resp.json()
130
+        assert 'Validation error of input data' in error.get('message')
131
+        assert {'i': ['Not a valid integer.']} == error.get('details')
132
+
133
+    async def test_aiohttp_output_body__ok__nominal_case(
134
+        self,
135
+        aiohttp_client,
136
+        loop,
137
+    ):
138
+        hapic = Hapic(async_=True)
139
+
140
+        class OuputBodySchema(marshmallow.Schema):
141
+            name = marshmallow.fields.String()
142
+
143
+        @hapic.output_body(OuputBodySchema())
144
+        async def hello(request):
145
+            return {
146
+                'name': 'bob',
147
+            }
148
+
149
+        app = web.Application(debug=True)
150
+        app.router.add_get('/', hello)
151
+        hapic.set_context(AiohttpContext(app))
152
+        client = await aiohttp_client(app)
153
+
154
+        resp = await client.get('/')
155
+        assert resp.status == 200
156
+
157
+        data = await resp.json()
158
+        assert 'bob' == data.get('name')
159
+
160
+    async def test_aiohttp_output_body__error__incorrect_output_body(
161
+        self,
162
+        aiohttp_client,
163
+        loop,
164
+    ):
165
+        hapic = Hapic(async_=True)
166
+
167
+        class OuputBodySchema(marshmallow.Schema):
168
+            i = marshmallow.fields.Integer(required=True)
169
+
170
+        @hapic.output_body(OuputBodySchema())
171
+        async def hello(request):
172
+            return {
173
+                'i': 'bob',  # NOTE: should be integer
174
+            }
175
+
176
+        app = web.Application(debug=True)
177
+        app.router.add_get('/', hello)
178
+        hapic.set_context(AiohttpContext(app))
179
+        client = await aiohttp_client(app)
180
+
181
+        resp = await client.get('/')
182
+        assert resp.status == 500
183
+
184
+        data = await resp.json()
185
+        assert 'Validation error of output data' == data.get('message')
186
+        assert {
187
+                   'i': ['Missing data for required field.'],
188
+               } == data.get('details')
189
+
190
+    async def test_aiohttp_output_stream__ok__nominal_case(
191
+        self,
192
+        aiohttp_client,
193
+        loop,
194
+    ):
195
+        hapic = Hapic(async_=True)
196
+
197
+        class AsyncGenerator:
198
+            def __init__(self):
199
+                self._iterator = iter([
200
+                    {'name': 'Hello, bob'},
201
+                    {'name': 'Hello, franck'},
202
+                ])
203
+
204
+            async def __aiter__(self):
205
+                return self
206
+
207
+            async def __anext__(self):
208
+                return next(self._iterator)
209
+
210
+        class OuputStreamItemSchema(marshmallow.Schema):
211
+            name = marshmallow.fields.String()
212
+
213
+        @hapic.output_stream(OuputStreamItemSchema())
214
+        async def hello(request):
215
+            return AsyncGenerator()
216
+
217
+        app = web.Application(debug=True)
218
+        app.router.add_get('/', hello)
219
+        hapic.set_context(AiohttpContext(app))
220
+        client = await aiohttp_client(app)
221
+
222
+        resp = await client.get('/')
223
+        assert resp.status == 200
224
+
225
+        line = await resp.content.readline()
226
+        assert b'{"name": "Hello, bob"}\n' == line
227
+
228
+        line = await resp.content.readline()
229
+        assert b'{"name": "Hello, franck"}\n' == line
230
+
231
+        # TODO BS 2018-07-26: How to ensure we are at end of response ?