diff --git a/MANIFEST.in b/MANIFEST.in index 25f694e..bfeadb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,6 @@ include docs/source/index.rst include LICENSE include docs/source/changelog.rst recursive-include flask_inputfilter *.py *.pyx *.c *.cpp +recursive-include flask_inputfilter/include *.h prune tests recursive-prune __pycache__ diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 5f2ad6b..3df6a25 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -3,6 +3,16 @@ Changelog All notable changes to this project will be documented in this file. +[0.4.1] - 2025-04-24 +-------------------- + +Changed +^^^^^^^ +- Introduced first c++ vector in ``InputFilter`` to improve performance. +- Updated required ``cython`` version to 3.0 or higher for python 3.7 - 3.11. +- Moved static methods outside of pure InputFilter class. + + [0.4.0] - 2025-04-20 -------------------- diff --git a/docs/source/guides/create_own_components.rst b/docs/source/guides/create_own_components.rst index fa6b876..e5eb2da 100644 --- a/docs/source/guides/create_own_components.rst +++ b/docs/source/guides/create_own_components.rst @@ -31,7 +31,7 @@ Example implementation .. code-block:: python - from typing import Any, Dict + from typing import Any from flask_inputfilter.Condition import BaseCondition @@ -45,7 +45,7 @@ Example implementation self.first_field = first_field self.second_field = second_field - def check(self, data: Dict[str, Any]) -> bool: + def check(self, data: dict[str, Any]) -> bool: return data.get(self.first_field) == data.get(self.second_field) @@ -69,7 +69,7 @@ Example implementation .. code-block:: python from datetime import date, datetime - from typing import Any, Union + from typing import Any from flask_inputfilter.Filter import BaseFilter @@ -80,7 +80,7 @@ Example implementation Supports ISO 8601 formatted strings. """ - def apply(self, value: Any) -> Union[datetime, Any]: + def apply(self, value: Any) -> datetime|Any: if isinstance(value, datetime): return value @@ -94,8 +94,7 @@ Example implementation except ValueError: return value - else: - return value + return value Validator @@ -116,7 +115,7 @@ Example implementation .. code-block:: python - from typing import Any, List, Optional + from typing import Any, Optional from flask_inputfilter.Exception import ValidationError from flask_inputfilter.Validator import BaseValidator @@ -129,7 +128,7 @@ Example implementation def __init__( self, - haystack: List[Any], + haystack: list[Any], strict: bool = False, error_message: Optional[str] = None, ) -> None: diff --git a/docs/source/options/copy.rst b/docs/source/options/copy.rst index b7b62b9..f78885f 100644 --- a/docs/source/options/copy.rst +++ b/docs/source/options/copy.rst @@ -44,7 +44,7 @@ Basic Copy Integration def test_route(): validated_data = g.validated_data - # Cotains the same value as username but escaped eg. "very-important-user" + # Contains the same value as username but escaped eg. "very-important-user" print(validated_data["escapedUsername"]) diff --git a/env_configs/cython.Dockerfile b/env_configs/cython.Dockerfile index 27ff547..a274669 100644 --- a/env_configs/cython.Dockerfile +++ b/env_configs/cython.Dockerfile @@ -2,7 +2,9 @@ FROM python:3.7-slim WORKDIR /app -RUN apt-get update && apt-get install -y g++ git +RUN apt-get update \ + && apt-get install -y g++ git \ + && apt-get clean COPY pyproject.toml /app diff --git a/env_configs/env.Dockerfile b/env_configs/env.Dockerfile index e2df914..9658efc 100644 --- a/env_configs/env.Dockerfile +++ b/env_configs/env.Dockerfile @@ -2,35 +2,37 @@ FROM debian:buster-slim WORKDIR /app -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - g++ \ - git \ - libbz2-dev \ - libffi-dev \ - libjpeg-dev \ - liblzma-dev \ - libncurses5-dev \ - libncursesw5-dev \ - libreadline-dev \ - libsqlite3-dev \ - libssl-dev \ - llvm \ - make \ - python3-dev \ - tk-dev \ - wget \ - xz-utils \ - zlib1g-dev +RUN apt-get update \ + && apt-get install -y \ + build-essential \ + curl \ + g++ \ + git \ + libbz2-dev \ + libffi-dev \ + libjpeg-dev \ + liblzma-dev \ + libncurses5-dev \ + libncursesw5-dev \ + libreadline-dev \ + libsqlite3-dev \ + libssl-dev \ + llvm \ + make \ + python3-dev \ + tk-dev \ + wget \ + xz-utils \ + zlib1g-dev \ + && apt-get clean RUN curl https://pyenv.run | bash ENV PATH="/root/.pyenv/bin:/root/.pyenv/shims:/root/.pyenv/versions/3.7.12/bin:$PATH" -RUN echo 'export PATH="/root/.pyenv/bin:$PATH"' >> ~/.bashrc -RUN echo 'eval "$(pyenv init --path)"' >> ~/.bashrc -RUN echo 'eval "$(pyenv init -)"' >> ~/.bashrc -RUN echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc +RUN echo 'export PATH="/root/.pyenv/bin:$PATH"' >> ~/.bashrc \ + && echo 'eval "$(pyenv init --path)"' >> ~/.bashrc \ + && echo 'eval "$(pyenv init -)"' >> ~/.bashrc \ + && echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc RUN /root/.pyenv/bin/pyenv install 3.7.12 RUN /root/.pyenv/bin/pyenv install 3.8.12 diff --git a/env_configs/pure.Dockerfile b/env_configs/pure.Dockerfile index 487fbe6..adb5ec1 100644 --- a/env_configs/pure.Dockerfile +++ b/env_configs/pure.Dockerfile @@ -2,7 +2,9 @@ FROM python:3.7-slim WORKDIR /app -RUN apt-get update && apt-get install -y git +RUN apt-get update \ + && apt-get install -y git \ + && apt-get clean COPY pyproject.toml /app diff --git a/env_configs/requirements-py310.txt b/env_configs/requirements-py310.txt index 84f3eee..219f8b8 100644 --- a/env_configs/requirements-py310.txt +++ b/env_configs/requirements-py310.txt @@ -1,4 +1,4 @@ -cython==0.29.24 +cython==3.0 flask==2.1 pillow==8.0.0 pytest diff --git a/env_configs/requirements-py311.txt b/env_configs/requirements-py311.txt index d1575ca..219f8b8 100644 --- a/env_configs/requirements-py311.txt +++ b/env_configs/requirements-py311.txt @@ -1,4 +1,4 @@ -cython==0.29.32 +cython==3.0 flask==2.1 pillow==8.0.0 pytest diff --git a/env_configs/requirements-py37.txt b/env_configs/requirements-py37.txt index 1f98e15..9307d64 100644 --- a/env_configs/requirements-py37.txt +++ b/env_configs/requirements-py37.txt @@ -1,4 +1,4 @@ -cython==0.29 +cython==3.0 flask==2.1 pillow==8.0.0 pytest diff --git a/env_configs/requirements-py38.txt b/env_configs/requirements-py38.txt index 2ca2835..219f8b8 100644 --- a/env_configs/requirements-py38.txt +++ b/env_configs/requirements-py38.txt @@ -1,4 +1,4 @@ -cython==0.29 +cython==3.0 flask==2.1 pillow==8.0.0 pytest diff --git a/env_configs/requirements-py39.txt b/env_configs/requirements-py39.txt index faf3f4d..219f8b8 100644 --- a/env_configs/requirements-py39.txt +++ b/env_configs/requirements-py39.txt @@ -1,4 +1,4 @@ -cython==0.29.21 +cython==3.0 flask==2.1 pillow==8.0.0 pytest diff --git a/flask_inputfilter/Condition/CustomCondition.py b/flask_inputfilter/Condition/CustomCondition.py index a65779a..f88f27c 100644 --- a/flask_inputfilter/Condition/CustomCondition.py +++ b/flask_inputfilter/Condition/CustomCondition.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Callable, Dict +from collections.abc import Callable +from typing import Any, Dict from flask_inputfilter.Condition import BaseCondition diff --git a/flask_inputfilter/Filter/Base64ImageDownscaleFilter.py b/flask_inputfilter/Filter/Base64ImageDownscaleFilter.py index 0fbfd0b..73aed57 100644 --- a/flask_inputfilter/Filter/Base64ImageDownscaleFilter.py +++ b/flask_inputfilter/Filter/Base64ImageDownscaleFilter.py @@ -1,11 +1,10 @@ from __future__ import annotations import base64 -import binascii import io from typing import Any, Optional -from PIL import Image, UnidentifiedImageError +from PIL import Image from flask_inputfilter.Filter import BaseFilter @@ -39,13 +38,7 @@ def apply(self, value: Any) -> Any: image = Image.open(io.BytesIO(base64.b64decode(value))) return self.resize_picture(image) - except ( - binascii.Error, - UnidentifiedImageError, - OSError, - ValueError, - TypeError, - ): + except (OSError, ValueError, TypeError): return value def resize_picture(self, image: Image) -> str: diff --git a/flask_inputfilter/Filter/Base64ImageResizeFilter.py b/flask_inputfilter/Filter/Base64ImageResizeFilter.py index 514a9e7..a86cded 100644 --- a/flask_inputfilter/Filter/Base64ImageResizeFilter.py +++ b/flask_inputfilter/Filter/Base64ImageResizeFilter.py @@ -1,11 +1,10 @@ from __future__ import annotations import base64 -import binascii import io from typing import Any -from PIL import Image, UnidentifiedImageError +from PIL import Image from flask_inputfilter.Enum import ImageFormatEnum from flask_inputfilter.Filter import BaseFilter @@ -46,13 +45,7 @@ def apply(self, value: Any) -> Any: value = Image.open(io.BytesIO(base64.b64decode(value))) return self.reduce_image(value) - except ( - binascii.Error, - UnidentifiedImageError, - OSError, - ValueError, - TypeError, - ): + except (OSError, ValueError, TypeError): return value def reduce_image(self, image: Image) -> Image: diff --git a/flask_inputfilter/Filter/ToEnumFilter.py b/flask_inputfilter/Filter/ToEnumFilter.py index 17accdb..8cc3c75 100644 --- a/flask_inputfilter/Filter/ToEnumFilter.py +++ b/flask_inputfilter/Filter/ToEnumFilter.py @@ -17,10 +17,7 @@ def __init__(self, enum_class: Type[Enum]) -> None: self.enum_class = enum_class def apply(self, value: Any) -> Union[Enum, Any]: - if not isinstance(value, (str, int)): - return value - - elif isinstance(value, Enum): + if not isinstance(value, (str, int)) or isinstance(value, Enum): return value try: diff --git a/flask_inputfilter/Filter/ToIsoFilter.py b/flask_inputfilter/Filter/ToIsoFilter.py index a58e76d..ef64815 100644 --- a/flask_inputfilter/Filter/ToIsoFilter.py +++ b/flask_inputfilter/Filter/ToIsoFilter.py @@ -12,10 +12,7 @@ class ToIsoFilter(BaseFilter): """ def apply(self, value: Any) -> Union[str, Any]: - if isinstance(value, datetime): - return value.isoformat() - - elif isinstance(value, date): + if isinstance(value, (datetime, date)): return value.isoformat() return value diff --git a/flask_inputfilter/InputFilter.py b/flask_inputfilter/InputFilter.py index 2992c03..c610a8b 100644 --- a/flask_inputfilter/InputFilter.py +++ b/flask_inputfilter/InputFilter.py @@ -2,6 +2,7 @@ import json import logging +from collections.abc import Callable from typing import Any, Dict, List, Optional, Type, TypeVar, Union from flask import Response, g, request @@ -9,7 +10,7 @@ from flask_inputfilter.Condition import BaseCondition from flask_inputfilter.Exception import ValidationError from flask_inputfilter.Filter import BaseFilter -from flask_inputfilter.Mixin import ExternalApiMixin +from flask_inputfilter.Mixin import ExternalApiMixin, FieldMixin from flask_inputfilter.Model import ExternalApiConfig, FieldModel from flask_inputfilter.Validator import BaseValidator @@ -36,9 +37,9 @@ def __init__(self, methods: Optional[List[str]] = None) -> None: self.data: Dict[str, Any] = {} self.validated_data: Dict[str, Any] = {} self.errors: Dict[str, str] = {} - self.model_class: Optional = None + self.model_class: Optional[Type[T]] = None - def isValid(self): + def isValid(self) -> bool: """ Checks if the object's state or its attributes meet certain conditions to be considered valid. This function is typically used to @@ -61,7 +62,13 @@ def isValid(self): @classmethod def validate( cls, - ): + ) -> Callable[ + [Any], + Callable[ + [tuple[Any, ...], Dict[str, Any]], + Union[Response, tuple[Any, Dict[str, Any]]], + ], + ]: """ Decorator for validating input data in routes. @@ -72,15 +79,15 @@ def validate( Callable[ [Any], Callable[ - [Tuple[Any, ...], Dict[str, Any]], - Union[Response, Tuple[Any, Dict[str, Any]]], + [tuple[Any, ...], Dict[str, Any]], + Union[Response, tuple[Any, Dict[str, Any]]], ], ] """ def decorator( - f, - ): + f: Callable, + ) -> Callable[[Any, Any], Union[Response, tuple[Any, Dict[str, Any]]]]: """ Decorator function to validate input data for a Flask route. @@ -92,12 +99,14 @@ def decorator( [Any, Any], Union[ Response, - Tuple[Any, Dict[str, Any]] + tuple[Any, Dict[str, Any]] ] ]: The wrapped function with input validation. """ - def wrapper(*args, **kwargs): + def wrapper( + *args, **kwargs + ) -> Union[Response, tuple[Any, Dict[str, Any]]]: """ Wrapper function to handle input validation and error handling for the decorated route function. @@ -107,7 +116,7 @@ def wrapper(*args, **kwargs): **kwargs: Keyword arguments for the route function. Returns: - Union[Response, Tuple[Any, Dict[str, Any]]]: The response + Union[Response, tuple[Any, Dict[str, Any]]]: The response from the route function or an error response. """ @@ -144,7 +153,9 @@ def wrapper(*args, **kwargs): return decorator - def validateData(self, data: Optional[Dict[str, Any]] = None): + def validateData( + self, data: Optional[Dict[str, Any]] = None + ) -> Union[Dict[str, Any], Type[T]]: """ Validates input data against defined field rules, including applying filters, validators, custom logic steps, and fallback mechanisms. The @@ -175,8 +186,8 @@ def validateData(self, data: Optional[Dict[str, Any]] = None): required = field_info.required default = field_info.default fallback = field_info.fallback - filters = field_info.filters - validators = field_info.validators + filters = field_info.filters + self.global_filters + validators = field_info.validators + self.global_validators steps = field_info.steps external_api = field_info.external_api copy = field_info.copy @@ -186,16 +197,17 @@ def validateData(self, data: Optional[Dict[str, Any]] = None): value = self.validated_data.get(copy) if external_api: - value = ExternalApiMixin().callExternalApi( + value = ExternalApiMixin.callExternalApi( external_api, fallback, self.validated_data ) - value = self.applyFilters(filters, value) + value = FieldMixin.applyFilters(filters, value) value = ( - self.validateField(validators, fallback, value) or value + FieldMixin.validateField(validators, fallback, value) + or value ) - value = self.applySteps(steps, fallback, value) or value - value = InputFilter.checkForRequired( + value = FieldMixin.applySteps(steps, fallback, value) or value + value = FieldMixin.checkForRequired( field_name, required, default, fallback, value ) @@ -205,7 +217,7 @@ def validateData(self, data: Optional[Dict[str, Any]] = None): errors[field_name] = str(e) try: - self.checkConditions(self.validated_data) + FieldMixin.checkConditions(self.conditions, self.validated_data) except ValidationError as e: errors["_condition"] = str(e) @@ -217,7 +229,7 @@ def validateData(self, data: Optional[Dict[str, Any]] = None): return self.validated_data - def addCondition(self, condition: BaseCondition): + def addCondition(self, condition: BaseCondition) -> None: """ Add a condition to the input filter. @@ -226,7 +238,7 @@ def addCondition(self, condition: BaseCondition): """ self.conditions.append(condition) - def getConditions(self): + def getConditions(self) -> List[BaseCondition]: """ Retrieve the list of all registered conditions. @@ -240,26 +252,7 @@ def getConditions(self): """ return self.conditions - def checkConditions(self, validated_data: Dict[str, Any]): - """ - Checks if all conditions are met. - - This method iterates through all registered conditions and checks - if they are satisfied based on the provided validated data. If any - condition is not met, a ValidationError is raised with an appropriate - message indicating which condition failed. - - Args: - validated_data (Dict[str, Any]): - The validated data to check against the conditions. - """ - for condition in self.conditions: - if not condition.check(validated_data): - raise ValidationError( - f"Condition '{condition.__class__.__name__}' not met." - ) - - def setData(self, data: Dict[str, Any]): + def setData(self, data: Dict[str, Any]) -> None: """ Filters and sets the provided data into the object's internal storage, ensuring that only the specified fields are considered and @@ -274,14 +267,14 @@ def setData(self, data: Dict[str, Any]): self.data = {} for field_name, field_value in data.items(): if field_name in self.fields: - field_value = self.applyFilters( + field_value = FieldMixin.applyFilters( filters=self.fields[field_name].filters, value=field_value, ) self.data[field_name] = field_value - def getValue(self, name: str): + def getValue(self, name: str) -> Any: """ This method retrieves a value associated with the provided name. It searches for the value based on the given identifier and returns the @@ -302,7 +295,7 @@ def getValue(self, name: str): """ return self.validated_data.get(name) - def getValues(self): + def getValues(self) -> Dict[str, Any]: """ Retrieves a dictionary of key-value pairs from the current object. This method provides access to the internal state or configuration of @@ -315,7 +308,7 @@ def getValues(self): """ return self.validated_data - def getRawValue(self, name: str): + def getRawValue(self, name: str) -> Any: """ Fetches the raw value associated with the provided key. @@ -332,9 +325,9 @@ def getRawValue(self, name: str): Returns: Any: The raw value associated with the provided key. """ - return self.data.get(name) if name in self.data else None + return self.data.get(name) - def getRawValues(self): + def getRawValues(self) -> Dict[str, Any]: """ Retrieves raw values from a given source and returns them as a dictionary. @@ -358,7 +351,7 @@ def getRawValues(self): if field in self.data } - def getUnfilteredData(self): + def getUnfilteredData(self) -> Dict[str, Any]: """ Fetches unfiltered data from the data source. @@ -375,7 +368,7 @@ def getUnfilteredData(self): """ return self.data - def setUnfilteredData(self, data: Dict[str, Any]): + def setUnfilteredData(self, data: Dict[str, Any]) -> None: """ Sets unfiltered data for the current instance. This method assigns a given dictionary of data to the instance for further processing. It @@ -403,7 +396,7 @@ def hasUnknown(self) -> bool: for field_name in self.data.keys() ) - def getErrorMessage(self, field_name: str): + def getErrorMessage(self, field_name: str) -> Optional[str]: """ Retrieves and returns a predefined error message. @@ -419,11 +412,11 @@ def getErrorMessage(self, field_name: str): message is being retrieved. Returns: - str: A string representing the predefined error message. + Optional[str]: A string representing the predefined error message. """ return self.errors.get(field_name) - def getErrorMessages(self): + def getErrorMessages(self) -> Dict[str, str]: """ Retrieves all error messages associated with the fields in the input filter. @@ -449,7 +442,7 @@ def add( steps: Optional[List[Union[BaseFilter, BaseValidator]]] = None, external_api: Optional[ExternalApiConfig] = None, copy: Optional[str] = None, - ): + ) -> None: """ Add the field to the input filter. @@ -492,7 +485,7 @@ def add( copy=copy, ) - def has(self, field_name: str): + def has(self, field_name: str) -> bool: """ This method checks the existence of a specific field within the input filter values, identified by its field name. It does not return a @@ -507,7 +500,7 @@ def has(self, field_name: str): """ return field_name in self.fields - def getInput(self, field_name: str): + def getInput(self, field_name: str) -> Optional[FieldModel]: """ Represents a method to retrieve a field by its name. @@ -526,7 +519,7 @@ def getInput(self, field_name: str): """ return self.fields.get(field_name) - def getInputs(self): + def getInputs(self) -> Dict[str, FieldModel]: """ Retrieve the dictionary of input fields associated with the object. @@ -536,7 +529,7 @@ def getInputs(self): """ return self.fields - def remove(self, field_name: str): + def remove(self, field_name: str) -> Optional[FieldModel]: """ Removes the specified field from the instance or collection. @@ -553,7 +546,7 @@ def remove(self, field_name: str): """ return self.fields.pop(field_name, None) - def count(self): + def count(self) -> int: """ Counts the total number of elements in the collection. @@ -577,7 +570,7 @@ def replace( steps: Optional[List[Union[BaseFilter, BaseValidator]]] = None, external_api: Optional[ExternalApiConfig] = None, copy: Optional[str] = None, - ): + ) -> None: """ Replaces a field in the input filter. @@ -617,99 +610,7 @@ def replace( copy=copy, ) - def applySteps( - self, - steps: List[Union[BaseFilter, BaseValidator]], - fallback: Any, - value: Any, - ): - """ - Apply multiple filters and validators in a specific order. - - This method processes a given value by sequentially applying a list of - filters and validators. Filters modify the value, while validators - ensure the value meets specific criteria. If a validation error occurs - and a fallback value is provided, the fallback is returned. Otherwise, - the validation error is raised. - - Args: - steps (List[Union[BaseFilter, BaseValidator]]): - A list of filters and validators to be applied in order. - fallback (Any): - A fallback value to return if validation fails. - value (Any): - The initial value to be processed. - - Returns: - Any: The processed value after applying all filters and validators. - If a validation error occurs and a fallback is provided, the - fallback value is returned. - - Raises: - ValidationError: If validation fails and no fallback value is - provided. - """ - if value is None: - return - - try: - for step in steps: - if isinstance(step, BaseFilter): - value = step.apply(value) - elif isinstance(step, BaseValidator): - step.validate(value) - except ValidationError: - if fallback is None: - raise - return fallback - return value - - @staticmethod - def checkForRequired( - field_name: str, - required: bool, - default: Any, - fallback: Any, - value: Any, - ): - """ - Determine the value of the field, considering the required and - fallback attributes. - - If the field is not required and no value is provided, the default - value is returned. If the field is required and no value is provided, - the fallback value is returned. If no of the above conditions are met, - a ValidationError is raised. - - Args: - field_name (str): The name of the field being processed. - required (bool): Indicates whether the field is required. - default (Any): The default value to use if the field is not - provided and not required. - fallback (Any): The fallback value to use if the field is required - but not provided. - value (Any): The current value of the field being processed. - - Returns: - Any: The determined value of the field after considering required, - default, and fallback attributes. - - Raises: - ValidationError: If the field is required and no value or fallback - is provided. - """ - if value is not None: - return value - - if not required: - return default - - if fallback is not None: - return fallback - - raise ValidationError(f"Field '{field_name}' is required.") - - def addGlobalFilter(self, filter: BaseFilter): + def addGlobalFilter(self, filter: BaseFilter) -> None: """ Add a global filter to be applied to all fields. @@ -718,7 +619,7 @@ def addGlobalFilter(self, filter: BaseFilter): """ self.global_filters.append(filter) - def getGlobalFilters(self): + def getGlobalFilters(self) -> List[BaseFilter]: """ Retrieve all global filters associated with this InputFilter instance. @@ -731,28 +632,7 @@ def getGlobalFilters(self): """ return self.global_filters - def applyFilters(self, filters: List[BaseFilter], value: Any): - """ - Apply filters to the field value. - - Args: - filters (List[BaseFilter]): A list of filters to apply to the - value. - value (Any): The value to be processed by the filters. - - Returns: - Any: The processed value after applying all filters. - If the value is None, None is returned. - """ - if value is None: - return - - for filter in self.global_filters + filters: - value = filter.apply(value) - - return value - - def clear(self): + def clear(self) -> None: """ Resets all fields of the InputFilter instance to their initial empty state. @@ -857,32 +737,3 @@ def getGlobalValidators(self) -> List[BaseValidator]: List[BaseValidator]: A list of global validators. """ return self.global_validators - - def validateField( - self, validators: List[BaseValidator], fallback: Any, value: Any - ) -> Any: - """ - Validate the field value. - - Args: - validators (List[BaseValidator]): A list of validators to apply - to the field value. - fallback (Any): A fallback value to return if validation fails. - value (Any): The value to be validated. - - Returns: - Any: The validated value if all validators pass. If validation - fails and a fallback is provided, the fallback value is - returned. - """ - if value is None: - return - - try: - for validator in self.global_validators + validators: - validator.validate(value) - except ValidationError: - if fallback is None: - raise - - return fallback diff --git a/flask_inputfilter/InputFilter.pyi b/flask_inputfilter/InputFilter.pyi index 861acac..64d7170 100644 --- a/flask_inputfilter/InputFilter.pyi +++ b/flask_inputfilter/InputFilter.pyi @@ -1,7 +1,10 @@ from __future__ import annotations +from collections.abc import Callable from typing import Any, Dict, List, Optional, Type, TypeVar, Union +from flask import Response + from flask_inputfilter.Condition import BaseCondition from flask_inputfilter.Filter import BaseFilter from flask_inputfilter.Model import ExternalApiConfig, FieldModel @@ -23,13 +26,20 @@ class InputFilter: def __init__(self, methods: Optional[List[str]] = ...) -> None: ... def isValid(self) -> bool: ... @classmethod - def validate(cls) -> Any: ... + def validate( + cls, + ) -> Callable[ + [Any], + Callable[ + [tuple[Any, ...], Dict[str, Any]], + Union[Response, tuple[Any, Dict[str, Any]]], + ], + ]: ... def validateData( self, data: Optional[Dict[str, Any]] = ... ) -> Union[Dict[str, Any], Type[T]]: ... def addCondition(self, condition: BaseCondition) -> None: ... def getConditions(self) -> List[BaseCondition]: ... - def checkConditions(self, validated_data: Dict[str, Any]) -> None: ... def setData(self, data: Dict[str, Any]) -> None: ... def getValue(self, name: str) -> Any: ... def getValues(self) -> Dict[str, Any]: ... @@ -69,29 +79,11 @@ class InputFilter: external_api: Optional[ExternalApiConfig] = ..., copy: Optional[str] = ..., ) -> None: ... - def applySteps( - self, - steps: List[Union[BaseFilter, BaseValidator]], - fallback: Any, - value: Any, - ) -> Any: ... - @staticmethod - def checkForRequired( - field_name: str, - required: bool, - default: Any, - fallback: Any, - value: Any, - ) -> Any: ... def addGlobalFilter(self, filter: BaseFilter) -> None: ... def getGlobalFilters(self) -> List[BaseFilter]: ... - def applyFilters(self, filters: List[BaseFilter], value: Any) -> Any: ... def clear(self) -> None: ... def merge(self, other: InputFilter) -> None: ... def setModel(self, model_class: Type[T]) -> None: ... def serialize(self) -> Union[Dict[str, Any], T]: ... def addGlobalValidator(self, validator: BaseValidator) -> None: ... def getGlobalValidators(self) -> List[BaseValidator]: ... - def validateField( - self, validators: List[BaseValidator], fallback: Any, value: Any - ) -> Any: ... diff --git a/flask_inputfilter/Mixin/ExternalApiMixin.py b/flask_inputfilter/Mixin/ExternalApiMixin.py index d6c84c0..ba3f620 100644 --- a/flask_inputfilter/Mixin/ExternalApiMixin.py +++ b/flask_inputfilter/Mixin/ExternalApiMixin.py @@ -8,8 +8,8 @@ class ExternalApiMixin: + @staticmethod def callExternalApi( - self, config: ExternalApiConfig, fallback: Any, validated_data: Dict[str, Any], @@ -55,39 +55,39 @@ def callExternalApi( data_key = config.data_key - requestData = { + request_data = { "headers": {}, "params": {}, } if config.api_key: - requestData["headers"]["Authorization"] = ( - f"Bearer " f"{config.api_key}" - ) + request_data["headers"][ + "Authorization" + ] = f"Bearer {config.api_key}" if config.headers: - requestData["headers"].update(config.headers) + request_data["headers"].update(config.headers) if config.params: - requestData[ + request_data[ "params" ] = ExternalApiMixin.replacePlaceholdersInParams( config.params, validated_data ) - requestData["url"] = ExternalApiMixin.replacePlaceholders( + request_data["url"] = ExternalApiMixin.replacePlaceholders( config.url, validated_data ) - requestData["method"] = config.method + request_data["method"] = config.method try: - response = requests.request(**requestData) + response = requests.request(**request_data) result = response.json() except requests.exceptions.RequestException: if fallback is None: logger.exception("External API request failed unexpectedly.") raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field'{data_key}'." ) return fallback except ValueError: @@ -96,7 +96,7 @@ def callExternalApi( "External API response could not be parsed to json." ) raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field '{data_key}'." ) return fallback @@ -107,7 +107,7 @@ def callExternalApi( f"{response.status_code}: {response.text}" ) raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field '{data_key}'." ) return fallback diff --git a/flask_inputfilter/Mixin/FieldMixin.py b/flask_inputfilter/Mixin/FieldMixin.py new file mode 100644 index 0000000..a25ee15 --- /dev/null +++ b/flask_inputfilter/Mixin/FieldMixin.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Union + +from flask_inputfilter.Condition import BaseCondition +from flask_inputfilter.Exception import ValidationError +from flask_inputfilter.Filter import BaseFilter +from flask_inputfilter.Validator import BaseValidator + + +class FieldMixin: + @staticmethod + def applyFilters(filters: List[BaseFilter], value: Any) -> Any: + """ + Apply filters to the field value. + + Args: + filters (List[BaseFilter]): A list of filters to apply to the + value. + value (Any): The value to be processed by the filters. + + Returns: + Any: The processed value after applying all filters. + If the value is None, None is returned. + """ + if value is None: + return + + for filter in filters: + value = filter.apply(value) + + return value + + @staticmethod + def validateField( + validators: List[BaseValidator], fallback: Any, value: Any + ) -> Any: + """ + Validate the field value. + + Args: + validators (List[BaseValidator]): A list of validators to apply + to the field value. + fallback (Any): A fallback value to return if validation fails. + value (Any): The value to be validated. + + Returns: + Any: The validated value if all validators pass. If validation + fails and a fallback is provided, the fallback value is + returned. + """ + if value is None: + return + + try: + for validator in validators: + validator.validate(value) + except ValidationError: + if fallback is None: + raise + + return fallback + + @staticmethod + def applySteps( + steps: List[Union[BaseFilter, BaseValidator]], + fallback: Any, + value: Any, + ) -> Any: + """ + Apply multiple filters and validators in a specific order. + + This method processes a given value by sequentially applying a list of + filters and validators. Filters modify the value, while validators + ensure the value meets specific criteria. If a validation error occurs + and a fallback value is provided, the fallback is returned. Otherwise, + the validation error is raised. + + Args: + steps (List[Union[BaseFilter, BaseValidator]]): + A list of filters and validators to be applied in order. + fallback (Any): + A fallback value to return if validation fails. + value (Any): + The initial value to be processed. + + Returns: + Any: The processed value after applying all filters and validators. + If a validation error occurs and a fallback is provided, the + fallback value is returned. + + Raises: + ValidationError: If validation fails and no fallback value is + provided. + """ + if value is None: + return + + try: + for step in steps: + if isinstance(step, BaseFilter): + value = step.apply(value) + elif isinstance(step, BaseValidator): + step.validate(value) + except ValidationError: + if fallback is None: + raise + return fallback + return value + + @staticmethod + def checkConditions( + conditions: List[BaseCondition], validated_data: Dict[str, Any] + ) -> None: + """ + Checks if all conditions are met. + + This method iterates through all registered conditions and checks + if they are satisfied based on the provided validated data. If any + condition is not met, a ValidationError is raised with an appropriate + message indicating which condition failed. + + Args: + conditions (List[BaseCondition]): + A list of conditions to be checked against the validated + validated_data (Dict[str, Any]): + The validated data to check against the conditions. + """ + for condition in conditions: + if not condition.check(validated_data): + raise ValidationError( + f"Condition '{condition.__class__.__name__}' not met." + ) + + @staticmethod + def checkForRequired( + field_name: str, + required: bool, + default: Any, + fallback: Any, + value: Any, + ) -> Any: + """ + Determine the value of the field, considering the required and + fallback attributes. + + If the field is not required and no value is provided, the default + value is returned. If the field is required and no value is provided, + the fallback value is returned. If no of the above conditions are met, + a ValidationError is raised. + + Args: + field_name (str): The name of the field being processed. + required (bool): Indicates whether the field is required. + default (Any): The default value to use if the field is not + provided and not required. + fallback (Any): The fallback value to use if the field is required + but not provided. + value (Any): The current value of the field being processed. + + Returns: + Any: The determined value of the field after considering required, + default, and fallback attributes. + + Raises: + ValidationError: If the field is required and no value or fallback + is provided. + """ + if value is not None: + return value + + if not required: + return default + + if fallback is not None: + return fallback + + raise ValidationError(f"Field '{field_name}' is required.") diff --git a/flask_inputfilter/Mixin/_ExternalApiMixin.pyx b/flask_inputfilter/Mixin/_ExternalApiMixin.pyx index 9618768..3d6d9ab 100644 --- a/flask_inputfilter/Mixin/_ExternalApiMixin.pyx +++ b/flask_inputfilter/Mixin/_ExternalApiMixin.pyx @@ -64,7 +64,7 @@ cdef class ExternalApiMixin: if config.api_key: requestData["headers"]["Authorization"] = ( - f"Bearer " f"{config.api_key}" + f"Bearer {config.api_key}" ) if config.headers: @@ -87,7 +87,7 @@ cdef class ExternalApiMixin: if fallback is None: logger.exception("External API request failed unexpectedly.") raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field '{data_key}'." ) return fallback except ValueError: @@ -96,7 +96,7 @@ cdef class ExternalApiMixin: "External API response could not be parsed to json." ) raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field '{data_key}'." ) return fallback @@ -107,7 +107,7 @@ cdef class ExternalApiMixin: f"{response.status_code}: {response.text}" ) raise ValidationError( - f"External API call failed for field " f"'{data_key}'." + f"External API call failed for field '{data_key}'." ) return fallback diff --git a/flask_inputfilter/Mixin/__init__.py b/flask_inputfilter/Mixin/__init__.py index a16d856..f58184b 100644 --- a/flask_inputfilter/Mixin/__init__.py +++ b/flask_inputfilter/Mixin/__init__.py @@ -5,3 +5,4 @@ else: from .ExternalApiMixin import ExternalApiMixin + from .FieldMixin import FieldMixin diff --git a/flask_inputfilter/Model/FieldModel.py b/flask_inputfilter/Model/FieldModel.py index e5117d4..55bbe95 100644 --- a/flask_inputfilter/Model/FieldModel.py +++ b/flask_inputfilter/Model/FieldModel.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass, field from typing import Any, List, Optional, Union from flask_inputfilter.Filter import BaseFilter @@ -8,17 +7,27 @@ from flask_inputfilter.Validator import BaseValidator -@dataclass class FieldModel: """ FieldModel is a dataclass that represents a field in the input data. """ - required: bool = False - default: Any = None - fallback: Any = None - filters: List[BaseFilter] = field(default_factory=list) - validators: List[BaseValidator] = field(default_factory=list) - steps: List[Union[BaseFilter, BaseValidator]] = field(default_factory=list) - external_api: Optional[ExternalApiConfig] = None - copy: Optional[str] = None + def __init__( + self, + required: bool = False, + default: Any = None, + fallback: Any = None, + filters: Optional[List[BaseFilter]] = None, + validators: Optional[List[BaseValidator]] = None, + steps: Optional[List[Union[BaseFilter, BaseValidator]]] = None, + external_api: Optional[ExternalApiConfig] = None, + copy: Optional[str] = None, + ): + self.required = required + self.default = default + self.fallback = fallback + self.filters = filters or [] + self.validators = validators or [] + self.steps = steps or [] + self.external_api = external_api + self.copy = copy diff --git a/flask_inputfilter/Model/_FieldModel.pyx b/flask_inputfilter/Model/_FieldModel.pyx new file mode 100644 index 0000000..c00cf57 --- /dev/null +++ b/flask_inputfilter/Model/_FieldModel.pyx @@ -0,0 +1,56 @@ +# cython: language=c++ +# cython: language_level=3 +# cython: binding=True +# cython: cdivision=True +# cython: boundscheck=False +# cython: initializedcheck=False +from __future__ import annotations + +from typing import Any, List, Optional, Union + +from flask_inputfilter.Filter import BaseFilter +from flask_inputfilter.Model import ExternalApiConfig +from flask_inputfilter.Validator import BaseValidator + + +cdef class FieldModel: + """ + FieldModel is a dataclass that represents a field in the input data. + """ + + cdef public bint required + cdef public object _default + cdef public object fallback + cdef public list filters + cdef public list validators + cdef public list steps + cdef public object external_api + cdef public str copy + + @property + def default(self): + return self._default + + @default.setter + def default(self, value): + self._default = value + + def __init__( + self, + required: bool = False, + default: Any = None, + fallback: Any = None, + filters: List[BaseFilter] = None, + validators: List[BaseValidator] = None, + steps: List[Union[BaseFilter, BaseValidator]] = None, + external_api: Optional[ExternalApiConfig] = None, + copy: Optional[str] = None + ) -> None: + self.required = required + self.default = default + self.fallback = fallback + self.filters = filters or [] + self.validators = validators or [] + self.steps = steps or [] + self.external_api = external_api + self.copy = copy diff --git a/flask_inputfilter/Model/__init__.py b/flask_inputfilter/Model/__init__.py index f2dcacd..9ffe898 100644 --- a/flask_inputfilter/Model/__init__.py +++ b/flask_inputfilter/Model/__init__.py @@ -1,2 +1,9 @@ +import shutil + from .ExternalApiConfig import ExternalApiConfig -from .FieldModel import FieldModel + +if shutil.which("g++") is not None: + from ._FieldModel import FieldModel + +else: + from .FieldModel import FieldModel diff --git a/flask_inputfilter/Validator/CustomJsonValidator.py b/flask_inputfilter/Validator/CustomJsonValidator.py index 79f7b35..4f12218 100644 --- a/flask_inputfilter/Validator/CustomJsonValidator.py +++ b/flask_inputfilter/Validator/CustomJsonValidator.py @@ -43,14 +43,15 @@ def validate(self, value: Any) -> bool: if field not in value: raise ValidationError(f"Missing required field '{field}'.") - if self.schema: - for field, expected_type in self.schema.items(): - if field in value: - if not isinstance(value[field], expected_type): - raise ValidationError( - self.error_message - or f"Field '{field}' has to be of type " - f"'{expected_type.__name__}'." - ) + if not self.schema: + return True + + for field, expected_type in self.schema.items(): + if field in value and not isinstance(value[field], expected_type): + raise ValidationError( + self.error_message + or f"Field '{field}' has to be of type " + f"'{expected_type.__name__}'." + ) return True diff --git a/flask_inputfilter/Validator/IsBase64ImageValidator.py b/flask_inputfilter/Validator/IsBase64ImageValidator.py index 6cb626d..4844be1 100644 --- a/flask_inputfilter/Validator/IsBase64ImageValidator.py +++ b/flask_inputfilter/Validator/IsBase64ImageValidator.py @@ -5,7 +5,7 @@ import io from typing import Any, Optional -from PIL import Image, UnidentifiedImageError +from PIL import Image from flask_inputfilter.Exception import ValidationError from flask_inputfilter.Validator import BaseValidator @@ -28,7 +28,7 @@ def validate(self, value: Any) -> None: try: Image.open(io.BytesIO(base64.b64decode(value))).verify() - except (binascii.Error, UnidentifiedImageError, OSError): + except (binascii.Error, OSError): raise ValidationError( self.error_message or "The image is invalid or does not have an allowed size." diff --git a/flask_inputfilter/Validator/IsHorizontalImageValidator.py b/flask_inputfilter/Validator/IsHorizontalImageValidator.py index 8581dc4..f05fff8 100644 --- a/flask_inputfilter/Validator/IsHorizontalImageValidator.py +++ b/flask_inputfilter/Validator/IsHorizontalImageValidator.py @@ -4,7 +4,7 @@ import binascii import io -from PIL import Image, UnidentifiedImageError +from PIL import Image from PIL.Image import Image as ImageType from flask_inputfilter.Exception import ValidationError @@ -39,7 +39,7 @@ def validate(self, value): if value.width < value.height: raise ValidationError - except (binascii.Error, UnidentifiedImageError, OSError): + except (binascii.Error, OSError): raise ValidationError( self.error_message or "The image is not horizontally oriented." ) diff --git a/flask_inputfilter/Validator/IsVerticalImageValidator.py b/flask_inputfilter/Validator/IsVerticalImageValidator.py index 9a87ea7..25d9b44 100644 --- a/flask_inputfilter/Validator/IsVerticalImageValidator.py +++ b/flask_inputfilter/Validator/IsVerticalImageValidator.py @@ -5,7 +5,7 @@ import io from typing import Any -from PIL import Image, UnidentifiedImageError +from PIL import Image from PIL.Image import Image as ImageType from flask_inputfilter.Exception import ValidationError @@ -40,7 +40,7 @@ def validate(self, value: Any) -> None: if value.width > value.height: raise ValidationError - except (binascii.Error, UnidentifiedImageError, OSError): + except (binascii.Error, OSError): raise ValidationError( self.error_message or "The image is not vertically oriented." ) diff --git a/flask_inputfilter/_InputFilter.pyx b/flask_inputfilter/_InputFilter.pyx index 53f71fc..ee0ab44 100644 --- a/flask_inputfilter/_InputFilter.pyx +++ b/flask_inputfilter/_InputFilter.pyx @@ -6,7 +6,7 @@ # cython: initializedcheck=False import json import logging -from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Dict, List, Optional, Type, TypeVar, Union from flask import Response, g, request @@ -17,6 +17,13 @@ from flask_inputfilter.Mixin import ExternalApiMixin from flask_inputfilter.Model import ExternalApiConfig, FieldModel from flask_inputfilter.Validator import BaseValidator +from libcpp.string cimport string +from libcpp.vector cimport vector + + +cdef extern from "helper.h": + vector[string] make_default_methods() + T = TypeVar("T") @@ -25,7 +32,7 @@ cdef class InputFilter: Base class for all input filters. """ - cdef readonly list methods + cdef readonly vector[string] methods cdef readonly dict fields cdef readonly list conditions cdef readonly list global_filters @@ -33,10 +40,10 @@ cdef class InputFilter: cdef readonly dict data cdef readonly dict validated_data cdef readonly dict errors - cdef readonly model_class + cdef readonly object model_class def __cinit__(self) -> None: - self.methods: List[str] = ["GET", "POST", "PATCH", "PUT", "DELETE"] + self.methods = make_default_methods() self.fields: Dict[str, FieldModel] = {} self.conditions: List[BaseCondition] = [] self.global_filters: List[BaseFilter] = [] @@ -44,11 +51,12 @@ cdef class InputFilter: self.data: Dict[str, Any] = {} self.validated_data: Dict[str, Any] = {} self.errors: Dict[str, str] = {} - self.model_class: Optional = None + self.model_class: Optional[Type[T]] = None def __init__(self, methods: Optional[List[str]] = None) -> None: if methods is not None: - self.methods: List[str] = methods + self.methods.clear() + [self.methods.push_back(method.encode()) for method in methods] cpdef bint isValid(self): """ @@ -84,8 +92,8 @@ cdef class InputFilter: Callable[ [Any], Callable[ - [Tuple[Any, ...], Dict[str, Any]], - Union[Response, Tuple[Any, Dict[str, Any]]], + [tuple[Any, ...], Dict[str, Any]], + Union[Response, tuple[Any, Dict[str, Any]]], ], ] """ @@ -99,7 +107,7 @@ cdef class InputFilter: f (Callable): The Flask route function to be decorated. Returns: - Callable[[Any, Any], Union[Response, Tuple[Any, Dict[str, Any]]]]: The wrapped function with input validation. + Callable[[Any, Any], Union[Response, tuple[Any, Dict[str, Any]]]]: The wrapped function with input validation. """ def wrapper( @@ -114,11 +122,12 @@ cdef class InputFilter: **kwargs: Keyword arguments for the route function. Returns: - Union[Response, Tuple[Any, Dict[str, Any]]]: The response from the route function or an error response. + Union[Response, tuple[Any, Dict[str, Any]]]: The response from the route function or an error response. """ cdef InputFilter input_filter = cls() - if request.method not in input_filter.methods: + cdef string request_method = request.method.encode() + if not any(request_method == method for method in input_filter.methods): return Response(status=405, response="Method Not Allowed") data = request.json if request.is_json else request.args @@ -190,8 +199,8 @@ cdef class InputFilter: required = field_info.required default = field_info.default fallback = field_info.fallback - filters = field_info.filters - validators = field_info.validators + filters = field_info.filters + self.global_filters + validators = field_info.validators + self.global_validators steps = field_info.steps external_api = field_info.external_api copy = field_info.copy @@ -206,16 +215,8 @@ cdef class InputFilter: ) value = self.applyFilters(filters, value) - value = ( - self.validateField( - validators, fallback, value - ) - or value - ) - value = ( - self.applySteps(steps, fallback, value) - or value - ) + value = self.validateField(validators, fallback, value) or value + value = self.applySteps(steps, fallback, value) or value value = InputFilter.checkForRequired( field_name, required, default, fallback, value ) @@ -226,7 +227,7 @@ cdef class InputFilter: errors[field_name] = str(e) try: - self.checkConditions(self.validated_data) + self.checkConditions(self.conditions, self.validated_data) except ValidationError as e: errors["_condition"] = str(e) @@ -261,7 +262,7 @@ cdef class InputFilter: """ return self.conditions - cdef void checkConditions(self, validated_data: Dict[str, Any]) except *: + cdef void checkConditions(self, conditions: List[BaseCondition], validated_data: Dict[str, Any]) except *: """ Checks if all conditions are met. @@ -271,10 +272,12 @@ cdef class InputFilter: message indicating which condition failed. Args: + conditions (List[BaseCondition]): + A list of conditions to be checked against the validated data. validated_data (Dict[str, Any]): The validated data to check against the conditions. """ - for condition in self.conditions: + for condition in conditions: if not condition.check(validated_data): raise ValidationError( f"Condition '{condition.__class__.__name__}' not met." @@ -296,7 +299,7 @@ cdef class InputFilter: for field_name, field_value in data.items(): if field_name in self.fields: field_value = self.applyFilters( - filters=self.fields[field_name].filters, + filters=self.fields[field_name].filters + self.global_filters, value=field_value, ) @@ -440,7 +443,7 @@ cdef class InputFilter: message is being retrieved. Returns: - str: A string representing the predefined error message. + Optional[str]: A string representing the predefined error message. """ return self.errors.get(field_name) @@ -500,7 +503,6 @@ cdef class InputFilter: from. """ if name in self.fields: - print(self.fields) raise ValueError(f"Field '{name}' already exists.") self.fields[name] = FieldModel( @@ -765,7 +767,7 @@ cdef class InputFilter: if value is None: return - for filter in self.global_filters + filters: + for filter in filters: value = filter.apply(value) return value @@ -897,7 +899,7 @@ cdef class InputFilter: return try: - for validator in self.global_validators + validators: + for validator in validators: validator.validate(value) except ValidationError: if fallback is None: diff --git a/flask_inputfilter/__init__.py b/flask_inputfilter/__init__.py index 99152da..a8f4b8e 100644 --- a/flask_inputfilter/__init__.py +++ b/flask_inputfilter/__init__.py @@ -1,20 +1,31 @@ -import logging -import shutil - try: from ._InputFilter import InputFilter except ImportError: + import shutil + if shutil.which("g++") is not None: - import logging + import os import pyximport - pyximport.install(setup_args={"script_args": ["--quiet"]}) + THIS_DIR = os.path.dirname(__file__) + INCLUDE_DIR = os.path.join(THIS_DIR, "include") + + pyximport.install( + language_level=3, + setup_args={ + "script_args": ["--quiet"], + "include_dirs": [INCLUDE_DIR], + }, + reload_support=True, + ) from ._InputFilter import InputFilter else: + import logging + logging.getLogger(__name__).warning( "Cython or g++ not available. Falling back to pure Python implementation.\n" "Consult docs for better performance: https://leandercs.github.io/flask-inputfilter/guides/compile.html" diff --git a/flask_inputfilter/include/helper.h b/flask_inputfilter/include/helper.h new file mode 100644 index 0000000..862a90a --- /dev/null +++ b/flask_inputfilter/include/helper.h @@ -0,0 +1,17 @@ +#ifndef HELPER_H +#define HELPER_H + +#include +#include + +inline std::vector make_default_methods() { + return { + "GET", + "POST", + "PATCH", + "PUT", + "DELETE" + }; +} + +#endif diff --git a/pyproject.toml b/pyproject.toml index f74c44a..d16c988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "flask_inputfilter" -version = "0.4.0" +version = "0.4.1" description = "A library to easily filter and validate input data in Flask applications" readme = "README.rst" requires-python = ">=3.7" @@ -49,19 +49,11 @@ dev = [ optional = [ "pillow>=8.0.0", "requests>=2.22.0", - "cython>=0.29; python_version <= '3.8'", - "cython>=0.29.21; python_version == '3.9'", - "cython>=0.29.24; python_version == '3.10'", - "cython>=0.29.32; python_version == '3.11'", - "cython>=3.0; python_version == '3.12'", + "cython>=3.0; python_version <= '3.12'", "cython>=3.0.12; python_version >= '3.13'", ] compile = [ - "cython>=0.29; python_version <= '3.8'", - "cython>=0.29.21; python_version == '3.9'", - "cython>=0.29.24; python_version == '3.10'", - "cython>=0.29.32; python_version == '3.11'", - "cython>=3.0; python_version == '3.12'", + "cython>=3.0; python_version <= '3.12'", "cython>=3.0.12; python_version >= '3.13'", ] diff --git a/setup.py b/setup.py index ba3a26d..3e121c5 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,20 @@ ext_modules = cythonize( [ "flask_inputfilter/Mixin/_ExternalApiMixin.pyx", + "flask_inputfilter/Model/_FieldModel.pyx", "flask_inputfilter/_InputFilter.pyx", ], language_level=3, ) + options = { + "build_ext": {"include_dirs": ["flask_inputfilter/include"]}, + } + else: ext_modules = [] + options = {} -setup(ext_modules=ext_modules) +setup( + ext_modules=ext_modules, + options=options, +) diff --git a/tests/test_filter.py b/tests/test_filter.py index 0744706..27ac6b0 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -448,7 +448,7 @@ def test_to_float_filter(self) -> None: self.assertEqual(validated_data["price"], 19.99) validated_data = self.inputFilter.validateData({"price": False}) - self.assertEqual(validated_data["price"], False) + self.assertFalse(validated_data["price"]) validated_data = self.inputFilter.validateData({"price": "no float"}) self.assertEqual(validated_data["price"], "no float") @@ -466,7 +466,7 @@ def test_to_integer_filter(self) -> None: self.assertEqual(validated_data["age"], 25) validated_data = self.inputFilter.validateData({"age": False}) - self.assertEqual(validated_data["age"], False) + self.assertFalse(validated_data["age"]) validated_data = self.inputFilter.validateData({"age": "no integer"}) self.assertEqual(validated_data["age"], "no integer") diff --git a/tests/test_input_filter.py b/tests/test_input_filter.py index 579ecaa..b943d45 100644 --- a/tests/test_input_filter.py +++ b/tests/test_input_filter.py @@ -14,7 +14,7 @@ ToLowerFilter, ToUpperFilter, ) -from flask_inputfilter.Model import ExternalApiConfig, FieldModel +from flask_inputfilter.Model import ExternalApiConfig from flask_inputfilter.Validator import ( InArrayValidator, IsIntegerValidator, @@ -185,10 +185,10 @@ def test_default(self) -> None: self.inputFilter.add("available", default=True) validated_data = self.inputFilter.validateData({}) - self.assertEqual(validated_data["available"], True) + self.assertTrue(validated_data["available"]) validated_data = self.inputFilter.validateData({"available": False}) - self.assertEqual(validated_data["available"], False) + self.assertFalse(validated_data["available"]) def test_fallback(self) -> None: """ @@ -204,14 +204,14 @@ def test_fallback(self) -> None: validated_data = self.inputFilter.validateData({"color": "yellow"}) - self.assertEqual(validated_data["available"], True) + self.assertTrue(validated_data["available"]) self.assertEqual(validated_data["color"], "red") validated_data = self.inputFilter.validateData( {"available": False, "color": "green"} ) - self.assertEqual(validated_data["available"], False) + self.assertFalse(validated_data["available"]) self.assertEqual(validated_data["color"], "green") def test_fallback_with_default(self) -> None: @@ -231,12 +231,12 @@ def test_fallback_with_default(self) -> None: validated_data = self.inputFilter.validateData({}) - self.assertEqual(validated_data["available"], False) + self.assertFalse(validated_data["available"]) self.assertEqual(validated_data["color"], "red") validated_data = self.inputFilter.validateData({"available": False}) - self.assertEqual(validated_data["available"], False) + self.assertFalse(validated_data["available"]) self.inputFilter.add("required_without_fallback", required=True) @@ -317,23 +317,24 @@ def test_get_input(self) -> None: self.inputFilter.add("field") self.inputFilter.setData({"field": "value"}) - self.assertEqual( - self.inputFilter.getInput("field"), - FieldModel(required=False, default=None), - ) + self.assertFalse(self.inputFilter.getInput("field").required) + + self.inputFilter.add("field2", required=True) + self.assertTrue(self.inputFilter.getInput("field2").required) def test_get_inputs(self) -> None: self.inputFilter.add("field1") - self.inputFilter.add("field2") + self.inputFilter.add("field2", required=True, default=True) self.inputFilter.setData({"field1": "value1", "field2": "value2"}) - self.assertEqual( - self.inputFilter.getInputs(), - { - "field1": FieldModel(required=False, default=None), - "field2": FieldModel(required=False, default=None), - }, - ) + field1 = self.inputFilter.getInputs().get("field1") + field2 = self.inputFilter.getInputs().get("field2") + + self.assertFalse(field1.required) + self.assertTrue(field2.required) + + self.assertIsNone(field1.default) + self.assertTrue(field2.default) def test_get_raw_value(self) -> None: self.inputFilter.add("field") @@ -458,12 +459,14 @@ def test_merge_overrides_field(self) -> None: self.inputFilter.add("field1") input_filter = InputFilter() - filter = ToIntegerFilter() - input_filter.add("field1", filters=[filter]) + filter_ = ToIntegerFilter() + input_filter.add("field1", filters=[filter_]) self.inputFilter.merge(input_filter) self.inputFilter.isValid() - self.assertEqual(self.inputFilter.getInput("field1").filters, [filter]) + self.assertEqual( + self.inputFilter.getInput("field1").filters, [filter_] + ) def test_merge_combined_conditions(self) -> None: condition1 = ExactlyOneOfCondition(["test"]) @@ -565,7 +568,7 @@ def test_clear(self) -> None: self.assertEqual(self.inputFilter.getValue("field"), "value") self.inputFilter.clear() - self.assertEqual(self.inputFilter.getValue("field"), None) + self.assertIsNone(self.inputFilter.getValue("field")) def test_set_unfiltered_data(self) -> None: self.inputFilter.add("field") @@ -590,15 +593,16 @@ def test_steps(self) -> None: ) self.assertEqual(validated_data["name_upper"], "maurice") + validated_data = None with self.assertRaises(ValidationError): validated_data = self.inputFilter.validateData( {"name_upper": "Alice"} ) - self.assertEqual(validated_data["name_upper"], "ALICE") + self.assertIsNone(validated_data) self.inputFilter.add( "fallback", - fallback="fallback", + fallback="FALLBACK", steps=[ ToUpperFilter(), InArrayValidator(["FALLBACK"]), @@ -613,7 +617,7 @@ def test_steps(self) -> None: self.inputFilter.add( "default", - default="default", + default="DEFAULT", steps=[ ToUpperFilter(), InArrayValidator(["DEFAULT"]), @@ -622,7 +626,7 @@ def test_steps(self) -> None: ) validated_data = self.inputFilter.validateData({}) - self.assertEqual(validated_data["default"], "default") + self.assertEqual(validated_data["default"], "DEFAULT") self.inputFilter.add( "fallback_with_default", @@ -682,7 +686,7 @@ def test_external_api(self, mock_request: Mock) -> None: # API returns valid result validated_data = self.inputFilter.validateData({}) - self.assertEqual(validated_data["is_valid"], True) + self.assertTrue(validated_data["is_valid"]) expected_url = "https://api.example.com/validate_user/test_user" mock_request.assert_called_with( headers={}, method="GET", url=expected_url, params={} @@ -725,7 +729,7 @@ def test_external_api_params(self, mock_request: Mock) -> None: {"name": "test_user", "hash": "1234"} ) - self.assertEqual(validated_data["is_valid"], True) + self.assertTrue(validated_data["is_valid"]) expected_url = "https://api.example.com/validate_user/test_user" mock_request.assert_called_with( headers={"Authorization": "Bearer 1234", "custom_header": "value"}, @@ -1035,7 +1039,7 @@ def test_custom_route(): response = client.get("/test-custom", query_string={"age": 20}) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json, None) + self.assertIsNone(response.json) if __name__ == "__main__":