diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c4baec6..a86b51c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,7 +49,7 @@ jobs: set -e # Exit immediately if a command exits with a non-zero status set -u # Exit immediately if a variable is not defined - docker run --rm flask-inputfilter-pure flake8 + docker run --rm flask-inputfilter-pure ruff check build-and-test-cython: runs-on: ubuntu-latest diff --git a/MANIFEST.in b/MANIFEST.in index 0fb0f71..4003c7e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,9 @@ include docs/source/index.rst include LICENSE include docs/source/changelog.rst -recursive-include flask_inputfilter *.py *.pyi *.pyx *.pxd *.c *.cpp -recursive-include flask_inputfilter/include *.h +recursive-include flask_inputfilter *.py *.pyi *.pyx *.pxd *.h + +recursive-exclude flask_inputfilter *.cpp + prune tests recursive-prune __pycache__ diff --git a/docs/source/conf.py b/docs/source/conf.py index 9d12b72..0986b34 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,5 @@ -import os import sys +from pathlib import Path project = "flask-inputfilter" copyright = "2025, Leander Cain Slotosch" @@ -31,7 +31,7 @@ napoleon_google_docstring = True napoleon_numpy_docstring = False -sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, str(Path("../..").resolve())) templates_path = [] exclude_patterns = [] diff --git a/docs/source/guides/create_own_components.rst b/docs/source/guides/create_own_components.rst index da9cb5f..e195d28 100644 --- a/docs/source/guides/create_own_components.rst +++ b/docs/source/guides/create_own_components.rst @@ -33,7 +33,7 @@ Example implementation from typing import Any - from flask_inputfilter.conditions import BaseCondition + from flask_inputfilter.models import BaseCondition class EqualCondition(BaseCondition): @@ -71,7 +71,7 @@ Example implementation from datetime import date, datetime from typing import Any - from flask_inputfilter.filters import BaseFilter + from flask_inputfilter.models import BaseFilter class ToDateTimeFilter(BaseFilter): @@ -118,7 +118,7 @@ Example implementation from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError - from flask_inputfilter.validators import BaseValidator + from flask_inputfilter.models import BaseValidator class InArrayValidator(BaseValidator): diff --git a/examples/example 1/README.md b/examples/basic/README.md similarity index 100% rename from examples/example 1/README.md rename to examples/basic/README.md diff --git a/examples/example 1/__init__.py b/examples/basic/__init__.py similarity index 100% rename from examples/example 1/__init__.py rename to examples/basic/__init__.py diff --git a/examples/example 1/app.py b/examples/basic/app.py similarity index 100% rename from examples/example 1/app.py rename to examples/basic/app.py diff --git a/examples/example 1/filters/__init__.py b/examples/basic/filters/__init__.py similarity index 66% rename from examples/example 1/filters/__init__.py rename to examples/basic/filters/__init__.py index ad3dc9e..95e0b0a 100644 --- a/examples/example 1/filters/__init__.py +++ b/examples/basic/filters/__init__.py @@ -1,3 +1,5 @@ from .product_inputfilter import ProductInputFilter from .profile_inputfilter import ProfileInputFilter from .user_inputfilter import UserInputFilter + +__all__ = ["ProductInputFilter", "ProfileInputFilter", "UserInputFilter"] diff --git a/examples/example 1/filters/product_inputfilter.py b/examples/basic/filters/product_inputfilter.py similarity index 100% rename from examples/example 1/filters/product_inputfilter.py rename to examples/basic/filters/product_inputfilter.py diff --git a/examples/example 1/filters/profile_inputfilter.py b/examples/basic/filters/profile_inputfilter.py similarity index 100% rename from examples/example 1/filters/profile_inputfilter.py rename to examples/basic/filters/profile_inputfilter.py diff --git a/examples/example 1/filters/user_inputfilter.py b/examples/basic/filters/user_inputfilter.py similarity index 100% rename from examples/example 1/filters/user_inputfilter.py rename to examples/basic/filters/user_inputfilter.py diff --git a/examples/example 1/test.http b/examples/basic/test.http similarity index 100% rename from examples/example 1/test.http rename to examples/basic/test.http diff --git a/flask_inputfilter/__init__.py b/flask_inputfilter/__init__.py index 188cf6e..64f7fd1 100644 --- a/flask_inputfilter/__init__.py +++ b/flask_inputfilter/__init__.py @@ -3,31 +3,47 @@ except ImportError: import shutil + from pathlib import Path - if shutil.which("g++") is not None: - import os + _HAS_GPP = shutil.which("g++") is not None - import pyximport + if _HAS_GPP: + try: + import pyximport - THIS_DIR = os.path.dirname(__file__) - INCLUDE_DIR = os.path.join(THIS_DIR, "include") + THIS_DIR = Path(__file__).parent + INCLUDE_DIR = THIS_DIR / "include" - pyximport.install( - language_level=3, - setup_args={ - "script_args": ["--quiet"], - "include_dirs": [INCLUDE_DIR], - }, - reload_support=True, - ) + pyximport.install( + language_level=3, + setup_args={ + "script_args": ["--quiet"], + "include_dirs": [str(INCLUDE_DIR)], + }, + reload_support=True, + ) + + from ._input_filter import InputFilter - from ._input_filter import InputFilter + except ImportError: + import logging + + logging.getLogger(__name__).debug( + "Pyximport failed, falling back to pure Python implementation." + ) + from .input_filter 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" + "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" ) from .input_filter import InputFilter + +__all__ = [ + "InputFilter", +] diff --git a/flask_inputfilter/_input_filter.pxd b/flask_inputfilter/_input_filter.pxd new file mode 100644 index 0000000..5ae595c --- /dev/null +++ b/flask_inputfilter/_input_filter.pxd @@ -0,0 +1,76 @@ +# cython: language=c++ +from typing import Any + +from flask_inputfilter.models.cimports cimport BaseCondition, BaseFilter, BaseValidator, ExternalApiConfig, FieldModel + +from libcpp.vector cimport vector +from libcpp.string cimport string + + +cdef extern from "helper.h": + vector[string] make_default_methods() + + +cdef class InputFilter: + cdef readonly: + vector[string] methods + dict[str, FieldModel] fields + list[BaseCondition] conditions + list[BaseFilter] global_filters + list[BaseValidator] global_validators + dict[str, Any] data + dict[str, Any] validated_data + dict[str, str] errors + object model_class + + cpdef bint is_valid(self) + cpdef object validate_data(self, dict data=*) + cpdef void add_condition(self, BaseCondition condition) + cpdef list get_conditions(self) + cpdef void set_data(self, dict data) + cpdef object get_value(self, str name) + cpdef dict get_values(self) + cpdef object get_raw_value(self, str name) + cpdef dict get_raw_values(self) + cpdef dict get_unfiltered_data(self) + cpdef void set_unfiltered_data(self, dict data) + cpdef bint has_unknown(self) + cpdef str get_error_message(self, str field_name) + cpdef dict get_error_messages(self) + cpdef void add( + self, + str name, + bint required=*, + object default=*, + object fallback=*, + list filters=*, + list validators=*, + list steps=*, + ExternalApiConfig external_api=*, + str copy=*, + ) except* + cpdef bint has(self, str field_name) + cpdef FieldModel get_input(self, str field_name) + cpdef dict get_inputs(self) + cpdef object remove(self, str field_name) + cpdef int count(self) + cpdef void replace( + self, + str name, + bint required=*, + object default=*, + object fallback=*, + list[BaseFilter] filters=*, + list[BaseValidator] validators=*, + list steps=*, + ExternalApiConfig external_api=*, + str copy=*, + ) + cpdef void add_global_filter(self, BaseFilter filter) + cpdef list get_global_filters(self) + cpdef void clear(self) + cpdef void merge(self, InputFilter other) + cpdef void set_model(self, object model_class) + cpdef object serialize(self) + cpdef void add_global_validator(self, BaseValidator validator) + cpdef list[BaseValidator] get_global_validators(self) diff --git a/flask_inputfilter/_input_filter.pyx b/flask_inputfilter/_input_filter.pyx index c74057e..91e963f 100644 --- a/flask_inputfilter/_input_filter.pyx +++ b/flask_inputfilter/_input_filter.pyx @@ -1,21 +1,37 @@ # cython: language=c++ + import json import logging +import sys from typing import Any, Optional, Type, TypeVar, Union from flask import Response, g, request -from flask_inputfilter.conditions import BaseCondition from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.mixins._external_api_mixin cimport ExternalApiMixin -from flask_inputfilter.mixins._field_mixin cimport FieldMixin -from flask_inputfilter.models import ExternalApiConfig, FieldModel -from flask_inputfilter.validators import BaseValidator -from libcpp.string cimport string +from flask_inputfilter.mixins.cimports cimport DataMixin +from flask_inputfilter.models.cimports cimport BaseCondition, BaseFilter, BaseValidator, ExternalApiConfig, FieldModel + from libcpp.vector cimport vector +from libcpp.string cimport string +cdef dict _INTERNED_STRINGS = { + "_condition": sys.intern("_condition"), + "_error": sys.intern("_error"), + "copy": sys.intern("copy"), + "default": sys.intern("default"), + "DELETE": sys.intern("DELETE"), + "external_api": sys.intern("external_api"), + "fallback": sys.intern("fallback"), + "filters": sys.intern("filters"), + "GET": sys.intern("GET"), + "PATCH": sys.intern("PATCH"), + "POST": sys.intern("POST"), + "PUT": sys.intern("PUT"), + "required": sys.intern("required"), + "steps": sys.intern("steps"), + "validators": sys.intern("validators"), +} cdef extern from "helper.h": vector[string] make_default_methods() @@ -28,25 +44,15 @@ cdef class InputFilter: Base class for all input filters. """ - cdef readonly vector[string] methods - cdef readonly dict fields - cdef readonly list conditions - cdef readonly list global_filters - cdef readonly list global_validators - cdef readonly dict data - cdef readonly dict validated_data - cdef readonly dict errors - cdef readonly object model_class - def __cinit__(self) -> None: self.methods = make_default_methods() - self.fields: dict[str, FieldModel] = {} - self.conditions: list[BaseCondition] = [] - self.global_filters: list[BaseFilter] = [] - self.global_validators: list[BaseValidator] = [] - self.data: dict[str, Any] = {} - self.validated_data: dict[str, Any] = {} - self.errors: dict[str, str] = {} + self.fields = {} + self.conditions = [] + self.global_filters = [] + self.global_validators = [] + self.data = {} + self.validated_data = {} + self.errors = {} self.model_class: Optional[Type[T]] = None def __init__(self, methods: Optional[list[str]] = None) -> None: @@ -158,126 +164,36 @@ cdef class InputFilter: return decorator cpdef object validate_data( - self, data: Optional[dict[str, Any]] = None + self, dict[str, Any] data=None ): """ - Validates input data against defined field rules, including applying - filters, validators, custom logic steps, and fallback mechanisms. The - validation process also ensures the required fields are handled - appropriately and conditions are checked after processing. + Validates input data against defined field rules. Args: - data (dict[str, Any]): A dictionary containing the input data to - be validated where keys represent field names and values - represent the corresponding data. + data: Input data dictionary to validate. Returns: - Union[dict[str, Any], Type[T]]: A dictionary containing the validated data with - any modifications, default values, or processed values as - per the defined validation rules. + Validated data dictionary or model instance. Raises: - Any errors raised during external API calls, validation, or - logical steps execution of the respective fields or conditions - will propagate without explicit handling here. + ValidationError: If validation fails. """ data = data or self.data - cdef dict errors = {} - cdef dict validated_data = {} - - cdef: - list global_filters = self.global_filters - list global_validators = self.global_validators - bint has_global_filters = bool(global_filters) - bint has_global_validators = bool(global_validators) - - cdef: - int i = 0 - int n = len(self.fields) - list field_names = list(self.fields.keys()) - list field_infos = list(self.fields.values()) - cdef: - object default - object fallback - list filters - list validators - object external_api - str copy + dict[str, str] errors + dict[str, Any] validated_data - for i in range(n): - field_name = field_names[i] - field_info = field_infos[i] - try: - if field_info.copy: - value = validated_data.get(field_info.copy) - elif field_info.external_api: - value = ExternalApiMixin.call_external_api( - field_info.external_api, - field_info.fallback, - validated_data, - ) - else: - value = data.get(field_name) - - if field_info.filters or has_global_filters: - value = FieldMixin.apply_filters( - field_info.filters + global_filters - if has_global_filters - else field_info.filters, - value - ) - - if field_info.validators or has_global_validators: - value = FieldMixin.validate_field( - field_info.validators + global_validators - if has_global_validators - else field_info.validators, - field_info.fallback, - value - ) or value - - if field_info.steps: - value = FieldMixin.apply_steps( - field_info.steps, - field_info.fallback, value - ) or value - - if value is None: - if field_info.required: - if field_info.fallback is not None: - value = field_info.fallback - elif field_info.default is not None: - value = field_info.default - else: - raise ValidationError( - f"Field '{field_name}' is required." - ) - elif field_info.default is not None: - value = field_info.default - - validated_data[field_name] = value - - except ValidationError as e: - errors[field_name] = str(e) - - if self.conditions: - try: - FieldMixin.check_conditions(self.conditions, validated_data) - except ValidationError as e: - errors["_condition"] = str(e) + validated_data, errors = DataMixin.validate_with_conditions( + self.fields, data, self.global_filters, self.global_validators, self.conditions + ) if errors: raise ValidationError(errors) self.validated_data = validated_data + return self.serialize() - if self.model_class is not None: - return self.model_class(**validated_data) - - return validated_data - - cpdef void add_condition(self, condition: BaseCondition): + cpdef void add_condition(self, BaseCondition condition): """ Add a condition to the input filter. @@ -300,7 +216,7 @@ cdef class InputFilter: """ return self.conditions - cpdef void set_data(self, data: dict[str, Any]): + cpdef void set_data(self, dict[str, Any] data): """ Filters and sets the provided data into the object's internal storage, ensuring that only the specified fields are considered and @@ -312,25 +228,9 @@ cdef class InputFilter: represent field names and values represent the associated data to be filtered and stored. """ - self.data = {} - cdef: - int i = 0 - int n = len(data) - list keys = list(data.keys()) - list values = list(data.values()) - - for i in range(n): - field_name = keys[i] - field_value = values[i] - if field_name in self.fields: - field_value = FieldMixin.apply_filters( - filters=self.fields[field_name].filters + self.global_filters, - value=field_value, - ) + self.data = DataMixin.filter_data(data, self.fields, self.global_filters) - self.data[field_name] = field_value - - cpdef object get_value(self, name: str): + cpdef object get_value(self, str name): """ This method retrieves a value associated with the provided name. It searches for the value based on the given identifier and returns the @@ -351,7 +251,7 @@ cdef class InputFilter: """ return self.validated_data.get(name) - cpdef dict get_values(self): + cpdef dict[str, Any] get_values(self): """ Retrieves a dictionary of key-value pairs from the current object. This method provides access to the internal state or configuration of @@ -364,7 +264,7 @@ cdef class InputFilter: """ return self.validated_data - cpdef object get_raw_value(self, name: str): + cpdef object get_raw_value(self, str name): """ Fetches the raw value associated with the provided key. @@ -381,9 +281,9 @@ cdef class InputFilter: 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) - cpdef dict get_raw_values(self): + cpdef dict[str, Any] get_raw_values(self): """ Retrieves raw values from a given source and returns them as a dictionary. @@ -402,10 +302,10 @@ cdef class InputFilter: return {} cdef: - int i = 0 - int n = len(self.fields) + Py_ssize_t i, n = len(self.fields) dict result = {} list field_names = list(self.fields.keys()) + str field for i in range(n): field = field_names[i] @@ -413,7 +313,7 @@ cdef class InputFilter: result[field] = self.data[field] return result - cpdef dict get_unfiltered_data(self): + cpdef dict[str, Any] get_unfiltered_data(self): """ Fetches unfiltered data from the data source. @@ -430,16 +330,16 @@ cdef class InputFilter: """ return self.data - cpdef void set_unfiltered_data(self, data: dict[str, Any]): + cpdef void set_unfiltered_data(self, dict[str, Any] data): """ Sets unfiltered data for the current instance. This method assigns a given dictionary of data to the instance for further processing. It updates the internal state using the provided data. **Parameters**: - - - data (dict[str, Any]): A dictionary containing the unfiltered - data to be associated with the instance. + + - data (dict[str, Any]): A dictionary containing the unfiltered + data to be associated with the instance. """ self.data = data @@ -451,16 +351,9 @@ cdef class InputFilter: Returns: bool: True if there are any unknown fields; False otherwise. """ - if not self.data and self.fields: - return True - - for field_name in self.data.keys(): - if field_name not in self.fields: - return True - - return False + return DataMixin.has_unknown_fields(self.data, self.fields) - cpdef str get_error_message(self, field_name: str): + cpdef str get_error_message(self, str field_name): """ Retrieves and returns a predefined error message. @@ -480,7 +373,7 @@ cdef class InputFilter: """ return self.errors.get(field_name) - cpdef dict get_error_messages(self): + cpdef dict[str, str] get_error_messages(self): """ Retrieves all error messages associated with the fields in the input filter. @@ -497,15 +390,15 @@ cdef class InputFilter: cpdef void add( self, - name: str, - 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, + str name, + bint required=False, + object default=None, + object fallback=None, + list[BaseFilter] filters=None, + list[BaseValidator] validators=None, + list[BaseFilter | BaseValidator] steps=None, + ExternalApiConfig external_api=None, + str copy=None, ) except *: """ Add the field to the input filter. @@ -526,7 +419,7 @@ cdef class InputFilter: validators (Optional[list[BaseValidator]]): The validators to apply to the field value. - steps (Optional[list[Union[BaseFilter, BaseValidator]]]): Allows + steps (Optional[list[BaseFilter | BaseValidator]]): Allows to apply multiple filters and validators in a specific order. external_api (Optional[ExternalApiConfig]): Configuration for an @@ -539,17 +432,17 @@ cdef class InputFilter: raise ValueError(f"Field '{name}' already exists.") self.fields[name] = FieldModel( - required=required, - default=default, - fallback=fallback, - filters=filters or [], - validators=validators or [], - steps=steps or [], - external_api=external_api, - copy=copy, + required, + default, + fallback, + filters or [], + validators or [], + steps or [], + external_api, + copy, ) - cpdef bint has(self, field_name: str): + cpdef bint has(self, str field_name): """ This method checks the existence of a specific field within the input filter values, identified by its field name. It does not return a @@ -564,7 +457,7 @@ cdef class InputFilter: """ return field_name in self.fields - cpdef object get_input(self, field_name: str): + cpdef FieldModel get_input(self, str field_name): """ Represents a method to retrieve a field by its name. @@ -583,7 +476,7 @@ cdef class InputFilter: """ return self.fields.get(field_name) - cpdef dict get_inputs(self): + cpdef dict[str, FieldModel] get_inputs(self): """ Retrieve the dictionary of input fields associated with the object. @@ -625,15 +518,15 @@ cdef class InputFilter: cpdef void replace( self, - name: str, - 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, + str name, + bint required=False, + object default=None, + object fallback=None, + list[BaseFilter] filters=None, + list[BaseValidator] validators=None, + list[BaseFilter | BaseValidator] steps=None, + ExternalApiConfig external_api=None, + str copy=None, ): """ Replaces a field in the input filter. @@ -654,7 +547,7 @@ cdef class InputFilter: validators (Optional[list[BaseValidator]]): The validators to apply to the field value. - steps (Optional[list[Union[BaseFilter, BaseValidator]]]): Allows + steps (Optional[list[BaseFilter | BaseValidator]]): Allows to apply multiple filters and validators in a specific order. external_api (Optional[ExternalApiConfig]): Configuration for an @@ -664,17 +557,17 @@ cdef class InputFilter: from. """ self.fields[name] = FieldModel( - required=required, - default=default, - fallback=fallback, - filters=filters or [], - validators=validators or [], - steps=steps or [], - external_api=external_api, - copy=copy, + required, + default, + fallback, + filters or [], + validators or [], + steps or [], + external_api, + copy, ) - cpdef void add_global_filter(self, filter: BaseFilter): + cpdef void add_global_filter(self, BaseFilter filter): """ Add a global filter to be applied to all fields. @@ -683,7 +576,7 @@ cdef class InputFilter: """ self.global_filters.append(filter) - cpdef list get_global_filters(self): + cpdef list[BaseFilter] get_global_filters(self): """ Retrieve all global filters associated with this InputFilter instance. @@ -713,7 +606,7 @@ cdef class InputFilter: self.validated_data.clear() self.errors.clear() - cpdef void merge(self, other: InputFilter): + cpdef void merge(self, InputFilter other): """ Merges another InputFilter instance intelligently into the current instance. @@ -731,35 +624,7 @@ cdef class InputFilter: "Can only merge with another InputFilter instance." ) - cdef: - int i = 0 - int n = len(other.get_inputs()) - list keys = list(other.get_inputs().keys()) - list new_fields = list(other.get_inputs().values()) - - for i in range(n): - self.fields[keys[i]] = new_fields[i] - self.conditions += other.conditions - - for filter in other.global_filters: - existing_type_map = { - type(v): i for i, v in enumerate(self.global_filters) - } - if type(filter) in existing_type_map: - self.global_filters[existing_type_map[type(filter)]] = filter - else: - self.global_filters.append(filter) - - for validator in other.global_validators: - existing_type_map = { - type(v): i for i, v in enumerate(self.global_validators) - } - if type(validator) in existing_type_map: - self.global_validators[ - existing_type_map[type(validator)] - ] = validator - else: - self.global_validators.append(validator) + DataMixin.merge_input_filters(self, other) cpdef void set_model(self, model_class: Type[T]): """ @@ -784,7 +649,7 @@ cdef class InputFilter: return self.model_class(**self.validated_data) - cpdef void add_global_validator(self, validator: BaseValidator): + cpdef void add_global_validator(self, BaseValidator validator): """ Add a global validator to be applied to all fields. @@ -793,7 +658,7 @@ cdef class InputFilter: """ self.global_validators.append(validator) - cpdef list get_global_validators(self): + cpdef list[BaseValidator] get_global_validators(self): """ Retrieve all global validators associated with this InputFilter instance. diff --git a/flask_inputfilter/conditions/__init__.py b/flask_inputfilter/conditions/__init__.py index df676af..bec1741 100644 --- a/flask_inputfilter/conditions/__init__.py +++ b/flask_inputfilter/conditions/__init__.py @@ -1,4 +1,4 @@ -from flask_inputfilter.conditions.base_condition import BaseCondition +from flask_inputfilter.models import BaseCondition from .array_length_equal_condition import ArrayLengthEqualCondition from .array_longer_than_condition import ArrayLongerThanCondition diff --git a/flask_inputfilter/conditions/array_length_equal_condition.py b/flask_inputfilter/conditions/array_length_equal_condition.py index 7ec1987..e94ee87 100644 --- a/flask_inputfilter/conditions/array_length_equal_condition.py +++ b/flask_inputfilter/conditions/array_length_equal_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ArrayLengthEqualCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/array_longer_than_condition.py b/flask_inputfilter/conditions/array_longer_than_condition.py index a3dcf0e..e4c18e2 100644 --- a/flask_inputfilter/conditions/array_longer_than_condition.py +++ b/flask_inputfilter/conditions/array_longer_than_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ArrayLongerThanCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/custom_condition.py b/flask_inputfilter/conditions/custom_condition.py index d52d507..bcfd407 100644 --- a/flask_inputfilter/conditions/custom_condition.py +++ b/flask_inputfilter/conditions/custom_condition.py @@ -1,9 +1,11 @@ from __future__ import annotations -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition + +if TYPE_CHECKING: + from collections.abc import Callable class CustomCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/equal_condition.py b/flask_inputfilter/conditions/equal_condition.py index 923249a..a4f44c2 100644 --- a/flask_inputfilter/conditions/equal_condition.py +++ b/flask_inputfilter/conditions/equal_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class EqualCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/exactly_n_of_condition.py b/flask_inputfilter/conditions/exactly_n_of_condition.py index d0b8f1c..60b8e9c 100644 --- a/flask_inputfilter/conditions/exactly_n_of_condition.py +++ b/flask_inputfilter/conditions/exactly_n_of_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ExactlyNOfCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/exactly_n_of_matches_condition.py b/flask_inputfilter/conditions/exactly_n_of_matches_condition.py index f17cf46..b597b5b 100644 --- a/flask_inputfilter/conditions/exactly_n_of_matches_condition.py +++ b/flask_inputfilter/conditions/exactly_n_of_matches_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ExactlyNOfMatchesCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/exactly_one_of_condition.py b/flask_inputfilter/conditions/exactly_one_of_condition.py index c66b870..dbd6703 100644 --- a/flask_inputfilter/conditions/exactly_one_of_condition.py +++ b/flask_inputfilter/conditions/exactly_one_of_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ExactlyOneOfCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/exactly_one_of_matches_condition.py b/flask_inputfilter/conditions/exactly_one_of_matches_condition.py index 7ab3d45..4cb2921 100644 --- a/flask_inputfilter/conditions/exactly_one_of_matches_condition.py +++ b/flask_inputfilter/conditions/exactly_one_of_matches_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class ExactlyOneOfMatchesCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/integer_bigger_than_condition.py b/flask_inputfilter/conditions/integer_bigger_than_condition.py index cc00a49..f093859 100644 --- a/flask_inputfilter/conditions/integer_bigger_than_condition.py +++ b/flask_inputfilter/conditions/integer_bigger_than_condition.py @@ -1,6 +1,6 @@ from __future__ import annotations -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class IntegerBiggerThanCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/n_of_condition.py b/flask_inputfilter/conditions/n_of_condition.py index b6d2794..e88a472 100644 --- a/flask_inputfilter/conditions/n_of_condition.py +++ b/flask_inputfilter/conditions/n_of_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class NOfCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/n_of_matches_condition.py b/flask_inputfilter/conditions/n_of_matches_condition.py index e3085ed..cabdffe 100644 --- a/flask_inputfilter/conditions/n_of_matches_condition.py +++ b/flask_inputfilter/conditions/n_of_matches_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class NOfMatchesCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/not_equal_condition.py b/flask_inputfilter/conditions/not_equal_condition.py index 764dc82..015cf22 100644 --- a/flask_inputfilter/conditions/not_equal_condition.py +++ b/flask_inputfilter/conditions/not_equal_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class NotEqualCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/one_of_condition.py b/flask_inputfilter/conditions/one_of_condition.py index d0634bf..183ef85 100644 --- a/flask_inputfilter/conditions/one_of_condition.py +++ b/flask_inputfilter/conditions/one_of_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class OneOfCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/one_of_matches_condition.py b/flask_inputfilter/conditions/one_of_matches_condition.py index c8d6b96..7984bff 100644 --- a/flask_inputfilter/conditions/one_of_matches_condition.py +++ b/flask_inputfilter/conditions/one_of_matches_condition.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class OneOfMatchesCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/required_if_condition.py b/flask_inputfilter/conditions/required_if_condition.py index 6c91f14..2558fa8 100644 --- a/flask_inputfilter/conditions/required_if_condition.py +++ b/flask_inputfilter/conditions/required_if_condition.py @@ -2,7 +2,7 @@ from typing import Any, Optional, Union -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class RequiredIfCondition(BaseCondition): @@ -48,7 +48,7 @@ def __init__(self): ) """ - __slots__ = ("condition_field", "value", "required_field") + __slots__ = ("condition_field", "required_field", "value") def __init__( self, diff --git a/flask_inputfilter/conditions/string_longer_than_condition.py b/flask_inputfilter/conditions/string_longer_than_condition.py index 930f6ba..14f436e 100644 --- a/flask_inputfilter/conditions/string_longer_than_condition.py +++ b/flask_inputfilter/conditions/string_longer_than_condition.py @@ -1,6 +1,6 @@ from __future__ import annotations -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class StringLongerThanCondition(BaseCondition): diff --git a/flask_inputfilter/conditions/temporal_order_condition.py b/flask_inputfilter/conditions/temporal_order_condition.py index 1e34506..e14137a 100644 --- a/flask_inputfilter/conditions/temporal_order_condition.py +++ b/flask_inputfilter/conditions/temporal_order_condition.py @@ -2,8 +2,8 @@ from typing import Any -from flask_inputfilter.conditions import BaseCondition from flask_inputfilter.helpers import parse_date +from flask_inputfilter.models import BaseCondition class TemporalOrderCondition(BaseCondition): @@ -46,7 +46,7 @@ def __init__(self): ) """ - __slots__ = ("smaller_date_field", "larger_date_field") + __slots__ = ("larger_date_field", "smaller_date_field") def __init__( self, smaller_date_field: str, larger_date_field: str diff --git a/flask_inputfilter/filters/__init__.py b/flask_inputfilter/filters/__init__.py index 05e75cf..43dbbe8 100644 --- a/flask_inputfilter/filters/__init__.py +++ b/flask_inputfilter/filters/__init__.py @@ -1,4 +1,4 @@ -from flask_inputfilter.filters.base_filter import BaseFilter +from flask_inputfilter.models import BaseFilter from .array_element_filter import ArrayElementFilter from .array_explode_filter import ArrayExplodeFilter diff --git a/flask_inputfilter/filters/array_element_filter.py b/flask_inputfilter/filters/array_element_filter.py index 66c3f92..83d04df 100644 --- a/flask_inputfilter/filters/array_element_filter.py +++ b/flask_inputfilter/filters/array_element_filter.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Any, Optional, Union +from typing import Any, Union -from flask_inputfilter.filters.base_filter import BaseFilter +from flask_inputfilter.models import BaseFilter class ArrayElementFilter(BaseFilter): @@ -38,10 +38,8 @@ def __init__(self): def __init__( self, element_filter: Union[BaseFilter, list[BaseFilter]], - error_message: Optional[str] = None, ) -> None: self.element_filter = element_filter - self.error_message = error_message def apply(self, value: Any) -> list[Any]: if not isinstance(value, list): @@ -53,7 +51,7 @@ def apply(self, value: Any) -> list[Any]: result.append(self.element_filter.apply(element)) continue - elif isinstance(self.element_filter, list) and all( + if isinstance(self.element_filter, list) and all( isinstance(v, BaseFilter) for v in self.element_filter ): for filter_instance in self.element_filter: diff --git a/flask_inputfilter/filters/array_explode_filter.py b/flask_inputfilter/filters/array_explode_filter.py index 399241e..57e5467 100644 --- a/flask_inputfilter/filters/array_explode_filter.py +++ b/flask_inputfilter/filters/array_explode_filter.py @@ -2,7 +2,7 @@ from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ArrayExplodeFilter(BaseFilter): diff --git a/flask_inputfilter/filters/base_64_image_downscale_filter.py b/flask_inputfilter/filters/base_64_image_downscale_filter.py index cdf90db..d30d65b 100644 --- a/flask_inputfilter/filters/base_64_image_downscale_filter.py +++ b/flask_inputfilter/filters/base_64_image_downscale_filter.py @@ -6,7 +6,7 @@ from PIL import Image -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class Base64ImageDownscaleFilter(BaseFilter): @@ -46,7 +46,7 @@ def __init__(self): ]) """ - __slots__ = ("width", "height", "proportionally") + __slots__ = ("height", "proportionally", "width") def __init__( self, diff --git a/flask_inputfilter/filters/base_64_image_resize_filter.py b/flask_inputfilter/filters/base_64_image_resize_filter.py index 1a72c9d..ada2c5e 100644 --- a/flask_inputfilter/filters/base_64_image_resize_filter.py +++ b/flask_inputfilter/filters/base_64_image_resize_filter.py @@ -7,7 +7,7 @@ from PIL import Image from flask_inputfilter.enums import ImageFormatEnum -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class Base64ImageResizeFilter(BaseFilter): @@ -47,8 +47,8 @@ def __init__(self): """ __slots__ = ( - "max_size", "format", + "max_size", "preserve_icc_profile", "preserve_metadata", ) diff --git a/flask_inputfilter/filters/blacklist_filter.py b/flask_inputfilter/filters/blacklist_filter.py index 4324d97..0721569 100644 --- a/flask_inputfilter/filters/blacklist_filter.py +++ b/flask_inputfilter/filters/blacklist_filter.py @@ -2,7 +2,7 @@ from typing import Any -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class BlacklistFilter(BaseFilter): @@ -45,10 +45,10 @@ def apply(self, value: Any) -> Any: value = value.replace(item, "") return value.strip() - elif isinstance(value, list): + if isinstance(value, list): return [item for item in value if item not in self.blacklist] - elif isinstance(value, dict): + if isinstance(value, dict): return { key: value for key, value in value.items() diff --git a/flask_inputfilter/filters/string_remove_emojis_filter.py b/flask_inputfilter/filters/string_remove_emojis_filter.py index bd5b8b6..ae6c637 100644 --- a/flask_inputfilter/filters/string_remove_emojis_filter.py +++ b/flask_inputfilter/filters/string_remove_emojis_filter.py @@ -3,16 +3,16 @@ import re from typing import Any, Optional, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter emoji_pattern = ( r"[" - "\U0001F600-\U0001F64F" - "\U0001F300-\U0001F5FF" - "\U0001F680-\U0001F6FF" - "\U0001F1E0-\U0001F1FF" - "\U00002702-\U000027B0" - "\U000024C2-\U0001F251" + "\U0001f600-\U0001f64f" + "\U0001f300-\U0001f5ff" + "\U0001f680-\U0001f6ff" + "\U0001f1e0-\U0001f1ff" + "\U00002702-\U000027b0" + "\U000024c2-\U0001f251" "]+" ) diff --git a/flask_inputfilter/filters/string_slugify_filter.py b/flask_inputfilter/filters/string_slugify_filter.py index 93c6d02..d857aea 100644 --- a/flask_inputfilter/filters/string_slugify_filter.py +++ b/flask_inputfilter/filters/string_slugify_filter.py @@ -5,7 +5,7 @@ from typing import Any, Optional, Union from flask_inputfilter.enums import UnicodeFormEnum -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class StringSlugifyFilter(BaseFilter): @@ -50,6 +50,4 @@ def apply(self, value: Any) -> Union[Optional[str], Any]: value = value.lower() value = re.sub(r"[^\w\s-]", "", value) - value = re.sub(r"[\s]+", "-", value) - - return value + return re.sub(r"[\s]+", "-", value) diff --git a/flask_inputfilter/filters/string_trim_filter.py b/flask_inputfilter/filters/string_trim_filter.py index 5264ad0..44f7f42 100644 --- a/flask_inputfilter/filters/string_trim_filter.py +++ b/flask_inputfilter/filters/string_trim_filter.py @@ -2,7 +2,7 @@ from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class StringTrimFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_alpha_numeric_filter.py b/flask_inputfilter/filters/to_alpha_numeric_filter.py index bda06f4..fca0ca6 100644 --- a/flask_inputfilter/filters/to_alpha_numeric_filter.py +++ b/flask_inputfilter/filters/to_alpha_numeric_filter.py @@ -3,7 +3,7 @@ import re from typing import Any, Optional, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToAlphaNumericFilter(BaseFilter): @@ -35,6 +35,4 @@ def apply(self, value: Any) -> Union[Optional[str], Any]: if not isinstance(value, str): return value - value = re.sub(r"[^\w]", "", value) - - return value + return re.sub(r"[^\w]", "", value) diff --git a/flask_inputfilter/filters/to_boolean_filter.py b/flask_inputfilter/filters/to_boolean_filter.py index 6428dc2..fc9a157 100644 --- a/flask_inputfilter/filters/to_boolean_filter.py +++ b/flask_inputfilter/filters/to_boolean_filter.py @@ -2,7 +2,7 @@ from typing import Any, Optional, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToBooleanFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_camel_case_filter.py b/flask_inputfilter/filters/to_camel_case_filter.py index c28377e..636e8e6 100644 --- a/flask_inputfilter/filters/to_camel_case_filter.py +++ b/flask_inputfilter/filters/to_camel_case_filter.py @@ -3,7 +3,7 @@ import re from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToCamelCaseFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_dataclass_filter.py b/flask_inputfilter/filters/to_dataclass_filter.py index f514857..db10b97 100644 --- a/flask_inputfilter/filters/to_dataclass_filter.py +++ b/flask_inputfilter/filters/to_dataclass_filter.py @@ -2,7 +2,7 @@ from typing import Any, Type, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToDataclassFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_date_filter.py b/flask_inputfilter/filters/to_date_filter.py index 984d8cf..5814bce 100644 --- a/flask_inputfilter/filters/to_date_filter.py +++ b/flask_inputfilter/filters/to_date_filter.py @@ -3,7 +3,7 @@ from datetime import date, datetime from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToDateFilter(BaseFilter): @@ -36,7 +36,7 @@ def apply(self, value: Any) -> Union[date, Any]: if isinstance(value, datetime): return value.date() - elif isinstance(value, str): + if isinstance(value, str): try: return datetime.fromisoformat(value).date() diff --git a/flask_inputfilter/filters/to_datetime_filter.py b/flask_inputfilter/filters/to_datetime_filter.py index 9ab3eb0..f0b333c 100644 --- a/flask_inputfilter/filters/to_datetime_filter.py +++ b/flask_inputfilter/filters/to_datetime_filter.py @@ -3,7 +3,7 @@ from datetime import date, datetime from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToDateTimeFilter(BaseFilter): @@ -38,10 +38,10 @@ def apply(self, value: Any) -> Union[datetime, Any]: if isinstance(value, datetime): return value - elif isinstance(value, date): + if isinstance(value, date): return datetime.combine(value, datetime.min.time()) - elif isinstance(value, str): + if isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/flask_inputfilter/filters/to_digits_filter.py b/flask_inputfilter/filters/to_digits_filter.py index 66d44f7..e76d083 100644 --- a/flask_inputfilter/filters/to_digits_filter.py +++ b/flask_inputfilter/filters/to_digits_filter.py @@ -4,7 +4,7 @@ from typing import Any, Union from flask_inputfilter.enums import RegexEnum -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToDigitsFilter(BaseFilter): @@ -36,10 +36,10 @@ def apply(self, value: Any) -> Union[float, int, Any]: if not isinstance(value, str): return value - elif re.fullmatch(RegexEnum.INTEGER_PATTERN.value, value): + if re.fullmatch(RegexEnum.INTEGER_PATTERN.value, value): return int(value) - elif re.fullmatch(RegexEnum.FLOAT_PATTERN.value, value): + if re.fullmatch(RegexEnum.FLOAT_PATTERN.value, value): return float(value) return value diff --git a/flask_inputfilter/filters/to_enum_filter.py b/flask_inputfilter/filters/to_enum_filter.py index 9e8ff35..0d2b1e9 100644 --- a/flask_inputfilter/filters/to_enum_filter.py +++ b/flask_inputfilter/filters/to_enum_filter.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Any, Type, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToEnumFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_float_filter.py b/flask_inputfilter/filters/to_float_filter.py index c0d92a6..6edeb4e 100644 --- a/flask_inputfilter/filters/to_float_filter.py +++ b/flask_inputfilter/filters/to_float_filter.py @@ -2,7 +2,7 @@ from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToFloatFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_integer_filter.py b/flask_inputfilter/filters/to_integer_filter.py index 025ef4a..f58e76f 100644 --- a/flask_inputfilter/filters/to_integer_filter.py +++ b/flask_inputfilter/filters/to_integer_filter.py @@ -2,7 +2,7 @@ from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToIntegerFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_iso_filter.py b/flask_inputfilter/filters/to_iso_filter.py index b23a3be..dca5e16 100644 --- a/flask_inputfilter/filters/to_iso_filter.py +++ b/flask_inputfilter/filters/to_iso_filter.py @@ -3,7 +3,7 @@ from datetime import date, datetime from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToIsoFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_lower_filter.py b/flask_inputfilter/filters/to_lower_filter.py index 4a2247a..570fc11 100644 --- a/flask_inputfilter/filters/to_lower_filter.py +++ b/flask_inputfilter/filters/to_lower_filter.py @@ -2,7 +2,7 @@ from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToLowerFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_normalized_unicode_filter.py b/flask_inputfilter/filters/to_normalized_unicode_filter.py index ee27af0..4e1012e 100644 --- a/flask_inputfilter/filters/to_normalized_unicode_filter.py +++ b/flask_inputfilter/filters/to_normalized_unicode_filter.py @@ -5,7 +5,7 @@ from typing import Any, Optional, Union from flask_inputfilter.enums import UnicodeFormEnum -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToNormalizedUnicodeFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_null_filter.py b/flask_inputfilter/filters/to_null_filter.py index 6798f63..28f5315 100644 --- a/flask_inputfilter/filters/to_null_filter.py +++ b/flask_inputfilter/filters/to_null_filter.py @@ -2,7 +2,7 @@ from typing import Any, Optional -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToNullFilter(BaseFilter): diff --git a/flask_inputfilter/filters/to_pascal_case_filter.py b/flask_inputfilter/filters/to_pascal_case_filter.py index be5487b..74f7b1e 100644 --- a/flask_inputfilter/filters/to_pascal_case_filter.py +++ b/flask_inputfilter/filters/to_pascal_case_filter.py @@ -3,7 +3,7 @@ import re from typing import Any, Optional, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToPascalCaseFilter(BaseFilter): @@ -37,6 +37,4 @@ def apply(self, value: Any) -> Union[Optional[str], Any]: value = re.sub(r"[\s\-_]+", " ", value).strip() - value = "".join(word.capitalize() for word in value.split()) - - return value + return "".join(word.capitalize() for word in value.split()) diff --git a/flask_inputfilter/filters/to_snake_case_filter.py b/flask_inputfilter/filters/to_snake_case_filter.py index aa95aec..fd08956 100644 --- a/flask_inputfilter/filters/to_snake_case_filter.py +++ b/flask_inputfilter/filters/to_snake_case_filter.py @@ -3,7 +3,7 @@ import re from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class ToSnakeCaseFilter(BaseFilter): @@ -37,6 +37,4 @@ def apply(self, value: Any) -> Union[str, Any]: return value value = re.sub(r"(? None: + def __init__(self, whitelist: Optional[list[str]] = None) -> None: self.whitelist = whitelist def apply(self, value: Any) -> Any: @@ -47,10 +47,10 @@ def apply(self, value: Any) -> Any: [word for word in value.split() if word in self.whitelist] ) - elif isinstance(value, list): + if isinstance(value, list): return [item for item in value if item in self.whitelist] - elif isinstance(value, dict): + if isinstance(value, dict): return { key: value for key, value in value.items() diff --git a/flask_inputfilter/filters/whitespace_collapse_filter.py b/flask_inputfilter/filters/whitespace_collapse_filter.py index d995b66..0d2ca0c 100644 --- a/flask_inputfilter/filters/whitespace_collapse_filter.py +++ b/flask_inputfilter/filters/whitespace_collapse_filter.py @@ -3,7 +3,7 @@ import re from typing import Any, Union -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class WhitespaceCollapseFilter(BaseFilter): @@ -35,6 +35,4 @@ def apply(self, value: Any) -> Union[str, Any]: if not isinstance(value, str): return value - value = re.sub(r"\s+", " ", value).strip() - - return value + return re.sub(r"\s+", " ", value).strip() diff --git a/flask_inputfilter/helpers/__init__.py b/flask_inputfilter/helpers/__init__.py index 5b23e7b..7e0fbd6 100644 --- a/flask_inputfilter/helpers/__init__.py +++ b/flask_inputfilter/helpers/__init__.py @@ -1 +1,5 @@ from .parse_date import parse_date + +__all__ = [ + "parse_date", +] diff --git a/flask_inputfilter/helpers/parse_date.py b/flask_inputfilter/helpers/parse_date.py index d00b1d7..e60bd9d 100644 --- a/flask_inputfilter/helpers/parse_date.py +++ b/flask_inputfilter/helpers/parse_date.py @@ -15,10 +15,10 @@ def parse_date(value: Any) -> datetime: if isinstance(value, datetime): return value - elif isinstance(value, date): + if isinstance(value, date): return datetime.combine(value, datetime.min.time()) - elif isinstance(value, str): + if isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/flask_inputfilter/input_filter.py b/flask_inputfilter/input_filter.py index fdc1d01..cb045ea 100644 --- a/flask_inputfilter/input_filter.py +++ b/flask_inputfilter/input_filter.py @@ -3,36 +3,37 @@ import json import logging import sys -from collections.abc import Callable -from typing import Any, Optional, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union from flask import Response, g, request -from flask_inputfilter.conditions import BaseCondition from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.mixins import ExternalApiMixin, FieldMixin -from flask_inputfilter.models import ExternalApiConfig, FieldModel -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.mixins import DataMixin +from flask_inputfilter.models import BaseFilter, ExternalApiConfig, FieldModel + +if TYPE_CHECKING: + from collections.abc import Callable + + from flask_inputfilter.models import BaseCondition, BaseValidator T = TypeVar("T") _INTERNED_STRINGS = { - "required": sys.intern("required"), + "_condition": sys.intern("_condition"), + "_error": sys.intern("_error"), + "copy": sys.intern("copy"), "default": sys.intern("default"), + "DELETE": sys.intern("DELETE"), + "external_api": sys.intern("external_api"), "fallback": sys.intern("fallback"), "filters": sys.intern("filters"), - "validators": sys.intern("validators"), - "steps": sys.intern("steps"), - "external_api": sys.intern("external_api"), - "copy": sys.intern("copy"), "GET": sys.intern("GET"), + "PATCH": sys.intern("PATCH"), "POST": sys.intern("POST"), "PUT": sys.intern("PUT"), - "PATCH": sys.intern("PATCH"), - "DELETE": sys.intern("DELETE"), - "_condition": sys.intern("_condition"), - "_error": sys.intern("_error"), + "required": sys.intern("required"), + "steps": sys.intern("steps"), + "validators": sys.intern("validators"), } @@ -41,11 +42,11 @@ class InputFilter: def __init__(self, methods: Optional[list[str]] = None) -> None: self.methods: list[str] = methods or [ + "DELETE", "GET", - "POST", "PATCH", + "POST", "PUT", - "DELETE", ] self.fields: dict[str, FieldModel] = {} self.conditions: list[BaseCondition] = [] @@ -183,88 +184,20 @@ def validate_data( will propagate without explicit handling here. """ data = data or self.data - errors = {} - validated_data = {} - - global_filters = self.global_filters - global_validators = self.global_validators - has_global_filters = bool(global_filters) - has_global_validators = bool(global_validators) - - for field_name, field_info in self.fields.items(): - try: - if field_info.copy: - value = validated_data.get(field_info.copy) - elif field_info.external_api: - value = ExternalApiMixin.call_external_api( - field_info.external_api, - field_info.fallback, - validated_data, - ) - else: - value = data.get(field_name) - - if field_info.filters or has_global_filters: - value = FieldMixin.apply_filters( - field_info.filters + global_filters - if has_global_filters - else field_info.filters, - value, - ) - - if field_info.validators or has_global_validators: - value = ( - FieldMixin.validate_field( - field_info.validators + global_validators - if has_global_validators - else field_info.validators, - field_info.fallback, - value, - ) - or value - ) - if field_info.steps: - value = ( - FieldMixin.apply_steps( - field_info.steps, field_info.fallback, value - ) - or value - ) - - if value is None: - if field_info.required: - if field_info.fallback is not None: - value = field_info.fallback - elif field_info.default is not None: - value = field_info.default - else: - raise ValidationError( - f"Field '{field_name}' is required." - ) - elif field_info.default is not None: - value = field_info.default - - validated_data[field_name] = value - - except ValidationError as e: - errors[field_name] = str(e) - - if self.conditions: - try: - FieldMixin.check_conditions(self.conditions, validated_data) - except ValidationError as e: - errors["_condition"] = str(e) + validated_data, errors = DataMixin.validate_with_conditions( + self.fields, + data, + self.global_filters, + self.global_validators, + self.conditions, + ) if errors: raise ValidationError(errors) self.validated_data = validated_data - - if self.model_class is not None: - return self.model_class(**validated_data) - - return validated_data + return self.serialize() def add_condition(self, condition: BaseCondition) -> None: """ @@ -301,15 +234,9 @@ def set_data(self, data: dict[str, Any]) -> None: represent field names and values represent the associated data to be filtered and stored. """ - self.data = {} - for field_name, field_value in data.items(): - if field_name in self.fields: - field_value = FieldMixin.apply_filters( - filters=self.fields[field_name].filters, - value=field_value, - ) - - self.data[field_name] = field_value + self.data = DataMixin.filter_data( + data, self.fields, self.global_filters + ) def get_value(self, name: str) -> Any: """ @@ -382,6 +309,12 @@ def get_raw_values(self) -> dict[str, Any]: if not self.fields: return {} + # Use optimized intersection for larger datasets + if len(self.fields) > 10: + field_set = set(self.fields.keys()) + data_set = set(self.data.keys()) + common_fields = field_set & data_set + return {field: self.data[field] for field in common_fields} return { field: self.data[field] for field in self.fields @@ -425,13 +358,7 @@ def has_unknown(self) -> bool: Returns: bool: True if there are any unknown fields; False otherwise. """ - if not self.data and self.fields: - return True - - return any( - field_name not in self.fields.keys() - for field_name in self.data.keys() - ) + return DataMixin.has_unknown_fields(self.data, self.fields) def get_error_message(self, field_name: str) -> Optional[str]: """ @@ -512,14 +439,14 @@ def add( raise ValueError(f"Field '{name}' already exists.") self.fields[name] = FieldModel( - required=required, - default=default, - fallback=fallback, - filters=filters or [], - validators=validators or [], - steps=steps or [], - external_api=external_api, - copy=copy, + required, + default, + fallback, + filters or [], + validators or [], + steps or [], + external_api, + copy, ) def has(self, field_name: str) -> bool: @@ -637,14 +564,14 @@ def replace( from. """ self.fields[name] = FieldModel( - required=required, - default=default, - fallback=fallback, - filters=filters or [], - validators=validators or [], - steps=steps or [], - external_api=external_api, - copy=copy, + required, + default, + fallback, + filters or [], + validators or [], + steps or [], + external_api, + copy, ) def add_global_filter(self, filter: BaseFilter) -> None: @@ -686,14 +613,14 @@ def clear(self) -> None: self.validated_data.clear() self.errors.clear() - def merge(self, other: "InputFilter") -> None: + def merge(self, other: InputFilter) -> None: """ Merges another InputFilter instance intelligently into the current instance. - Fields with the same name are merged recursively if possible, otherwise overwritten. - - Conditions, are combined and duplicated. + - Conditions are combined and duplicated. - Global filters and validators are merged without duplicates. Args: @@ -704,30 +631,7 @@ def merge(self, other: "InputFilter") -> None: "Can only merge with another InputFilter instance." ) - for key, new_field in other.get_inputs().items(): - self.fields[key] = new_field - - self.conditions += other.conditions - - for filter in other.global_filters: - existing_type_map = { - type(v): i for i, v in enumerate(self.global_filters) - } - if type(filter) in existing_type_map: - self.global_filters[existing_type_map[type(filter)]] = filter - else: - self.global_filters.append(filter) - - for validator in other.global_validators: - existing_type_map = { - type(v): i for i, v in enumerate(self.global_validators) - } - if type(validator) in existing_type_map: - self.global_validators[ - existing_type_map[type(validator)] - ] = validator - else: - self.global_validators.append(validator) + DataMixin.merge_input_filters(self, other) def set_model(self, model_class: Type[T]) -> None: """ diff --git a/flask_inputfilter/input_filter.pyi b/flask_inputfilter/input_filter.pyi index d03b519..8fd488f 100644 --- a/flask_inputfilter/input_filter.pyi +++ b/flask_inputfilter/input_filter.pyi @@ -5,10 +5,13 @@ from typing import Any, Optional, Type, TypeVar, Union from flask import Response -from flask_inputfilter.conditions import BaseCondition -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.models import ExternalApiConfig, FieldModel -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import ( + BaseCondition, + BaseFilter, + BaseValidator, + ExternalApiConfig, + FieldModel, +) T = TypeVar("T") diff --git a/flask_inputfilter/mixins/__init__.py b/flask_inputfilter/mixins/__init__.py index b036618..07f5325 100644 --- a/flask_inputfilter/mixins/__init__.py +++ b/flask_inputfilter/mixins/__init__.py @@ -1,14 +1,17 @@ import shutil if shutil.which("g++") is not None: - from ._external_api_mixin import ExternalApiMixin - from ._field_mixin import FieldMixin + from .data_mixin._data_mixin import DataMixin + from .external_api_mixin._external_api_mixin import ExternalApiMixin + from .validation_mixin._validation_mixin import ValidationMixin else: - from .external_api_mixin import ExternalApiMixin - from .field_mixin import FieldMixin + from .data_mixin.data_mixin import DataMixin + from .external_api_mixin.external_api_mixin import ExternalApiMixin + from .validation_mixin.validation_mixin import ValidationMixin __all__ = [ + "DataMixin", "ExternalApiMixin", - "FieldMixin", + "ValidationMixin", ] diff --git a/flask_inputfilter/mixins/_external_api_mixin.pxd b/flask_inputfilter/mixins/_external_api_mixin.pxd deleted file mode 100644 index 141bde4..0000000 --- a/flask_inputfilter/mixins/_external_api_mixin.pxd +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from flask_inputfilter.models import ExternalApiConfig - - -cdef class ExternalApiMixin: - @staticmethod - cdef str replace_placeholders( - str value, - dict validated_data - ) - @staticmethod - cdef dict replace_placeholders_in_params( - dict params, dict validated_data - ) - @staticmethod - cdef object call_external_api(object config, object fallback, dict validated_data) diff --git a/flask_inputfilter/mixins/_field_mixin.pxd b/flask_inputfilter/mixins/_field_mixin.pxd deleted file mode 100644 index 3f49169..0000000 --- a/flask_inputfilter/mixins/_field_mixin.pxd +++ /dev/null @@ -1,12 +0,0 @@ -cdef class FieldMixin: - - @staticmethod - cdef object apply_filters(list filters, object value) - @staticmethod - cdef object validate_field(list validators, object fallback, object value) - @staticmethod - cdef object apply_steps(list steps, object fallback, object value) - @staticmethod - cdef void check_conditions(list conditions, dict validated_data) except * - @staticmethod - cdef object check_for_required(str field_name, bint required, object default, object fallback, object value) diff --git a/flask_inputfilter/mixins/_field_mixin.pyx b/flask_inputfilter/mixins/_field_mixin.pyx deleted file mode 100644 index b8f9629..0000000 --- a/flask_inputfilter/mixins/_field_mixin.pyx +++ /dev/null @@ -1,215 +0,0 @@ -# cython: language=c++ -from typing import Union - -import cython - -from flask_inputfilter.conditions import BaseCondition -from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.validators import BaseValidator - - -cdef class FieldMixin: - - @staticmethod - @cython.exceptval(check=False) - cdef object apply_filters(list filters, object value): - """ - Apply filters to the field value. - - **Parameters:** - - - **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 None - - cdef: - int i - int n = len(filters) - object current_filter - - for i in range(n): - current_filter = filters[i] - value = current_filter.apply(value) - - return value - - @staticmethod - cdef object apply_steps( - list steps, - object fallback, - object value - ): - """ - 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. - - **Parameters:** - - - **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 None - - cdef: - int i - int n = len(steps) - object current_step - - try: - for i in range(n): - current_step = steps[i] - if isinstance(current_step, BaseFilter): - value = current_step.apply(value) - elif isinstance(current_step, BaseValidator): - current_step.validate(value) - except ValidationError: - if fallback is None: - raise - return fallback - return value - - @staticmethod - cdef void check_conditions(list conditions, dict validated_data) except *: - """ - 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. - - **Parameters:** - - - **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. - """ - cdef: - int i - int n = len(conditions) - object current_condition - - for i in range(n): - current_condition = conditions[i] - if not current_condition.check(validated_data): - raise ValidationError( - f"Condition '{current_condition.__class__.__name__}' " - f"not met." - ) - - @staticmethod - cdef inline object check_for_required( - str field_name, - bint required, - object default, - object fallback, - object value, - ): - """ - 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. - - **Parameters:** - - - **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.") - - @staticmethod - cdef object validate_field( - list validators, object fallback, object value - ): - """ - Validate the field value. - - **Parameters:** - - - **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 None - - cdef: - int i - int n = len(validators) - object current_validator - - try: - for i in range(n): - current_validator = validators[i] - current_validator.validate(value) - except ValidationError: - if fallback is None: - raise - - return fallback diff --git a/flask_inputfilter/mixins/cimports.pxd b/flask_inputfilter/mixins/cimports.pxd new file mode 100644 index 0000000..e731382 --- /dev/null +++ b/flask_inputfilter/mixins/cimports.pxd @@ -0,0 +1,3 @@ +from .external_api_mixin._external_api_mixin cimport ExternalApiMixin +from .validation_mixin._validation_mixin cimport ValidationMixin +from .data_mixin._data_mixin cimport DataMixin diff --git a/flask_inputfilter/mixins/data_mixin/__init__.py b/flask_inputfilter/mixins/data_mixin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/mixins/data_mixin/_data_mixin.pxd b/flask_inputfilter/mixins/data_mixin/_data_mixin.pxd new file mode 100644 index 0000000..619b3e3 --- /dev/null +++ b/flask_inputfilter/mixins/data_mixin/_data_mixin.pxd @@ -0,0 +1,27 @@ +from typing import Any + +from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, FieldModel, BaseCondition, InputFilter + + +cdef class DataMixin: + + @staticmethod + cdef bint has_unknown_fields(dict[str, Any] data, dict[str, FieldModel] fields) + + @staticmethod + cdef dict[str, Any] filter_data(dict[str, Any] data, dict[str, FieldModel] fields, list[BaseFilter] global_filters) + + @staticmethod + cdef tuple validate_with_conditions( + dict[str, FieldModel] fields, + dict[str, Any] data, + list[BaseFilter] global_filters, + list[BaseValidator] global_validators, + list[BaseCondition] conditions + ) + + @staticmethod + cdef void merge_input_filters(InputFilter target_filter, InputFilter source_filter) except * + + @staticmethod + cdef void _merge_component_list(list target_list, list source_list) \ No newline at end of file diff --git a/flask_inputfilter/mixins/data_mixin/_data_mixin.pyx b/flask_inputfilter/mixins/data_mixin/_data_mixin.pyx new file mode 100644 index 0000000..3261a92 --- /dev/null +++ b/flask_inputfilter/mixins/data_mixin/_data_mixin.pyx @@ -0,0 +1,195 @@ +# cython: language=c++ + +from typing import Any + +from flask_inputfilter.exceptions import ValidationError + +from flask_inputfilter.mixins.cimports cimport ValidationMixin + +from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, FieldModel, BaseCondition, InputFilter + +# Compile-time constants for performance thresholds +DEF LARGE_DATASET_THRESHOLD = 10 + + +cdef class DataMixin: + + @staticmethod + cdef bint has_unknown_fields( + dict[str, Any] data, + dict[str, FieldModel] fields + ): + """ + Check if data contains fields not defined in fields configuration. + Uses optimized lookup strategy based on field count. + + **Parameters:** + + - **data** (*dict[str, Any]*): The input data to check. + - **fields** (*dict[str, FieldModel]*): The field definitions. + + **Returns:** + + - (*bool*): True if unknown fields exist, False otherwise. + """ + if not data and fields: + return True + + cdef set field_set + + # Use set operations for faster lookup when there are many fields + if len(fields) > LARGE_DATASET_THRESHOLD: + field_set = set(fields.keys()) + for field_name in data.keys(): + if field_name not in field_set: + return True + else: + # Use direct dict lookup for smaller field counts + for field_name in data.keys(): + if field_name not in fields: + return True + + return False + + @staticmethod + cdef dict[str, Any] filter_data( + dict[str, Any] data, + dict[str, FieldModel] fields, + list[BaseFilter] global_filters + ): + """ + Filter input data through field-specific and global filters. + + **Parameters:** + + - **data** (*dict[str, Any]*): The input data to filter. + - **fields** (*dict[str, FieldModel]*): Field definitions with filters. + - **global_filters** (*list[BaseFilter]*): Global filters to apply. + + **Returns:** + + - (*dict[str, Any]*): The filtered data. + """ + cdef: + dict[str, Any] filtered_data = {} + Py_ssize_t i, n = len(data) if data else 0 + list keys = list(data.keys()) if n > 0 else [] + list values = list(data.values()) if n > 0 else [] + str field_name + object field_value + + for i in range(n): + field_name = keys[i] + field_value = values[i] + + if field_name in fields: + field_value = ValidationMixin.apply_filters( + fields[field_name].filters, + global_filters, + field_value, + ) + + filtered_data[field_name] = field_value + + return filtered_data + + @staticmethod + cdef tuple validate_with_conditions( + dict[str, FieldModel] fields, + dict[str, Any] data, + list[BaseFilter] global_filters, + list[BaseValidator] global_validators, + list[BaseCondition] conditions + ): + """ + Complete validation pipeline including conditions check. + + **Parameters:** + + - **fields** (*dict[str, FieldModel]*): Field definitions. + - **data** (*dict[str, Any]*): Input data to validate. + - **global_filters** (*list[BaseFilter]*): Global filters. + - **global_validators** (*list[BaseValidator]*): Global validators. + - **conditions** (*list[BaseCondition]*): Conditions to check. + + **Returns:** + + - (*tuple*): (validated_data, errors) tuple. + """ + cdef: + dict[str, Any] validated_data + dict[str, str] errors + + # Validate fields + validated_data, errors = ValidationMixin.validate_fields( + fields, data, global_filters, global_validators + ) + + # Check conditions if present and no errors yet + if conditions and not errors: + try: + ValidationMixin.check_conditions(conditions, validated_data) + except ValidationError as e: + errors["_condition"] = str(e) + + return validated_data, errors + + @staticmethod + cdef void merge_input_filters( + InputFilter target_filter, + InputFilter source_filter + ) except *: + """ + Efficiently merge one InputFilter into another. + + **Parameters:** + + - **target_filter** (*InputFilter*): The InputFilter to merge into. + - **source_filter** (*InputFilter*): The InputFilter to merge from. + """ + cdef: + Py_ssize_t i, n + dict source_inputs = source_filter.get_inputs() + list keys = list(source_inputs.keys()) if source_inputs else [] + list new_fields = list(source_inputs.values()) if source_inputs else [] + + # Merge fields efficiently + n = len(keys) + for i in range(n): + target_filter.fields[keys[i]] = new_fields[i] + + # Merge conditions + target_filter.conditions.extend(source_filter.conditions) + + # Merge global filters (avoid duplicates by type) + DataMixin._merge_component_list( + target_filter.global_filters, + source_filter.global_filters + ) + + # Merge global validators (avoid duplicates by type) + DataMixin._merge_component_list( + target_filter.global_validators, + source_filter.global_validators + ) + + @staticmethod + cdef void _merge_component_list(list target_list, list source_list): + """ + Helper method to merge component lists avoiding duplicates by type. + + **Parameters:** + + - **target_list** (*list*): The list to merge into. + - **source_list** (*list*): The list to merge from. + """ + cdef dict existing_type_map + + for component in source_list: + existing_type_map = { + type(v): i for i, v in enumerate(target_list) + } + if type(component) in existing_type_map: + target_list[existing_type_map[type(component)]] = component + else: + target_list.append(component) \ No newline at end of file diff --git a/flask_inputfilter/mixins/data_mixin/data_mixin.py b/flask_inputfilter/mixins/data_mixin/data_mixin.py new file mode 100644 index 0000000..14f50a5 --- /dev/null +++ b/flask_inputfilter/mixins/data_mixin/data_mixin.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from flask_inputfilter.exceptions import ValidationError + +if TYPE_CHECKING: + from flask_inputfilter import InputFilter + from flask_inputfilter.models import ( + BaseCondition, + BaseFilter, + BaseValidator, + FieldModel, + ) + +LARGE_DATASET_THRESHOLD = 10 + + +class DataMixin: + __slots__ = () + + @staticmethod + def has_unknown_fields( + data: dict[str, Any], fields: dict[str, FieldModel] + ) -> bool: + """ + Check if data contains fields not defined in fields configuration. Uses + optimized lookup strategy based on field count. + + **Parameters:** + + - **data** (*dict[str, Any]*): The input data to check. + - **fields** (*dict[str, FieldModel]*): The field definitions. + + **Returns:** + + - (*bool*): True if unknown fields exist, False otherwise. + """ + if not data and fields: + return True + + # Use set operations for faster lookup when there are many fields + if len(fields) > LARGE_DATASET_THRESHOLD: + field_set = set(fields.keys()) + return any(field_name not in field_set for field_name in data) + # Use direct dict lookup for smaller field counts + return any(field_name not in fields for field_name in data) + + @staticmethod + def filter_data( + data: dict[str, Any], + fields: dict[str, FieldModel], + global_filters: list[BaseFilter], + ) -> dict[str, Any]: + """ + Filter input data through field-specific and global filters. + + **Parameters:** + + - **data** (*dict[str, Any]*): The input data to filter. + - **fields** (*dict[str, FieldModel]*): Field definitions with filters. + - **global_filters** (*list[BaseFilter]*): Global filters to apply. + + **Returns:** + + - (*dict[str, Any]*): The filtered data. + """ + # Import here to avoid circular imports + from flask_inputfilter.mixins import ValidationMixin + + filtered_data = {} + for field_name, field_value in data.items(): + if field_name in fields: + field_value = ValidationMixin.apply_filters( + fields[field_name].filters, + global_filters, + field_value, + ) + filtered_data[field_name] = field_value + return filtered_data + + @staticmethod + def validate_with_conditions( + fields: dict[str, FieldModel], + data: dict[str, Any], + global_filters: list[BaseFilter], + global_validators: list[BaseValidator], + conditions: list[BaseCondition], + ) -> tuple[dict[str, Any], dict[str, str]]: + """ + Complete validation pipeline including conditions check. + + **Parameters:** + + - **fields** (*dict[str, FieldModel]*): Field definitions. + - **data** (*dict[str, Any]*): Input data to validate. + - **global_filters** (*list[BaseFilter]*): Global filters. + - **global_validators** (*list[BaseValidator]*): Global validators. + - **conditions** (*list[BaseCondition]*): Conditions to check. + + **Returns:** + + - (*tuple*): (validated_data, errors) tuple. + """ + from flask_inputfilter.mixins import ValidationMixin + + # Validate fields + validated_data, errors = ValidationMixin.validate_fields( + fields, data, global_filters, global_validators + ) + + # Check conditions if present and no errors yet + if conditions and not errors: + try: + ValidationMixin.check_conditions(conditions, validated_data) + except ValidationError as e: + errors["_condition"] = str(e) + + return validated_data, errors + + @staticmethod + def merge_input_filters( + target_filter: InputFilter, source_filter: InputFilter + ) -> None: + """ + Efficiently merge one InputFilter into another. + + **Parameters:** + + - **target_filter** (*InputFilter*): The InputFilter to merge into. + - **source_filter** (*InputFilter*): The InputFilter to merge from. + """ + # Merge fields + target_filter.fields.update(source_filter.get_inputs()) + + # Merge conditions + target_filter.conditions.extend(source_filter.conditions) + + # Merge global filters (avoid duplicates by type) + DataMixin._merge_component_list( + target_filter.global_filters, source_filter.global_filters + ) + + # Merge global validators (avoid duplicates by type) + DataMixin._merge_component_list( + target_filter.global_validators, source_filter.global_validators + ) + + @staticmethod + def _merge_component_list(target_list: list, source_list: list) -> None: + """ + Helper method to merge component lists avoiding duplicates by type. + + **Parameters:** + + - **target_list** (*list*): The list to merge into. + - **source_list** (*list*): The list to merge from. + """ + existing_type_map = {type(v): i for i, v in enumerate(target_list)} + + for component in source_list: + component_type = type(component) + if component_type in existing_type_map: + target_list[existing_type_map[component_type]] = component + else: + target_list.append(component) diff --git a/flask_inputfilter/mixins/external_api_mixin/__init__.py b/flask_inputfilter/mixins/external_api_mixin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pxd b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pxd new file mode 100644 index 0000000..d228a4b --- /dev/null +++ b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pxd @@ -0,0 +1,23 @@ +from typing import Any + +from flask_inputfilter.models.cimports cimport ExternalApiConfig + + +cdef class ExternalApiMixin: + @staticmethod + cdef str replace_placeholders( + str value, + dict[str, Any] validated_data + ) + + @staticmethod + cdef dict[str, Any] replace_placeholders_in_params( + dict[str, Any] params, dict[str, Any] validated_data + ) + + @staticmethod + cdef object call_external_api( + ExternalApiConfig config, + object fallback, + dict[str, Any] validated_data + ) diff --git a/flask_inputfilter/mixins/_external_api_mixin.pyx b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx similarity index 90% rename from flask_inputfilter/mixins/_external_api_mixin.pyx rename to flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx index 34e8aa6..8db0a98 100644 --- a/flask_inputfilter/mixins/_external_api_mixin.pyx +++ b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx @@ -1,6 +1,9 @@ # cython: language=c++ + import re +from typing import Any +from flask_inputfilter.models.cimports cimport ExternalApiConfig from flask_inputfilter.exceptions import ValidationError @@ -8,7 +11,9 @@ cdef class ExternalApiMixin: @staticmethod cdef object call_external_api( - object config, object fallback, dict validated_data + ExternalApiConfig config, + object fallback, + dict[str, Any] validated_data ): """ The function constructs a request based on the given API @@ -109,20 +114,20 @@ cdef class ExternalApiMixin: @staticmethod cdef inline str replace_placeholders( str value, - dict validated_data + dict[str, Any] validated_data ): """ Replace all placeholders, marked with '{{ }}' in value with the corresponding values from validated_data. **Parameters:** - + - **value** (**str**): The string containing placeholders to be replaced. - **validated_data** (**dict[str, Any]**): The dictionary containing the values to replace the placeholders with. **Returns:** - + - (*str*): The value with all placeholders replaced with the corresponding values from validated_data. """ @@ -132,8 +137,8 @@ cdef class ExternalApiMixin: ) @staticmethod - cdef dict replace_placeholders_in_params( - dict params, dict validated_data + cdef dict[str, Any] replace_placeholders_in_params( + dict[str, Any] params, dict[str, Any] validated_data ): """ Replace all placeholders in params with the corresponding @@ -141,13 +146,13 @@ cdef class ExternalApiMixin: **Parameters:** - - **params** (*dict*): The params dictionary containing placeholders. + - **params** (*dict[str, Any]*): The params dictionary containing placeholders. - **validated_data** (*dict[str, Any]*): The dictionary containing the values to replace the placeholders with. **Returns:** - - - (*dict*): The params dictionary with all placeholders replaced + + - (*dict[str, Any]*): The params dictionary with all placeholders replaced with the corresponding values from validated_data. """ return { diff --git a/flask_inputfilter/mixins/external_api_mixin.py b/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py similarity index 92% rename from flask_inputfilter/mixins/external_api_mixin.py rename to flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py index 86824f0..74190cd 100644 --- a/flask_inputfilter/mixins/external_api_mixin.py +++ b/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py @@ -1,10 +1,12 @@ from __future__ import annotations import re -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.models import ExternalApiConfig + +if TYPE_CHECKING: + from flask_inputfilter.models import ExternalApiConfig class ExternalApiMixin: @@ -61,18 +63,18 @@ def call_external_api( } if config.api_key: - request_data["headers"][ - "Authorization" - ] = f"Bearer {config.api_key}" + request_data["headers"]["Authorization"] = ( + f"Bearer {config.api_key}" + ) if config.headers: request_data["headers"].update(config.headers) if config.params: - request_data[ - "params" - ] = ExternalApiMixin.replace_placeholders_in_params( - config.params, validated_data + request_data["params"] = ( + ExternalApiMixin.replace_placeholders_in_params( + config.params, validated_data + ) ) request_data["url"] = ExternalApiMixin.replace_placeholders( diff --git a/flask_inputfilter/mixins/validation_mixin/__init__.py b/flask_inputfilter/mixins/validation_mixin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pxd b/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pxd new file mode 100644 index 0000000..f1d7481 --- /dev/null +++ b/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pxd @@ -0,0 +1,32 @@ +from typing import Any + +from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, FieldModel, BaseCondition + + +cdef class ValidationMixin: + + @staticmethod + cdef object apply_filters(list[BaseFilter] filters1, list[BaseFilter] filters2, object value) + + @staticmethod + cdef object validate_field(list[BaseValidator] validators1, list[BaseValidator] validators2, object fallback, object value) + + @staticmethod + cdef object apply_steps(list[BaseFilter | BaseValidator] steps, object fallback, object value) + + @staticmethod + cdef void check_conditions(list[BaseCondition] conditions, dict[str, Any] validated_data) except * + + @staticmethod + cdef object check_for_required(str field_name, FieldModel field_info, object value) + + @staticmethod + cdef tuple validate_fields( + dict[str, FieldModel] fields, + dict[str, Any] data, + list[BaseFilter] global_filters, + list[BaseValidator] global_validators + ) + + @staticmethod + cdef object get_field_value(str field_name, FieldModel field_info, dict[str, Any] data, dict[str, Any] validated_data) \ No newline at end of file diff --git a/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pyx b/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pyx new file mode 100644 index 0000000..7884b32 --- /dev/null +++ b/flask_inputfilter/mixins/validation_mixin/_validation_mixin.pyx @@ -0,0 +1,355 @@ +# cython: language=c++ + +import cython +from typing import Any + +from flask_inputfilter.exceptions import ValidationError + +from flask_inputfilter.mixins.cimports cimport ExternalApiMixin +from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, FieldModel + + +cdef class ValidationMixin: + + @staticmethod + @cython.exceptval(check=False) + cdef object apply_filters(list[BaseFilter] filters1, list[BaseFilter] filters2, object value): + """ + Apply filters to the field value. + + **Parameters:** + + - **filters1** (*list[BaseFilter]*): A list of filters to apply to the + value. + - **filters2** (*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 None + + cdef: + Py_ssize_t i, n + BaseFilter current_filter + + n = len(filters1) if filters1 else 0 + for i in range(n): + current_filter = filters1[i] + value = current_filter.apply(value) + + n = len(filters2) if filters2 else 0 + for i in range(n): + current_filter = filters2[i] + value = current_filter.apply(value) + + return value + + @staticmethod + cdef object apply_steps( + list[BaseFilter | BaseValidator] steps, + object fallback, + object value + ): + """ + 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. + + **Parameters:** + + - **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 None + + cdef: + Py_ssize_t i, n = len(steps) if steps else 0 + object current_step + + try: + for i in range(n): + current_step = steps[i] + if isinstance(current_step, BaseFilter): + value = current_step.apply(value) + elif isinstance(current_step, BaseValidator): + current_step.validate(value) + except ValidationError: + if fallback is None: + raise + return fallback + return value + + @staticmethod + cdef void check_conditions(list[BaseCondition] conditions, dict[str, Any] validated_data) except *: + """ + 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. + + **Parameters:** + + - **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. + """ + cdef: + Py_ssize_t i, n = len(conditions) if conditions else 0 + object current_condition + + for i in range(n): + current_condition = conditions[i] + if not current_condition.check(validated_data): + raise ValidationError( + f"Condition '{current_condition.__class__.__name__}' " + f"not met." + ) + + @staticmethod + cdef inline object check_for_required( + str field_name, + FieldModel field_info, + object value, + ): + """ + 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. + + **Parameters:** + + - **field_name** (*str*): The name of the field being processed. + - **field_info** (*FieldModel*): The object of the field. + - **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 field_info.required: + return field_info.default + + if field_info.fallback is not None: + return field_info.fallback + + raise ValidationError(f"Field '{field_name}' is required.") + + @staticmethod + cdef object validate_field( + list[BaseValidator] validators1, + list[BaseValidator] validators2, + object fallback, + object value + ): + """ + Validate the field value. + + **Parameters:** + + - **validators1** (*list[BaseValidator]*): A list of validators to + apply to the field value. + - **validators2** (*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 None + + cdef: + Py_ssize_t i, n + BaseValidator current_validator + + try: + n = len(validators1) if validators1 else 0 + for i in range(n): + current_validator = validators1[i] + current_validator.validate(value) + + n = len(validators2) if validators2 else 0 + for i in range(n): + current_validator = validators2[i] + current_validator.validate(value) + except ValidationError: + if fallback is None: + raise + + return fallback + + return value + + @staticmethod + cdef tuple validate_fields( + dict[str, FieldModel] fields, + dict[str, Any] data, + list[BaseFilter] global_filters, + list[BaseValidator] global_validators + ): + """ + Validate multiple fields based on their configurations. + + **Parameters:** + + - **fields** (*dict[str, FieldModel]*): A dictionary where keys are + field names and values are FieldModel objects containing field + configurations. + - **data** (*dict[str, Any]*): The input data dictionary containing + the values to be validated. + - **global_filters** (*list[BaseFilter]*): A list of global filters + to be applied to all fields. + - **global_validators** (*list[BaseValidator]*): A list of global + validators to be applied to all fields. + + **Returns:** + + - (*tuple*): A tuple containing two dictionaries: + - **validated_data** (*dict[str, Any]*): A dictionary of field names + and their validated values. + - **errors** (*dict[str, str]*): A dictionary of field names and + error messages for any validation failures. + """ + cdef: + dict[str, Any] validated_data = {} + dict[str, str] errors = {} + Py_ssize_t i, n = len(fields) if fields else 0 + + cdef: + list field_names = list(fields.keys()) if n > 0 else [] + list field_infos = list(fields.values()) if n > 0 else [] + str field_name + FieldModel field_info + object value + + for i in range(n): + field_name = field_names[i] + field_info = field_infos[i] + + try: + # Get initial value + value = ValidationMixin.get_field_value( + field_name, + field_info, + data, + validated_data + ) + + # Apply filters + value = ValidationMixin.apply_filters( + field_info.filters, + global_filters, + value + ) + + # Apply validators + value = ValidationMixin.validate_field( + field_info.validators, + global_validators, + field_info.fallback, + value + ) + + # Apply steps + value = ValidationMixin.apply_steps( + field_info.steps, + field_info.fallback, + value + ) + + # Handle required fields and defaults + value = ValidationMixin.check_for_required( + field_name, + field_info, + value + ) + + validated_data[field_name] = value + except ValidationError as e: + errors[field_name] = str(e) + + return validated_data, errors + + @staticmethod + cdef inline object get_field_value( + str field_name, + FieldModel field_info, + dict[str, Any] data, + dict[str, Any] validated_data + ): + """ + Retrieve the value of a field based on its configuration. + + **Parameters:** + + - **field_name** (*str*): The name of the field to retrieve. + - **field_info** (*FieldModel*): The object containing field + configuration, including copy, external_api, and fallback + attributes. + - **data** (*dict[str, Any]*): The original data dictionary from which + the field value is to be retrieved. + - **validated_data** (*dict[str, Any]*): The dictionary containing + already validated data, which may include copied or externally + fetched values. + + **Returns:** + + - (*Any*): The value of the field, either from the validated data, + copied from another field, fetched from an external API, or directly + from the original data dictionary. + """ + if field_info.copy: + return validated_data.get(field_info.copy) + elif field_info.external_api: + return ExternalApiMixin.call_external_api( + field_info.external_api, + field_info.fallback, + validated_data + ) + else: + return data.get(field_name) \ No newline at end of file diff --git a/flask_inputfilter/mixins/field_mixin.py b/flask_inputfilter/mixins/validation_mixin/validation_mixin.py similarity index 54% rename from flask_inputfilter/mixins/field_mixin.py rename to flask_inputfilter/mixins/validation_mixin/validation_mixin.py index 0e20150..46dcf17 100644 --- a/flask_inputfilter/mixins/field_mixin.py +++ b/flask_inputfilter/mixins/validation_mixin/validation_mixin.py @@ -1,24 +1,30 @@ from __future__ import annotations -from typing import Any, Union +from itertools import chain +from typing import TYPE_CHECKING, Any, Union -from flask_inputfilter.conditions import BaseCondition from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseFilter, BaseValidator, FieldModel +if TYPE_CHECKING: + from flask_inputfilter.models import BaseCondition -class FieldMixin: + +class ValidationMixin: __slots__ = () @staticmethod - def apply_filters(filters: list[BaseFilter], value: Any) -> Any: + def apply_filters( + filters1: list[BaseFilter], filters2: list[BaseFilter], value: Any + ) -> Any: """ Apply filters to the field value. **Parameters:** - - **filters** (*list[BaseFilter]*): A list of filters to apply to the + - **filters1** (*list[BaseFilter]*): A list of filters to apply to the + value. + - **filters2** (*list[BaseFilter]*): A list of filters to apply to the value. - **value** (*Any*): The value to be processed by the filters. @@ -30,21 +36,26 @@ def apply_filters(filters: list[BaseFilter], value: Any) -> Any: if value is None: return None - for filter in filters: + for filter in chain(filters1, filters2): value = filter.apply(value) return value @staticmethod def validate_field( - validators: list[BaseValidator], fallback: Any, value: Any + validators1: list[BaseValidator], + validators2: list[BaseValidator], + fallback: Any, + value: Any, ) -> Any: """ Validate the field value. **Parameters:** - - **validators** (*list[BaseValidator]*): A list of validators to + - **validators1** (*list[BaseValidator]*): A list of validators to + apply to the field value. + - **validators2** (*list[BaseValidator]*): A list of validators to apply to the field value. - **fallback** (*Any*): A fallback value to return if validation fails. @@ -59,7 +70,7 @@ def validate_field( return None try: - for validator in validators: + for validator in chain(validators1, validators2): validator.validate(value) except ValidationError: if fallback is None: @@ -67,6 +78,8 @@ def validate_field( return fallback + return value + @staticmethod def apply_steps( steps: list[Union[BaseFilter, BaseValidator]], @@ -145,9 +158,7 @@ def check_conditions( @staticmethod def check_for_required( field_name: str, - required: bool, - default: Any, - fallback: Any, + field_info: FieldModel, value: Any, ) -> Any: """ @@ -162,11 +173,7 @@ def check_for_required( **Parameters:** - **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. + - **field_info** (*FieldModel*): The object of the field. - **value** (*Any*): The current value of the field being processed. **Returns:** @@ -182,10 +189,97 @@ def check_for_required( if value is not None: return value - if not required: - return default + if not field_info.required: + return field_info.default - if fallback is not None: - return fallback + if field_info.fallback is not None: + return field_info.fallback raise ValidationError(f"Field '{field_name}' is required.") + + @staticmethod + def validate_fields( + fields: dict[str, Any], + data: dict[str, Any], + global_filters: list[BaseFilter], + global_validators: list[BaseValidator], + ) -> tuple: + """Process and validate all fields.""" + + validated_data = {} + errors = {} + + for field_name, field_info in fields.items(): + try: + # Get initial value + value = ValidationMixin.get_field_value( + field_name, field_info, data, validated_data + ) + + # Apply filters + value = ValidationMixin.apply_filters( + field_info.filters, global_filters, value + ) + + # Apply validators + value = ValidationMixin.validate_field( + field_info.validators, + global_validators, + field_info.fallback, + value, + ) + + # Apply steps + value = ValidationMixin.apply_steps( + field_info.steps, field_info.fallback, value + ) + + # Handle required fields and defaults + value = ValidationMixin.check_for_required( + field_name, field_info, value + ) + + validated_data[field_name] = value + except ValidationError as e: + errors[field_name] = str(e) + + return validated_data, errors + + @staticmethod + def get_field_value( + field_name: str, + field_info: FieldModel, + data: dict[str, Any], + validated_data: dict[str, Any], + ) -> Any: + """ + Retrieve the value of a field based on its configuration. + + **Parameters:** + + - **field_name** (*str*): The name of the field to retrieve. + - **field_info** (*FieldModel*): The object containing field + configuration, including copy, external_api, and fallback + attributes. + - **data** (*dict[str, Any]*): The original data dictionary from which + the field value is to be retrieved. + - **validated_data** (*dict[str, Any]*): The dictionary containing + already validated data, which may include copied or externally + fetched values. + + **Returns:** + + - (*Any*): The value of the field, either from the validated data, + copied from another field, fetched from an external API, or directly + from the original data dictionary. + """ + if field_info.copy: + return validated_data.get(field_info.copy) + if field_info.external_api: + # Import here to avoid circular imports + from flask_inputfilter.mixins import ExternalApiMixin + + return ExternalApiMixin.call_external_api( + field_info.external_api, field_info.fallback, validated_data + ) + return data.get(field_name) diff --git a/flask_inputfilter/models/__init__.py b/flask_inputfilter/models/__init__.py index d8867dd..04c8566 100644 --- a/flask_inputfilter/models/__init__.py +++ b/flask_inputfilter/models/__init__.py @@ -1,14 +1,23 @@ import shutil -from .external_api_config import ExternalApiConfig - if shutil.which("g++") is not None: - from ._field_model import FieldModel + from .base_condition._base_condition import BaseCondition + from .base_filter._base_filter import BaseFilter + from .base_validator._base_validator import BaseValidator + from .external_api_config._external_api_config import ExternalApiConfig + from .field_model._field_model import FieldModel else: - from .field_model import FieldModel + from .base_condition.base_condition import BaseCondition + from .base_filter.base_filter import BaseFilter + from .base_validator.base_validator import BaseValidator + from .external_api_config.external_api_config import ExternalApiConfig + from .field_model.field_model import FieldModel __all__ = [ - "FieldModel", + "BaseCondition", + "BaseFilter", + "BaseValidator", "ExternalApiConfig", + "FieldModel", ] diff --git a/flask_inputfilter/models/base_condition/__init__.py b/flask_inputfilter/models/base_condition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/models/base_condition/_base_condition.pxd b/flask_inputfilter/models/base_condition/_base_condition.pxd new file mode 100644 index 0000000..dd6a56e --- /dev/null +++ b/flask_inputfilter/models/base_condition/_base_condition.pxd @@ -0,0 +1,5 @@ +from typing import Any + + +cdef class BaseCondition: + cpdef bint check(self, dict[str, Any] data) \ No newline at end of file diff --git a/flask_inputfilter/models/base_condition/_base_condition.pyx b/flask_inputfilter/models/base_condition/_base_condition.pyx new file mode 100644 index 0000000..0312ef2 --- /dev/null +++ b/flask_inputfilter/models/base_condition/_base_condition.pyx @@ -0,0 +1,26 @@ +# cython: language=c++ + +from typing import Any + + +cdef class BaseCondition: + """ + Base class for defining conditions. + + Each condition should implement the `check` method. + """ + + cpdef bint check(self, dict[str, Any] data): + """ + Check if the condition is met based on the provided data. + + Args: + data: Dictionary containing the data to validate against. + + Returns: + True if the condition is satisfied, False otherwise. + + Raises: + NotImplementedError: If the method is not implemented by subclasses. + """ + raise NotImplementedError("Subclasses must implement the check method") \ No newline at end of file diff --git a/flask_inputfilter/models/base_condition/base_condition.py b/flask_inputfilter/models/base_condition/base_condition.py new file mode 100644 index 0000000..a0e06d4 --- /dev/null +++ b/flask_inputfilter/models/base_condition/base_condition.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + + +class BaseCondition: + """ + Base class for defining conditions. + + Each condition should implement the `check` method. + """ + + def check(self, data: dict[str, Any]) -> bool: + raise NotImplementedError( + "The check method must be implemented in conditions." + ) diff --git a/flask_inputfilter/models/base_filter/__init__.py b/flask_inputfilter/models/base_filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/models/base_filter/_base_filter.pxd b/flask_inputfilter/models/base_filter/_base_filter.pxd new file mode 100644 index 0000000..0577bdd --- /dev/null +++ b/flask_inputfilter/models/base_filter/_base_filter.pxd @@ -0,0 +1,2 @@ +cdef class BaseFilter: + cpdef object apply(self, object value) diff --git a/flask_inputfilter/models/base_filter/_base_filter.pyx b/flask_inputfilter/models/base_filter/_base_filter.pyx new file mode 100644 index 0000000..bc654a2 --- /dev/null +++ b/flask_inputfilter/models/base_filter/_base_filter.pyx @@ -0,0 +1,23 @@ +# cython: language=c++ + +cdef class BaseFilter: + """ + BaseFilter-Class. + + Every filter should inherit from it. + """ + + cpdef object apply(self, object value): + """ + Apply the filter to the given value. + + Args: + value: The value to apply the filter to. + + Returns: + The filtered value. + + Raises: + NotImplementedError: If the method is not implemented by subclasses. + """ + raise NotImplementedError("Subclasses must implement the apply method") diff --git a/flask_inputfilter/filters/base_filter.py b/flask_inputfilter/models/base_filter/base_filter.py similarity index 59% rename from flask_inputfilter/filters/base_filter.py rename to flask_inputfilter/models/base_filter/base_filter.py index aa2674e..b74d7df 100644 --- a/flask_inputfilter/filters/base_filter.py +++ b/flask_inputfilter/models/base_filter/base_filter.py @@ -1,16 +1,16 @@ from __future__ import annotations -from abc import ABC, abstractmethod from typing import Any -class BaseFilter(ABC): +class BaseFilter: """ BaseFilter-Class. Every filter should inherit from it. """ - @abstractmethod def apply(self, value: Any) -> Any: - pass + raise NotImplementedError( + "The apply method must be implemented in filters." + ) diff --git a/flask_inputfilter/models/base_validator/__init__.py b/flask_inputfilter/models/base_validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/models/base_validator/_base_validator.pxd b/flask_inputfilter/models/base_validator/_base_validator.pxd new file mode 100644 index 0000000..3cad826 --- /dev/null +++ b/flask_inputfilter/models/base_validator/_base_validator.pxd @@ -0,0 +1,2 @@ +cdef class BaseValidator: + cpdef void validate(self, object value) except * \ No newline at end of file diff --git a/flask_inputfilter/models/base_validator/_base_validator.pyx b/flask_inputfilter/models/base_validator/_base_validator.pyx new file mode 100644 index 0000000..f3d6bd7 --- /dev/null +++ b/flask_inputfilter/models/base_validator/_base_validator.pyx @@ -0,0 +1,21 @@ +# cython: language=c++ + +cdef class BaseValidator: + """ + BaseValidator-Class. + + Every validator should inherit from it. + """ + + cpdef void validate(self, object value) except *: + """ + Validate the given value. + + Args: + value: The value to validate. + + Raises: + ValidationError: If the value is invalid. + NotImplementedError: If the method is not implemented by subclasses. + """ + raise NotImplementedError("Subclasses must implement the validate method") \ No newline at end of file diff --git a/flask_inputfilter/models/base_validator/base_validator.py b/flask_inputfilter/models/base_validator/base_validator.py new file mode 100644 index 0000000..0395a9c --- /dev/null +++ b/flask_inputfilter/models/base_validator/base_validator.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any + + +class BaseValidator: + """ + BaseValidator-Class. + + Every validator should inherit from it. + """ + + def validate(self, value: Any) -> None: + raise NotImplementedError( + "The validate method must be implemented in validators." + ) diff --git a/flask_inputfilter/models/cimports.pxd b/flask_inputfilter/models/cimports.pxd new file mode 100644 index 0000000..90d6e3a --- /dev/null +++ b/flask_inputfilter/models/cimports.pxd @@ -0,0 +1,23 @@ +from .base_filter._base_filter cimport BaseFilter +from .base_validator._base_validator cimport BaseValidator +from .base_condition._base_condition cimport BaseCondition +from .external_api_config._external_api_config cimport ExternalApiConfig +from .field_model._field_model cimport FieldModel + +from .._input_filter cimport InputFilter + +from cpython.object cimport PyObject + +ctypedef object Any +ctypedef object Optional +ctypedef object Union +ctypedef object Type +ctypedef object Dict +ctypedef object List +ctypedef object Tuple + +ctypedef dict PyDict +ctypedef list PyList +ctypedef tuple PyTuple +ctypedef str PyStr +ctypedef bint PyBool \ No newline at end of file diff --git a/flask_inputfilter/models/external_api_config/__init__.py b/flask_inputfilter/models/external_api_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/models/external_api_config/_external_api_config.pxd b/flask_inputfilter/models/external_api_config/_external_api_config.pxd new file mode 100644 index 0000000..63f82ff --- /dev/null +++ b/flask_inputfilter/models/external_api_config/_external_api_config.pxd @@ -0,0 +1,11 @@ +from typing import Any + + +cdef class ExternalApiConfig: + cdef public: + str url + str method + dict[str, Any] params + str data_key + str api_key + dict[str, str] headers diff --git a/flask_inputfilter/models/external_api_config/_external_api_config.pyx b/flask_inputfilter/models/external_api_config/_external_api_config.pyx new file mode 100644 index 0000000..4c09bc7 --- /dev/null +++ b/flask_inputfilter/models/external_api_config/_external_api_config.pyx @@ -0,0 +1,38 @@ +# cython: language=c++ +# cython: freelist=256 + +import cython +from typing import Any + + +@cython.final +cdef class ExternalApiConfig: + """ + Configuration for an external API call. + + **Parameters:** + + - **url** (*str*): The URL of the external API. + - **method** (*str*): The HTTP method to use. + - **params** (*Optional[dict[str, Any]]*): The parameters to send to + the API. + - **data_key** (*Optional[str]*): The key in the response JSON to use + - **api_key** (*Optional[str]*): The API key to use. + - **headers** (*Optional[dict[str, str]]*): The headers to send to the API. + """ + + def __init__( + self, + str url, + str method, + dict[str, Any] params=None, + str data_key=None, + str api_key=None, + dict[str, str] headers=None + ) -> None: + self.url = url + self.method = method + self.params = params + self.data_key = data_key + self.api_key = api_key + self.headers = headers diff --git a/flask_inputfilter/models/external_api_config.py b/flask_inputfilter/models/external_api_config/external_api_config.py similarity index 100% rename from flask_inputfilter/models/external_api_config.py rename to flask_inputfilter/models/external_api_config/external_api_config.py diff --git a/flask_inputfilter/models/field_model.py b/flask_inputfilter/models/field_model.py deleted file mode 100644 index ae0b6af..0000000 --- a/flask_inputfilter/models/field_model.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from typing import Any, Optional, Union - -from flask_inputfilter.filters import BaseFilter -from flask_inputfilter.models import ExternalApiConfig -from flask_inputfilter.validators import BaseValidator - - -class FieldModel: - """FieldModel is a dataclass that represents a field in the input data.""" - - 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/models/field_model/__init__.py b/flask_inputfilter/models/field_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_inputfilter/models/field_model/_field_model.pxd b/flask_inputfilter/models/field_model/_field_model.pxd new file mode 100644 index 0000000..1e187b3 --- /dev/null +++ b/flask_inputfilter/models/field_model/_field_model.pxd @@ -0,0 +1,13 @@ +from flask_inputfilter.models.cimports cimport BaseValidator, BaseFilter, ExternalApiConfig + + +cdef class FieldModel: + cdef public: + bint required + object _default + object fallback + list[BaseFilter] filters + list[BaseValidator] validators + list steps + ExternalApiConfig external_api + str copy diff --git a/flask_inputfilter/models/_field_model.pyx b/flask_inputfilter/models/field_model/_field_model.pyx similarity index 56% rename from flask_inputfilter/models/_field_model.pyx rename to flask_inputfilter/models/field_model/_field_model.pyx index 9e5bbe8..2985134 100644 --- a/flask_inputfilter/models/_field_model.pyx +++ b/flask_inputfilter/models/field_model/_field_model.pyx @@ -1,9 +1,10 @@ # cython: language=c++ # cython: freelist=256 -from __future__ import annotations import cython +from typing import Any +from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, ExternalApiConfig cdef list EMPTY_LIST = [] @@ -14,33 +15,24 @@ 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): + def default(self) -> Any: return self._default @default.setter - def default(self, value): + def default(self, value: Any) -> None: self._default = value def __init__( self, - bint required = False, - object default = None, - object fallback = None, - list filters = None, - list validators = None, - list steps = None, - object external_api = None, - str copy = None + bint required=False, + object default=None, + object fallback=None, + list[BaseFilter] filters=None, + list[BaseValidator] validators=None, + list steps=None, + ExternalApiConfig external_api=None, + str copy=None ) -> None: self.required = required self._default = default diff --git a/flask_inputfilter/models/field_model/field_model.py b/flask_inputfilter/models/field_model/field_model.py new file mode 100644 index 0000000..a2d1832 --- /dev/null +++ b/flask_inputfilter/models/field_model/field_model.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Optional, Union + +if TYPE_CHECKING: + from flask_inputfilter.models import ( + BaseFilter, + BaseValidator, + ExternalApiConfig, + ) + + +@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 diff --git a/flask_inputfilter/validators/__init__.py b/flask_inputfilter/validators/__init__.py index 4628c09..931f60a 100644 --- a/flask_inputfilter/validators/__init__.py +++ b/flask_inputfilter/validators/__init__.py @@ -1,4 +1,4 @@ -from flask_inputfilter.validators.base_validator import BaseValidator +from flask_inputfilter.models import BaseValidator from .and_validator import AndValidator from .array_element_validator import ArrayElementValidator @@ -49,10 +49,10 @@ from .xor_validator import XorValidator __all__ = [ - "BaseValidator", "AndValidator", "ArrayElementValidator", "ArrayLengthValidator", + "BaseValidator", "CustomJsonValidator", "DateAfterValidator", "DateBeforeValidator", @@ -82,9 +82,9 @@ "IsRgbColorValidator", "IsStringValidator", "IsTypedDictValidator", + "IsUUIDValidator", "IsUppercaseValidator", "IsUrlValidator", - "IsUUIDValidator", "IsVerticalImageValidator", "IsWeekdayValidator", "IsWeekendValidator", @@ -94,4 +94,5 @@ "OrValidator", "RangeValidator", "RegexValidator", + "XorValidator", ] diff --git a/flask_inputfilter/validators/and_validator.py b/flask_inputfilter/validators/and_validator.py index cf90352..da539c5 100644 --- a/flask_inputfilter/validators/and_validator.py +++ b/flask_inputfilter/validators/and_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class AndValidator(BaseValidator): @@ -41,7 +41,7 @@ def __init__(self): ]) """ - __slots__ = ("validators", "error_message") + __slots__ = ("error_message", "validators") def __init__( self, diff --git a/flask_inputfilter/validators/array_element_validator.py b/flask_inputfilter/validators/array_element_validator.py index cb3f36d..5c2897e 100644 --- a/flask_inputfilter/validators/array_element_validator.py +++ b/flask_inputfilter/validators/array_element_validator.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator if TYPE_CHECKING: from flask_inputfilter import InputFilter @@ -63,9 +63,7 @@ def __init__(self): def __init__( self, - element_filter: Union[ - "InputFilter", BaseValidator, list[BaseValidator] - ], + element_filter: Union[InputFilter, BaseValidator, list[BaseValidator]], error_message: Optional[str] = None, ) -> None: self.element_filter = element_filter @@ -83,7 +81,7 @@ def validate(self, value: Any) -> None: value[i] = element continue - elif isinstance(self.element_filter, list) and all( + if isinstance(self.element_filter, list) and all( isinstance(v, BaseValidator) for v in self.element_filter ): for validator in self.element_filter: diff --git a/flask_inputfilter/validators/array_length_validator.py b/flask_inputfilter/validators/array_length_validator.py index e1069b4..86d7c66 100644 --- a/flask_inputfilter/validators/array_length_validator.py +++ b/flask_inputfilter/validators/array_length_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class ArrayLengthValidator(BaseValidator): @@ -37,7 +37,7 @@ def __init__(self): ]) """ - __slots__ = ("min_length", "max_length", "error_message") + __slots__ = ("error_message", "max_length", "min_length") def __init__( self, diff --git a/flask_inputfilter/validators/custom_json_validator.py b/flask_inputfilter/validators/custom_json_validator.py index ca7bcc4..3c78ccb 100644 --- a/flask_inputfilter/validators/custom_json_validator.py +++ b/flask_inputfilter/validators/custom_json_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class CustomJsonValidator(BaseValidator): @@ -44,12 +44,12 @@ def __init__(self): ]) """ - __slots__ = ("required_fields", "schema", "error_message") + __slots__ = ("error_message", "required_fields", "schema") def __init__( self, - required_fields: list[str] = None, - schema: dict = None, + required_fields: Optional[list[str]] = None, + schema: Optional[dict] = None, error_message: Optional[str] = None, ) -> None: self.required_fields = required_fields or [] diff --git a/flask_inputfilter/validators/date_after_validator.py b/flask_inputfilter/validators/date_after_validator.py index ce9fc12..1b3256a 100644 --- a/flask_inputfilter/validators/date_after_validator.py +++ b/flask_inputfilter/validators/date_after_validator.py @@ -1,11 +1,13 @@ from __future__ import annotations -from datetime import date, datetime -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator + +if TYPE_CHECKING: + from datetime import date, datetime class DateAfterValidator(BaseValidator): @@ -39,7 +41,7 @@ def __init__(self): ]) """ - __slots__ = ("reference_date", "error_message") + __slots__ = ("error_message", "reference_date") def __init__( self, diff --git a/flask_inputfilter/validators/date_before_validator.py b/flask_inputfilter/validators/date_before_validator.py index 5105941..a82cb5f 100644 --- a/flask_inputfilter/validators/date_before_validator.py +++ b/flask_inputfilter/validators/date_before_validator.py @@ -1,11 +1,13 @@ from __future__ import annotations -from datetime import date, datetime -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator + +if TYPE_CHECKING: + from datetime import date, datetime class DateBeforeValidator(BaseValidator): @@ -38,7 +40,7 @@ def __init__(self): ]) """ - __slots__ = ("reference_date", "error_message") + __slots__ = ("error_message", "reference_date") def __init__( self, diff --git a/flask_inputfilter/validators/date_range_validator.py b/flask_inputfilter/validators/date_range_validator.py index d78ed6d..eccbbac 100644 --- a/flask_inputfilter/validators/date_range_validator.py +++ b/flask_inputfilter/validators/date_range_validator.py @@ -1,11 +1,13 @@ from __future__ import annotations -from datetime import date, datetime -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator + +if TYPE_CHECKING: + from datetime import date, datetime class DateRangeValidator(BaseValidator): @@ -42,7 +44,7 @@ def __init__(self): ]) """ - __slots__ = ("min_date", "max_date", "error_message") + __slots__ = ("error_message", "max_date", "min_date") def __init__( self, diff --git a/flask_inputfilter/validators/float_precision_validator.py b/flask_inputfilter/validators/float_precision_validator.py index aa2392b..0ff4452 100644 --- a/flask_inputfilter/validators/float_precision_validator.py +++ b/flask_inputfilter/validators/float_precision_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class FloatPrecisionValidator(BaseValidator): @@ -39,7 +39,7 @@ def __init__(self): ]) """ - __slots__ = ("precision", "scale", "error_message") + __slots__ = ("error_message", "precision", "scale") def __init__( self, diff --git a/flask_inputfilter/validators/in_array_validator.py b/flask_inputfilter/validators/in_array_validator.py index abe72ee..1422fae 100644 --- a/flask_inputfilter/validators/in_array_validator.py +++ b/flask_inputfilter/validators/in_array_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class InArrayValidator(BaseValidator): @@ -38,7 +38,7 @@ def __init__(self): ]) """ - __slots__ = ("haystack", "strict", "error_message") + __slots__ = ("error_message", "haystack", "strict") def __init__( self, diff --git a/flask_inputfilter/validators/in_enum_validator.py b/flask_inputfilter/validators/in_enum_validator.py index 48e3ef2..32e0d7e 100644 --- a/flask_inputfilter/validators/in_enum_validator.py +++ b/flask_inputfilter/validators/in_enum_validator.py @@ -1,10 +1,12 @@ from __future__ import annotations -from enum import Enum -from typing import Any, Optional, Type +from typing import TYPE_CHECKING, Any, Optional, Type from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator + +if TYPE_CHECKING: + from enum import Enum class InEnumValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_array_validator.py b/flask_inputfilter/validators/is_array_validator.py index 32e341c..a9bcd3a 100644 --- a/flask_inputfilter/validators/is_array_validator.py +++ b/flask_inputfilter/validators/is_array_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsArrayValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_base_64_image_correct_size_validator.py b/flask_inputfilter/validators/is_base_64_image_correct_size_validator.py index 79eb89c..08eb978 100644 --- a/flask_inputfilter/validators/is_base_64_image_correct_size_validator.py +++ b/flask_inputfilter/validators/is_base_64_image_correct_size_validator.py @@ -5,12 +5,12 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsBase64ImageCorrectSizeValidator(BaseValidator): """ - Hecks whether a Base64 encoded image has a size within the allowed range. + Checks whether a Base64 encoded image has a size within the allowed range. By default, the image size must be between 1 and 4MB. **Parameters:** @@ -43,7 +43,7 @@ def __init__(self): ]) """ - __slots__ = ("min_size", "max_size", "error_message") + __slots__ = ("error_message", "max_size", "min_size") def __init__( self, diff --git a/flask_inputfilter/validators/is_base_64_image_validator.py b/flask_inputfilter/validators/is_base_64_image_validator.py index 98dbdfb..cd77e54 100644 --- a/flask_inputfilter/validators/is_base_64_image_validator.py +++ b/flask_inputfilter/validators/is_base_64_image_validator.py @@ -8,7 +8,7 @@ from PIL import Image from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsBase64ImageValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_boolean_validator.py b/flask_inputfilter/validators/is_boolean_validator.py index 9eb19ef..86b08a4 100644 --- a/flask_inputfilter/validators/is_boolean_validator.py +++ b/flask_inputfilter/validators/is_boolean_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsBooleanValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_dataclass_validator.py b/flask_inputfilter/validators/is_dataclass_validator.py index f50b844..9887d50 100644 --- a/flask_inputfilter/validators/is_dataclass_validator.py +++ b/flask_inputfilter/validators/is_dataclass_validator.py @@ -1,10 +1,10 @@ from __future__ import annotations import dataclasses -from typing import Any, Optional, Type, TypeVar, Union, _GenericAlias +from typing import Any, ClassVar, Optional, Type, TypeVar, Union, _GenericAlias from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator T = TypeVar("T") @@ -74,7 +74,7 @@ def __init__(self): __slots__ = ("dataclass_type", "error_message") - _ERROR_TEMPLATES = { + _ERROR_TEMPLATES: ClassVar = { "not_dict": "The provided value is not a dict instance.", "not_dataclass": "'{dataclass_type}' is not a valid dataclass.", "missing_field": "Missing required field '{field_name}' in value " diff --git a/flask_inputfilter/validators/is_date_validator.py b/flask_inputfilter/validators/is_date_validator.py index ff38f16..6f99761 100644 --- a/flask_inputfilter/validators/is_date_validator.py +++ b/flask_inputfilter/validators/is_date_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsDateValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_datetime_validator.py b/flask_inputfilter/validators/is_datetime_validator.py index 991002e..2827657 100644 --- a/flask_inputfilter/validators/is_datetime_validator.py +++ b/flask_inputfilter/validators/is_datetime_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsDateTimeValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_float_validator.py b/flask_inputfilter/validators/is_float_validator.py index e866991..afc73b6 100644 --- a/flask_inputfilter/validators/is_float_validator.py +++ b/flask_inputfilter/validators/is_float_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsFloatValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_future_date_validator.py b/flask_inputfilter/validators/is_future_date_validator.py index 24e0605..9d55ea9 100644 --- a/flask_inputfilter/validators/is_future_date_validator.py +++ b/flask_inputfilter/validators/is_future_date_validator.py @@ -1,11 +1,11 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsFutureDateValidator(BaseValidator): @@ -15,6 +15,8 @@ class IsFutureDateValidator(BaseValidator): **Parameters:** + - **tz** (*Optional[timezone]*, default: ``timezone.utc``): Timezone to + use for comparison. - **error_message** (*Optional[str]*): Custom error message if the date is not in the future. @@ -37,13 +39,24 @@ def __init__(self): ]) """ - __slots__ = ("error_message",) + __slots__ = ("error_message", "tz") - def __init__(self, error_message: Optional[str] = None) -> None: + def __init__( + self, + tz: Optional[timezone] = None, + error_message: Optional[str] = None, + ) -> None: + self.tz = tz or timezone.utc self.error_message = error_message def validate(self, value: Any) -> None: - if parse_date(value) <= datetime.now(): + parsed_date = parse_date(value) + current_time = datetime.now(self.tz) + + if parsed_date.tzinfo is None: + parsed_date = parsed_date.replace(tzinfo=self.tz) + + if parsed_date <= current_time: raise ValidationError( self.error_message or f"Date '{value}' is not in the future." ) diff --git a/flask_inputfilter/validators/is_hexadecimal_validator.py b/flask_inputfilter/validators/is_hexadecimal_validator.py index 0322474..1944db1 100644 --- a/flask_inputfilter/validators/is_hexadecimal_validator.py +++ b/flask_inputfilter/validators/is_hexadecimal_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsHexadecimalValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_horizontal_image_validator.py b/flask_inputfilter/validators/is_horizontal_image_validator.py index 8d635cb..7bd54a9 100644 --- a/flask_inputfilter/validators/is_horizontal_image_validator.py +++ b/flask_inputfilter/validators/is_horizontal_image_validator.py @@ -8,7 +8,7 @@ from PIL.Image import Image as ImageType from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsHorizontalImageValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_html_validator.py b/flask_inputfilter/validators/is_html_validator.py index ffb93de..308d05c 100644 --- a/flask_inputfilter/validators/is_html_validator.py +++ b/flask_inputfilter/validators/is_html_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsHtmlValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_instance_validator.py b/flask_inputfilter/validators/is_instance_validator.py index 12c0806..8155ae4 100644 --- a/flask_inputfilter/validators/is_instance_validator.py +++ b/flask_inputfilter/validators/is_instance_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional, Type from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsInstanceValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_integer_validator.py b/flask_inputfilter/validators/is_integer_validator.py index e0e501f..953d9bd 100644 --- a/flask_inputfilter/validators/is_integer_validator.py +++ b/flask_inputfilter/validators/is_integer_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsIntegerValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_json_validator.py b/flask_inputfilter/validators/is_json_validator.py index f5e7b5d..331118c 100644 --- a/flask_inputfilter/validators/is_json_validator.py +++ b/flask_inputfilter/validators/is_json_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsJsonValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_lowercase_validator.py b/flask_inputfilter/validators/is_lowercase_validator.py index bed3d42..1564557 100644 --- a/flask_inputfilter/validators/is_lowercase_validator.py +++ b/flask_inputfilter/validators/is_lowercase_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsLowercaseValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_mac_address_validator.py b/flask_inputfilter/validators/is_mac_address_validator.py index 64b61ae..50b879d 100644 --- a/flask_inputfilter/validators/is_mac_address_validator.py +++ b/flask_inputfilter/validators/is_mac_address_validator.py @@ -5,7 +5,7 @@ from flask_inputfilter.enums import RegexEnum from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator MAC_ADDRESS_PATTERN = re.compile(RegexEnum.MAC_ADDRESS.value) diff --git a/flask_inputfilter/validators/is_past_date_validator.py b/flask_inputfilter/validators/is_past_date_validator.py index 12249fb..dbbb086 100644 --- a/flask_inputfilter/validators/is_past_date_validator.py +++ b/flask_inputfilter/validators/is_past_date_validator.py @@ -1,11 +1,11 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsPastDateValidator(BaseValidator): @@ -15,6 +15,8 @@ class IsPastDateValidator(BaseValidator): **Parameters:** + - **tz** (*Optional[timezone]*, default: ``timezone.utc``): Timezone to + use for comparison. - **error_message** (*Optional[str]*): Custom error message if the date is not in the past. @@ -36,13 +38,24 @@ def __init__(self): ]) """ - __slots__ = ("error_message",) + __slots__ = ("error_message", "tz") - def __init__(self, error_message: Optional[str] = None) -> None: + def __init__( + self, + tz: Optional[timezone] = None, + error_message: Optional[str] = None, + ) -> None: + self.tz = tz or timezone.utc self.error_message = error_message def validate(self, value: Any) -> None: - if parse_date(value) >= datetime.now(): + parsed_date = parse_date(value) + current_time = datetime.now(self.tz) + + if parsed_date.tzinfo is None: + parsed_date = parsed_date.replace(tzinfo=self.tz) + + if parsed_date >= current_time: raise ValidationError( self.error_message or f"Date '{value}' is not in the past." ) diff --git a/flask_inputfilter/validators/is_port_validator.py b/flask_inputfilter/validators/is_port_validator.py index 7fb55ce..5b4d943 100644 --- a/flask_inputfilter/validators/is_port_validator.py +++ b/flask_inputfilter/validators/is_port_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsPortValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_rgb_color_validator.py b/flask_inputfilter/validators/is_rgb_color_validator.py index 2c2a803..dd065b4 100644 --- a/flask_inputfilter/validators/is_rgb_color_validator.py +++ b/flask_inputfilter/validators/is_rgb_color_validator.py @@ -5,7 +5,7 @@ from flask_inputfilter.enums import RegexEnum from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator RGB_COLOR_PATTERN = re.compile(RegexEnum.RGB_COLOR.value) diff --git a/flask_inputfilter/validators/is_string_validator.py b/flask_inputfilter/validators/is_string_validator.py index 356f1e9..a4eaac0 100644 --- a/flask_inputfilter/validators/is_string_validator.py +++ b/flask_inputfilter/validators/is_string_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsStringValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_typed_dict_validator.py b/flask_inputfilter/validators/is_typed_dict_validator.py index 45a6019..9722242 100644 --- a/flask_inputfilter/validators/is_typed_dict_validator.py +++ b/flask_inputfilter/validators/is_typed_dict_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional, Type from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsTypedDictValidator(BaseValidator): @@ -42,9 +42,9 @@ def __init__(self): """ __slots__ = ( + "error_message", "typed_dict_expected_keys", "typed_dict_name", - "error_message", ) def __init__( diff --git a/flask_inputfilter/validators/is_uppercase_validator.py b/flask_inputfilter/validators/is_uppercase_validator.py index b2d8f42..5b3bea7 100644 --- a/flask_inputfilter/validators/is_uppercase_validator.py +++ b/flask_inputfilter/validators/is_uppercase_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsUppercaseValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_url_validator.py b/flask_inputfilter/validators/is_url_validator.py index 4302f33..235e470 100644 --- a/flask_inputfilter/validators/is_url_validator.py +++ b/flask_inputfilter/validators/is_url_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsUrlValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_uuid_validator.py b/flask_inputfilter/validators/is_uuid_validator.py index b3792ad..e9d92aa 100644 --- a/flask_inputfilter/validators/is_uuid_validator.py +++ b/flask_inputfilter/validators/is_uuid_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsUUIDValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_vertical_image_validator.py b/flask_inputfilter/validators/is_vertical_image_validator.py index 7f261c2..d8a1f8c 100644 --- a/flask_inputfilter/validators/is_vertical_image_validator.py +++ b/flask_inputfilter/validators/is_vertical_image_validator.py @@ -9,7 +9,7 @@ from PIL.Image import Image as ImageType from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsVerticalImageValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_weekday_validator.py b/flask_inputfilter/validators/is_weekday_validator.py index 42216be..d9baba7 100644 --- a/flask_inputfilter/validators/is_weekday_validator.py +++ b/flask_inputfilter/validators/is_weekday_validator.py @@ -4,7 +4,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsWeekdayValidator(BaseValidator): diff --git a/flask_inputfilter/validators/is_weekend_validator.py b/flask_inputfilter/validators/is_weekend_validator.py index 16cbb6b..5a69acb 100644 --- a/flask_inputfilter/validators/is_weekend_validator.py +++ b/flask_inputfilter/validators/is_weekend_validator.py @@ -4,7 +4,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.helpers import parse_date -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class IsWeekendValidator(BaseValidator): diff --git a/flask_inputfilter/validators/length_validator.py b/flask_inputfilter/validators/length_validator.py index 1bd697d..5ac7a53 100644 --- a/flask_inputfilter/validators/length_validator.py +++ b/flask_inputfilter/validators/length_validator.py @@ -4,7 +4,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class LengthEnum(Enum): @@ -43,7 +43,7 @@ def __init__(self): ]) """ - __slots__ = ("min_length", "max_length", "error_message") + __slots__ = ("error_message", "max_length", "min_length") def __init__( self, diff --git a/flask_inputfilter/validators/not_in_array_validator.py b/flask_inputfilter/validators/not_in_array_validator.py index 4e86a45..62986f9 100644 --- a/flask_inputfilter/validators/not_in_array_validator.py +++ b/flask_inputfilter/validators/not_in_array_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class NotInArrayValidator(BaseValidator): @@ -37,7 +37,7 @@ def __init__(self): ]) """ - __slots__ = ("haystack", "strict", "error_message") + __slots__ = ("error_message", "haystack", "strict") def __init__( self, diff --git a/flask_inputfilter/validators/not_validator.py b/flask_inputfilter/validators/not_validator.py index a8d7e24..c7c9aeb 100644 --- a/flask_inputfilter/validators/not_validator.py +++ b/flask_inputfilter/validators/not_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class NotValidator(BaseValidator): @@ -37,7 +37,7 @@ def __init__(self): ]) """ - __slots__ = ("validator", "error_message") + __slots__ = ("error_message", "validator") def __init__( self, diff --git a/flask_inputfilter/validators/or_validator.py b/flask_inputfilter/validators/or_validator.py index b2619ea..b597cb8 100644 --- a/flask_inputfilter/validators/or_validator.py +++ b/flask_inputfilter/validators/or_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class OrValidator(BaseValidator): @@ -40,7 +40,7 @@ def __init__(self): ]) """ - __slots__ = ("validators", "error_message") + __slots__ = ("error_message", "validators") def __init__( self, diff --git a/flask_inputfilter/validators/range_validator.py b/flask_inputfilter/validators/range_validator.py index eb8d484..7b2cf81 100644 --- a/flask_inputfilter/validators/range_validator.py +++ b/flask_inputfilter/validators/range_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional, Union from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class RangeValidator(BaseValidator): @@ -35,7 +35,7 @@ def __init__(self): ]) """ - __slots__ = ("min_value", "max_value", "error_message") + __slots__ = ("error_message", "max_value", "min_value") def __init__( self, diff --git a/flask_inputfilter/validators/regex_validator.py b/flask_inputfilter/validators/regex_validator.py index f4614ee..4b458c1 100644 --- a/flask_inputfilter/validators/regex_validator.py +++ b/flask_inputfilter/validators/regex_validator.py @@ -4,7 +4,7 @@ from typing import Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class RegexValidator(BaseValidator): @@ -36,7 +36,7 @@ def __init__(self): ]) """ - __slots__ = ("pattern", "error_message") + __slots__ = ("error_message", "pattern") def __init__( self, diff --git a/flask_inputfilter/validators/xor_validator.py b/flask_inputfilter/validators/xor_validator.py index 6f23f76..f92e8a6 100644 --- a/flask_inputfilter/validators/xor_validator.py +++ b/flask_inputfilter/validators/xor_validator.py @@ -3,7 +3,7 @@ from typing import Any, Optional from flask_inputfilter.exceptions import ValidationError -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class XorValidator(BaseValidator): @@ -40,7 +40,7 @@ def __init__(self): ]) """ - __slots__ = ("validators", "error_message") + __slots__ = ("error_message", "validators") def __init__( self, diff --git a/pyproject.toml b/pyproject.toml index a03944d..bec7db7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,15 +33,11 @@ classifiers = [ [project.optional-dependencies] dev = [ - "autoflake", - "black", "build", "coverage", "coveralls", "cython", - "flake8-pyproject==1.2.3", - "flake8==5.0.4", - "isort", + "docformatter", "pillow>=8.0.0", "pytest", "requests>=2.22.0", @@ -49,7 +45,7 @@ dev = [ "sphinx-autobuild", "sphinx_design", "sphinx_rtd_theme", - "docformatter", + "ruff" ] optional = [ "pillow>=8.0.0", @@ -100,21 +96,54 @@ source = ["flask_inputfilter"] [tool.coverage.report] omit = ["__init__.py", "*/tests/*"] -[tool.flake8] -exclude = ["__init__.py", "*.md", ".*"] -max-line-length = 79 - -[tool.black] +[tool.ruff] line-length = 79 +#extend-include = ["*.pyx", "*.pxd"] +target-version = "py37" +fix = true +exclude = [ + "tests", +] -[tool.isort] -profile = 'black' -line_length = 79 -known_first_party = [ - "flask_inputfilter/conditions/base_condition", - "flask_inputfilter/filters/base_filter", - "flask_inputfilter/validators/base_validator" +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "N", # pep8-naming + "YTT", # flake8-2020 + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "EM", # flake8-errmsg + "PIE", # flake8-pie + "RSE", # flake8-raise + "RET", # flake8-return + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "PTH", # flake8-use-pathlib + "RUF", # Ruff-specific rules ] +fixable = ["ALL"] +unfixable = [] +ignore = [ + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use a f-string literal, assign to variable first + "UP045", # X | None syntax (Python 3.10+) + "UP006", # Use `list` instead of `List` (Python 3.9+) + "UP007", # Use `X | Y` for unions (Python 3.10+) + "UP035", # Import from collections.abc (Python 3.9+) +] +pyupgrade = [ + true +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" [tool.docformatter] wrap-summaries = 79 diff --git a/scripts/lint b/scripts/lint index 83eecbf..ffdb7ab 100644 --- a/scripts/lint +++ b/scripts/lint @@ -7,14 +7,9 @@ find /app/flask_inputfilter -name "*.py" ! -name "__init__.py" | while read -r f fi done -echo "Running autoflake" -autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive /app - -echo "Running isort" -isort /app - -echo "Running black" -black /app +echo "Running Ruff (lint and fix)" +ruff check --fix /app +ruff format /app echo "Running docformatter" docformatter --in-place --wrap-summaries 79 --wrap-descriptions 79 --recursive --pre-summary-newline /app diff --git a/setup.py b/setup.py index 8246eb9..42ca32f 100644 --- a/setup.py +++ b/setup.py @@ -3,14 +3,15 @@ from setuptools import setup if shutil.which("g++") is not None: + from pathlib import Path + from Cython.Build import cythonize from setuptools.extension import Extension + pyx_files = Path("flask_inputfilter").rglob("*.pyx") + pyx_modules = [ - "flask_inputfilter.mixins._external_api_mixin", - "flask_inputfilter.mixins._field_mixin", - "flask_inputfilter.models._field_model", - "flask_inputfilter._input_filter", + str(path).replace(".pyx", "").replace("/", ".") for path in pyx_files ] ext_modules = cythonize( diff --git a/tests/conditions/test_base_condition.py b/tests/conditions/test_base_condition.py index 8dc0074..3987e2e 100644 --- a/tests/conditions/test_base_condition.py +++ b/tests/conditions/test_base_condition.py @@ -1,10 +1,10 @@ import unittest -from flask_inputfilter.conditions import BaseCondition +from flask_inputfilter.models import BaseCondition class TestBaseCondition(unittest.TestCase): def test_raises_error_when_check_called(self) -> None: """Test that BaseCondition raises a TypeError.""" - with self.assertRaises(TypeError): + with self.assertRaises(NotImplementedError): BaseCondition().check({}) diff --git a/tests/filters/test_base64_image_downscale_filter.py b/tests/filters/test_base64_image_downscale_filter.py index 954ed54..ec11162 100644 --- a/tests/filters/test_base64_image_downscale_filter.py +++ b/tests/filters/test_base64_image_downscale_filter.py @@ -2,10 +2,9 @@ import io import unittest -from PIL import Image - from flask_inputfilter import InputFilter from flask_inputfilter.filters import Base64ImageDownscaleFilter +from PIL import Image class TestBase64ImageDownscaleFilter(unittest.TestCase): @@ -18,7 +17,7 @@ def test_downscale_base64_image_string(self) -> None: "image", filters=[Base64ImageDownscaleFilter(size=144)], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: validated_data = self.input_filter.validate_data( {"image": file.read()} ) @@ -33,7 +32,7 @@ def test_downscale_image_object(self) -> None: "image", filters=[Base64ImageDownscaleFilter(size=144)], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: validated_data = self.input_filter.validate_data( { "image": Image.open( diff --git a/tests/filters/test_base64_image_size_reduce_filter.py b/tests/filters/test_base64_image_size_reduce_filter.py index cf112dc..fc3c736 100644 --- a/tests/filters/test_base64_image_size_reduce_filter.py +++ b/tests/filters/test_base64_image_size_reduce_filter.py @@ -2,10 +2,9 @@ import io import unittest -from PIL import Image - from flask_inputfilter import InputFilter from flask_inputfilter.filters import Base64ImageResizeFilter +from PIL import Image class TestBase64ImageResizeFilter(unittest.TestCase): @@ -20,7 +19,7 @@ def test_resize_base64_image_string_to_max_size(self) -> None: "image", filters=[Base64ImageResizeFilter(max_size=1024)], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: validated_data = self.input_filter.validate_data( {"image": file.read()} ) @@ -40,7 +39,7 @@ def test_resize_image_object_to_max_size(self) -> None: "image", filters=[Base64ImageResizeFilter(max_size=1024)], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: validated_data = self.input_filter.validate_data( { "image": Image.open( diff --git a/tests/filters/test_base_filter.py b/tests/filters/test_base_filter.py index ddfa1f5..acb067e 100644 --- a/tests/filters/test_base_filter.py +++ b/tests/filters/test_base_filter.py @@ -1,10 +1,10 @@ import unittest -from flask_inputfilter.filters import BaseFilter +from flask_inputfilter.models import BaseFilter class TestBaseFilter(unittest.TestCase): def test_apply_raises_type_error(self) -> None: """Should raise TypeError when calling apply on BaseFilter directly.""" - with self.assertRaises(TypeError): + with self.assertRaises(NotImplementedError): BaseFilter().apply("test") diff --git a/tests/test_input_filter.py b/tests/test_input_filter.py index af519ae..fc44c8e 100644 --- a/tests/test_input_filter.py +++ b/tests/test_input_filter.py @@ -3,9 +3,9 @@ from unittest.mock import Mock, patch from flask import Flask, g, jsonify, request - from flask_inputfilter import InputFilter -from flask_inputfilter.conditions import BaseCondition, ExactlyOneOfCondition +from flask_inputfilter.models import BaseCondition +from flask_inputfilter.conditions import ExactlyOneOfCondition from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.filters import ( StringSlugifyFilter, diff --git a/tests/validators/__init__.py b/tests/validators/__init__.py index c078303..08cbcf3 100644 --- a/tests/validators/__init__.py +++ b/tests/validators/__init__.py @@ -1 +1,3 @@ from .base_validator_test import BaseValidatorTest + +__all__ = ["BaseValidatorTest"] diff --git a/tests/validators/test_and_validator.py b/tests/validators/test_and_validator.py index ab52fa8..0588033 100644 --- a/tests/validators/test_and_validator.py +++ b/tests/validators/test_and_validator.py @@ -4,6 +4,7 @@ IsIntegerValidator, RangeValidator, ) + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_array_element_validator.py b/tests/validators/test_array_element_validator.py index 4a600d8..e5e72f5 100644 --- a/tests/validators/test_array_element_validator.py +++ b/tests/validators/test_array_element_validator.py @@ -7,6 +7,7 @@ IsStringValidator, LengthValidator, ) + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_array_length_validator.py b/tests/validators/test_array_length_validator.py index 03e364e..758388b 100644 --- a/tests/validators/test_array_length_validator.py +++ b/tests/validators/test_array_length_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import ArrayLengthValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_base_validator.py b/tests/validators/test_base_validator.py index ce21da7..7b96ccd 100644 --- a/tests/validators/test_base_validator.py +++ b/tests/validators/test_base_validator.py @@ -1,9 +1,9 @@ import unittest -from flask_inputfilter.validators import BaseValidator +from flask_inputfilter.models import BaseValidator class TestBaseValidator(unittest.TestCase): def test_validate_method_raises_type_error(self) -> None: - with self.assertRaises(TypeError): + with self.assertRaises(NotImplementedError): BaseValidator().validate("value") diff --git a/tests/validators/test_custom_json_validator.py b/tests/validators/test_custom_json_validator.py index 442e7ad..4141295 100644 --- a/tests/validators/test_custom_json_validator.py +++ b/tests/validators/test_custom_json_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import CustomJsonValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_date_after_validator.py b/tests/validators/test_date_after_validator.py index 7b96550..06951fe 100644 --- a/tests/validators/test_date_after_validator.py +++ b/tests/validators/test_date_after_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import DateAfterValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_date_before_validator.py b/tests/validators/test_date_before_validator.py index 8497dc3..8471598 100644 --- a/tests/validators/test_date_before_validator.py +++ b/tests/validators/test_date_before_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import DateBeforeValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_date_range_validator.py b/tests/validators/test_date_range_validator.py index 0eec502..27ecf4a 100644 --- a/tests/validators/test_date_range_validator.py +++ b/tests/validators/test_date_range_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import DateRangeValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_float_precision_validator.py b/tests/validators/test_float_precision_validator.py index bfe64f6..1df2816 100644 --- a/tests/validators/test_float_precision_validator.py +++ b/tests/validators/test_float_precision_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import FloatPrecisionValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_in_array_validator.py b/tests/validators/test_in_array_validator.py index 956f56b..0187616 100644 --- a/tests/validators/test_in_array_validator.py +++ b/tests/validators/test_in_array_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import InArrayValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_in_enum_validator.py b/tests/validators/test_in_enum_validator.py index dc15436..ff374ea 100644 --- a/tests/validators/test_in_enum_validator.py +++ b/tests/validators/test_in_enum_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import InEnumValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_array_validator.py b/tests/validators/test_is_array_validator.py index 9a8ee36..6e22191 100644 --- a/tests/validators/test_is_array_validator.py +++ b/tests/validators/test_is_array_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsArrayValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_base64_image_correct_size_validator.py b/tests/validators/test_is_base64_image_correct_size_validator.py index fd018fd..295b306 100644 --- a/tests/validators/test_is_base64_image_correct_size_validator.py +++ b/tests/validators/test_is_base64_image_correct_size_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsBase64ImageCorrectSizeValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_base64_image_validator.py b/tests/validators/test_is_base64_image_validator.py index f90ee37..82c440a 100644 --- a/tests/validators/test_is_base64_image_validator.py +++ b/tests/validators/test_is_base64_image_validator.py @@ -1,12 +1,13 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsBase64ImageValidator + from tests.validators import BaseValidatorTest class TestIsBase64ImageValidator(BaseValidatorTest): def test_valid_base64_image(self) -> None: self.input_filter.add("image", validators=[IsBase64ImageValidator()]) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: self.input_filter.validate_data({"image": file.read()}) def test_invalid_base64_image(self) -> None: diff --git a/tests/validators/test_is_boolean_validator.py b/tests/validators/test_is_boolean_validator.py index 91c991a..da971c1 100644 --- a/tests/validators/test_is_boolean_validator.py +++ b/tests/validators/test_is_boolean_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsBooleanValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_dataclass_validator.py b/tests/validators/test_is_dataclass_validator.py index 56335dd..651dd19 100644 --- a/tests/validators/test_is_dataclass_validator.py +++ b/tests/validators/test_is_dataclass_validator.py @@ -2,6 +2,7 @@ from typing import Optional from flask_inputfilter.validators import IsDataclassValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_date_validator.py b/tests/validators/test_is_date_validator.py index 9ff7ed7..1f8cda0 100644 --- a/tests/validators/test_is_date_validator.py +++ b/tests/validators/test_is_date_validator.py @@ -1,6 +1,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.filters import ToDateFilter from flask_inputfilter.validators import IsDateValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_datetime_validator.py b/tests/validators/test_is_datetime_validator.py index b4d09c1..a7e38c4 100644 --- a/tests/validators/test_is_datetime_validator.py +++ b/tests/validators/test_is_datetime_validator.py @@ -1,6 +1,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.filters import ToDateTimeFilter from flask_inputfilter.validators import IsDateTimeValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_float_validator.py b/tests/validators/test_is_float_validator.py index a79e058..70580be 100644 --- a/tests/validators/test_is_float_validator.py +++ b/tests/validators/test_is_float_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsFloatValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_future_date_validator.py b/tests/validators/test_is_future_date_validator.py index 4eaff30..5c215c8 100644 --- a/tests/validators/test_is_future_date_validator.py +++ b/tests/validators/test_is_future_date_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsFutureDateValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_hexadecimal_validator.py b/tests/validators/test_is_hexadecimal_validator.py index 90dae1f..84c5d13 100644 --- a/tests/validators/test_is_hexadecimal_validator.py +++ b/tests/validators/test_is_hexadecimal_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsHexadecimalValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_horizontally_image_validator.py b/tests/validators/test_is_horizontally_image_validator.py index 8ba3c71..7396c29 100644 --- a/tests/validators/test_is_horizontally_image_validator.py +++ b/tests/validators/test_is_horizontally_image_validator.py @@ -1,10 +1,10 @@ import base64 import io -from PIL import Image - from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsHorizontalImageValidator +from PIL import Image + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_html_validator.py b/tests/validators/test_is_html_validator.py index 1911f53..e2d7521 100644 --- a/tests/validators/test_is_html_validator.py +++ b/tests/validators/test_is_html_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsHtmlValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_instance_validator.py b/tests/validators/test_is_instance_validator.py index a82318a..b4d4ffe 100644 --- a/tests/validators/test_is_instance_validator.py +++ b/tests/validators/test_is_instance_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsInstanceValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_integer_validator.py b/tests/validators/test_is_integer_validator.py index b970b5c..52fb433 100644 --- a/tests/validators/test_is_integer_validator.py +++ b/tests/validators/test_is_integer_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsIntegerValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_json_validator.py b/tests/validators/test_is_json_validator.py index d2665cb..0edea38 100644 --- a/tests/validators/test_is_json_validator.py +++ b/tests/validators/test_is_json_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsJsonValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_lowercase_validator.py b/tests/validators/test_is_lowercase_validator.py index 15b4347..0495373 100644 --- a/tests/validators/test_is_lowercase_validator.py +++ b/tests/validators/test_is_lowercase_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsLowercaseValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_mac_address_validator.py b/tests/validators/test_is_mac_address_validator.py index 6b88028..374349a 100644 --- a/tests/validators/test_is_mac_address_validator.py +++ b/tests/validators/test_is_mac_address_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsMacAddressValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_past_date_validator.py b/tests/validators/test_is_past_date_validator.py index 3cc792b..ec2ad47 100644 --- a/tests/validators/test_is_past_date_validator.py +++ b/tests/validators/test_is_past_date_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsPastDateValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_port_validator.py b/tests/validators/test_is_port_validator.py index 2c757d0..ddd872f 100644 --- a/tests/validators/test_is_port_validator.py +++ b/tests/validators/test_is_port_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsPortValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_rgb_color_validator.py b/tests/validators/test_is_rgb_color_validator.py index 0fa1576..b2309ad 100644 --- a/tests/validators/test_is_rgb_color_validator.py +++ b/tests/validators/test_is_rgb_color_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsRgbColorValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_string_validator.py b/tests/validators/test_is_string_validator.py index cb2f3f4..19bcff3 100644 --- a/tests/validators/test_is_string_validator.py +++ b/tests/validators/test_is_string_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsStringValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_typed_dict_validator.py b/tests/validators/test_is_typed_dict_validator.py index 8d2a934..46e4ccd 100644 --- a/tests/validators/test_is_typed_dict_validator.py +++ b/tests/validators/test_is_typed_dict_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsTypedDictValidator + from tests.validators import BaseValidatorTest # TODO: Readd when Python 3.7 support is dropped diff --git a/tests/validators/test_is_uppercase_validator.py b/tests/validators/test_is_uppercase_validator.py index 6478ecd..746f5c9 100644 --- a/tests/validators/test_is_uppercase_validator.py +++ b/tests/validators/test_is_uppercase_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsUppercaseValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_url_validator.py b/tests/validators/test_is_url_validator.py index 3c69870..67cd8ed 100644 --- a/tests/validators/test_is_url_validator.py +++ b/tests/validators/test_is_url_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsUrlValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_uuid_validator.py b/tests/validators/test_is_uuid_validator.py index e5d6058..6692367 100644 --- a/tests/validators/test_is_uuid_validator.py +++ b/tests/validators/test_is_uuid_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsUUIDValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_vertically_image_validator.py b/tests/validators/test_is_vertically_image_validator.py index ee57c60..d7119ec 100644 --- a/tests/validators/test_is_vertically_image_validator.py +++ b/tests/validators/test_is_vertically_image_validator.py @@ -1,6 +1,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.filters import Base64ImageDownscaleFilter from flask_inputfilter.validators import IsVerticalImageValidator + from tests.validators import BaseValidatorTest @@ -15,7 +16,7 @@ def test_valid_vertical_image(self): ], validators=[IsVerticalImageValidator()], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: self.input_filter.validate_data({"vertically_image": file.read()}) def test_invalid_not_base64(self): @@ -33,7 +34,7 @@ def test_invalid_horizontal_image(self): ], validators=[IsVerticalImageValidator()], ) - with open("tests/data/base64_image.txt", "r") as file: + with open("tests/data/base64_image.txt") as file: with self.assertRaises(ValidationError): self.input_filter.validate_data( {"horizontally_image": file.read()} diff --git a/tests/validators/test_is_weekday_validator.py b/tests/validators/test_is_weekday_validator.py index be00a78..5aea77a 100644 --- a/tests/validators/test_is_weekday_validator.py +++ b/tests/validators/test_is_weekday_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsWeekdayValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_is_weekend_validator.py b/tests/validators/test_is_weekend_validator.py index 68dfe05..1295e71 100644 --- a/tests/validators/test_is_weekend_validator.py +++ b/tests/validators/test_is_weekend_validator.py @@ -2,6 +2,7 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsWeekendValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_length_validator.py b/tests/validators/test_length_validator.py index 9bed924..8cb72e2 100644 --- a/tests/validators/test_length_validator.py +++ b/tests/validators/test_length_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import LengthValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_not_in_array_validator.py b/tests/validators/test_not_in_array_validator.py index 164f19b..bd9591b 100644 --- a/tests/validators/test_not_in_array_validator.py +++ b/tests/validators/test_not_in_array_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import NotInArrayValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_not_validator.py b/tests/validators/test_not_validator.py index b2b47e1..35a18f7 100644 --- a/tests/validators/test_not_validator.py +++ b/tests/validators/test_not_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import IsIntegerValidator, NotValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_or_validator.py b/tests/validators/test_or_validator.py index 1076781..3ab5327 100644 --- a/tests/validators/test_or_validator.py +++ b/tests/validators/test_or_validator.py @@ -4,6 +4,7 @@ IsIntegerValidator, OrValidator, ) + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_range_validator.py b/tests/validators/test_range_validator.py index 6cabeeb..c509e97 100644 --- a/tests/validators/test_range_validator.py +++ b/tests/validators/test_range_validator.py @@ -1,5 +1,6 @@ from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import RangeValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_regex_validator.py b/tests/validators/test_regex_validator.py index 1ba236a..245acf1 100644 --- a/tests/validators/test_regex_validator.py +++ b/tests/validators/test_regex_validator.py @@ -1,6 +1,7 @@ from flask_inputfilter.enums import RegexEnum from flask_inputfilter.exceptions import ValidationError from flask_inputfilter.validators import RegexValidator + from tests.validators import BaseValidatorTest diff --git a/tests/validators/test_xor_validator.py b/tests/validators/test_xor_validator.py index 09f0b1a..e590cfa 100644 --- a/tests/validators/test_xor_validator.py +++ b/tests/validators/test_xor_validator.py @@ -4,6 +4,7 @@ RangeValidator, XorValidator, ) + from tests.validators import BaseValidatorTest