query string accept multiple values for same parameter key (#11)

This commit is contained in:
MAILLOL Vincent 2021-03-27 11:56:19 +01:00 committed by GitHub
parent 81d4e93a1d
commit 145d2fc0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 13 deletions

View File

@ -54,7 +54,7 @@ Example:
return web.json_response({'name': article.name,
'number_of_page': article.nb_page})
async def get(self, with_comments: Optional[bool]):
async def get(self, with_comments: bool=False):
return web.json_response({'with_comments': with_comments})
@ -101,7 +101,7 @@ API:
Inject Path Parameters
~~~~~~~~~~~~~~~~~~~~~~
To declare a path parameters, you must declare your argument as a `positional-only parameters`_:
To declare a path parameter, you must declare your argument as a `positional-only parameters`_:
Example:
@ -118,18 +118,33 @@ Example:
Inject Query String Parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To declare a query parameters, you must declare your argument as a simple argument:
To declare a query parameter, you must declare your argument as a simple argument:
.. code-block:: python3
class AccountView(PydanticView):
async def get(self, customer_id: str):
async def get(self, customer_id: Optional[str] = None):
...
app = web.Application()
app.router.add_get('/customers', AccountView)
A query string parameter is generally optional and we do not want to force the user to set it in the URL.
It's recommended to define a default value. It's possible to get a multiple value for the same parameter using
the List type
.. code-block:: python3
class AccountView(PydanticView):
async def get(self, tags: List[str] = []):
...
app = web.Application()
app.router.add_get('/customers', AccountView)
Inject Request Body
~~~~~~~~~~~~~~~~~~~
@ -152,7 +167,7 @@ To declare a body parameter, you must declare your argument as a simple argument
Inject HTTP headers
~~~~~~~~~~~~~~~~~~~
To declare a HTTP headers parameters, you must declare your argument as a `keyword-only argument`_.
To declare a HTTP headers parameter, you must declare your argument as a `keyword-only argument`_.
.. code-block:: python3

View File

@ -1,10 +1,12 @@
import abc
import typing
from inspect import signature
from json.decoder import JSONDecodeError
from typing import Callable, Tuple
from aiohttp.web_exceptions import HTTPBadRequest
from aiohttp.web_request import BaseRequest
from multidict import MultiDict
from pydantic import BaseModel
from .utils import is_pydantic_base_model
@ -94,9 +96,27 @@ class QueryGetter(AbstractInjector):
attrs = {"__annotations__": args_spec}
attrs.update(default_values)
self.model = type("QueryModel", (BaseModel,), attrs)
self.args_spec = args_spec
self._is_multiple = frozenset(
name for name, spec in args_spec.items() if typing.get_origin(spec) is list
)
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
kwargs_view.update(self.model(**request.query).dict())
kwargs_view.update(self.model(**self._query_to_dict(request.query)).dict())
def _query_to_dict(self, query: MultiDict):
"""
Return a dict with list as value from the MultiDict.
The value will be wrapped in a list if the args spec is define as a list or if
the multiple values are sent (i.e ?foo=1&foo=2)
"""
return {
key: values
if len(values := query.getall(key)) > 1 or key in self._is_multiple
else value
for key, value in query.items()
}
class HeadersGetter(AbstractInjector):

View File

@ -59,7 +59,7 @@ class PetItemView(PydanticView):
return web.json_response()
class TestResponseReturnASimpleType(PydanticView):
class ViewResponseReturnASimpleType(PydanticView):
async def get(self) -> r200[int]:
"""
Status Codes:
@ -73,7 +73,7 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
app = web.Application()
app.router.add_view("/pets", PetCollectionView)
app.router.add_view("/pets/{id}", PetItemView)
app.router.add_view("/simple-type", TestResponseReturnASimpleType)
app.router.add_view("/simple-type", ViewResponseReturnASimpleType)
oas.setup(app)
client = await aiohttp_client(app)

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from aiohttp import web
@ -7,10 +7,19 @@ from aiohttp_pydantic import PydanticView
class ArticleView(PydanticView):
async def get(
self, with_comments: bool, age: Optional[int] = None, nb_items: int = 7
self,
with_comments: bool,
age: Optional[int] = None,
nb_items: int = 7,
tags: List[str] = [],
):
return web.json_response(
{"with_comments": with_comments, "age": age, "nb_items": nb_items}
{
"with_comments": with_comments,
"age": age,
"nb_items": nb_items,
"tags": tags,
}
)
@ -65,7 +74,12 @@ async def test_get_article_with_valid_qs_should_return_the_parsed_type(
resp = await client.get("/article", params={"with_comments": "yes", "age": 3})
assert resp.status == 200
assert resp.content_type == "application/json"
assert await resp.json() == {"with_comments": True, "age": 3, "nb_items": 7}
assert await resp.json() == {
"with_comments": True,
"age": 3,
"nb_items": 7,
"tags": [],
}
async def test_get_article_with_valid_qs_and_omitted_optional_should_return_default_value(
@ -77,6 +91,70 @@ async def test_get_article_with_valid_qs_and_omitted_optional_should_return_defa
client = await aiohttp_client(app)
resp = await client.get("/article", params={"with_comments": "yes"})
assert await resp.json() == {"with_comments": True, "age": None, "nb_items": 7}
assert await resp.json() == {
"with_comments": True,
"age": None,
"nb_items": 7,
"tags": [],
}
assert resp.status == 200
assert resp.content_type == "application/json"
async def test_get_article_with_multiple_value_for_qs_age_must_failed(
aiohttp_client, loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
client = await aiohttp_client(app)
resp = await client.get("/article", params={"age": ["2", "3"], "with_comments": 1})
assert await resp.json() == [
{
"in": "query string",
"loc": ["age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
]
assert resp.status == 400
assert resp.content_type == "application/json"
async def test_get_article_with_multiple_value_of_tags(aiohttp_client, loop):
app = web.Application()
app.router.add_view("/article", ArticleView)
client = await aiohttp_client(app)
resp = await client.get(
"/article", params={"age": 2, "with_comments": 1, "tags": ["aa", "bb"]}
)
assert await resp.json() == {
"age": 2,
"nb_items": 7,
"tags": ["aa", "bb"],
"with_comments": True,
}
assert resp.status == 200
assert resp.content_type == "application/json"
async def test_get_article_with_one_value_of_tags_must_be_a_list(aiohttp_client, loop):
app = web.Application()
app.router.add_view("/article", ArticleView)
client = await aiohttp_client(app)
resp = await client.get(
"/article", params={"age": 2, "with_comments": 1, "tags": ["aa"]}
)
assert await resp.json() == {
"age": 2,
"nb_items": 7,
"tags": ["aa"],
"with_comments": True,
}
assert resp.status == 200
assert resp.content_type == "application/json"