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
 import marshmallow
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
 class HelloResponseSchema(marshmallow.Schema):
10
 class HelloResponseSchema(marshmallow.Schema):
6
     sentence = marshmallow.fields.String(required=True)
11
     sentence = marshmallow.fields.String(required=True)
7
     name = marshmallow.fields.String(required=True)
12
     name = marshmallow.fields.String(required=True)
9
 
14
 
10
 
15
 
11
 class HelloPathSchema(marshmallow.Schema):
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
 class HelloJsonSchema(marshmallow.Schema):
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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import bottle
2
 import bottle
3
 import hapic
3
 import hapic
4
-from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema
4
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
5
+    ErrorResponseSchema
5
 from hapic.hapic import HapicData
6
 from hapic.hapic import HapicData
6
 
7
 
7
 app = bottle.Bottle()
8
 app = bottle.Bottle()
15
 
16
 
16
 @hapic.with_api_doc()
17
 @hapic.with_api_doc()
17
 @hapic.ext.bottle.bottle_context()
18
 @hapic.ext.bottle.bottle_context()
19
+@hapic.error_schema(ErrorResponseSchema())
18
 @hapic.input_path(HelloPathSchema())
20
 @hapic.input_path(HelloPathSchema())
19
 @hapic.output(HelloResponseSchema())
21
 @hapic.output(HelloResponseSchema())
20
 def hello(name: str, hapic_data: HapicData):
22
 def hello(name: str, hapic_data: HapicData):
26
 
28
 
27
 @hapic.with_api_doc()
29
 @hapic.with_api_doc()
28
 @hapic.ext.bottle.bottle_context()
30
 @hapic.ext.bottle.bottle_context()
31
+@hapic.error_schema(ErrorResponseSchema())
29
 @hapic.input_path(HelloPathSchema())
32
 @hapic.input_path(HelloPathSchema())
30
 @hapic.input_body(HelloJsonSchema())
33
 @hapic.input_body(HelloJsonSchema())
31
 @hapic.output(HelloResponseSchema())
34
 @hapic.output(HelloResponseSchema())
42
 
45
 
43
 @hapic.with_api_doc()
46
 @hapic.with_api_doc()
44
 @hapic.ext.bottle.bottle_context()
47
 @hapic.ext.bottle.bottle_context()
48
+@hapic.error_schema(ErrorResponseSchema())
45
 @hapic.output(HelloResponseSchema())
49
 @hapic.output(HelloResponseSchema())
46
 def hello3(name: str, hapic_data: HapicData):
50
 def hello3(name: str, hapic_data: HapicData):
47
     return {
51
     return {

+ 1 - 0
hapic/__init__.py View File

17
 from hapic.hapic import input_headers
17
 from hapic.hapic import input_headers
18
 from hapic.hapic import BottleContext
18
 from hapic.hapic import BottleContext
19
 from hapic.hapic import set_fake_default_context
19
 from hapic.hapic import set_fake_default_context
20
+from hapic.hapic import error_schema
20
 
21
 
21
 
22
 
22
 class FakeSetContext(object):
23
 class FakeSetContext(object):

+ 29 - 0
hapic/exception.py View File

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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import json
2
 import json
3
 import typing
3
 import typing
4
+from http import HTTPStatus
4
 
5
 
5
 import functools
6
 import functools
6
 
7
 
11
 
12
 
12
 
13
 
13
 # CHANGE
14
 # CHANGE
15
+from hapic.exception import InputValidationException, \
16
+    OutputValidationException, InputWorkflowException, ProcessException
17
+
14
 flatten = lambda l: [item for sublist in l for item in sublist]
18
 flatten = lambda l: [item for sublist in l for item in sublist]
15
 
19
 
16
 
20
 
17
 _waiting = {}
21
 _waiting = {}
18
 _endpoints = {}
22
 _endpoints = {}
19
 _default_global_context = None
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
 def set_fake_default_context(context):
39
 def set_fake_default_context(context):
91
         self.header_parameters = header_parameters
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
 class ContextInterface(object):
121
 class ContextInterface(object):
95
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
122
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
96
         raise NotImplementedError()
123
         raise NotImplementedError()
99
         self,
126
         self,
100
         response: dict,
127
         response: dict,
101
         http_code: int,
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
         raise NotImplementedError()
137
         raise NotImplementedError()
104
 
138
 
105
 
139
 
126
             status=http_code,
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
 class OutputProcessorInterface(object):
185
 class OutputProcessorInterface(object):
131
     def __init__(self):
186
     def __init__(self):
134
     def process(self, value):
189
     def process(self, value):
135
         raise NotImplementedError
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
 class InputProcessorInterface(object):
199
 class InputProcessorInterface(object):
139
     def __init__(self):
200
     def __init__(self):
140
         self.schema = None
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
         raise NotImplementedError
213
         raise NotImplementedError
144
 
214
 
145
 
215
 
146
 class MarshmallowOutputProcessor(OutputProcessorInterface):
216
 class MarshmallowOutputProcessor(OutputProcessorInterface):
147
     def process(self, data: typing.Any):
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
 class MarshmallowInputProcessor(OutputProcessorInterface):
236
 class MarshmallowInputProcessor(OutputProcessorInterface):
152
     def process(self, data: dict):
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
 class HapicData(object):
256
 class HapicData(object):
214
         return wrapper
289
         return wrapper
215
     return decorator
290
     return decorator
216
 
291
 
217
-
218
 # TODO: raccourcis 'input' tout court ?
292
 # TODO: raccourcis 'input' tout court ?
219
 def input_body(
293
 def input_body(
220
     schema,
294
     schema,
221
     processor: InputProcessorInterface=None,
295
     processor: InputProcessorInterface=None,
222
     context: ContextInterface=None,
296
     context: ContextInterface=None,
223
-    error_http_code=400,
297
+    error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
224
 ):
298
 ):
225
     processor = processor or MarshmallowInputProcessor()
299
     processor = processor or MarshmallowInputProcessor()
226
     processor.schema = schema
300
     processor.schema = schema
234
             updated_kwargs.update(kwargs)
308
             updated_kwargs.update(kwargs)
235
             hapic_data = updated_kwargs['hapic_data']
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
             return func(*args, **updated_kwargs)
330
             return func(*args, **updated_kwargs)
241
 
331
 

+ 3 - 1
setup.py View File

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