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
     This controller wrapper produce a wrapper who caught the http view items
341
     This controller wrapper produce a wrapper who caught the http view items
342
     to check and serialize them into a stream response.
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
     def get_wrapper(
360
     def get_wrapper(
345
         self,
361
         self,
346
         func: 'typing.Callable[..., typing.Any]',
362
         func: 'typing.Callable[..., typing.Any]',
362
                 args,
378
                 args,
363
                 kwargs,
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
             return stream_response
393
             return stream_response
372
 
394
 
376
         self,
398
         self,
377
         item_object: typing.Any,
399
         item_object: typing.Any,
378
     ) -> dict:
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
 class OutputHeadersControllerWrapper(OutputControllerWrapper):
404
 class OutputHeadersControllerWrapper(OutputControllerWrapper):

+ 4 - 0
hapic/hapic.py View File

179
         context: ContextInterface = None,
179
         context: ContextInterface = None,
180
         error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
180
         error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
181
         default_http_code: HTTPStatus = HTTPStatus.OK,
181
         default_http_code: HTTPStatus = HTTPStatus.OK,
182
+        ignore_on_error: bool = True,
182
     ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
183
     ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
183
         """
184
         """
184
         Decorate with a wrapper who check and serialize each items in output
185
         Decorate with a wrapper who check and serialize each items in output
190
         :param context: Context to use here
191
         :param context: Context to use here
191
         :param error_http_code: http code in case of error
192
         :param error_http_code: http code in case of error
192
         :param default_http_code: http code in case of success
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
         :return: decorator
196
         :return: decorator
194
         """
197
         """
195
         processor = processor or MarshmallowOutputProcessor()
198
         processor = processor or MarshmallowOutputProcessor()
202
                 processor=processor,
205
                 processor=processor,
203
                 error_http_code=error_http_code,
206
                 error_http_code=error_http_code,
204
                 default_http_code=default_http_code,
207
                 default_http_code=default_http_code,
208
+                ignore_on_error=ignore_on_error,
205
             )
209
             )
206
         else:
210
         else:
207
             # TODO BS 2018-07-25: To do
211
             # TODO BS 2018-07-25: To do

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

228
         line = await resp.content.readline()
228
         line = await resp.content.readline()
229
         assert b'{"name": "Hello, franck"}\n' == line
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
     def test_unit__generate_doc__ok__nominal_case(
315
     def test_unit__generate_doc__ok__nominal_case(
234
         self,
316
         self,