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
 # TODO BS 20171010: bottle specific !  # see #5
6
 # TODO BS 20171010: bottle specific !  # see #5
7
 import marshmallow
7
 import marshmallow
8
 from bottle import HTTPResponse
8
 from bottle import HTTPResponse
9
+from multidict import MultiDict
9
 
10
 
10
 from hapic.data import HapicData
11
 from hapic.data import HapicData
11
 from hapic.description import ControllerDescription
12
 from hapic.description import ControllerDescription
270
     ) -> None:
271
     ) -> None:
271
         hapic_data.path = processed_data
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
         return request_parameters.path_parameters
275
         return request_parameters.path_parameters
275
 
276
 
276
 
277
 
277
 class InputQueryControllerWrapper(InputControllerWrapper):
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
     def update_hapic_data(
295
     def update_hapic_data(
279
         self, hapic_data: HapicData,
296
         self, hapic_data: HapicData,
280
         processed_data: typing.Any,
297
         processed_data: typing.Any,
281
     ) -> None:
298
     ) -> None:
282
         hapic_data.query = processed_data
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
         return request_parameters.query_parameters
321
         return request_parameters.query_parameters
286
 
322
 
287
 
323
 
292
     ) -> None:
328
     ) -> None:
293
         hapic_data.body = processed_data
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
         return request_parameters.body_parameters
332
         return request_parameters.body_parameters
297
 
333
 
298
 
334
 
303
     ) -> None:
339
     ) -> None:
304
         hapic_data.headers = processed_data
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
         return request_parameters.header_parameters
343
         return request_parameters.header_parameters
308
 
344
 
309
 
345
 
314
     ) -> None:
350
     ) -> None:
315
         hapic_data.forms = processed_data
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
         return request_parameters.form_parameters
354
         return request_parameters.form_parameters
319
 
355
 
320
 
356
 
325
     ) -> None:
361
     ) -> None:
326
         hapic_data.files = processed_data
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
         return request_parameters.files_parameters
365
         return request_parameters.files_parameters
330
 
366
 
331
 
367
 

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

12
 
12
 
13
 class BottleContext(ContextInterface):
13
 class BottleContext(ContextInterface):
14
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
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
         return RequestParameters(
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
     def get_response(
31
     def get_response(

+ 20 - 6
hapic/processor.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
 
3
 
4
+from multidict import MultiDict
5
+
4
 from hapic.exception import OutputValidationException
6
 from hapic.exception import OutputValidationException
5
 from hapic.exception import ConfigurationException
7
 from hapic.exception import ConfigurationException
6
 
8
 
8
 class RequestParameters(object):
10
 class RequestParameters(object):
9
     def __init__(
11
     def __init__(
10
         self,
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
         self.path_parameters = path_parameters
32
         self.path_parameters = path_parameters
19
         self.query_parameters = query_parameters
33
         self.query_parameters = query_parameters
20
         self.body_parameters = body_parameters
34
         self.body_parameters = body_parameters

+ 1 - 0
setup.py View File

12
     'bottle',
12
     'bottle',
13
     'marshmallow',
13
     'marshmallow',
14
     'apispec==0.25.4-algoo',
14
     'apispec==0.25.4-algoo',
15
+    'multidict'
15
 ]
16
 ]
16
 dependency_links = [
17
 dependency_links = [
17
     'git+https://github.com/algoo/apispec.git@dev-algoo#egg=apispec-0.25.4-algoo'  # nopep8
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
 import typing
2
 import typing
3
 from http import HTTPStatus
3
 from http import HTTPStatus
4
 
4
 
5
+from multidict import MultiDict
6
+
5
 from hapic.context import ContextInterface
7
 from hapic.context import ContextInterface
6
 from hapic.processor import RequestParameters
8
 from hapic.processor import RequestParameters
7
 from hapic.processor import ProcessValidationError
9
 from hapic.processor import ProcessValidationError
22
         fake_files_parameters=None,
24
         fake_files_parameters=None,
23
     ) -> None:
25
     ) -> None:
24
         self.fake_path_parameters = fake_path_parameters or {}
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
         self.fake_body_parameters = fake_body_parameters or {}
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
         self.fake_header_parameters = fake_header_parameters or {}
30
         self.fake_header_parameters = fake_header_parameters or {}
29
         self.fake_files_parameters = fake_files_parameters or {}
31
         self.fake_files_parameters = fake_files_parameters or {}
30
 
32
 

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

3
 from http import HTTPStatus
3
 from http import HTTPStatus
4
 
4
 
5
 import marshmallow
5
 import marshmallow
6
+from multidict import MultiDict
6
 
7
 
7
 from hapic.data import HapicData
8
 from hapic.data import HapicData
8
 from hapic.decorator import ExceptionHandlerControllerWrapper
9
 from hapic.decorator import ExceptionHandlerControllerWrapper
10
+from hapic.decorator import InputQueryControllerWrapper
9
 from hapic.decorator import InputControllerWrapper
11
 from hapic.decorator import InputControllerWrapper
10
 from hapic.decorator import InputOutputControllerWrapper
12
 from hapic.decorator import InputOutputControllerWrapper
11
 from hapic.decorator import OutputControllerWrapper
13
 from hapic.decorator import OutputControllerWrapper
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
 class MyControllerWrapper(InputOutputControllerWrapper):
55
 class MyControllerWrapper(InputOutputControllerWrapper):
38
     def before_wrapped_func(
56
     def before_wrapped_func(
39
         self,
57
         self,
115
 
133
 
116
 class TestInputControllerWrapper(Base):
134
 class TestInputControllerWrapper(Base):
117
     def test_unit__input_data_wrapping__ok__nominal_case(self):
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
         processor = MyProcessor()
141
         processor = MyProcessor()
122
         wrapper = MyInputQueryControllerWrapper(context, processor)
142
         wrapper = MyInputQueryControllerWrapper(context, processor)
123
 
143
 
132
         result = func(42)
152
         result = func(42)
133
         assert result == 42
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
 class TestOutputControllerWrapper(Base):
205
 class TestOutputControllerWrapper(Base):
137
     def test_unit__output_data_wrapping__ok__nominal_case(self):
206
     def test_unit__output_data_wrapping__ok__nominal_case(self):