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, return web.json_response({'name': article.name,
'number_of_page': article.nb_page}) '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}) return web.json_response({'with_comments': with_comments})
@ -101,7 +101,7 @@ API:
Inject Path Parameters 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: Example:
@ -118,18 +118,33 @@ Example:
Inject Query String Parameters 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 .. code-block:: python3
class AccountView(PydanticView): class AccountView(PydanticView):
async def get(self, customer_id: str): async def get(self, customer_id: Optional[str] = None):
... ...
app = web.Application() app = web.Application()
app.router.add_get('/customers', AccountView) 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 Inject Request Body
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -152,7 +167,7 @@ To declare a body parameter, you must declare your argument as a simple argument
Inject HTTP headers 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 .. code-block:: python3

View File

@ -1,10 +1,12 @@
import abc import abc
import typing
from inspect import signature from inspect import signature
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from typing import Callable, Tuple from typing import Callable, Tuple
from aiohttp.web_exceptions import HTTPBadRequest from aiohttp.web_exceptions import HTTPBadRequest
from aiohttp.web_request import BaseRequest from aiohttp.web_request import BaseRequest
from multidict import MultiDict
from pydantic import BaseModel from pydantic import BaseModel
from .utils import is_pydantic_base_model from .utils import is_pydantic_base_model
@ -94,9 +96,27 @@ class QueryGetter(AbstractInjector):
attrs = {"__annotations__": args_spec} attrs = {"__annotations__": args_spec}
attrs.update(default_values) attrs.update(default_values)
self.model = type("QueryModel", (BaseModel,), attrs) 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): 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): class HeadersGetter(AbstractInjector):

View File

@ -59,7 +59,7 @@ class PetItemView(PydanticView):
return web.json_response() return web.json_response()
class TestResponseReturnASimpleType(PydanticView): class ViewResponseReturnASimpleType(PydanticView):
async def get(self) -> r200[int]: async def get(self) -> r200[int]:
""" """
Status Codes: Status Codes:
@ -73,7 +73,7 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
app = web.Application() app = web.Application()
app.router.add_view("/pets", PetCollectionView) app.router.add_view("/pets", PetCollectionView)
app.router.add_view("/pets/{id}", PetItemView) app.router.add_view("/pets/{id}", PetItemView)
app.router.add_view("/simple-type", TestResponseReturnASimpleType) app.router.add_view("/simple-type", ViewResponseReturnASimpleType)
oas.setup(app) oas.setup(app)
client = await aiohttp_client(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 from aiohttp import web
@ -7,10 +7,19 @@ from aiohttp_pydantic import PydanticView
class ArticleView(PydanticView): class ArticleView(PydanticView):
async def get( 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( 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}) resp = await client.get("/article", params={"with_comments": "yes", "age": 3})
assert resp.status == 200 assert resp.status == 200
assert resp.content_type == "application/json" 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( 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) client = await aiohttp_client(app)
resp = await client.get("/article", params={"with_comments": "yes"}) 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.status == 200
assert resp.content_type == "application/json" assert resp.content_type == "application/json"