Source code for flask_utils.decorators

from typing import Any
from typing import Dict
from typing import Type
from typing import Union
from typing import Callable
from typing import Optional
from typing import get_args
from typing import get_origin
from functools import wraps

from flask import Response
from flask import jsonify
from flask import request
from flask import current_app
from flask import make_response
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import UnsupportedMediaType

from flask_utils.errors import BadRequestError

VALIDATE_PARAMS_MAX_DEPTH = 4


def _handle_bad_request(
    use_error_handlers: bool,
    message: str,
    solution: Optional[str] = None,
    status_code: int = 400,
    original_exception: Optional[Exception] = None,
) -> Response:
    if use_error_handlers:
        raise BadRequestError(message, solution) from original_exception
    else:
        error_response = {"error": message}
        if solution:
            error_response["solution"] = solution
        return make_response(jsonify(error_response), status_code)


[docs] def _is_optional(type_hint: Type) -> bool: # type: ignore """Check if the type hint is :data:`~typing.Optional`. :param type_hint: Type hint to check. :type type_hint: Type :return: True if the type hint is :data:`~typing.Optional`, False otherwise. :rtype: bool :Example: .. code-block:: python from typing import Optional from flask_utils.decorators import _is_optional _is_optional(Optional[str]) # True _is_optional(str) # False .. versionadded:: 0.2.0 """ return get_origin(type_hint) is Union and type(None) in get_args(type_hint)
[docs] def _make_optional(type_hint: Type) -> Type: # type: ignore """Wrap type hint with :data:`~typing.Optional` if it's not already. :param type_hint: Type hint to wrap. :type type_hint: Type :return: Type hint wrapped with :data:`~typing.Optional`. :rtype: Type :Example: .. code-block:: python from typing import Optional from flask_utils.decorators import _make_optional _make_optional(str) # Optional[str] _make_optional(Optional[str]) # Optional[str] .. versionadded:: 0.2.0 """ if not _is_optional(type_hint): return Optional[type_hint] # type: ignore return type_hint
[docs] def _is_allow_empty(value: Any, type_hint: Type, allow_empty: bool) -> bool: # type: ignore """Determine if the value is considered empty and whether it's allowed. :param value: Value to check. :type value: Any :param type_hint: Type hint to check against. :type type_hint: Type :param allow_empty: Whether to allow empty values. :type allow_empty: bool :return: True if the value is empty and allowed, False otherwise. :rtype: bool :Example: .. code-block:: python from typing import Optional from flask_utils.decorators import _is_allow_empty _is_allow_empty(None, str, False) # False _is_allow_empty("", str, False) # False _is_allow_empty(None, Optional[str], False) # True _is_allow_empty("", Optional[str], False) # True _is_allow_empty("", Optional[str], True) # True _is_allow_empty("", str, True) # True _is_allow_empty([], Optional[list], False) # True .. versionadded:: 0.2.0 """ if value in [None, "", [], {}]: # Check if type is explicitly Optional or allow_empty is True if _is_optional(type_hint) or allow_empty: return True return False
[docs] def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_depth: int = 0) -> bool: # type: ignore """Check if the value matches the expected type, recursively if necessary. :param value: Value to check. :type value: Any :param expected_type: Expected type. :type expected_type: Type :param allow_empty: Whether to allow empty values. :type allow_empty: bool :param curr_depth: Current depth of the recursive check. :type curr_depth: int :return: True if the value matches the expected type, False otherwise. :rtype: bool :Example: .. code-block:: python from typing import List, Dict from flask_utils.decorators import _check_type _check_type("hello", str) # True _check_type(42, int) # True _check_type(42.0, float) # True _check_type(True, bool) # True _check_type(["hello", "world"], List[str]) # True _check_type({"name": "Jules", "city": "Rouen"}, Dict[str, str]) # True It also works recursively: .. code-block:: python from typing import List, Dict from flask_utils.decorators import _check_type _check_type(["hello", "world"], List[str]) # True _check_type(["hello", 42], List[str]) # False _check_type([{"name": "Jules", "city": "Rouen"}, {"name": "John", "city": "Paris"}], List[Dict[str, str]]) # True _check_type([{"name": "Jules", "city": "Rouen"}, {"name": "John", "city": 42}], List[Dict[str, str]]) # False .. versionadded:: 0.2.0 """ if curr_depth >= VALIDATE_PARAMS_MAX_DEPTH: return True if expected_type is Any or _is_allow_empty(value, expected_type, allow_empty): # type: ignore return True if isinstance(value, bool): if expected_type is bool or expected_type is Optional[bool]: # type: ignore return True if get_origin(expected_type) is Union: return any(arg is bool for arg in get_args(expected_type)) return False origin = get_origin(expected_type) args = get_args(expected_type) if origin is Union: return any(_check_type(value, arg, allow_empty, (curr_depth + 1)) for arg in args) elif origin is list: return isinstance(value, list) and all( _check_type(item, args[0], allow_empty, (curr_depth + 1)) for item in value ) elif origin is dict: key_type, val_type = args if not isinstance(value, dict): return False for k, v in value.items(): if not isinstance(k, key_type): return False if not _check_type(v, val_type, allow_empty, (curr_depth + 1)): return False return True else: return isinstance(value, expected_type)
[docs] def validate_params( parameters: Dict[Any, Any], allow_empty: bool = False, ) -> Callable: # type: ignore """ Decorator to validate request JSON body parameters. This decorator ensures that the JSON body of a request matches the specified parameter types and includes all required parameters. :param parameters: Dictionary of parameters to validate. The keys are parameter names and the values are the expected types. :type parameters: Dict[Any, Any] :param allow_empty: Allow empty values for parameters. Defaults to False. :type allow_empty: bool :raises BadRequestError: If the JSON body is malformed, the Content-Type header is missing or incorrect, required parameters are missing, or parameters are of the wrong type. :Example: .. code-block:: python from flask import Flask, request from typing import List, Dict from flask_utils.decorators import validate_params from flask_utils.errors.badrequest import BadRequestError app = Flask(__name__) @app.route("/example", methods=["POST"]) @validate_params( { "name": str, "age": int, "is_student": bool, "courses": List[str], "grades": Dict[str, int], } ) def example(): \""" This route expects a JSON body with the following: - name: str - age: int (optional) - is_student: bool - courses: list of str - grades: dict with str keys and int values \""" data = request.get_json() return data .. tip:: You can use any of the following types: * str * int * float * bool * List * Dict * Any * Optional * Union .. versionchanged:: 0.7.0 The decorator will now use the custom error handlers if ``register_error_handlers`` has been set to ``True`` when initializing the :class:`~flask_utils.extension.FlaskUtils` extension. .. versionadded:: 0.2.0 """ def decorator(fn): # type: ignore @wraps(fn) def wrapper(*args, **kwargs): # type: ignore use_error_handlers = ( current_app.extensions.get("flask_utils") is not None and current_app.extensions["flask_utils"].has_error_handlers_registered ) try: data = request.get_json() except BadRequest as e: return _handle_bad_request(use_error_handlers, "The Json Body is malformed.", original_exception=e) except UnsupportedMediaType as e: return _handle_bad_request( use_error_handlers, "The Content-Type header is missing or is not set to application/json, " "or the JSON body is missing.", original_exception=e, ) if not data: return _handle_bad_request(use_error_handlers, "Missing json body.") if not isinstance(data, dict): return _handle_bad_request(use_error_handlers, "JSON body must be a dict") for key, type_hint in parameters.items(): if not _is_optional(type_hint) and key not in data: return _handle_bad_request( use_error_handlers, f"Missing key: {key}", f"Expected keys are: {list(parameters.keys())}" ) for key in data: if key not in parameters: return _handle_bad_request( use_error_handlers, f"Unexpected key: {key}.", f"Expected keys are: {list(parameters.keys())}" ) for key in data: if key in parameters and not _check_type(data[key], parameters[key], allow_empty): return _handle_bad_request( use_error_handlers, f"Wrong type for key {key}.", f"It should be {getattr(parameters[key], '__name__', str(parameters[key]))}", ) return fn(*args, **kwargs) return wrapper return decorator