query string accept multiple values for same parameter key (#11)
This commit is contained in:
parent
81d4e93a1d
commit
145d2fc0f2
25
README.rst
25
README.rst
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user