Browse Source

output_stream: permit ingore or interrupt when serialization error

Bastien Sevajol 5 years ago
parent
commit
7c36cc8768
3 changed files with 115 additions and 12 deletions
  1. 28 11
      hapic/decorator.py
  2. 4 0
      hapic/hapic.py
  3. 83 1
      tests/ext/unit/test_aiohttp.py

+ 28 - 11
hapic/decorator.py View File

@@ -341,6 +341,22 @@ class AsyncOutputStreamControllerWrapper(OutputControllerWrapper):
341 341
     This controller wrapper produce a wrapper who caught the http view items
342 342
     to check and serialize them into a stream response.
343 343
     """
344
+    def __init__(
345
+        self,
346
+        context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]],  # nopep8
347
+        processor: ProcessorInterface,
348
+        error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
349
+        default_http_code: HTTPStatus=HTTPStatus.OK,
350
+        ignore_on_error: bool = True,
351
+    ) -> None:
352
+        super().__init__(
353
+            context,
354
+            processor,
355
+            error_http_code,
356
+            default_http_code,
357
+        )
358
+        self.ignore_on_error = ignore_on_error
359
+
344 360
     def get_wrapper(
345 361
         self,
346 362
         func: 'typing.Callable[..., typing.Any]',
@@ -362,11 +378,17 @@ class AsyncOutputStreamControllerWrapper(OutputControllerWrapper):
362 378
                 args,
363 379
                 kwargs,
364 380
             ):
365
-                serialized_item = self._get_serialized_item(stream_item)
366
-                await self.context.feed_stream_response(
367
-                    stream_response,
368
-                    serialized_item,
369
-                )
381
+                try:
382
+                    serialized_item = self._get_serialized_item(stream_item)
383
+                    await self.context.feed_stream_response(
384
+                        stream_response,
385
+                        serialized_item,
386
+                    )
387
+                except OutputValidationException as exc:
388
+                    if not self.ignore_on_error:
389
+                        # TODO BS 2018-07-31: Something should inform about
390
+                        # error, a log ?
391
+                        return stream_response
370 392
 
371 393
             return stream_response
372 394
 
@@ -376,12 +398,7 @@ class AsyncOutputStreamControllerWrapper(OutputControllerWrapper):
376 398
         self,
377 399
         item_object: typing.Any,
378 400
     ) -> dict:
379
-        try:
380
-            return self.processor.process(item_object)
381
-        except ProcessException:
382
-            # TODO BS 2018-07-25: Must interrupt stream response: but how
383
-            # inform about error ?
384
-            raise NotImplementedError('todo')
401
+        return self.processor.process(item_object)
385 402
 
386 403
 
387 404
 class OutputHeadersControllerWrapper(OutputControllerWrapper):

+ 4 - 0
hapic/hapic.py View File

@@ -179,6 +179,7 @@ class Hapic(object):
179 179
         context: ContextInterface = None,
180 180
         error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
181 181
         default_http_code: HTTPStatus = HTTPStatus.OK,
182
+        ignore_on_error: bool = True,
182 183
     ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
183 184
         """
184 185
         Decorate with a wrapper who check and serialize each items in output
@@ -190,6 +191,8 @@ class Hapic(object):
190 191
         :param context: Context to use here
191 192
         :param error_http_code: http code in case of error
192 193
         :param default_http_code: http code in case of success
194
+        :param ignore_on_error: if set, an error of serialization will be
195
+        ignored: stream will not send this failed object
193 196
         :return: decorator
194 197
         """
195 198
         processor = processor or MarshmallowOutputProcessor()
@@ -202,6 +205,7 @@ class Hapic(object):
202 205
                 processor=processor,
203 206
                 error_http_code=error_http_code,
204 207
                 default_http_code=default_http_code,
208
+                ignore_on_error=ignore_on_error,
205 209
             )
206 210
         else:
207 211
             # TODO BS 2018-07-25: To do

+ 83 - 1
tests/ext/unit/test_aiohttp.py View File

@@ -228,7 +228,89 @@ class TestAiohttpExt(object):
228 228
         line = await resp.content.readline()
229 229
         assert b'{"name": "Hello, franck"}\n' == line
230 230
 
231
-        # TODO BS 2018-07-26: How to ensure we are at end of response ?
231
+    async def test_aiohttp_output_stream__error__ignore(
232
+        self,
233
+        aiohttp_client,
234
+        loop,
235
+    ):
236
+        hapic = Hapic(async_=True)
237
+
238
+        class AsyncGenerator:
239
+            def __init__(self):
240
+                self._iterator = iter([
241
+                    {'name': 'Hello, bob'},
242
+                    {'nameZ': 'Hello, Z'},  # This line is incorrect
243
+                    {'name': 'Hello, franck'},
244
+                ])
245
+
246
+            async def __aiter__(self):
247
+                return self
248
+
249
+            async def __anext__(self):
250
+                return next(self._iterator)
251
+
252
+        class OuputStreamItemSchema(marshmallow.Schema):
253
+            name = marshmallow.fields.String(required=True)
254
+
255
+        @hapic.output_stream(OuputStreamItemSchema(), ignore_on_error=True)
256
+        async def hello(request):
257
+            return AsyncGenerator()
258
+
259
+        app = web.Application(debug=True)
260
+        app.router.add_get('/', hello)
261
+        hapic.set_context(AiohttpContext(app))
262
+        client = await aiohttp_client(app)
263
+
264
+        resp = await client.get('/')
265
+        assert resp.status == 200
266
+
267
+        line = await resp.content.readline()
268
+        assert b'{"name": "Hello, bob"}\n' == line
269
+
270
+        line = await resp.content.readline()
271
+        assert b'{"name": "Hello, franck"}\n' == line
272
+
273
+    async def test_aiohttp_output_stream__error__interrupt(
274
+        self,
275
+        aiohttp_client,
276
+        loop,
277
+    ):
278
+        hapic = Hapic(async_=True)
279
+
280
+        class AsyncGenerator:
281
+            def __init__(self):
282
+                self._iterator = iter([
283
+                    {'name': 'Hello, bob'},
284
+                    {'nameZ': 'Hello, Z'},  # This line is incorrect
285
+                    {'name': 'Hello, franck'},  # This line must not be reached
286
+                ])
287
+
288
+            async def __aiter__(self):
289
+                return self
290
+
291
+            async def __anext__(self):
292
+                return next(self._iterator)
293
+
294
+        class OuputStreamItemSchema(marshmallow.Schema):
295
+            name = marshmallow.fields.String(required=True)
296
+
297
+        @hapic.output_stream(OuputStreamItemSchema(), ignore_on_error=False)
298
+        async def hello(request):
299
+            return AsyncGenerator()
300
+
301
+        app = web.Application(debug=True)
302
+        app.router.add_get('/', hello)
303
+        hapic.set_context(AiohttpContext(app))
304
+        client = await aiohttp_client(app)
305
+
306
+        resp = await client.get('/')
307
+        assert resp.status == 200
308
+
309
+        line = await resp.content.readline()
310
+        assert b'{"name": "Hello, bob"}\n' == line
311
+
312
+        line = await resp.content.readline()
313
+        assert b'' == line
232 314
 
233 315
     def test_unit__generate_doc__ok__nominal_case(
234 316
         self,