Skip to content

Commit

Permalink
fix: allow excluding pydantic error url, context or input values #22
Browse files Browse the repository at this point in the history
  • Loading branch information
stevanmilic committed Oct 8, 2024
1 parent 8595fa8 commit 437d524
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 19 deletions.
90 changes: 76 additions & 14 deletions flask_pydantic/core.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from functools import wraps
from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, Union
from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, Union, cast

from flask import Response, current_app, jsonify, make_response, request
from pydantic import BaseModel, ValidationError, TypeAdapter, RootModel
from pydantic_core import ErrorDetails

from .converters import convert_query_params
from .exceptions import (
Expand Down Expand Up @@ -50,7 +51,13 @@ def is_iterable_of_models(content: Any) -> bool:
return False


def validate_many_models(model: Type[BaseModel], content: Any) -> List[BaseModel]:
def validate_many_models(
model: Type[BaseModel],
content: Any,
include_error_url: bool = True,
include_error_context: bool = True,
include_error_input: bool = True,
) -> List[BaseModel]:
try:
return [model(**fields) for fields in content]
except TypeError:
Expand All @@ -64,10 +71,22 @@ def validate_many_models(model: Type[BaseModel], content: Any) -> List[BaseModel
]
raise ManyModelValidationError(err)
except ValidationError as ve:
raise ManyModelValidationError(ve.errors())


def validate_path_params(func: Callable, kwargs: dict) -> Tuple[dict, list]:
raise ManyModelValidationError(
ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
)


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 @@ -77,7 +96,11 @@ def validate_path_params(func: Callable, kwargs: dict) -> Tuple[dict, list]:
adapter = TypeAdapter(type_)
validated[name] = adapter.validate_python(kwargs.get(name))
except ValidationError as e:
err = e.errors()[0]
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)
kwargs = {**kwargs, **validated}
Expand All @@ -101,6 +124,9 @@ def validate(
response_by_alias: bool = False,
get_json_params: Optional[dict] = None,
form: Optional[Type[BaseModel]] = 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 @@ -122,6 +148,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 @@ -166,7 +195,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 @@ -176,7 +211,12 @@ def wrapper(*args, **kwargs):
try:
q = query_model(**query_params)
except ValidationError as ve:
err["query_params"] = ve.errors()
err["query_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)

body_in_kwargs = func.__annotations__.get("body")
body_model = body_in_kwargs or body
if body_model:
Expand All @@ -185,10 +225,20 @@ def wrapper(*args, **kwargs):
try:
b = body_model(body_params)
except ValidationError as ve:
err["body_params"] = ve.errors()
err["body_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
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 @@ -202,7 +252,11 @@ def wrapper(*args, **kwargs):
else:
raise JsonBodyParsingError()
except ValidationError as ve:
err["body_params"] = ve.errors()
err["body_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
form_in_kwargs = func.__annotations__.get("form")
form_model = form_in_kwargs or form
if form_model:
Expand All @@ -211,7 +265,11 @@ def wrapper(*args, **kwargs):
try:
f = form_model(form_params)
except ValidationError as ve:
err["form_params"] = ve.errors()
err["form_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
else:
try:
f = form_model(**form_params)
Expand All @@ -223,7 +281,11 @@ def wrapper(*args, **kwargs):
else:
raise JsonBodyParsingError
except ValidationError as ve:
err["form_params"] = ve.errors()
err["form_params"] = ve.errors(
include_url=include_error_url,
include_context=include_error_context,
include_input=include_error_input,
)
request.query_params = q
request.body_params = b
request.form_params = f
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

0 comments on commit 437d524

Please sign in to comment.