Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest, macOS-latest]
# Python 3.7 is not supported on Apple ARM64,
# or the latest Ubuntu 2404
exclude:
- python-version: "3.7"
os: macos-latest
- python-version: "3.7"
os: ubuntu-latest
include: # Python 3.7 is tested with a x86 macOS version
- python-version: "3.7"
os: macos-13


steps:
Expand Down
108 changes: 94 additions & 14 deletions flask_pydantic/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic.v1 import BaseModel as V1BaseModel
from pydantic.v1.error_wrappers import ValidationError as V1ValidationError
from pydantic.v1.tools import parse_obj_as
from pydantic_core import ErrorDetails

from .converters import convert_query_params
from .exceptions import (
Expand Down Expand Up @@ -64,7 +65,11 @@ def is_iterable_of_models(content: Any) -> bool:


def validate_many_models(
model: Type[V1OrV2BaseModel], content: Any
model: Type[V1OrV2BaseModel],
content: Any,
include_error_url: bool = True,
include_error_context: bool = True,
include_error_input: bool = True,
) -> List[V1OrV2BaseModel]:
try:
return [model(**fields) for fields in content]
Expand All @@ -77,13 +82,26 @@ def validate_many_models(
"type": "type_error.array",
}
]

raise ManyModelValidationError(err) from te
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
raise ManyModelValidationError(
ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
) from ve
except V1ValidationError as ve:
raise ManyModelValidationError(ve.errors()) from ve


def validate_path_params(func: Callable, kwargs: dict) -> Tuple[dict, list]:
def validate_path_params(
func: Callable,
kwargs: dict,
include_error_url: bool = True,
include_error_context: bool = True,
include_error_input: bool = True,
) -> Tuple[dict, List[ErrorDetails]]:
errors = []
validated = {}
for name, type_ in func.__annotations__.items():
Expand All @@ -96,7 +114,15 @@ def validate_path_params(func: Callable, kwargs: dict) -> Tuple[dict, list]:
else:
value = parse_obj_as(type_, kwargs.get(name))
validated[name] = value
except (ValidationError, V1ValidationError) as e:
except ValidationError as e:
err = e.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)[0]
err["loc"] = [name]
errors.append(err)
except V1ValidationError as e:
err = e.errors()[0]
err["loc"] = [name]
errors.append(err)
Expand All @@ -121,6 +147,9 @@ def validate(
response_by_alias: bool = False,
get_json_params: Optional[dict] = None,
form: Optional[Type[V1OrV2BaseModel]] = None,
include_error_url: bool = True,
include_error_context: bool = True,
include_error_input: bool = True,
):
"""
Decorator for route methods which will validate query, body and form parameters
Expand All @@ -142,6 +171,9 @@ def validate(
(request.body_params then contains list of models i. e. List[BaseModel])
`response_by_alias` whether Pydantic's alias is used
`get_json_params` - parameters to be passed to Request.get_json() function
`include_error_url` whether to include a URL to documentation on the error each error
`include_error_context` whether to include the context of each error
`include_error_input` whether to include the input value of each error

example::

Expand Down Expand Up @@ -186,7 +218,13 @@ def decorate(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
q, b, f, err = None, None, None, {}
kwargs, path_err = validate_path_params(func, kwargs)
kwargs, path_err = validate_path_params(
func,
kwargs,
include_error_url=include_error_url,
include_error_context=include_error_context,
include_error_input=include_error_input,
)
if path_err:
err["path_params"] = path_err
query_in_kwargs = func.__annotations__.get("query")
Expand All @@ -195,7 +233,13 @@ def wrapper(*args, **kwargs):
query_params = convert_query_params(request.args, query_model)
try:
q = query_model(**query_params)
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
err["query_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["query_params"] = ve.errors()
body_in_kwargs = func.__annotations__.get("body")
body_model = body_in_kwargs or body
Expand All @@ -212,11 +256,23 @@ def wrapper(*args, **kwargs):
elif issubclass(body_model, RootModel):
try:
b = body_model(body_params)
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
err["body_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["body_params"] = ve.errors()
elif request_body_many:
try:
b = validate_many_models(body_model, body_params)
b = validate_many_models(
body_model,
body_params,
include_error_url,
include_error_context,
include_error_input,
)
except ManyModelValidationError as e:
err["body_params"] = e.errors()
else:
Expand All @@ -229,7 +285,13 @@ def wrapper(*args, **kwargs):
return unsupported_media_type_response(content_type)
else:
raise JsonBodyParsingError() from te
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
err["body_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["body_params"] = ve.errors()
form_in_kwargs = func.__annotations__.get("form")
form_model = form_in_kwargs or form
Expand All @@ -241,12 +303,24 @@ def wrapper(*args, **kwargs):
):
try:
f = form_model(form_params)
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
err["form_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["form_params"] = ve.errors()
elif issubclass(form_model, RootModel):
try:
f = form_model(form_params)
except (ValidationError, V1ValidationError) as ve:
except ValidationError as ve:
err["form_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["form_params"] = ve.errors()
else:
try:
Expand All @@ -257,8 +331,14 @@ def wrapper(*args, **kwargs):
if media_type != "multipart/form-data":
return unsupported_media_type_response(content_type)
else:
raise JsonBodyParsingError from te
except (ValidationError, V1ValidationError) as ve:
raise JsonBodyParsingError() from te
except ValidationError as ve:
err["form_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
except V1ValidationError as ve:
err["form_params"] = ve.errors()
request.query_params = q
request.body_params = b
Expand Down
12 changes: 7 additions & 5 deletions flask_pydantic/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import List, Optional

from pydantic_core import ErrorDetails


class BaseFlaskPydanticException(Exception):
"""Base exc class for all exception from this library"""
Expand All @@ -24,7 +26,7 @@ class ManyModelValidationError(BaseFlaskPydanticException):
"""This exception is raised if there is a failure during validation of many
models in an iterable"""

def __init__(self, errors: List[dict], *args):
def __init__(self, errors: List[ErrorDetails], *args):
self._errors = errors
super().__init__(*args)

Expand All @@ -38,10 +40,10 @@ class ValidationError(BaseFlaskPydanticException):

def __init__(
self,
body_params: Optional[List[dict]] = None,
form_params: Optional[List[dict]] = None,
path_params: Optional[List[dict]] = None,
query_params: Optional[List[dict]] = None,
body_params: Optional[List[ErrorDetails]] = None,
form_params: Optional[List[ErrorDetails]] = None,
path_params: Optional[List[ErrorDetails]] = None,
query_params: Optional[List[ErrorDetails]] = None,
):
super().__init__()
self.body_params = body_params
Expand Down
Loading
Loading