Browse Source

error management (coded in only one input)

Bastien Sevajol 6 years ago
parent
commit
35a24c5dd7
6 changed files with 174 additions and 37 deletions
  1. 13 2
      example.py
  2. 5 1
      example_a.py
  3. 1 0
      hapic/__init__.py
  4. 29 0
      hapic/exception.py
  5. 123 33
      hapic/hapic.py
  6. 3 1
      setup.py

+ 13 - 2
example.py View File

@@ -2,6 +2,11 @@
2 2
 import marshmallow
3 3
 
4 4
 
5
+class ErrorResponseSchema(marshmallow.Schema):
6
+    error_message = marshmallow.fields.String(required=True)
7
+    error_details = marshmallow.fields.Dict(required=True)
8
+
9
+
5 10
 class HelloResponseSchema(marshmallow.Schema):
6 11
     sentence = marshmallow.fields.String(required=True)
7 12
     name = marshmallow.fields.String(required=True)
@@ -9,8 +14,14 @@ class HelloResponseSchema(marshmallow.Schema):
9 14
 
10 15
 
11 16
 class HelloPathSchema(marshmallow.Schema):
12
-    name = marshmallow.fields.String(required=True)
17
+    name = marshmallow.fields.String(
18
+        required=True,
19
+        validate=marshmallow.validate.Length(min=3),
20
+    )
13 21
 
14 22
 
15 23
 class HelloJsonSchema(marshmallow.Schema):
16
-    color = marshmallow.fields.String(required=True)
24
+    color =marshmallow.fields.String(
25
+        required=True,
26
+        validate=marshmallow.validate.Length(min=3),
27
+    )

+ 5 - 1
example_a.py View File

@@ -1,7 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import bottle
3 3
 import hapic
4
-from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema
4
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
5
+    ErrorResponseSchema
5 6
 from hapic.hapic import HapicData
6 7
 
7 8
 app = bottle.Bottle()
@@ -15,6 +16,7 @@ def bob(f):
15 16
 
16 17
 @hapic.with_api_doc()
17 18
 @hapic.ext.bottle.bottle_context()
19
+@hapic.error_schema(ErrorResponseSchema())
18 20
 @hapic.input_path(HelloPathSchema())
19 21
 @hapic.output(HelloResponseSchema())
20 22
 def hello(name: str, hapic_data: HapicData):
@@ -26,6 +28,7 @@ def hello(name: str, hapic_data: HapicData):
26 28
 
27 29
 @hapic.with_api_doc()
28 30
 @hapic.ext.bottle.bottle_context()
31
+@hapic.error_schema(ErrorResponseSchema())
29 32
 @hapic.input_path(HelloPathSchema())
30 33
 @hapic.input_body(HelloJsonSchema())
31 34
 @hapic.output(HelloResponseSchema())
@@ -42,6 +45,7 @@ kwargs = {'validated_data': {'name': 'bob'}, 'name': 'bob'}
42 45
 
43 46
 @hapic.with_api_doc()
44 47
 @hapic.ext.bottle.bottle_context()
48
+@hapic.error_schema(ErrorResponseSchema())
45 49
 @hapic.output(HelloResponseSchema())
46 50
 def hello3(name: str, hapic_data: HapicData):
47 51
     return {

+ 1 - 0
hapic/__init__.py View File

@@ -17,6 +17,7 @@ from hapic.hapic import input_path
17 17
 from hapic.hapic import input_headers
18 18
 from hapic.hapic import BottleContext
19 19
 from hapic.hapic import set_fake_default_context
20
+from hapic.hapic import error_schema
20 21
 
21 22
 
22 23
 class FakeSetContext(object):

+ 29 - 0
hapic/exception.py View File

@@ -0,0 +1,29 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+class HapicException(Exception):
5
+    pass
6
+
7
+
8
+class WorkflowException(HapicException):
9
+    pass
10
+
11
+
12
+class ProcessException(HapicException):
13
+    pass
14
+
15
+
16
+class InputWorkflowException(WorkflowException):
17
+    pass
18
+
19
+
20
+class OutputWorkflowException(WorkflowException):
21
+    pass
22
+
23
+
24
+class InputValidationException(InputWorkflowException, ProcessException):
25
+    pass
26
+
27
+
28
+class OutputValidationException(InputWorkflowException, ProcessException):
29
+    pass

+ 123 - 33
hapic/hapic.py View File

@@ -1,6 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import json
3 3
 import typing
4
+from http import HTTPStatus
4 5
 
5 6
 import functools
6 7
 
@@ -11,12 +12,28 @@ import bottle
11 12
 
12 13
 
13 14
 # CHANGE
15
+from hapic.exception import InputValidationException, \
16
+    OutputValidationException, InputWorkflowException, ProcessException
17
+
14 18
 flatten = lambda l: [item for sublist in l for item in sublist]
15 19
 
16 20
 
17 21
 _waiting = {}
18 22
 _endpoints = {}
19 23
 _default_global_context = None
24
+_default_global_error_schema = None
25
+
26
+
27
+def error_schema(schema):
28
+    global _default_global_error_schema
29
+    _default_global_error_schema = schema
30
+
31
+    def decorator(func):
32
+        @functools.wraps(func)
33
+        def wrapper(*args, **kwargs):
34
+            return func(*args, **kwargs)
35
+        return wrapper
36
+    return decorator
20 37
 
21 38
 
22 39
 def set_fake_default_context(context):
@@ -91,6 +108,16 @@ class RequestParameters(object):
91 108
         self.header_parameters = header_parameters
92 109
 
93 110
 
111
+class ProcessValidationError(object):
112
+    def __init__(
113
+        self,
114
+        error_message: str,
115
+        error_details: dict,
116
+    ) -> None:
117
+        self.error_message = error_message
118
+        self.error_details = error_details
119
+
120
+
94 121
 class ContextInterface(object):
95 122
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
96 123
         raise NotImplementedError()
@@ -99,7 +126,14 @@ class ContextInterface(object):
99 126
         self,
100 127
         response: dict,
101 128
         http_code: int,
102
-    ):
129
+    ) -> typing.Any:
130
+        raise NotImplementedError()
131
+
132
+    def get_validation_error_response(
133
+        self,
134
+        error: ProcessValidationError,
135
+        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
136
+    ) -> typing.Any:
103 137
         raise NotImplementedError()
104 138
 
105 139
 
@@ -126,6 +160,27 @@ class BottleContext(ContextInterface):
126 160
             status=http_code,
127 161
         )
128 162
 
163
+    def get_validation_error_response(
164
+        self,
165
+        error: ProcessValidationError,
166
+        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
167
+    ) -> typing.Any:
168
+        unmarshall = _default_global_error_schema.dump(error)
169
+        if unmarshall.errors:
170
+            raise OutputValidationException(
171
+                'Validation error during dump of error response: {}'.format(
172
+                    str(unmarshall.errors)
173
+                )
174
+            )
175
+
176
+        return bottle.HTTPResponse(
177
+            body=json.dumps(unmarshall.data),
178
+            headers=[
179
+                ('Content-Type', 'application/json'),
180
+            ],
181
+            status=int(http_code),
182
+        )
183
+
129 184
 
130 185
 class OutputProcessorInterface(object):
131 186
     def __init__(self):
@@ -134,48 +189,68 @@ class OutputProcessorInterface(object):
134 189
     def process(self, value):
135 190
         raise NotImplementedError
136 191
 
192
+    def get_validation_error(
193
+        self,
194
+        request_context: RequestParameters,
195
+    ) -> ProcessValidationError:
196
+        raise NotImplementedError
197
+
137 198
 
138 199
 class InputProcessorInterface(object):
139 200
     def __init__(self):
140 201
         self.schema = None
141 202
 
142
-    def process(self, request_context: RequestParameters):
203
+    def process(
204
+        self,
205
+        request_context: RequestParameters,
206
+    ) -> typing.Any:
207
+        raise NotImplementedError
208
+
209
+    def get_validation_error(
210
+        self,
211
+        request_context: RequestParameters,
212
+    ) -> ProcessValidationError:
143 213
         raise NotImplementedError
144 214
 
145 215
 
146 216
 class MarshmallowOutputProcessor(OutputProcessorInterface):
147 217
     def process(self, data: typing.Any):
148
-        return self.schema.dump(data).data
218
+        unmarshall = self.schema.dump(data)
219
+        if unmarshall.errors:
220
+            raise InputValidationException(
221
+                'Error when validate input: {}'.format(
222
+                    str(unmarshall.errors),
223
+                )
224
+            )
225
+
226
+        return unmarshall.data
227
+
228
+    def get_validation_error(self, data: dict) -> ProcessValidationError:
229
+        marshmallow_errors = self.schema.dump(data).errors
230
+        return ProcessValidationError(
231
+            error_message='Validation error of output data',
232
+            error_details=marshmallow_errors,
233
+        )
149 234
 
150 235
 
151 236
 class MarshmallowInputProcessor(OutputProcessorInterface):
152 237
     def process(self, data: dict):
153
-        return self.schema.load(data).data
154
-
155
-
156
-# class MarshmallowPathInputProcessor(OutputProcessorInterface):
157
-#     def process(self, request_context: RequestParameters):
158
-#         return self.schema.load(request_context.path_parameters).data
159
-#
160
-#
161
-# class MarshmallowQueryInputProcessor(OutputProcessorInterface):
162
-#     def process(self, request_context: RequestParameters):
163
-#         return self.schema.load(request_context.query_parameters).data
164
-#
165
-#
166
-# class MarshmallowJsonInputProcessor(OutputProcessorInterface):
167
-#     def process(self, request_context: RequestParameters):
168
-#         return self.schema.load(request_context.json_parameters).data
169
-
170
-
171
-# class MarshmallowFormInputProcessor(OutputProcessorInterface):
172
-#     def process(self, request_context: RequestParameters):
173
-#         return self.schema.load(xxx).data
174
-#
175
-#
176
-# class MarshmallowHeaderInputProcessor(OutputProcessorInterface):
177
-#     def process(self, request_context: RequestParameters):
178
-#         return self.schema.load(xxx).data
238
+        unmarshall = self.schema.load(data)
239
+        if unmarshall.errors:
240
+            raise OutputValidationException(
241
+                'Error when validate ouput: {}'.format(
242
+                    str(unmarshall.errors),
243
+                )
244
+            )
245
+
246
+        return unmarshall.data
247
+
248
+    def get_validation_error(self, data: dict) -> ProcessValidationError:
249
+        marshmallow_errors = self.schema.load(data).errors
250
+        return ProcessValidationError(
251
+            error_message='Validation error of input data',
252
+            error_details=marshmallow_errors,
253
+        )
179 254
 
180 255
 
181 256
 class HapicData(object):
@@ -214,13 +289,12 @@ def output(
214 289
         return wrapper
215 290
     return decorator
216 291
 
217
-
218 292
 # TODO: raccourcis 'input' tout court ?
219 293
 def input_body(
220 294
     schema,
221 295
     processor: InputProcessorInterface=None,
222 296
     context: ContextInterface=None,
223
-    error_http_code=400,
297
+    error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
224 298
 ):
225 299
     processor = processor or MarshmallowInputProcessor()
226 300
     processor.schema = schema
@@ -234,8 +308,24 @@ def input_body(
234 308
             updated_kwargs.update(kwargs)
235 309
             hapic_data = updated_kwargs['hapic_data']
236 310
 
237
-            request_parameters = context.get_request_parameters(*args, **updated_kwargs)
238
-            hapic_data.body = processor.process(request_parameters.body_parameters)
311
+            request_parameters = context.get_request_parameters(
312
+                *args,
313
+                **updated_kwargs
314
+            )
315
+
316
+            try:
317
+                hapic_data.body = processor.process(
318
+                    request_parameters.body_parameters,
319
+                )
320
+            except ProcessException:
321
+                error = processor.get_validation_error(
322
+                    request_parameters.body_parameters,
323
+                )
324
+                error_response = context.get_validation_error_response(
325
+                    error,
326
+                    http_code=error_http_code,
327
+                )
328
+                return error_response
239 329
 
240 330
             return func(*args, **updated_kwargs)
241 331
 

+ 3 - 1
setup.py View File

@@ -11,7 +11,9 @@ install_requires = [
11 11
     'bottle==0.12.13',
12 12
     'marshmallow==2.13.6',
13 13
 ]
14
-tests_require = []
14
+tests_require = [
15
+    'pytest',
16
+]
15 17
 
16 18
 setup(
17 19
     name='hapic',