Compare commits

17 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
Georg K
26fd6fa19f fix: handle discriminated model with root definition 2023-02-09 12:25:28 +03:00
Georg K
be944ac98e fix: pass tests 2022-11-22 17:28:30 +03:00
14 changed files with 329 additions and 264 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,10 @@ class BodyGetter(AbstractInjector):
def __init__(self, args_spec: dict, default_values: dict):
self.arg_name, self.model = next(iter(args_spec.items()))
self._expect_object = self.model.schema()["type"] == "object"
schema = self.model.model_json_schema()
if "type" not in schema:
schema["type"] = "object"
self._expect_object = schema["type"] == "object"
async def inject(self, request: BaseRequest, args_view: list, kwargs_view: dict):
try:
@@ -82,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
@@ -117,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:
@@ -163,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.
@@ -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,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,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.5/v/int_parsing'
}
]

View File

@@ -1,12 +1,12 @@
from __future__ import annotations
from enum import Enum
from typing import List, Optional, Union, Literal
from typing import List, Optional, Union, Literal, Annotated
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"
@@ -42,6 +45,20 @@ class Error(BaseModel):
text: str
class Cat(BaseModel):
pet_type: Literal['cat']
meows: int
class Dog(BaseModel):
pet_type: Literal['dog']
barks: float
class Animal(RootModel):
root: Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
class PetCollectionView(PydanticView):
async def get(
self, format: str, lang: Lang = Lang.EN, name: Optional[str] = None, *, promo: Optional[UUID] = None
@@ -91,6 +108,11 @@ class ViewResponseReturnASimpleType(PydanticView):
return web.json_response()
class DiscriminatedView(PydanticView):
async def post(self, /, request: Animal) -> r200[int]:
return web.json_response()
async def ensure_content_durability(client):
"""
Reload the page 2 times to ensure that content is always the same
@@ -115,6 +137,7 @@ async def generated_oas(aiohttp_client, event_loop) -> web.Application:
app.router.add_view("/pets", PetCollectionView)
app.router.add_view("/pets/{id}", PetItemView)
app.router.add_view("/simple-type", ViewResponseReturnASimpleType)
app.router.add_view("/animals", DiscriminatedView)
oas.setup(app)
return await ensure_content_durability(await aiohttp_client(app))
@@ -122,14 +145,31 @@ 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': {'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': {'const': 'dog', 'title': 'Pet Type'}},
'required': ['pet_type', 'barks'],
'title': 'Dog',
'type': 'object'},
'Error': {
'properties': {
'code': {'title': 'Code', 'type': 'integer'},
'text': {'title': 'Text', 'type': 'string'}},
'required': ['code', 'text'],
'title': 'Error',
'type': 'object'
},
'Lang': {
'description': 'An enumeration.',
'enum': ['en', 'fr'],
'title': 'Lang',
'type': 'string'
@@ -143,6 +183,26 @@ async def test_generated_oas_should_have_components_schemas(generated_oas):
"title": "Toy",
"type": "object",
},
'Pet': {
'properties': {
'id': {'title': 'Id', 'type': 'integer'},
'name': {
'anyOf': [
{'type': 'string'},
{'type': 'null'}
],
'default': None,
'title': 'Name'},
'toys': {
'items': {'$ref': '#/components/schemas/Toy'},
'title': 'Toys',
'type': 'array'
}
},
'required': ['id', 'toys'],
'title': 'Pet',
'type': 'object'
}
}
@@ -176,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": {
@@ -191,20 +262,7 @@ async def test_pets_route_should_have_get_method(generated_oas):
"content": {
"application/json": {
"schema": {
"items": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
},
"required": ["id", "toys"],
"title": "Pet",
"type": "object",
},
"items": {'$ref': '#/components/schemas/Pet'},
"type": "array",
}
}
@@ -223,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",
@@ -242,20 +307,7 @@ async def test_pets_route_should_have_post_method(generated_oas):
"description": "",
"content": {
"application/json": {
"schema": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
},
"required": ["id", "toys"],
"title": "Pet",
"type": "object",
}
"schema": {'$ref': '#/components/schemas/Pet'}
}
},
}
@@ -308,37 +360,21 @@ 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'
},
},
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
},
"required": ["id", "toys"],
"title": "Pet",
"type": "object",
'responses': {
'200': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Pet'}}},
'description': ''},
'400': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Error'}}},
'description': ''},
'404': {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Error'}}},
'description': ''}
}
}
},
},
"404": {"description": "", "content": {}},
},
}
async def test_pets_id_route_should_have_put_method(generated_oas):
@@ -357,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):
@@ -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.5/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.5/v/int_parsing'
}
]
@@ -92,10 +97,10 @@ 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'
}
]
@@ -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

@@ -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.5/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.5/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.5/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.5/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.5/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.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