Browse Source

manage correctly multiple values for query parameters

Bastien Sevajol 6 years ago
parent
commit
9fbbbad943
6 changed files with 152 additions and 23 deletions
  1. 42 6
      hapic/decorator.py
  2. 13 6
      hapic/ext/bottle/context.py
  3. 20 6
      hapic/processor.py
  4. 1 0
      setup.py
  5. 4 2
      tests/base.py
  6. 72 3
      tests/unit/test_decorator.py

+ 42 - 6
hapic/decorator.py View File

@@ -6,6 +6,7 @@ from http import HTTPStatus
6 6
 # TODO BS 20171010: bottle specific !  # see #5
7 7
 import marshmallow
8 8
 from bottle import HTTPResponse
9
+from multidict import MultiDict
9 10
 
10 11
 from hapic.data import HapicData
11 12
 from hapic.description import ControllerDescription
@@ -270,18 +271,53 @@ class InputPathControllerWrapper(InputControllerWrapper):
270 271
     ) -> None:
271 272
         hapic_data.path = processed_data
272 273
 
273
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
274
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
274 275
         return request_parameters.path_parameters
275 276
 
276 277
 
277 278
 class InputQueryControllerWrapper(InputControllerWrapper):
279
+    def __init__(
280
+        self,
281
+        context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]],  # nopep8
282
+        processor: ProcessorInterface,
283
+        error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
284
+        default_http_code: HTTPStatus=HTTPStatus.OK,
285
+        as_list: typing.List[str]=None
286
+    ) -> None:
287
+        super().__init__(
288
+            context,
289
+            processor,
290
+            error_http_code,
291
+            default_http_code,
292
+        )
293
+        self.as_list = as_list or []  # FDV
294
+
278 295
     def update_hapic_data(
279 296
         self, hapic_data: HapicData,
280 297
         processed_data: typing.Any,
281 298
     ) -> None:
282 299
         hapic_data.query = processed_data
283 300
 
284
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
301
+    def get_parameters_data(self, request_parameters: RequestParameters) -> MultiDict:  # nopep8
302
+        # Parameters are updated considering eventual as_list parameters
303
+        if self.as_list:
304
+            query_parameters = MultiDict()
305
+            for parameter_name in request_parameters.query_parameters.keys():
306
+                if parameter_name in query_parameters:
307
+                    continue
308
+
309
+                if parameter_name in self.as_list:
310
+                    query_parameters[parameter_name] = \
311
+                        request_parameters.query_parameters.getall(
312
+                            parameter_name,
313
+                        )
314
+                else:
315
+                    query_parameters[parameter_name] = \
316
+                        request_parameters.query_parameters.get(
317
+                            parameter_name,
318
+                        )
319
+            return query_parameters
320
+
285 321
         return request_parameters.query_parameters
286 322
 
287 323
 
@@ -292,7 +328,7 @@ class InputBodyControllerWrapper(InputControllerWrapper):
292 328
     ) -> None:
293 329
         hapic_data.body = processed_data
294 330
 
295
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
331
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
296 332
         return request_parameters.body_parameters
297 333
 
298 334
 
@@ -303,7 +339,7 @@ class InputHeadersControllerWrapper(InputControllerWrapper):
303 339
     ) -> None:
304 340
         hapic_data.headers = processed_data
305 341
 
306
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
342
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
307 343
         return request_parameters.header_parameters
308 344
 
309 345
 
@@ -314,7 +350,7 @@ class InputFormsControllerWrapper(InputControllerWrapper):
314 350
     ) -> None:
315 351
         hapic_data.forms = processed_data
316 352
 
317
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
353
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
318 354
         return request_parameters.form_parameters
319 355
 
320 356
 
@@ -325,7 +361,7 @@ class InputFilesControllerWrapper(InputControllerWrapper):
325 361
     ) -> None:
326 362
         hapic_data.files = processed_data
327 363
 
328
-    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
364
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:  # nopep8
329 365
         return request_parameters.files_parameters
330 366
 
331 367
 

+ 13 - 6
hapic/ext/bottle/context.py View File

@@ -12,13 +12,20 @@ from hapic.processor import RequestParameters, ProcessValidationError
12 12
 
13 13
 class BottleContext(ContextInterface):
14 14
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
15
+        path_parameters = dict(bottle.request.url_args)
16
+        query_parameters = bottle.MultiDict(bottle.request.query)
17
+        body_parameters = dict(bottle.request.json)
18
+        form_parameters = bottle.MultiDict(bottle.request.forms)
19
+        header_parameters = dict(bottle.request.headers)
20
+        files_parameters = dict(bottle.request.files)
21
+
15 22
         return RequestParameters(
16
-            path_parameters=bottle.request.url_args,
17
-            query_parameters=bottle.request.query.dict,
18
-            body_parameters=bottle.request.json,
19
-            form_parameters=bottle.request.forms,
20
-            header_parameters=bottle.request.headers,
21
-            files_parameters=bottle.request.files,
23
+            path_parameters=path_parameters,
24
+            query_parameters=query_parameters,
25
+            body_parameters=body_parameters,
26
+            form_parameters=form_parameters,
27
+            header_parameters=header_parameters,
28
+            files_parameters=files_parameters,
22 29
         )
23 30
 
24 31
     def get_response(

+ 20 - 6
hapic/processor.py View File

@@ -1,6 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3 3
 
4
+from multidict import MultiDict
5
+
4 6
 from hapic.exception import OutputValidationException
5 7
 from hapic.exception import ConfigurationException
6 8
 
@@ -8,13 +10,25 @@ from hapic.exception import ConfigurationException
8 10
 class RequestParameters(object):
9 11
     def __init__(
10 12
         self,
11
-        path_parameters,
12
-        query_parameters,
13
-        body_parameters,
14
-        form_parameters,
15
-        header_parameters,
16
-        files_parameters,
13
+        path_parameters: dict,
14
+        query_parameters: MultiDict,
15
+        body_parameters: dict,
16
+        form_parameters: MultiDict,
17
+        header_parameters: dict,
18
+        files_parameters: dict,
17 19
     ):
20
+        """
21
+        :param path_parameters:
22
+
23
+        TODO Documenter + example pour chaque
24
+        ex: /api/user/<user_id> -> {'user_id': 'abc'}
25
+        ex: /api/user/<user_id> -> {'user_id': 'abc'}
26
+        ?resource_id=abc&resource_id=def ->
27
+
28
+        """
29
+        assert isinstance(query_parameters, MultiDict)
30
+        assert isinstance(form_parameters, MultiDict)
31
+
18 32
         self.path_parameters = path_parameters
19 33
         self.query_parameters = query_parameters
20 34
         self.body_parameters = body_parameters

+ 1 - 0
setup.py View File

@@ -12,6 +12,7 @@ install_requires = [
12 12
     'bottle',
13 13
     'marshmallow',
14 14
     'apispec==0.25.4-algoo',
15
+    'multidict'
15 16
 ]
16 17
 dependency_links = [
17 18
     'git+https://github.com/algoo/apispec.git@dev-algoo#egg=apispec-0.25.4-algoo'  # nopep8

+ 4 - 2
tests/base.py View File

@@ -2,6 +2,8 @@
2 2
 import typing
3 3
 from http import HTTPStatus
4 4
 
5
+from multidict import MultiDict
6
+
5 7
 from hapic.context import ContextInterface
6 8
 from hapic.processor import RequestParameters
7 9
 from hapic.processor import ProcessValidationError
@@ -22,9 +24,9 @@ class MyContext(ContextInterface):
22 24
         fake_files_parameters=None,
23 25
     ) -> None:
24 26
         self.fake_path_parameters = fake_path_parameters or {}
25
-        self.fake_query_parameters = fake_query_parameters or {}
27
+        self.fake_query_parameters = fake_query_parameters or MultiDict()
26 28
         self.fake_body_parameters = fake_body_parameters or {}
27
-        self.fake_form_parameters = fake_form_parameters or {}
29
+        self.fake_form_parameters = fake_form_parameters or MultiDict()
28 30
         self.fake_header_parameters = fake_header_parameters or {}
29 31
         self.fake_files_parameters = fake_files_parameters or {}
30 32
 

+ 72 - 3
tests/unit/test_decorator.py View File

@@ -3,9 +3,11 @@ import typing
3 3
 from http import HTTPStatus
4 4
 
5 5
 import marshmallow
6
+from multidict import MultiDict
6 7
 
7 8
 from hapic.data import HapicData
8 9
 from hapic.decorator import ExceptionHandlerControllerWrapper
10
+from hapic.decorator import InputQueryControllerWrapper
9 11
 from hapic.decorator import InputControllerWrapper
10 12
 from hapic.decorator import InputOutputControllerWrapper
11 13
 from hapic.decorator import OutputControllerWrapper
@@ -34,6 +36,22 @@ class MyProcessor(ProcessorInterface):
34 36
         )
35 37
 
36 38
 
39
+class MySimpleProcessor(ProcessorInterface):
40
+    def process(self, value):
41
+        return value
42
+
43
+    def get_validation_error(
44
+        self,
45
+        request_context: RequestParameters,
46
+    ) -> ProcessValidationError:
47
+        return ProcessValidationError(
48
+            details={
49
+                'original_request_context': request_context,
50
+            },
51
+            message='ERROR',
52
+        )
53
+
54
+
37 55
 class MyControllerWrapper(InputOutputControllerWrapper):
38 56
     def before_wrapped_func(
39 57
         self,
@@ -115,9 +133,11 @@ class TestControllerWrapper(Base):
115 133
 
116 134
 class TestInputControllerWrapper(Base):
117 135
     def test_unit__input_data_wrapping__ok__nominal_case(self):
118
-        context = MyContext(fake_query_parameters={
119
-            'foo': 'bar',
120
-        })
136
+        context = MyContext(fake_query_parameters=MultiDict(
137
+            (
138
+                ('foo', 'bar',),
139
+            )
140
+        ))
121 141
         processor = MyProcessor()
122 142
         wrapper = MyInputQueryControllerWrapper(context, processor)
123 143
 
@@ -132,6 +152,55 @@ class TestInputControllerWrapper(Base):
132 152
         result = func(42)
133 153
         assert result == 42
134 154
 
155
+    def test_unit__multi_query_param_values__ok__use_as_list(self):
156
+        context = MyContext(fake_query_parameters=MultiDict(
157
+            (
158
+                ('user_id', 'abc'),
159
+                ('user_id', 'def'),
160
+            ),
161
+        ))
162
+        processor = MySimpleProcessor()
163
+        wrapper = InputQueryControllerWrapper(
164
+            context,
165
+            processor,
166
+            as_list=['user_id'],
167
+        )
168
+
169
+        @wrapper.get_wrapper
170
+        def func(hapic_data=None):
171
+            assert hapic_data
172
+            assert isinstance(hapic_data, HapicData)
173
+            # see MyControllerWrapper#before_wrapped_func
174
+            assert ['abc', 'def'] == hapic_data.query.get('user_id')
175
+            return hapic_data.query.get('user_id')
176
+
177
+        result = func()
178
+        assert result == ['abc', 'def']
179
+
180
+    def test_unit__multi_query_param_values__ok__without_as_list(self):
181
+        context = MyContext(fake_query_parameters=MultiDict(
182
+            (
183
+                ('user_id', 'abc'),
184
+                ('user_id', 'def'),
185
+            ),
186
+        ))
187
+        processor = MySimpleProcessor()
188
+        wrapper = InputQueryControllerWrapper(
189
+            context,
190
+            processor,
191
+        )
192
+
193
+        @wrapper.get_wrapper
194
+        def func(hapic_data=None):
195
+            assert hapic_data
196
+            assert isinstance(hapic_data, HapicData)
197
+            # see MyControllerWrapper#before_wrapped_func
198
+            assert 'abc' == hapic_data.query.get('user_id')
199
+            return hapic_data.query.get('user_id')
200
+
201
+        result = func()
202
+        assert result == 'abc'
203
+
135 204
 
136 205
 class TestOutputControllerWrapper(Base):
137 206
     def test_unit__output_data_wrapping__ok__nominal_case(self):