feat: update pydantic

This commit is contained in:
Georg K 2023-07-14 03:31:33 +03:00
parent 26fd6fa19f
commit a94c9d4863
9 changed files with 159 additions and 138 deletions

View File

@ -10,6 +10,8 @@ from aiohttp.web_exceptions import HTTPMethodNotAllowed
from aiohttp.web_response import StreamResponse
from pydantic import ValidationError
from pydantic_core import ErrorDetails
from .injectors import (
AbstractInjector,
BodyGetter,
@ -22,6 +24,10 @@ from .injectors import (
)
class PydanticValidationError(ErrorDetails):
loc_in: CONTEXT
class PydanticView(AbstractView):
"""
An AIOHTTP View that validate request using function annotations.
@ -101,10 +107,9 @@ class PydanticView(AbstractView):
"headers", "path" or "query string"
"""
errors = exception.errors()
for error in errors:
error["in"] = context
own_errors = [PydanticValidationError(**x, loc_in=context) for x in errors]
return json_response(data=errors, status=400)
return json_response(data=own_errors, status=400)
def inject_params(
@ -146,6 +151,7 @@ def is_pydantic_view(obj) -> bool:
__all__ = (
"PydanticValidationError",
"AbstractInjector",
"BodyGetter",
"HeadersGetter",

View File

@ -1,28 +1,8 @@
aiohttp==3.8.1
aiosignal==1.2.0
async-timeout==4.0.2
atomicwrites==1.4.1
attrs==21.4.0
bleach==5.0.1
charset-normalizer==2.1.0
colorama==0.4.5
coverage==6.4.2
docutils==0.19
frozenlist==1.3.0
idna==3.3
iniconfig==1.1.1
multidict==6.0.2
packaging==21.3
pluggy==1.0.0
py==1.11.0
Pygments==2.12.0
pyparsing==3.0.9
pytest==7.1.2
aiohttp==3.8.4
pydantic==2.0.2
jinja2==3.1.2
swagger-4-ui-bundle==0.0.4
pytest==7.4.0
pytest-aiohttp==1.0.4
pytest-asyncio==0.19.0
pytest-cov==3.0.0
readme-renderer==35.0
six==1.16.0
tomli==2.0.1
webencodings==0.5.1
yarl==1.7.2
pytest-asyncio==0.21.1
pytest-cov==4.1.0

View File

@ -18,8 +18,8 @@ classifiers =
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Software Development :: Libraries :: Application Frameworks
Framework :: aiohttp
License :: OSI Approved :: MIT License
@ -28,10 +28,10 @@ classifiers =
zip_safe = False
include_package_data = True
packages = find:
python_requires = >=3.8
python_requires = >=3.10
install_requires =
aiohttp
pydantic>=1.7
pydantic>=2.0.0
swagger-4-ui-bundle
[options.extras_require]

View File

@ -4,9 +4,10 @@ from typing import Iterator, List, Optional
from aiohttp import web
from aiohttp.web_response import json_response
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
from aiohttp_pydantic import PydanticView
from aiohttp_pydantic.view import PydanticValidationError
class ArticleModel(BaseModel):
@ -14,11 +15,11 @@ class ArticleModel(BaseModel):
nb_page: Optional[int]
class ArticleModels(BaseModel):
__root__: List[ArticleModel]
class ArticleModels(RootModel):
root: List[ArticleModel]
def __iter__(self) -> Iterator[ArticleModel]:
return iter(self.__root__)
return iter(self.root)
class ArticleView(PydanticView):
@ -30,10 +31,8 @@ class ArticleView(PydanticView):
async def on_validation_error(self, exception, context):
errors = exception.errors()
for error in errors:
error["in"] = context
error["custom"] = "custom"
return json_response(data=errors, status=400)
own_errors = [PydanticValidationError(**x, loc_in=context) for x in errors]
return json_response(data=own_errors, status=400)
async def test_post_an_article_with_wrong_type_field_should_return_an_error_message(
@ -47,12 +46,14 @@ async def test_post_an_article_with_wrong_type_field_should_return_an_error_mess
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["nb_page"],
"msg": "value is not a valid integer",
"custom": "custom",
"type": "type_error.integer",
'loc_in': 'body',
'input': 'foo',
'loc': ['nb_page'],
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'type': 'int_parsing',
'url': 'https://errors.pydantic.dev/2.1.2/v/int_parsing'
}
]

View File

@ -6,7 +6,7 @@ from uuid import UUID
import pytest
from aiohttp import web
from pydantic import Field
from pydantic import Field, RootModel
from pydantic.main import BaseModel
from aiohttp_pydantic import PydanticView, oas
@ -52,8 +52,7 @@ class Dog(BaseModel):
barks: float
class Animal(BaseModel):
__root__: Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
Animal = RootModel[Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]]
class PetCollectionView(PydanticView):

View File

@ -3,21 +3,21 @@ from __future__ import annotations
from typing import Iterator, List, Optional
from aiohttp import web
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
from aiohttp_pydantic import PydanticView
class ArticleModel(BaseModel):
name: str
nb_page: Optional[int]
nb_page: Optional[int] = None
class ArticleModels(BaseModel):
__root__: List[ArticleModel]
class ArticleModels(RootModel):
root: List[ArticleModel]
def __iter__(self) -> Iterator[ArticleModel]:
return iter(self.__root__)
return iter(self.root)
class ArticleView(PydanticView):
@ -38,12 +38,15 @@ async def test_post_an_article_without_required_field_should_return_an_error_mes
resp = await client.post("/article", json={})
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["name"],
"msg": "field required",
"type": "value_error.missing",
'input': {},
'loc': ['name'],
'loc_in': 'body',
'msg': 'Field required',
'type': 'missing',
'url': 'https://errors.pydantic.dev/2.1.2/v/missing'
}
]
@ -60,10 +63,12 @@ async def test_post_an_article_with_wrong_type_field_should_return_an_error_mess
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["nb_page"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
'input': 'foo',
'loc': ['nb_page'],
'loc_in': 'body',
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'type': 'int_parsing',
'url': 'https://errors.pydantic.dev/2.1.2/v/int_parsing'
}
]
@ -110,12 +115,15 @@ async def test_post_an_object_json_to_a_list_model_should_return_an_error(
resp = await client.put("/article", json={"name": "foo", "nb_page": 3})
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["__root__"],
"msg": "value is not a valid list",
"type": "type_error.list",
'input': {'name': 'foo', 'nb_page': 3},
'loc': [],
'loc_in': 'body',
'msg': 'Input should be a valid list',
'type': 'list_type',
'url': 'https://errors.pydantic.dev/2.1.2/v/list_type'
}
]

View File

@ -70,12 +70,18 @@ async def test_get_article_without_required_header_should_return_an_error_messag
resp = await client.get("/article", headers={})
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
result = await resp.json()
assert len(result) == 1
result[0].pop('input')
assert result == [
{
"in": "headers",
"loc": ["signature_expired"],
"msg": "field required",
"type": "value_error.missing",
'type': 'missing',
'loc': ['signature_expired'],
'msg': 'Field required',
'url': 'https://errors.pydantic.dev/2.1.2/v/missing',
'loc_in': 'headers'
}
]
@ -90,12 +96,16 @@ async def test_get_article_with_wrong_header_type_should_return_an_error_message
resp = await client.get("/article", headers={"signature_expired": "foo"})
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "headers",
"loc": ["signature_expired"],
"msg": "invalid datetime format",
"type": "value_error.datetime",
'type': 'datetime_parsing',
'loc': ['signature_expired'],
'msg': 'Input should be a valid datetime, input is too short',
'input': 'foo',
'ctx': {'error': 'input is too short'},
'url': 'https://errors.pydantic.dev/2.1.2/v/datetime_parsing',
'loc_in': 'headers'
}
]
@ -136,15 +146,17 @@ async def test_wrong_value_to_header_defined_with_str_enum(aiohttp_client, event
client = await aiohttp_client(app)
resp = await client.get("/coord", headers={"format": "WGS84"})
assert (
await resp.json()
== [
{
"ctx": {"enum_values": ["UMT", "MGRS"]},
"in": "headers",
"loc": ["format"],
"msg": "value is not a valid enumeration member; permitted: 'UMT', 'MGRS'",
"type": "type_error.enum",
'ctx': {'expected': "'UMT' or 'MGRS'"},
'input': 'WGS84',
'loc': ['format'],
'loc_in': 'headers',
'msg': "Input should be 'UMT' or 'MGRS'",
'type': 'enum'
}
]
!= {"signature": "2020-10-04T18:01:00"}

View File

@ -33,11 +33,14 @@ async def test_get_article_with_wrong_path_parameters_should_return_error(
resp = await client.get("/article/1234/tag/music/before/now")
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "path",
"loc": ["date"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
'input': 'now',
'loc': ['date'],
'loc_in': 'path',
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'type': 'int_parsing',
'url': 'https://errors.pydantic.dev/2.1.2/v/int_parsing'
}
]

View File

@ -79,12 +79,15 @@ async def test_get_article_without_required_qs_should_return_an_error_message(
resp = await client.get("/article")
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "query string",
"loc": ["with_comments"],
"msg": "field required",
"type": "value_error.missing",
'input': {},
'loc': ['with_comments'],
'loc_in': 'query string',
'msg': 'Field required',
'type': 'missing',
'url': 'https://errors.pydantic.dev/2.1.2/v/missing'
}
]
@ -101,10 +104,12 @@ async def test_get_article_with_wrong_qs_type_should_return_an_error_message(
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "query string",
"loc": ["with_comments"],
"msg": "value could not be parsed to a boolean",
"type": "type_error.bool",
'input': 'foo',
'loc': ['with_comments'],
'loc_in': 'query string',
'msg': 'Input should be a valid boolean, unable to interpret input',
'type': 'bool_parsing',
'url': 'https://errors.pydantic.dev/2.1.2/v/bool_parsing'
}
]
@ -158,10 +163,12 @@ async def test_get_article_with_multiple_value_for_qs_age_must_failed(
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",
'input': ['2', '3'],
'loc': ['age'],
'loc_in': 'query string',
'msg': 'Input should be a valid integer',
'type': 'int_type',
'url': 'https://errors.pydantic.dev/2.1.2/v/int_type'
}
]
assert resp.status == 400
@ -215,10 +222,12 @@ async def test_get_article_without_required_field_page(aiohttp_client, event_loo
resp = await client.get("/article", params={"with_comments": 1})
assert await resp.json() == [
{
"in": "query string",
"loc": ["page_num"],
"msg": "field required",
"type": "value_error.missing",
'input': {'with_comments': '1'},
'loc': ['page_num'],
'loc_in': 'query string',
'msg': 'Field required',
'type': 'missing',
'url': 'https://errors.pydantic.dev/2.1.2/v/missing'
}
]
assert resp.status == 400
@ -276,10 +285,13 @@ async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, event_l
)
assert await resp.json() == [
{
"in": "query string",
"loc": ["page_size"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
'input': 'large',
'loc': ['page_size'],
'loc_in': 'query string',
'msg': 'Input should be a valid integer, unable to parse string as an '
'integer',
'type': 'int_parsing',
'url': 'https://errors.pydantic.dev/2.1.2/v/int_parsing'
}
]
assert resp.status == 400