Compare commits

15 Commits

Author SHA1 Message Date
Georg K
ad2c111fa1 fix: ci
All checks were successful
Release / Distribution (push) Successful in 43s
2024-12-28 02:38:57 +03:00
Georg K
5058b58991 fix: to py3.11
Some checks failed
Release / Distribution (push) Failing after 59s
2024-12-28 02:34:19 +03:00
Georg K
23f08ad5a5 fix: on tags
Some checks failed
Release / Distribution (push) Failing after 42s
2024-12-28 02:32:09 +03:00
Georg K
77cef551a1 fix: on tags
Some checks failed
Release / Distribution (push) Failing after 7s
2024-12-28 02:30:46 +03:00
Georg K
5c5a701daa fix: on tags
Some checks failed
Release / Distribution (push) Failing after 9s
2024-12-28 02:29:41 +03:00
Georg K
5ead48ec92 fix: on tags
Some checks failed
Release / Distribution (push) Failing after 8s
2024-12-28 02:28:55 +03:00
Georg K
1f2ac30106 fix: on tags
Some checks failed
Release / Distribution (push) Failing after 8s
2024-12-28 02:28:04 +03:00
Georg K
483b457b14 fix: on tags
Some checks failed
Release / Distribution (push) Failing after 8s
2024-12-28 02:25:50 +03:00
Georg K
3126c2fd2e fix: update deploy
Some checks failed
Release / Distribution (push) Failing after 33s
2024-12-28 02:22:32 +03:00
Georg K
a45101637c feat: bump packages, fix tests; fix: pydantic .dict() to .model_dump(); bump: 2.0.4 2023-11-15 19:44:30 +03:00
Georg K
937c09e2b7 feat: add description to parameters; bump: libs and tests; bump: 2.0.3 2023-11-01 03:55:10 +03:00
Georg K
82c638c1e0 bump: pydatic 2.3.0, tests 2023-08-29 14:28:27 +03:00
Georg K
7ce5e5d0d4 feat: update root model defs 2023-07-14 05:21:43 +03:00
Georg K
93e391b7b2 feat: update pydantic 2023-07-14 04:40:57 +03:00
Georg K
a94c9d4863 feat: update pydantic 2023-07-14 03:31:33 +03:00
14 changed files with 268 additions and 220 deletions

View File

@@ -0,0 +1,24 @@
name: Release
run-name: ${{ gitea.actor }} is runs ci pipeline
on:
push:
tags:
- 'v*.*.*'
jobs:
packaging:
name: Distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Update version
run: VSN=${{ gitea.ref_name }} && sed -i -e "s/1.12.1/${VSN:1}/g" aiohttp_pydantic/__init__.py
- name: Install invoke
run: python -m pip install setuptools wheel invoke
- name: Push to PyPi
run: invoke upload --pypi-user ${{ secrets.REPO_USER }} --pypi-password ${{ secrets.REPO_PASS }} --pypi-url https://git.ahax86.ru/api/packages/pub/pypi

View File

@@ -3,7 +3,7 @@ stages:
publish-pypi:
stage: package
image: python:3.10
image: python:3.11
script:
- sed -i -e "s/1.12.1/${CI_COMMIT_TAG:1}/g" aiohttp_pydantic/__init__.py
- pip install -U setuptools wheel pip; pip install invoke

View File

@@ -56,7 +56,7 @@ class MatchInfoGetter(AbstractInjector):
self.model = type("PathModel", (BaseModel,), attrs)
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
args_view.extend(self.model(**request.match_info).dict().values())
args_view.extend(self.model(**request.match_info).model_dump().values())
class BodyGetter(AbstractInjector):
@@ -68,7 +68,7 @@ class BodyGetter(AbstractInjector):
def __init__(self, args_spec: dict, default_values: dict):
self.arg_name, self.model = next(iter(args_spec.items()))
schema = self.model.schema()
schema = self.model.model_json_schema()
if "type" not in schema:
schema["type"] = "object"
self._expect_object = schema["type"] == "object"
@@ -85,7 +85,7 @@ class BodyGetter(AbstractInjector):
# to a dict. Prevent this by requiring the body to be a dict for object models.
if self._expect_object and not isinstance(body, dict):
raise HTTPBadRequest(
text='[{"in": "body", "loc": ["__root__"], "msg": "value is not a '
text='[{"loc_in": "body", "loc": ["root"], "msg": "value is not a '
'valid dict", "type": "type_error.dict"}]',
content_type="application/json",
) from None
@@ -120,7 +120,7 @@ class QueryGetter(AbstractInjector):
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
data = self._query_to_dict(request.query)
cleaned = self.model(**data).dict()
cleaned = self.model(**data).model_dump()
for group_name, (group_cls, group_attrs) in self._groups.items():
group = group_cls()
for attr_name in group_attrs:
@@ -166,7 +166,7 @@ class HeadersGetter(AbstractInjector):
def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
header = {k.lower().replace("-", "_"): v for k, v in request.headers.items()}
cleaned = self.model(**header).dict()
cleaned = self.model(**header).model_dump()
for group_name, (group_cls, group_attrs) in self._groups.items():
group = group_cls()
for attr_name in group_attrs:

View File

@@ -5,7 +5,7 @@ from typing import List, Type, Optional, get_type_hints
from aiohttp.web import Response, json_response
from aiohttp.web_app import Application
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
from . import docstring_parser
@@ -32,7 +32,7 @@ class _OASResponseBuilder:
response_schema = obj.schema(
ref_template="#/components/schemas/{model}"
).copy()
if def_sub_schemas := response_schema.pop("definitions", None):
if def_sub_schemas := response_schema.pop("$defs", None):
self._oas.components.schemas.update(def_sub_schemas)
self._oas.components.schemas.update({response_schema['title']: response_schema})
return {'$ref': f'#/components/schemas/{response_schema["title"]}'}
@@ -99,7 +99,7 @@ def _add_http_method_to_oas(
.schema(ref_template="#/components/schemas/{model}")
.copy()
)
if def_sub_schemas := body_schema.pop("definitions", None):
if def_sub_schemas := body_schema.pop("$defs", None):
oas.components.schemas.update(def_sub_schemas)
oas_operation.request_body.content = {
@@ -117,19 +117,22 @@ def _add_http_method_to_oas(
oas_operation.parameters[i].in_ = args_location
oas_operation.parameters[i].name = name
attrs = {"__annotations__": {"__root__": type_}}
attrs = {"__annotations__": {"root": type_}}
if name in defaults:
attrs["__root__"] = defaults[name]
attrs["root"] = defaults[name]
oas_operation.parameters[i].required = False
else:
oas_operation.parameters[i].required = True
oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema(
oas_operation.parameters[i].schema = type(name, (RootModel,), attrs).schema(
ref_template="#/components/schemas/{model}"
)
if 'description' in oas_operation.parameters[i].schema:
oas_operation.parameters[i].description = oas_operation.parameters[i].schema['description']
# move definitions
if def_sub_schemas := oas_operation.parameters[i].schema.pop("definitions", None):
if def_sub_schemas := oas_operation.parameters[i].schema.pop("$defs", None):
oas.components.schemas.update(def_sub_schemas)
return_type = get_type_hints(handler).get("return")

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.
@@ -91,7 +97,7 @@ class PydanticView(AbstractView):
return injectors
async def on_validation_error(
self, exception: ValidationError, context: CONTEXT
self, exception: ValidationError, context: CONTEXT
) -> StreamResponse:
"""
This method is a hook to intercept ValidationError.
@@ -101,14 +107,13 @@ 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(
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
):
"""
Decorator to unpack the query string, route path, body and http header in
@@ -146,6 +151,7 @@ def is_pydantic_view(obj) -> bool:
__all__ = (
"PydanticValidationError",
"AbstractInjector",
"BodyGetter",
"HeadersGetter",

View File

@@ -1,42 +1,12 @@
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
certifi==2022.6.15
charset-normalizer==2.1.0
codecov==2.1.12
colorama==0.4.5
commonmark==0.9.1
coverage==6.4.2
docutils==0.19
frozenlist==1.3.0
idna==3.3
importlib-metadata==4.12.0
iniconfig==1.1.1
keyring==23.7.0
multidict==6.0.2
packaging==21.3
pkginfo==1.8.3
pluggy==1.0.0
py==1.11.0
Pygments==2.12.0
pyparsing==3.0.9
pytest==7.1.2
pytest-aiohttp==1.0.4
pytest-asyncio==0.19.0
pytest-cov==3.0.0
pywin32-ctypes==0.2.0
readme-renderer==35.0
requests==2.28.1
requests-toolbelt==0.9.1
rfc3986==2.0.0
rich==12.5.1
six==1.16.0
tomli==2.0.1
twine==4.0.1
urllib3==1.26.11
webencodings==0.5.1
yarl==1.7.2
zipp==3.8.1
aiohttp==3.8.6
pydantic==2.5.1
jinja2==3.1.2
swagger-4-ui-bundle==0.0.4
pytest==7.4.3
pytest-aiohttp==1.0.5
pytest-asyncio==0.21.1
pytest-cov==4.1.0
readme-renderer==42.0
codecov==2.1.13
twine==4.0.2
importlib-metadata==7.1.0

View File

@@ -1,28 +1,9 @@
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
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
aiohttp==3.8.6
pydantic==2.5.1
jinja2==3.1.2
swagger-4-ui-bundle==0.0.4
pytest==7.4.3
pytest-aiohttp==1.0.5
pytest-asyncio==0.21.1
pytest-cov==4.1.0
readme-renderer==42.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,22 +28,22 @@ 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.5.0
swagger-4-ui-bundle
[options.extras_require]
test =
pytest==7.1.2
pytest-aiohttp==1.0.4
pytest-cov==3.0.0
readme-renderer==35.0
pytest==7.4.0
pytest-aiohttp==1.0.5
pytest-cov==4.1.0
readme-renderer==42.0
ci =
%(test)s
codecov==2.1.12
twine==4.0.1
codecov==2.1.13
twine==4.0.2
[options.packages.find]
exclude =

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,14 +31,12 @@ 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(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -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.5/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
@@ -16,6 +16,9 @@ from aiohttp_pydantic.oas.view import generate_oas
class Color(str, Enum):
"""
Pet color
"""
RED = "red"
GREEN = "green"
PINK = "pink"
@@ -52,8 +55,8 @@ class Dog(BaseModel):
barks: float
class Animal(BaseModel):
__root__: Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
class Animal(RootModel):
root: Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
class PetCollectionView(PydanticView):
@@ -143,22 +146,18 @@ async def generated_oas(aiohttp_client, event_loop) -> web.Application:
async def test_generated_oas_should_have_components_schemas(generated_oas):
assert generated_oas["components"]["schemas"] == {
'Cat': {'properties': {'meows': {'title': 'Meows', 'type': 'integer'},
'pet_type': {'enum': ['cat'],
'title': 'Pet Type',
'type': 'string'}},
'pet_type': {'const': 'cat', 'title': 'Pet Type'}},
'required': ['pet_type', 'meows'],
'title': 'Cat',
'type': 'object'},
"Color": {
"description": "An enumeration.",
"description": "Pet color",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
'Dog': {'properties': {'barks': {'title': 'Barks', 'type': 'number'},
'pet_type': {'enum': ['dog'],
'title': 'Pet Type',
'type': 'string'}},
'pet_type': {'const': 'dog', 'title': 'Pet Type'}},
'required': ['pet_type', 'barks'],
'title': 'Dog',
'type': 'object'},
@@ -171,7 +170,6 @@ async def test_generated_oas_should_have_components_schemas(generated_oas):
'type': 'object'
},
'Lang': {
'description': 'An enumeration.',
'enum': ['en', 'fr'],
'title': 'Lang',
'type': 'string'
@@ -188,7 +186,13 @@ async def test_generated_oas_should_have_components_schemas(generated_oas):
'Pet': {
'properties': {
'id': {'title': 'Id', 'type': 'integer'},
'name': {'title': 'Name', 'type': 'string'},
'name': {
'anyOf': [
{'type': 'string'},
{'type': 'null'}
],
'default': None,
'title': 'Name'},
'toys': {
'items': {'$ref': '#/components/schemas/Toy'},
'title': 'Toys',
@@ -232,13 +236,24 @@ async def test_pets_route_should_have_get_method(generated_oas):
"in": "query",
"name": "name",
"required": False,
"schema": {"title": "name", "type": "string"},
"schema": {
'anyOf': [{'type': 'string'}, {'type': 'null'}],
'default': None,
'title': 'name'
},
},
{
"in": "header",
"name": "promo",
"required": False,
"schema": {"format": "uuid", "title": "promo", "type": "string"},
"schema": {
'anyOf': [
{'format': 'uuid', 'type': 'string'},
{'type': 'null'}
],
'default': None,
'title': 'promo'
},
},
],
"responses": {
@@ -266,7 +281,14 @@ async def test_pets_route_should_have_post_method(generated_oas):
"schema": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"name": {
'anyOf': [
{'type': 'string'},
{'type': 'null'}
],
'default': None,
'title': 'Name'
},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
@@ -338,9 +360,9 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
"name": "day",
"required": False,
"schema": {
"anyOf": [{"type": "integer"}, {"enum": ["now"], "type": "string"}],
"default": "now",
"title": "day",
'anyOf': [{'type': 'integer'}, {'const': 'now'}],
'default': 'now',
'title': 'day'
},
},
],
@@ -371,7 +393,13 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
"schema": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"name": {
'anyOf': [
{'type': 'string'},
{'type': 'null'}
],
'default': None,
'title': 'Name'},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",

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):
@@ -29,7 +29,7 @@ class ArticleView(PydanticView):
async def test_post_an_article_without_required_field_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -38,18 +38,21 @@ 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.5/v/missing'
}
]
async def test_post_an_article_with_wrong_type_field_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -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.5/v/int_parsing'
}
]
@@ -81,7 +86,7 @@ async def test_post_an_array_json_is_supported(aiohttp_client, event_loop):
async def test_post_an_array_json_to_an_object_model_should_return_an_error(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -92,16 +97,16 @@ async def test_post_an_array_json_to_an_object_model_should_return_an_error(
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["__root__"],
"msg": "value is not a valid dict",
"type": "type_error.dict",
'loc': ['root'],
'loc_in': 'body',
'msg': 'value is not a valid dict',
'type': 'type_error.dict'
}
]
async def test_post_an_object_json_to_a_list_model_should_return_an_error(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -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.5/v/list_type'
}
]

View File

@@ -50,9 +50,9 @@ class Signature(Group):
class ArticleViewWithSignatureGroup(PydanticView):
async def get(
self,
*,
signature: Signature,
self,
*,
signature: Signature,
):
return web.json_response(
{"expired": signature.expired, "scope": signature.scope},
@@ -61,7 +61,7 @@ class ArticleViewWithSignatureGroup(PydanticView):
async def test_get_article_without_required_header_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -70,18 +70,24 @@ 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.5/v/missing',
'loc_in': 'headers'
}
]
async def test_get_article_with_wrong_header_type_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -90,18 +96,22 @@ 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.5/v/datetime_parsing',
'loc_in': 'headers'
}
]
async def test_get_article_with_valid_header_should_return_the_parsed_type(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -116,7 +126,7 @@ async def test_get_article_with_valid_header_should_return_the_parsed_type(
async def test_get_article_with_valid_header_containing_hyphen_should_be_returned(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -136,18 +146,20 @@ 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",
}
]
!= {"signature": "2020-10-04T18:01:00"}
await resp.json()
== [
{
'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"}
)
assert resp.status == 400
assert resp.content_type == "application/json"

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.5/v/int_parsing'
}
]

View File

@@ -11,11 +11,11 @@ from aiohttp_pydantic.injectors import Group
class ArticleView(PydanticView):
async def get(
self,
with_comments: bool,
age: Optional[int] = None,
nb_items: int = 7,
tags: List[str] = Field(default_factory=list),
self,
with_comments: bool,
age: Optional[int] = None,
nb_items: int = 7,
tags: List[str] = Field(default_factory=list),
):
return web.json_response(
{
@@ -42,9 +42,9 @@ class Pagination(Group):
class ArticleViewWithPaginationGroup(PydanticView):
async def get(
self,
with_comments: bool,
page: Pagination,
self,
with_comments: bool,
page: Pagination,
):
return web.json_response(
{
@@ -70,7 +70,7 @@ class ArticleViewWithEnumInQuery(PydanticView):
async def test_get_article_without_required_qs_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -79,18 +79,21 @@ 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.5/v/missing'
}
]
async def test_get_article_with_wrong_qs_type_should_return_an_error_message(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -101,16 +104,18 @@ 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.5/v/bool_parsing'
}
]
async def test_get_article_with_valid_qs_should_return_the_parsed_type(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -129,7 +134,7 @@ async def test_get_article_with_valid_qs_should_return_the_parsed_type(
async def test_get_article_with_valid_qs_and_omitted_optional_should_return_default_value(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -148,7 +153,7 @@ async def test_get_article_with_valid_qs_and_omitted_optional_should_return_defa
async def test_get_article_with_multiple_value_for_qs_age_must_failed(
aiohttp_client, event_loop
aiohttp_client, event_loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
@@ -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.5/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.5/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.5/v/int_parsing'
}
]
assert resp.status == 400