From 256c6b713417707e626e76413e7671380df10629 Mon Sep 17 00:00:00 2001 From: LeanderCS Date: Wed, 8 Jan 2025 18:24:36 +0100 Subject: [PATCH] 4 | Add more filters and validators --- DEVELOPMENT.md | 2 + EXTERNAL_API.md | 118 +++++++++ README.md | 15 +- pyproject.toml | 6 +- requirements.txt | 5 +- setup.py | 6 +- src/flask_inputfilter/Enum/RegexEnum.py | 3 +- .../Exception/ValidationError.py | 2 - src/flask_inputfilter/Filter/README.md | 29 ++- src/flask_inputfilter/Filter/SlugifyFilter.py | 22 ++ .../Filter/StringTrimFilter.py | 2 +- .../Filter/ToAlphaNumericFilter.py | 19 ++ .../Filter/ToBooleanFilter.py | 2 +- .../Filter/ToCamelCaseFilter.py | 2 +- src/flask_inputfilter/Filter/ToFloatFilter.py | 2 +- .../Filter/ToIntegerFilter.py | 2 +- src/flask_inputfilter/Filter/ToLowerFilter.py | 2 +- .../Filter/ToNormalizedUnicodeFilter.py | 33 +++ src/flask_inputfilter/Filter/ToNullFilter.py | 2 +- .../Filter/ToStringFilter.py | 2 +- src/flask_inputfilter/Filter/ToUpperFilter.py | 2 +- .../Filter/TruncateFilter.py | 23 ++ src/flask_inputfilter/Filter/__init__.py | 4 + src/flask_inputfilter/InputFilter.py | 223 +++++++++++++++--- .../Validator/ArrayElementValidator.py | 3 +- .../Validator/BaseValidator.py | 4 +- .../Validator/FloatPrecisionValidator.py | 47 ++++ .../Validator/InArrayValidator.py | 6 +- .../Validator/InEnumValidator.py | 12 +- .../Validator/IsArrayValidator.py | 6 +- .../IsBase64ImageCorrectSizeValidator.py | 2 +- .../Validator/IsBase64ImageValidator.py | 5 +- ...BoolValidator.py => IsBooleanValidator.py} | 8 +- .../Validator/IsFloatValidator.py | 7 +- .../Validator/IsHexadecimalValidator.py | 3 +- .../Validator/IsInstanceValidator.py | 8 +- .../Validator/IsIntegerValidator.py | 6 +- .../Validator/IsStringValidator.py | 6 +- .../Validator/IsUUIDValidator.py | 4 +- .../Validator/LengthValidator.py | 2 +- src/flask_inputfilter/Validator/README.md | 37 +-- .../Validator/RangeValidator.py | 4 +- .../Validator/RegexValidator.py | 6 +- src/flask_inputfilter/Validator/__init__.py | 8 +- test/test_input_filter.py | 70 ------ {test => tests_package}/__init__.py | 0 {test => tests_package}/data/base64_image.txt | 0 {test => tests_package}/test_filter.py | 140 +++++++++-- tests_package/test_input_filter.py | 192 +++++++++++++++ {test => tests_package}/test_validator.py | 125 +++++++--- 50 files changed, 1000 insertions(+), 239 deletions(-) create mode 100644 EXTERNAL_API.md create mode 100644 src/flask_inputfilter/Filter/SlugifyFilter.py create mode 100644 src/flask_inputfilter/Filter/ToAlphaNumericFilter.py create mode 100644 src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py create mode 100644 src/flask_inputfilter/Filter/TruncateFilter.py create mode 100644 src/flask_inputfilter/Validator/FloatPrecisionValidator.py rename src/flask_inputfilter/Validator/{IsBoolValidator.py => IsBooleanValidator.py} (69%) delete mode 100644 test/test_input_filter.py rename {test => tests_package}/__init__.py (100%) rename {test => tests_package}/data/base64_image.txt (100%) rename {test => tests_package}/test_filter.py (54%) create mode 100644 tests_package/test_input_filter.py rename {test => tests_package}/test_validator.py (72%) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3c0b1a6..89a6b02 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -21,5 +21,7 @@ docker exec -it flask-inputfilter pytest ### Run linting ```bash +docker exec -it flask-inputfilter sh -c "isort ." +docker exec -it flask-inputfilter sh -c "autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ." docker exec -it flask-inputfilter black . ``` diff --git a/EXTERNAL_API.md b/EXTERNAL_API.md new file mode 100644 index 0000000..8e670c6 --- /dev/null +++ b/EXTERNAL_API.md @@ -0,0 +1,118 @@ +# External API Functionality in `InputFilter` + +This documentation provides a comprehensive overview of the external API functionality available in the `InputFilter` class. It covers the configuration, core methods, and examples of usage for interacting with external APIs. + +--- + +## 1. Overview + +The `InputFilter` class includes a mechanism for fetching data from external APIs during the input validation process. +This feature allows dynamic data retrieval based on user inputs, such as validating fields or fetching related data from an external service. + +Important to know, the external api functionality runs after all other filters and validators have been executed. +This means that the data fetched from the external API will not be validated or filtered. + +--- + +## 2. Configuration + +The external API functionality is configured via the `external_api` parameter in the `add` method. This parameter accepts a dictionary with the following structure: + +### `ExternalApiConfig` Fields + +| Field | Type | Description | +|------------|--------------------------|-----------------------------------------------------------------------------| +| `url` | `str` | The URL of the external API, with optional placeholders in `{{}}` format. | +| `method` | `str` | The HTTP method to use (e.g., `GET`, `POST`). | +| `params` | `Optional[Dict[str, str]]` | Query parameters for the API, with placeholders allowed. | +| `data_key` | `Optional[str]` | Key in the JSON response to extract the required data. | +| `api_key` | `Optional[str]` | API key for authorization, sent in the `Authorization` header. | + +--- + +## 3. Examples + +### 3.1 Basic External API Integration + +```python +from flask_inputfilter.InputFilter import InputFilter + +class MyInputFilter(InputFilter): + def __init__(self): + super().__init__() + + self.add( + "user_id", required=True + ) + self.add( + "is_active", + required=True, + external_api={ + "url": "https://api.example.com/users/{{user_id}}/status", + "method": "GET", + "data_key": "is_active", + }, + ) + +# Example usage +filter_instance = MyInputFilter() +validated_data = filter_instance.validateData({"user_id": 123}) +print(validated_data["is_active"]) # True or False based on API response +``` + +### 3.2 Using Query Parameters + +```python +self.add( + "is_valid", + required=True, + external_api={ + "url": "https://api.example.com/validate", + "method": "GET", + "params": {"user": "{{user_id}}", "hash": "{{hash}}"}, + "data_key": "is_valid", + }, +) +``` + +This configuration sends the `user_id` and `hash` as query parameters, replacing the placeholders with validated data. + +--- + +### 3.3 Handling Fallback Values + +If the external API call fails, a fallback value can be specified: + +```python +self.add( + "user_info", + required=True, + fallback={"name": "unknown", "age": 0}, + external_api={ + "url": "https://api.example.com/user/{{user_id}}", + "method": "GET", + "data_key": "user", + }, +) +``` + +--- + +## 4. Error Handling + +- `ValidationError` is raised when: + - The API call returns a non-200 status code. + - A required field is missing and no fallback/default is provided. + - Validation of the field value fails. + +--- + +## 7. Best Practices + +- **Required Fields:** Clearly define required fields and provide fallback values where necessary. +- **Placeholders:** Ensure placeholders in URLs and parameters match the keys in `validated_data`. +- **Fallbacks:** Always provide fallback values for critical fields to avoid disruptions in case of API failure. +- **Security:** Use HTTPS for API calls and secure sensitive data like API keys. +- **Testing:** Mock external API calls during unit testing to avoid dependencies on external systems. + +--- diff --git a/README.md b/README.md index 106f5e2..8ff2d15 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,16 @@ The `InputFilter` class is used to validate and filter input data in Flask applications. It provides a modular way to clean and ensure that incoming data meets expected format and type requirements before being processed. +--- + ## Installation ```bash pip install flask-inputfilter ``` +--- + ## Quickstart To use the `InputFilter` class, you need to create a new class that inherits from it and define the fields you want to validate and filter. @@ -72,6 +76,8 @@ def updateZipcode(): zipcode = data.get('zipcode') ``` +--- + ## Options The `add` method takes the following options: @@ -81,11 +87,12 @@ The `add` method takes the following options: - [`Validator`](src/flask_inputfilter/Validator/README.md) - [`Default`](#default) - [`Fallback`](#fallback) +- [`ExternalApi`](EXTERNAL_API.md) ### Required The `required` option is used to specify if the field is required or not. -If the field is required and not present in the input data, the `validate` method will return a 400 response with the error message. +If the field is required and not present in the input data, the `validate` method will return the `ValidationError` with an error message. ### Default @@ -93,4 +100,8 @@ The `default` option is used to specify a default value to use if the field is n ### Fallback -The `fallback` option is used to specify a fallback value to use if the field is not present in the input data, although it is required or the validation fails. +The `fallback` option is used to specify a fallback value to use if something unexpected happens, for example if the field is required but no value where provides + or the validation fails. + +This means that if the field is not required and no value is present, the fallback value will not be used. +In this case you have to use the `default` option. diff --git a/pyproject.toml b/pyproject.toml index d3148be..7101c81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,8 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.black] -line-length = 90 +line-length = 79 + +[tool.isort] +profile = 'black' +line_length = 79 diff --git a/requirements.txt b/requirements.txt index 7d79c96..9b50950 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ +autoflake +black flake8 flask==0.10 +isort pillow==2.0.0 pytest +requests==2.12.0 setuptools twine -black diff --git a/setup.py b/setup.py index 85e8a23..b7589e1 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,12 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="flask_inputfilter", version="0.0.3", author="Leander Cain Slotosch", author_email="slotosch.leander@outlook.de", - description="A library to filter and validate input data in" "Flask applications", + description="A library to filter and validate input data in" + "Flask applications", long_description=open("README.md").read(), long_description_content_type="text/markdown", url="https://github.com/LeanderCS/flask-inputfilter", @@ -14,6 +15,7 @@ install_requires=[ "Flask>=0.10", "pillow>=2.0.0", + "requests>=2.12.0", ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/src/flask_inputfilter/Enum/RegexEnum.py b/src/flask_inputfilter/Enum/RegexEnum.py index c3d4274..a05e613 100644 --- a/src/flask_inputfilter/Enum/RegexEnum.py +++ b/src/flask_inputfilter/Enum/RegexEnum.py @@ -13,7 +13,8 @@ class RegexEnum(Enum): ISO_DATE = r"^\d{4}-\d{2}-\d{2}$" ISO_DATETIME = ( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}" r"(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$" + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}" + r"(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$" ) PHONE_NUMBER = r"^\+?[\d\s\-()]{7,}$" diff --git a/src/flask_inputfilter/Exception/ValidationError.py b/src/flask_inputfilter/Exception/ValidationError.py index c383c95..4352041 100644 --- a/src/flask_inputfilter/Exception/ValidationError.py +++ b/src/flask_inputfilter/Exception/ValidationError.py @@ -2,5 +2,3 @@ class ValidationError(Exception): """ This class is used to raise an exception when a validation error occurs. """ - - pass diff --git a/src/flask_inputfilter/Filter/README.md b/src/flask_inputfilter/Filter/README.md index a943fcb..0121c9a 100644 --- a/src/flask_inputfilter/Filter/README.md +++ b/src/flask_inputfilter/Filter/README.md @@ -6,16 +6,19 @@ The `Filter` module contains the filters that can be used to filter the input da The following filters are available in the `Filter` module: -1. [`ArrayExplodeFilter`](src/flask_inputfilter/Filter/ArrayExplodeFilter.py) - Explodes the input string into an array. -2. [`StringTrimFilter`](src/flask_inputfilter/Filter/StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string. -3. [`ToBooleanFilter`](src/flask_inputfilter/Filter/ToBooleanFilter.py) - Converts the string to a boolean value. -4. [`ToCamelCaseFilter`](src/flask_inputfilter/Filter/ToCamelCaseFilter.py) - Converts the string to camel case. -5. [`ToFloatFilter`](src/flask_inputfilter/Filter/ToFloatFilter.py) - Converts the string to a float value. -5. [`ToIntegerFilter`](src/flask_inputfilter/Filter/ToIntegerFilter.py) - Converts the string to an integer value. -6. [`ToLowerFilter`](src/flask_inputfilter/Filter/ToLowerFilter.py) - Converts the string to lowercase. -7. [`ToNullFilter`](src/flask_inputfilter/Filter/ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string). -8. [`ToPascaleCaseFilter`](src/flask_inputfilter/Filter/ToPascaleCaseFilter.py) - Converts the string to pascal case. -9. [`ToSnakeCaseFilter`](src/flask_inputfilter/Filter/ToSnakeCaseFilter.py) - Converts the string to snake case. -9. [`ToStringFilter`](src/flask_inputfilter/Filter/ToStringFilter.py) - Converts the input to a string value. -9. [`ToUpperFilter`](src/flask_inputfilter/Filter/ToUpperFilter.py) - Converts the string to uppercase. -10. [`WhitespaceCollapseFilter`](src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py) - Collapses the whitespace in the string. +1. [`ArrayExplodeFilter`](ArrayExplodeFilter.py) - Explodes the input string into an array. +2. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug. +3. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string. +4. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string. +5. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value. +6. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case. +7. [`ToFloatFilter`](ToFloatFilter.py) - Converts the string to a float value. +8. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts the string to an integer value. +9. [`ToLowerFilter`](ToLowerFilter.py) - Converts the string to lowercase. +10. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes the unicode string. +11. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string). +12. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case. +13. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case. +14. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value. +15. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase. +16. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string. diff --git a/src/flask_inputfilter/Filter/SlugifyFilter.py b/src/flask_inputfilter/Filter/SlugifyFilter.py new file mode 100644 index 0000000..190fbab --- /dev/null +++ b/src/flask_inputfilter/Filter/SlugifyFilter.py @@ -0,0 +1,22 @@ +import re +from typing import Any, Optional + +from ..Filter.BaseFilter import BaseFilter + + +class SlugifyFilter(BaseFilter): + """ + Filter that converts a string to a slug. + """ + + def apply(self, value: Any) -> Optional[str]: + + if not isinstance(value, str): + return None + + value = value.lower() + + value = re.sub(r"[^\w\s-]", "", value) + value = re.sub(r"[\s]+", "-", value) + + return value diff --git a/src/flask_inputfilter/Filter/StringTrimFilter.py b/src/flask_inputfilter/Filter/StringTrimFilter.py index fad9f1f..1547391 100644 --- a/src/flask_inputfilter/Filter/StringTrimFilter.py +++ b/src/flask_inputfilter/Filter/StringTrimFilter.py @@ -1,4 +1,4 @@ -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class StringTrimFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py b/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py new file mode 100644 index 0000000..8c5841b --- /dev/null +++ b/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py @@ -0,0 +1,19 @@ +import re +from typing import Any, Optional + +from ..Filter.BaseFilter import BaseFilter + + +class ToAlphaNumericFilter(BaseFilter): + """ + Filter that ensures a string contains only alphanumeric characters. + """ + + def apply(self, value: Any) -> Optional[str]: + + if not isinstance(value, str): + return None + + value = re.sub(r"[^\w]", "", value) + + return value diff --git a/src/flask_inputfilter/Filter/ToBooleanFilter.py b/src/flask_inputfilter/Filter/ToBooleanFilter.py index a2c348c..fcfbce0 100644 --- a/src/flask_inputfilter/Filter/ToBooleanFilter.py +++ b/src/flask_inputfilter/Filter/ToBooleanFilter.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToBooleanFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToCamelCaseFilter.py b/src/flask_inputfilter/Filter/ToCamelCaseFilter.py index 433da22..29938d7 100644 --- a/src/flask_inputfilter/Filter/ToCamelCaseFilter.py +++ b/src/flask_inputfilter/Filter/ToCamelCaseFilter.py @@ -1,7 +1,7 @@ import re from typing import Any, Optional -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToCamelCaseFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToFloatFilter.py b/src/flask_inputfilter/Filter/ToFloatFilter.py index 45bfe02..9871e80 100644 --- a/src/flask_inputfilter/Filter/ToFloatFilter.py +++ b/src/flask_inputfilter/Filter/ToFloatFilter.py @@ -1,4 +1,4 @@ -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToFloatFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToIntegerFilter.py b/src/flask_inputfilter/Filter/ToIntegerFilter.py index 511e506..eddc674 100644 --- a/src/flask_inputfilter/Filter/ToIntegerFilter.py +++ b/src/flask_inputfilter/Filter/ToIntegerFilter.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToIntegerFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToLowerFilter.py b/src/flask_inputfilter/Filter/ToLowerFilter.py index 8135553..0d04774 100644 --- a/src/flask_inputfilter/Filter/ToLowerFilter.py +++ b/src/flask_inputfilter/Filter/ToLowerFilter.py @@ -1,4 +1,4 @@ -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToLowerFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py b/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py new file mode 100644 index 0000000..a70c8ea --- /dev/null +++ b/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py @@ -0,0 +1,33 @@ +import unicodedata +from typing import Any, Optional + +from typing_extensions import Literal + +from ..Filter.BaseFilter import BaseFilter + + +class ToNormalizedUnicodeFilter(BaseFilter): + """ + Filter that normalizes a string to a specified Unicode form. + """ + + def __init__( + self, form: Literal["NFC", "NFD", "NFKC", "NFKD"] = "NFC" + ) -> None: + + self.form = form + + def apply(self, value: Any) -> Optional[str]: + + if not isinstance(value, str): + return None + + value = unicodedata.normalize(self.form, value) + + value_without_accents = "".join( + char + for char in unicodedata.normalize("NFD", value) + if unicodedata.category(char) != "Mn" + ) + + return unicodedata.normalize(self.form, value_without_accents) diff --git a/src/flask_inputfilter/Filter/ToNullFilter.py b/src/flask_inputfilter/Filter/ToNullFilter.py index f2d7c60..c59732b 100644 --- a/src/flask_inputfilter/Filter/ToNullFilter.py +++ b/src/flask_inputfilter/Filter/ToNullFilter.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToNullFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToStringFilter.py b/src/flask_inputfilter/Filter/ToStringFilter.py index fcd970b..bd0b311 100644 --- a/src/flask_inputfilter/Filter/ToStringFilter.py +++ b/src/flask_inputfilter/Filter/ToStringFilter.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToStringFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToUpperFilter.py b/src/flask_inputfilter/Filter/ToUpperFilter.py index f00277a..a2dcaba 100644 --- a/src/flask_inputfilter/Filter/ToUpperFilter.py +++ b/src/flask_inputfilter/Filter/ToUpperFilter.py @@ -1,4 +1,4 @@ -from ..Filter import BaseFilter +from ..Filter.BaseFilter import BaseFilter class ToUpperFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/TruncateFilter.py b/src/flask_inputfilter/Filter/TruncateFilter.py new file mode 100644 index 0000000..972f7a7 --- /dev/null +++ b/src/flask_inputfilter/Filter/TruncateFilter.py @@ -0,0 +1,23 @@ +from typing import Any, Optional + +from ..Filter.BaseFilter import BaseFilter + + +class TruncateFilter(BaseFilter): + """ + Filter that truncates a string to a specified maximum length. + """ + + def __init__(self, max_length: int) -> None: + + self.max_length = max_length + + def apply(self, value: Any) -> Optional[str]: + + if not isinstance(value, str): + return None + + if len(value) > self.max_length: + value = value[: self.max_length] + + return value diff --git a/src/flask_inputfilter/Filter/__init__.py b/src/flask_inputfilter/Filter/__init__.py index 25f30e3..f351bb3 100644 --- a/src/flask_inputfilter/Filter/__init__.py +++ b/src/flask_inputfilter/Filter/__init__.py @@ -1,14 +1,18 @@ from .ArrayExplodeFilter import ArrayExplodeFilter from .BaseFilter import BaseFilter +from .SlugifyFilter import SlugifyFilter from .StringTrimFilter import StringTrimFilter +from .ToAlphaNumericFilter import ToAlphaNumericFilter from .ToBooleanFilter import ToBooleanFilter from .ToCamelCaseFilter import ToCamelCaseFilter from .ToFloatFilter import ToFloatFilter from .ToIntegerFilter import ToIntegerFilter from .ToLowerFilter import ToLowerFilter +from .ToNormalizedUnicodeFilter import ToNormalizedUnicodeFilter from .ToNullFilter import ToNullFilter from .ToPascaleCaseFilter import ToPascaleCaseFilter from .ToSnakeCaseFilter import ToSnakeCaseFilter from .ToStringFilter import ToStringFilter from .ToUpperFilter import ToUpperFilter +from .TruncateFilter import TruncateFilter from .WhitespaceCollapseFilter import WhitespaceCollapseFilter diff --git a/src/flask_inputfilter/InputFilter.py b/src/flask_inputfilter/InputFilter.py index 729c9be..0388b59 100644 --- a/src/flask_inputfilter/InputFilter.py +++ b/src/flask_inputfilter/InputFilter.py @@ -1,10 +1,30 @@ -from typing import Dict, Any, Optional, List +import re +from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from flask import request, Response, g +import requests +from flask import Response, g, request +from typing_extensions import TypedDict from .Exception import ValidationError -from .Filter import BaseFilter -from .Validator import BaseValidator +from .Filter.BaseFilter import BaseFilter +from .Validator.BaseValidator import BaseValidator + + +class ExternalApiConfig(TypedDict): + """ + Configuration for an external API call. + + :param url: The URL of the external API. + :param method: The HTTP method to use. + :param params: The parameters to send to the API. + :param data_key: The key in the response JSON to use + """ + + url: str + method: str + params: Optional[Dict[str, str]] + data_key: Optional[str] + api_key: Optional[str] class InputFilter: @@ -19,14 +39,23 @@ def __init__(self) -> None: def add( self, name: str, - required: bool = True, + required: bool = False, default: Any = None, fallback: Any = None, filters: Optional[List[BaseFilter]] = None, validators: Optional[List[BaseValidator]] = None, - ): + external_api: Optional[Dict[str, Union[str, Dict[str, str]]]] = None, + ) -> None: """ Add the field to the input filter. + + :param name: The name of the field. + :param required: Whether the field is required. + :param default: The default value of the field. + :param fallback: The fallback value of the field. + :param filters: The filters to apply to the field value. + :param validators: The validators to apply to the field value. + :param external_api: Configuration for an external API call. """ self.fields[name] = { @@ -35,9 +64,10 @@ def add( "fallback": fallback, "filters": filters or [], "validators": validators or [], + "external_api": external_api, } - def applyFilters(self, field_name: str, value: Any) -> Any: + def _applyFilters(self, field_name: str, value: Any) -> Any: """ Apply filters to the field value. """ @@ -52,12 +82,12 @@ def applyFilters(self, field_name: str, value: Any) -> Any: return value - def validateField(self, fieldName: str, value: Any) -> None: + def _validateField(self, field_name: str, value: Any) -> None: """ Validate the field value. """ - field = self.fields.get(fieldName) + field = self.fields.get(field_name) if not field: return @@ -65,6 +95,83 @@ def validateField(self, fieldName: str, value: Any) -> None: for validator in field["validators"]: validator.validate(value) + def _callExternalApi( + self, config: dict, validated_data: dict + ) -> Optional[Any]: + """ + Führt den API-Aufruf durch und gibt den Wert zurück, + der im Antwortkörper zu finden ist. + """ + + requestData = {} + + if "api_key" in config: + requestData["headers"][ + "Authorization" + ] = f"Bearer {config['api_key']}" + + if "headers" in config: + requestData["headers"].update(config["headers"]) + + if "params" in config: + requestData["params"] = self.__replacePlaceholdersInParams( + config["params"], validated_data + ) + + requestData["url"] = self.__replacePlaceholders( + config["url"], validated_data + ) + requestData["method"] = config["method"] + + response = requests.request(**requestData) + + if response.status_code != 200: + raise ValidationError( + f"External API call failed with status code {response.status_code}" + ) + + result = response.json() + + data_key = config.get("data_key", None) + if data_key: + return result.get(data_key) + + return result + + @staticmethod + def __replacePlaceholders(url: str, validated_data: dict) -> str: + """ + Ersetzt alle Platzhalter in der URL, die mit {{}} definiert sind, + durch die entsprechenden Werte aus den Parametern. + """ + + return re.sub( + r"{{(.*?)}}", + lambda match: str(validated_data.get(match.group(1))), + url, + ) + + @staticmethod + def __replacePlaceholdersInParams( + params: dict, validated_data: dict + ) -> dict: + """ + Replace all placeholders in params with the + corresponding values from validated_data. + """ + replaced_params = {} + for key, value in params.items(): + if isinstance(value, str): + replaced_value = re.sub( + r"{{(.*?)}}", + lambda match: str(validated_data.get(match.group(1), "")), + value, + ) + replaced_params[key] = replaced_value + else: + replaced_params[key] = value + return replaced_params + def validateData( self, data: Dict[str, Any], kwargs: Dict[str, Any] = None ) -> Dict[str, Any]: @@ -76,44 +183,98 @@ def validateData( if kwargs is None: kwargs = {} - validatedData = {} - combinedData = {**data, **kwargs} + validated_data = {} + combined_data = {**data, **kwargs} - for fieldName, fieldInfo in self.fields.items(): - value = combinedData.get(fieldName) + for field_name, field_info in self.fields.items(): + value = combined_data.get(field_name) - value = self.applyFilters(fieldName, value) + # Apply filters + value = self._applyFilters(field_name, value) - if value is None and fieldInfo["required"]: - if fieldInfo["fallback"] is None: - raise ValidationError(f"Field '{fieldName}' is required.") + # Check for required field + if value is None: + if ( + field_info["required"] + and field_info["external_api"] is None + ): + if field_info["fallback"] is None: + raise ValidationError( + f"Field '{field_name}' is required." + ) - value = fieldInfo["fallback"] + value = field_info["fallback"] - if value is None and fieldInfo["default"] is not None: - value = fieldInfo["default"] + if field_info["default"] is not None: + value = field_info["default"] + # Validate field if value is not None: try: - self.validateField(fieldName, value) + self._validateField(field_name, value) except ValidationError: - if fieldInfo["fallback"] is not None: - value = fieldInfo["fallback"] + if field_info["fallback"] is not None: + value = field_info["fallback"] else: raise - validatedData[fieldName] = value + # External API call + if field_info["external_api"]: + external_api_config = field_info["external_api"] + + try: + value = self._callExternalApi( + external_api_config, validated_data + ) + + except ValidationError as e: + if field_info["fallback"] is None: + print(e) + raise ValidationError( + f"External API call failed for field '{field_name}'." + ) + + value = field_info["fallback"] + + if value is None: + if field_info["required"]: + if field_info["fallback"] is None: + raise ValidationError( + f"Field '{field_name}' is required." + ) + + value = field_info["fallback"] + + if field_info["default"] is not None: + value = field_info["default"] + + validated_data[field_name] = value - return validatedData + return validated_data @classmethod - def validate(cls): + def validate( + cls, + ) -> Callable[ + [Any], + Callable[ + [Tuple[Any, ...], Dict[str, Any]], + Union[Response, Tuple[Any, Dict[str, Any]]], + ], + ]: """ Decorator for validating input data in routes. """ - def decorator(f): - def wrapper(*args, **kwargs): + def decorator( + f, + ) -> Callable[ + [Tuple[Any, ...], Dict[str, Any]], + Union[Response, Tuple[Any, Dict[str, Any]]], + ]: + def wrapper( + *args, **kwargs + ) -> Union[Response, Tuple[Any, Dict[str, Any]]]: if request.method == "GET": data = request.args @@ -125,12 +286,14 @@ def wrapper(*args, **kwargs): data = request.json else: - return Response(status=415, response="Unsupported method Type") + return Response( + status=415, response="Unsupported method Type" + ) inputFilter = cls() try: - g.validatedData = inputFilter.validateData(data, kwargs) + g.validated_data = inputFilter.validateData(data, kwargs) except ValidationError as e: return Response(status=400, response=str(e)) diff --git a/src/flask_inputfilter/Validator/ArrayElementValidator.py b/src/flask_inputfilter/Validator/ArrayElementValidator.py index b56e802..908f427 100644 --- a/src/flask_inputfilter/Validator/ArrayElementValidator.py +++ b/src/flask_inputfilter/Validator/ArrayElementValidator.py @@ -1,9 +1,8 @@ -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from ..Exception import ValidationError from ..Validator.BaseValidator import BaseValidator - if TYPE_CHECKING: from ..InputFilter import InputFilter diff --git a/src/flask_inputfilter/Validator/BaseValidator.py b/src/flask_inputfilter/Validator/BaseValidator.py index 63a2954..6e55ba2 100644 --- a/src/flask_inputfilter/Validator/BaseValidator.py +++ b/src/flask_inputfilter/Validator/BaseValidator.py @@ -8,4 +8,6 @@ class BaseValidator: def validate(self, value: Any) -> None: - raise NotImplementedError("Validator validate method must be implemented") + raise NotImplementedError( + "Validator validate method must be implemented" + ) diff --git a/src/flask_inputfilter/Validator/FloatPrecisionValidator.py b/src/flask_inputfilter/Validator/FloatPrecisionValidator.py new file mode 100644 index 0000000..f85049b --- /dev/null +++ b/src/flask_inputfilter/Validator/FloatPrecisionValidator.py @@ -0,0 +1,47 @@ +import re +from typing import Any + +from ..Exception import ValidationError +from ..Validator.BaseValidator import BaseValidator + + +class FloatPrecisionValidator(BaseValidator): + """ + Validator that checks the precision and scale of a float. + """ + + def __init__( + self, + precision: int, + scale: int, + error_message: str = "Value '{}' has more than {} digits in total or " + "{} digits after the decimal point.", + ) -> None: + + self.precision = precision + self.scale = scale + self.error_message = error_message + + def validate(self, value: Any) -> None: + + if not isinstance(value, (float, int)): + raise ValidationError("Value must be a float or an integer") + + value_str = str(value) + match = re.match(r"^-?(\d+)(\.(\d+))?$", value_str) + if not match: + raise ValidationError("Value is not a valid float") + + digits_before = len(match.group(1)) + digits_after = len(match.group(3)) if match.group(3) else 0 + total_digits = digits_before + digits_after + + if total_digits > self.precision or digits_after > self.scale: + if "{}" in self.error_message: + raise ValidationError( + self.error_message.format( + value, self.precision, self.scale + ) + ) + + raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/InArrayValidator.py b/src/flask_inputfilter/Validator/InArrayValidator.py index 43ed526..2873255 100644 --- a/src/flask_inputfilter/Validator/InArrayValidator.py +++ b/src/flask_inputfilter/Validator/InArrayValidator.py @@ -1,7 +1,7 @@ from typing import Any, List from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class InArrayValidator(BaseValidator): @@ -35,6 +35,8 @@ def validate(self, value: Any) -> None: except Exception: if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value, self.haystack)) + raise ValidationError( + self.error_message.format(value, self.haystack) + ) raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/InEnumValidator.py b/src/flask_inputfilter/Validator/InEnumValidator.py index 502f36a..e0c431d 100644 --- a/src/flask_inputfilter/Validator/InEnumValidator.py +++ b/src/flask_inputfilter/Validator/InEnumValidator.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import Type, Any +from typing import Any, Type from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class InEnumValidator(BaseValidator): @@ -21,8 +21,12 @@ def __init__( def validate(self, value: Any) -> None: - if not any(value.lower() == item.name.lower() for item in self.enumClass): + if not any( + value.lower() == item.name.lower() for item in self.enumClass + ): if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value, self.enumClass)) + raise ValidationError( + self.error_message.format(value, self.enumClass) + ) raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/IsArrayValidator.py b/src/flask_inputfilter/Validator/IsArrayValidator.py index faa09e7..b5bda4a 100644 --- a/src/flask_inputfilter/Validator/IsArrayValidator.py +++ b/src/flask_inputfilter/Validator/IsArrayValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsArrayValidator(BaseValidator): @@ -9,7 +9,9 @@ class IsArrayValidator(BaseValidator): Validator that checks if a value is an array. """ - def __init__(self, error_message: str = "Value '{}' is not an array.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not an array." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py b/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py index b473805..45f3aba 100644 --- a/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py +++ b/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py @@ -2,7 +2,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsBase64ImageCorrectSizeValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsBase64ImageValidator.py b/src/flask_inputfilter/Validator/IsBase64ImageValidator.py index 0b5442e..2bcb460 100644 --- a/src/flask_inputfilter/Validator/IsBase64ImageValidator.py +++ b/src/flask_inputfilter/Validator/IsBase64ImageValidator.py @@ -1,10 +1,11 @@ import base64 import io -from PIL import Image from typing import Any +from PIL import Image + from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsBase64ImageValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsBoolValidator.py b/src/flask_inputfilter/Validator/IsBooleanValidator.py similarity index 69% rename from src/flask_inputfilter/Validator/IsBoolValidator.py rename to src/flask_inputfilter/Validator/IsBooleanValidator.py index 0e3b22f..834c9be 100644 --- a/src/flask_inputfilter/Validator/IsBoolValidator.py +++ b/src/flask_inputfilter/Validator/IsBooleanValidator.py @@ -1,15 +1,17 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator -class IsBoolValidator(BaseValidator): +class IsBooleanValidator(BaseValidator): """ Validator that checks if a value is a bool. """ - def __init__(self, error_message: str = "Value '{}' is not a bool.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not a bool." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsFloatValidator.py b/src/flask_inputfilter/Validator/IsFloatValidator.py index 9366c3c..3b2d08e 100644 --- a/src/flask_inputfilter/Validator/IsFloatValidator.py +++ b/src/flask_inputfilter/Validator/IsFloatValidator.py @@ -1,6 +1,7 @@ from typing import Any + from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsFloatValidator(BaseValidator): @@ -8,7 +9,9 @@ class IsFloatValidator(BaseValidator): Validator that checks if a value is a float. """ - def __init__(self, error_message: str = "Value '{}' is not a float.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not a float." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsHexadecimalValidator.py b/src/flask_inputfilter/Validator/IsHexadecimalValidator.py index 3299df0..dc616f1 100644 --- a/src/flask_inputfilter/Validator/IsHexadecimalValidator.py +++ b/src/flask_inputfilter/Validator/IsHexadecimalValidator.py @@ -10,7 +10,8 @@ class IsHexadecimalValidator(BaseValidator): """ def __init__( - self, error_message: str = "Value '{}' is not a valid hexadecimal string." + self, + error_message: str = "Value '{}' is not a valid hexadecimal string.", ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsInstanceValidator.py b/src/flask_inputfilter/Validator/IsInstanceValidator.py index 6b6eb94..14bd37c 100644 --- a/src/flask_inputfilter/Validator/IsInstanceValidator.py +++ b/src/flask_inputfilter/Validator/IsInstanceValidator.py @@ -1,7 +1,7 @@ -from typing import Type, Any +from typing import Any, Type from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsInstanceValidator(BaseValidator): @@ -22,6 +22,8 @@ def validate(self, value: Any) -> None: if not isinstance(value, self.classType): if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value, self.classType)) + raise ValidationError( + self.error_message.format(value, self.classType) + ) raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/IsIntegerValidator.py b/src/flask_inputfilter/Validator/IsIntegerValidator.py index 490884f..0c1e687 100644 --- a/src/flask_inputfilter/Validator/IsIntegerValidator.py +++ b/src/flask_inputfilter/Validator/IsIntegerValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsIntegerValidator(BaseValidator): @@ -9,7 +9,9 @@ class IsIntegerValidator(BaseValidator): Validator that checks if a value is an integer. """ - def __init__(self, error_message: str = "Value '{}' is not an integer.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not an integer." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsStringValidator.py b/src/flask_inputfilter/Validator/IsStringValidator.py index 4ca2225..51eceda 100644 --- a/src/flask_inputfilter/Validator/IsStringValidator.py +++ b/src/flask_inputfilter/Validator/IsStringValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class IsStringValidator(BaseValidator): @@ -9,7 +9,9 @@ class IsStringValidator(BaseValidator): Validator that checks if a value is a string. """ - def __init__(self, error_message: str = "Value '{}' is not a string.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not a string." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsUUIDValidator.py b/src/flask_inputfilter/Validator/IsUUIDValidator.py index f74b05c..b28a0e3 100644 --- a/src/flask_inputfilter/Validator/IsUUIDValidator.py +++ b/src/flask_inputfilter/Validator/IsUUIDValidator.py @@ -10,7 +10,9 @@ class IsUUIDValidator(BaseValidator): Validator that checks if a value is a valid UUID string. """ - def __init__(self, error_message: str = "Value '{}' is not a valid UUID.") -> None: + def __init__( + self, error_message: str = "Value '{}' is not a valid UUID." + ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/LengthValidator.py b/src/flask_inputfilter/Validator/LengthValidator.py index 1e95956..acc6207 100644 --- a/src/flask_inputfilter/Validator/LengthValidator.py +++ b/src/flask_inputfilter/Validator/LengthValidator.py @@ -2,7 +2,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class LengthEnum(Enum): diff --git a/src/flask_inputfilter/Validator/README.md b/src/flask_inputfilter/Validator/README.md index 8fa70e1..da146f4 100644 --- a/src/flask_inputfilter/Validator/README.md +++ b/src/flask_inputfilter/Validator/README.md @@ -6,21 +6,22 @@ The `Validator` class is used to validate the data after the filters have been a The following validators are available in the `Validator` module: -1. [`ArrayElementValidator`](src/flask_inputfilter/Validator/ArrayElementValidator.py) - Validates each element of an array with its own defined InputFilter. -2. [`ArrayLengthValidator`](src/flask_inputfilter/Validator/ArrayLengthValidator.py) - Validates the length of an array. -3. [`InArrayValidator`](src/flask_inputfilter/Validator/InArrayValidator.py) - Validates that the value is in the given array. -4. [`InEnumValidator`](src/flask_inputfilter/Validator/InEnumValidator.py) - Validates that the value is in the given enum. -5. [`IsArrayValidator`](src/flask_inputfilter/Validator/IsArrayValidator.py) - Validates that the value is an array. -6. [`IsBase64ImageCorrectSizeValidator`](src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py) - Validates that the value is a base64 encoded string. -7. [`IsBase64ImageValidator`](src/flask_inputfilter/Validator/IsBase64ImageValidator.py) - Validates that the value is a base64 encoded string. -8. [`IsBooleanValidator`](src/flask_inputfilter/Validator/IsBooleanValidator.py) - Validates that the value is a boolean. -9. [`IsFloatValidator`](src/flask_inputfilter/Validator/IsFloatValidator.py) - Validates that the value is a float. -10. [`IsHexadecimalValidator`](src/flask_inputfilter/Validator/IsHexadecimalValidator.py) - Validates that the value is a hexadecimal string. -11. [`IsInstanceValidator`](src/flask_inputfilter/Validator/IsInstanceValidator.py) - Validates that the value is an instance of a class. -12. [`IsIntegerValidator`](src/flask_inputfilter/Validator/IsIntegerValidator.py) - Validates that the value is an integer. -13. [`IsJsonValidator`](src/flask_inputfilter/Validator/IsJsonValidator.py) - Validates that the value is a json string. -14. [`IsStringValidator`](src/flask_inputfilter/Validator/IsStringValidator.py) - Validates that the value is a string. -15. [`IsUUIDValidator`](src/flask_inputfilter/Validator/IsUUIDValidator.py) - Validates that the value is a UUID. -16. [`LengthValidator`](src/flask_inputfilter/Validator/LengthValidator.py) - Validates the length of the value. -17. [`RangeValidator`](src/flask_inputfilter/Validator/RangeValidator.py) - Validates that the value is within a specified range. -18. [`RegexValidator`](src/flask_inputfilter/Validator/RegexValidator.py) - Validates that the value matches a regex pattern. +1. [`ArrayElementValidator`](ArrayElementValidator.py) - Validates each element of an array with its own defined InputFilter. +2. [`ArrayLengthValidator`](ArrayLengthValidator.py) - Validates the length of an array. +3. [`FloatPrecisionValidator`](FloatPrecisionValidator.py) - Validates the precision of a float. +4. [`InArrayValidator`](InArrayValidator.py) - Validates that the value is in the given array. +5. [`InEnumValidator`](InEnumValidator.py) - Validates that the value is in the given enum. +6. [`IsArrayValidator`](IsArrayValidator.py) - Validates that the value is an array. +7. [`IsBase64ImageCorrectSizeValidator`](IsBase64ImageCorrectSizeValidator.py) - Validates that the value is a base64 encoded string. +8. [`IsBase64ImageValidator`](IsBase64ImageValidator.py) - Validates that the value is a base64 encoded string. +9. [`IsBooleanValidator`](IsBooleanValidator.py) - Validates that the value is a boolean. +10. [`IsFloatValidator`](IsFloatValidator.py) - Validates that the value is a float. +11. [`IsHexadecimalValidator`](IsHexadecimalValidator.py) - Validates that the value is a hexadecimal string. +12. [`IsInstanceValidator`](IsInstanceValidator.py) - Validates that the value is an instance of a class. +13. [`IsIntegerValidator`](IsIntegerValidator.py) - Validates that the value is an integer. +14. [`IsJsonValidator`](IsJsonValidator.py) - Validates that the value is a json string. +15. [`IsStringValidator`](IsStringValidator.py) - Validates that the value is a string. +16. [`IsUUIDValidator`](IsUUIDValidator.py) - Validates that the value is a UUID. +17. [`LengthValidator`](LengthValidator.py) - Validates the length of the value. +18. [`RangeValidator`](RangeValidator.py) - Validates that the value is within a specified range. +19. [`RegexValidator`](RegexValidator.py) - Validates that the value matches a regex pattern. diff --git a/src/flask_inputfilter/Validator/RangeValidator.py b/src/flask_inputfilter/Validator/RangeValidator.py index 6a2be76..7f80bc8 100644 --- a/src/flask_inputfilter/Validator/RangeValidator.py +++ b/src/flask_inputfilter/Validator/RangeValidator.py @@ -27,7 +27,9 @@ def validate(self, value: Any) -> None: ): if "{}" in self.error_message: raise ValidationError( - self.error_message.format(value, self.min_value, self.max_value) + self.error_message.format( + value, self.min_value, self.max_value + ) ) raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/RegexValidator.py b/src/flask_inputfilter/Validator/RegexValidator.py index f318f5e..8f1e102 100644 --- a/src/flask_inputfilter/Validator/RegexValidator.py +++ b/src/flask_inputfilter/Validator/RegexValidator.py @@ -1,7 +1,7 @@ import re from ..Exception import ValidationError -from ..Validator import BaseValidator +from ..Validator.BaseValidator import BaseValidator class RegexValidator(BaseValidator): @@ -23,6 +23,8 @@ def validate(self, value: str) -> None: if not re.match(self.pattern, value): if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value, self.pattern)) + raise ValidationError( + self.error_message.format(value, self.pattern) + ) raise ValidationError(self.error_message) diff --git a/src/flask_inputfilter/Validator/__init__.py b/src/flask_inputfilter/Validator/__init__.py index 849e76a..1cbbfb3 100644 --- a/src/flask_inputfilter/Validator/__init__.py +++ b/src/flask_inputfilter/Validator/__init__.py @@ -1,14 +1,16 @@ from .ArrayElementValidator import ArrayElementValidator from .ArrayLengthValidator import ArrayLengthValidator from .BaseValidator import BaseValidator -from .IsHexadecimalValidator import IsHexadecimalValidator from .InArrayValidator import InArrayValidator from .InEnumValidator import InEnumValidator from .IsArrayValidator import IsArrayValidator -from .IsBase64ImageCorrectSizeValidator import IsBase64ImageCorrectSizeValidator +from .IsBase64ImageCorrectSizeValidator import ( + IsBase64ImageCorrectSizeValidator, +) from .IsBase64ImageValidator import IsBase64ImageValidator -from .IsBoolValidator import IsBoolValidator +from .IsBooleanValidator import IsBooleanValidator from .IsFloatValidator import IsFloatValidator +from .IsHexadecimalValidator import IsHexadecimalValidator from .IsInstanceValidator import IsInstanceValidator from .IsIntegerValidator import IsIntegerValidator from .IsJsonValidator import IsJsonValidator diff --git a/test/test_input_filter.py b/test/test_input_filter.py deleted file mode 100644 index 98f58bb..0000000 --- a/test/test_input_filter.py +++ /dev/null @@ -1,70 +0,0 @@ -import unittest - -from src.flask_inputfilter.Exception import ValidationError -from src.flask_inputfilter.InputFilter import InputFilter -from src.flask_inputfilter.Validator import InArrayValidator - - -class TestInputFilter(unittest.TestCase): - def setUp(self) -> None: - """ - Set up a basic InputFilter instance for testing. - """ - - self.inputFilter = InputFilter() - - def test_optional(self) -> None: - """ - Test that optional field validation works. - """ - - self.inputFilter.add("name", required=True) - - self.inputFilter.validateData({"name": "Alice"}) - - with self.assertRaises(ValidationError): - self.inputFilter.validateData({}) - - def test_default(self) -> None: - """ - Test that default field works. - """ - - self.inputFilter.add("available", required=False, default=True) - - # Default case triggert - validated_data = self.inputFilter.validateData({}) - - self.assertEqual(validated_data["available"], True) - - # Override default case - validated_data = self.inputFilter.validateData({"available": False}) - - self.assertEqual(validated_data["available"], False) - - def test_fallback(self) -> None: - """ - Test that fallback field works. - """ - - self.inputFilter.add("available", required=True, fallback=True) - self.inputFilter.add( - "color", - required=False, - fallback="red", - validators=[InArrayValidator(["red", "green", "blue"])], - ) - - # Fallback case triggert - validated_data = self.inputFilter.validateData({"color": "yellow"}) - - self.assertEqual(validated_data["available"], True) - self.assertEqual(validated_data["color"], "red") - - # Override fallback case - validated_data = self.inputFilter.validateData( - {"available": False, "color": "green"} - ) - - self.assertEqual(validated_data["available"], False) - self.assertEqual(validated_data["color"], "green") diff --git a/test/__init__.py b/tests_package/__init__.py similarity index 100% rename from test/__init__.py rename to tests_package/__init__.py diff --git a/test/data/base64_image.txt b/tests_package/data/base64_image.txt similarity index 100% rename from test/data/base64_image.txt rename to tests_package/data/base64_image.txt diff --git a/test/test_filter.py b/tests_package/test_filter.py similarity index 54% rename from test/test_filter.py rename to tests_package/test_filter.py index 5cf40f7..db4cdd3 100644 --- a/test/test_filter.py +++ b/tests_package/test_filter.py @@ -1,17 +1,21 @@ import unittest from src.flask_inputfilter.Filter import ( - ToIntegerFilter, - ToNullFilter, + ArrayExplodeFilter, + SlugifyFilter, StringTrimFilter, + ToAlphaNumericFilter, + ToBooleanFilter, ToFloatFilter, + ToIntegerFilter, ToLowerFilter, - ToUpperFilter, - ToStringFilter, - ToBooleanFilter, - ArrayExplodeFilter, - ToSnakeCaseFilter, + ToNormalizedUnicodeFilter, + ToNullFilter, ToPascaleCaseFilter, + ToSnakeCaseFilter, + ToStringFilter, + ToUpperFilter, + TruncateFilter, WhitespaceCollapseFilter, ) from src.flask_inputfilter.InputFilter import InputFilter @@ -36,16 +40,39 @@ def test_array_explode_filter(self) -> None: filters=[ArrayExplodeFilter()], ) - validated_data = self.inputFilter.validateData({"tags": "tag1,tag2,tag3"}) + validated_data = self.inputFilter.validateData( + {"tags": "tag1,tag2,tag3"} + ) self.assertEqual(validated_data["tags"], ["tag1", "tag2", "tag3"]) - self.inputFilter.add("items", required=False, filters=[ArrayExplodeFilter(";")]) + self.inputFilter.add( + "items", required=False, filters=[ArrayExplodeFilter(";")] + ) - validated_data = self.inputFilter.validateData({"items": "item1;item2;item3"}) + validated_data = self.inputFilter.validateData( + {"items": "item1;item2;item3"} + ) self.assertEqual(validated_data["items"], ["item1", "item2", "item3"]) + def test_slugify_filter(self) -> None: + """ + Test that SlugifyFilter slugifies a string. + """ + + self.inputFilter.add( + "slug", + required=False, + filters=[SlugifyFilter()], + ) + + validated_data = self.inputFilter.validateData( + {"slug": "Hello World!"} + ) + + self.assertEqual(validated_data["slug"], "hello-world") + def test_string_trim_filter(self) -> None: """ Test that StringTrimFilter trims whitespace. @@ -61,12 +88,31 @@ def test_string_trim_filter(self) -> None: self.assertEqual(validated_data["trimmed_field"], "Hello World") + def test_to_alphanumeric_filter(self) -> None: + """ + Test that ToAlphaNumericFilter removes non-alphanumeric characters. + """ + + self.inputFilter.add( + "alphanumeric_field", + required=False, + filters=[ToAlphaNumericFilter()], + ) + + validated_data = self.inputFilter.validateData( + {"alphanumeric_field": "Hello World!123"} + ) + + self.assertEqual(validated_data["alphanumeric_field"], "HelloWorld123") + def test_to_bool_filter(self) -> None: """ Test that ToBooleanFilter converts string to boolean. """ - self.inputFilter.add("is_active", required=True, filters=[ToBooleanFilter()]) + self.inputFilter.add( + "is_active", required=True, filters=[ToBooleanFilter()] + ) validated_data = self.inputFilter.validateData({"is_active": "true"}) @@ -99,18 +145,41 @@ def test_to_lower_filter(self) -> None: Test that ToLowerFilter converts string to lowercase. """ - self.inputFilter.add("username", required=True, filters=[ToLowerFilter()]) + self.inputFilter.add( + "username", required=True, filters=[ToLowerFilter()] + ) - validated_data = self.inputFilter.validateData({"username": "TESTUSER"}) + validated_data = self.inputFilter.validateData( + {"username": "TESTUSER"} + ) self.assertEqual(validated_data["username"], "testuser") + def test_to_normalized_unicode_filter(self) -> None: + """ + Test that NormalizeUnicodeFilter normalizes Unicode characters. + """ + + self.inputFilter.add( + "unicode_field", + required=False, + filters=[ToNormalizedUnicodeFilter()], + ) + + validated_data = self.inputFilter.validateData( + {"unicode_field": "Héllô Wôrld"} + ) + + self.assertEqual(validated_data["unicode_field"], "Hello World") + def test_to_null_filter(self) -> None: """ Test that ToNullFilter transforms empty string to None. """ - self.inputFilter.add("optional_field", required=False, filters=[ToNullFilter()]) + self.inputFilter.add( + "optional_field", required=False, filters=[ToNullFilter()] + ) validated_data = self.inputFilter.validateData({"optional_field": ""}) @@ -121,9 +190,13 @@ def test_to_pascal_case_filter(self) -> None: Test that PascalCaseFilter converts string to pascal case. """ - self.inputFilter.add("username", required=True, filters=[ToPascaleCaseFilter()]) + self.inputFilter.add( + "username", required=True, filters=[ToPascaleCaseFilter()] + ) - validated_data = self.inputFilter.validateData({"username": "test user"}) + validated_data = self.inputFilter.validateData( + {"username": "test user"} + ) self.assertEqual(validated_data["username"], "TestUser") @@ -132,9 +205,13 @@ def test_snake_case_filter(self) -> None: Test that SnakeCaseFilter converts string to snake case. """ - self.inputFilter.add("username", required=True, filters=[ToSnakeCaseFilter()]) + self.inputFilter.add( + "username", required=True, filters=[ToSnakeCaseFilter()] + ) - validated_data = self.inputFilter.validateData({"username": "TestUser"}) + validated_data = self.inputFilter.validateData( + {"username": "TestUser"} + ) self.assertEqual(validated_data["username"], "test_user") @@ -154,19 +231,40 @@ def test_to_upper_filter(self) -> None: Test that ToUpperFilter converts string to uppercase. """ - self.inputFilter.add("username", required=True, filters=[ToUpperFilter()]) + self.inputFilter.add( + "username", required=True, filters=[ToUpperFilter()] + ) - validated_data = self.inputFilter.validateData({"username": "testuser"}) + validated_data = self.inputFilter.validateData( + {"username": "testuser"} + ) self.assertEqual(validated_data["username"], "TESTUSER") + def test_truncate_filter(self) -> None: + """ + Test that TruncateFilter truncates a string. + """ + + self.inputFilter.add( + "truncated_field", required=False, filters=[TruncateFilter(5)] + ) + + validated_data = self.inputFilter.validateData( + {"truncated_field": "Hello World"} + ) + + self.assertEqual(validated_data["truncated_field"], "Hello") + def test_whitespace_collapse_filter(self) -> None: """ Test that WhitespaceCollapseFilter collapses whitespace. """ self.inputFilter.add( - "collapsed_field", required=False, filters=[WhitespaceCollapseFilter()] + "collapsed_field", + required=False, + filters=[WhitespaceCollapseFilter()], ) validated_data = self.inputFilter.validateData( diff --git a/tests_package/test_input_filter.py b/tests_package/test_input_filter.py new file mode 100644 index 0000000..5101bc9 --- /dev/null +++ b/tests_package/test_input_filter.py @@ -0,0 +1,192 @@ +import unittest +from unittest.mock import Mock, patch + +from src.flask_inputfilter.Exception import ValidationError +from src.flask_inputfilter.InputFilter import InputFilter +from src.flask_inputfilter.Validator import InArrayValidator + + +class TestInputFilter(unittest.TestCase): + def setUp(self) -> None: + """ + Set up a basic InputFilter instance for testing. + """ + + self.inputFilter = InputFilter() + + def test_optional(self) -> None: + """ + Test that optional field validation works. + """ + + self.inputFilter.add("name", required=True) + + self.inputFilter.validateData({"name": "Alice"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + def test_default(self) -> None: + """ + Test that default field works. + """ + + self.inputFilter.add("available", required=False, default=True) + + # Default case triggert + validated_data = self.inputFilter.validateData({}) + + self.assertEqual(validated_data["available"], True) + + # Override default case + validated_data = self.inputFilter.validateData({"available": False}) + + self.assertEqual(validated_data["available"], False) + + def test_fallback(self) -> None: + """ + Test that fallback field works. + """ + + self.inputFilter.add("available", required=True, fallback=True) + self.inputFilter.add( + "color", + required=False, + fallback="red", + validators=[InArrayValidator(["red", "green", "blue"])], + ) + + # Fallback case triggert + validated_data = self.inputFilter.validateData({"color": "yellow"}) + + self.assertEqual(validated_data["available"], True) + self.assertEqual(validated_data["color"], "red") + + # Override fallback case + validated_data = self.inputFilter.validateData( + {"available": False, "color": "green"} + ) + + self.assertEqual(validated_data["available"], False) + self.assertEqual(validated_data["color"], "green") + + @patch("requests.request") + def test_external_api(self, mock_request: Mock) -> None: + """ + Test that external API calls work. + """ + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"is_valid": True} + mock_request.return_value = mock_response + + # Add a field where the external API receives its value + self.inputFilter.add("name", required=False, default="test_user") + + # Add a field with external API configuration + self.inputFilter.add( + "is_valid", + external_api={ + "url": "https://api.example.com/validate_user/{{name}}", + "method": "GET", + "data_key": "is_valid", + }, + ) + + # API returns valid result + validated_data = self.inputFilter.validateData({}) + + self.assertEqual(validated_data["is_valid"], True) + expected_url = "https://api.example.com/validate_user/test_user" + mock_request.assert_called_with( + method="GET", + url=expected_url, + ) + + # API returns invalid result + mock_response.status_code = 500 + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"name": "invalid_user"}) + + @patch("requests.request") + def test_external_api_params(self, mock_request: Mock) -> None: + """ + Test that external API calls work. + """ + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"is_valid": True} + mock_request.return_value = mock_response + + # Add fields where the external API receives its values + self.inputFilter.add("name", required=False) + + self.inputFilter.add("hash", required=False) + + # Add a field with external API configuration + self.inputFilter.add( + "is_valid", + required=True, + external_api={ + "url": "https://api.example.com/validate_user/{{name}}", + "method": "GET", + "params": {"hash": "{{hash}}"}, + "data_key": "is_valid", + }, + ) + + # API returns valid result + validated_data = self.inputFilter.validateData( + {"name": "test_user", "hash": "1234"} + ) + + self.assertEqual(validated_data["is_valid"], True) + expected_url = "https://api.example.com/validate_user/test_user" + mock_request.assert_called_with( + method="GET", url=expected_url, params={"hash": "1234"} + ) + + # API returns invalid status code + mock_response.status_code = 500 + mock_response.json.return_value = {"is_valid": False} + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"name": "invalid_user", "hash": "1234"} + ) + + # API returns invalid result + mock_response.json.return_value = {} + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"name": "invalid_user", "hash": "1234"} + ) + + @patch("requests.request") + def test_external_api_fallback(self, mock_request: Mock) -> None: + + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = {"name": True} + mock_request.return_value = mock_response + + # API call with fallback + self.inputFilter.add( + "username_with_fallback", + required=True, + fallback="fallback_user", + external_api={ + "url": "https://api.example.com/validate_user", + "method": "GET", + "params": {"user": "{{value}}"}, + "data_key": "name", + }, + ) + + validated_data = self.inputFilter.validateData( + {"username_with_fallback": None} + ) + self.assertEqual( + validated_data["username_with_fallback"], "fallback_user" + ) diff --git a/test/test_validator.py b/tests_package/test_validator.py similarity index 72% rename from test/test_validator.py rename to tests_package/test_validator.py index 202f684..c28de6a 100644 --- a/test/test_validator.py +++ b/tests_package/test_validator.py @@ -4,24 +4,27 @@ from src.flask_inputfilter.Exception import ValidationError from src.flask_inputfilter.InputFilter import InputFilter from src.flask_inputfilter.Validator import ( - IsIntegerValidator, - LengthValidator, - InArrayValidator, - RegexValidator, - IsArrayValidator, - IsFloatValidator, ArrayElementValidator, + ArrayLengthValidator, + InArrayValidator, InEnumValidator, + IsArrayValidator, IsBase64ImageCorrectSizeValidator, - IsBoolValidator, - IsInstanceValidator, - RangeValidator, - IsStringValidator, IsBase64ImageValidator, - ArrayLengthValidator, - IsJsonValidator, + IsBooleanValidator, + IsFloatValidator, IsHexadecimalValidator, + IsInstanceValidator, + IsIntegerValidator, + IsJsonValidator, + IsStringValidator, IsUUIDValidator, + LengthValidator, + RangeValidator, + RegexValidator, +) +from src.flask_inputfilter.Validator.FloatPrecisionValidator import ( + FloatPrecisionValidator, ) @@ -46,15 +49,21 @@ def test_array_element_validator(self) -> None: ) self.inputFilter.add( - "items", required=True, validators=[ArrayElementValidator(elementFilter)] + "items", + required=True, + validators=[ArrayElementValidator(elementFilter)], ) - validated_data = self.inputFilter.validateData({"items": [{"id": 1}, {"id": 2}]}) + validated_data = self.inputFilter.validateData( + {"items": [{"id": 1}, {"id": 2}]} + ) self.assertEqual(validated_data["items"], [{"id": 1}, {"id": 2}]) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"items": [{"id": 1}, {"id": "invalid"}]}) + self.inputFilter.validateData( + {"items": [{"id": 1}, {"id": "invalid"}]} + ) def test_array_length_validator(self) -> None: """ @@ -75,6 +84,22 @@ def test_array_length_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"items": [1, 2, 3, 4, 5, 6]}) + def test_float_precision_validator(self) -> None: + """ + Test FloatPrecisionValidator. + """ + + self.inputFilter.add( + "price", + required=True, + validators=[FloatPrecisionValidator(precision=5, scale=2)], + ) + + self.inputFilter.validateData({"price": 19.99}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"price": 19.999}) + def test_in_array_validator(self) -> None: """ Test InArrayValidator. @@ -101,7 +126,9 @@ class Color(Enum): GREEN = "green" BLUE = "blue" - self.inputFilter.add("color", required=True, validators=[InEnumValidator(Color)]) + self.inputFilter.add( + "color", required=True, validators=[InEnumValidator(Color)] + ) self.inputFilter.validateData({"color": "red"}) @@ -113,7 +140,9 @@ def test_is_array_validator(self) -> None: Test that IsArrayValidator validates array type. """ - self.inputFilter.add("tags", required=False, validators=[IsArrayValidator()]) + self.inputFilter.add( + "tags", required=False, validators=[IsArrayValidator()] + ) self.inputFilter.validateData({"tags": ["tag1", "tag2"]}) @@ -128,13 +157,19 @@ def test_is_base64_image_correct_size_validator(self) -> None: self.inputFilter.add( "image", required=True, - validators=[IsBase64ImageCorrectSizeValidator(minSize=10, maxSize=50)], + validators=[ + IsBase64ImageCorrectSizeValidator(minSize=10, maxSize=50) + ], ) - self.inputFilter.validateData({"image": "iVBORw0KGgoAAAANSUhEUgAAAAUA"}) + self.inputFilter.validateData( + {"image": "iVBORw0KGgoAAAANSUhEUgAAAAUA"} + ) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"image": "iVBORw0KGgoAAAANSUhEUgAAAAU"}) + self.inputFilter.validateData( + {"image": "iVBORw0KGgoAAAANSUhEUgAAAAU"} + ) def test_is_base64_image_validator(self) -> None: """ @@ -145,18 +180,20 @@ def test_is_base64_image_validator(self) -> None: "image", required=True, validators=[IsBase64ImageValidator()] ) - with open("test/data/base64_image.txt", "r") as file: + with open("tests_package/data/base64_image.txt", "r") as file: self.inputFilter.validateData({"image": file.read()}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"image": "not_a_base64_image"}) - def test_is_bool_validator(self) -> None: + def test_is_boolean_validator(self) -> None: """ - Test IsBoolValidator. + Test IsBooleanValidator. """ - self.inputFilter.add("is_active", required=True, validators=[IsBoolValidator()]) + self.inputFilter.add( + "is_active", required=True, validators=[IsBooleanValidator()] + ) self.inputFilter.validateData({"is_active": True}) @@ -168,7 +205,9 @@ def test_is_float_validator(self) -> None: Test that IsFloatValidator validates float type. """ - self.inputFilter.add("price", required=True, validators=[IsFloatValidator()]) + self.inputFilter.add( + "price", required=True, validators=[IsFloatValidator()] + ) self.inputFilter.validateData({"price": 19.99}) @@ -180,7 +219,9 @@ def test_is_hexadecimal_validator(self) -> None: Test that HexadecimalValidator validates hexadecimal format. """ - self.inputFilter.add("hex", required=True, validators=[IsHexadecimalValidator()]) + self.inputFilter.add( + "hex", required=True, validators=[IsHexadecimalValidator()] + ) self.inputFilter.validateData({"hex": "0x1234"}) @@ -206,7 +247,9 @@ def test_is_integer_validator(self) -> None: Test that IsIntegerValidator validates integer type. """ - self.inputFilter.add("age", required=True, validators=[IsIntegerValidator()]) + self.inputFilter.add( + "age", required=True, validators=[IsIntegerValidator()] + ) self.inputFilter.validateData({"age": 25}) @@ -218,7 +261,9 @@ def test_is_json_validator(self) -> None: Test that IsJsonValidator validates JSON format. """ - self.inputFilter.add("data", required=True, validators=[IsJsonValidator()]) + self.inputFilter.add( + "data", required=True, validators=[IsJsonValidator()] + ) self.inputFilter.validateData({"data": '{"name": "Alice"}'}) @@ -230,7 +275,9 @@ def test_is_string_validator(self) -> None: Test that IsStringValidator validates string type. """ - self.inputFilter.add("name", required=True, validators=[IsStringValidator()]) + self.inputFilter.add( + "name", required=True, validators=[IsStringValidator()] + ) self.inputFilter.validateData({"name": "obviously an string"}) @@ -242,9 +289,13 @@ def test_is_uuid_validator(self) -> None: Test that IsUuidValidator validates UUID format. """ - self.inputFilter.add("uuid", required=True, validators=[IsUUIDValidator()]) + self.inputFilter.add( + "uuid", required=True, validators=[IsUUIDValidator()] + ) - self.inputFilter.validateData({"uuid": "550e8400-e29b-41d4-a716-446655440000"}) + self.inputFilter.validateData( + {"uuid": "550e8400-e29b-41d4-a716-446655440000"} + ) with self.assertRaises(ValidationError): self.inputFilter.validateData({"uuid": "not_a_uuid"}) @@ -280,10 +331,14 @@ def test_range_validator(self) -> None: self.inputFilter.validateData({"name": "test", "range_field": 3.76}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"name": "test", "range_field": 1.22}) + self.inputFilter.validateData( + {"name": "test", "range_field": 1.22} + ) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"name": "test", "range_field": 7.89}) + self.inputFilter.validateData( + {"name": "test", "range_field": 7.89} + ) def test_regex_validator(self) -> None: """ @@ -296,7 +351,9 @@ def test_regex_validator(self) -> None: validators=[RegexValidator(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")], ) - validated_data = self.inputFilter.validateData({"email": "alice@example.com"}) + validated_data = self.inputFilter.validateData( + {"email": "alice@example.com"} + ) self.assertEqual(validated_data["email"], "alice@example.com")