diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d26f4aa Binary files /dev/null and b/.DS_Store differ diff --git a/.coveragerc b/.coveragerc index c470678..3374875 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,5 @@ [run] -omit = __init__.py, setup.py, test_*.py \ No newline at end of file +source = flask_inputfilter + +[report] +omit = __init__.py, setup.py, */test/* \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 9a3f553..2241f21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ **.md .gitignore -.git .github .idea .vscode diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2ddd129..13e2a83 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,6 @@ name: Run Tests -on: [ push ] +on: [push] permissions: actions: read @@ -23,7 +23,12 @@ 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 flask-inputfilter pytest + docker run flask-inputfilter coverage run --source=flask_inputfilter -m pytest test/ + + - name: Upload coverage to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: docker run -e COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_REPO_TOKEN }} flask-inputfilter coveralls - name: Run code style checks run: | diff --git a/CHAGELOG.md b/CHAGELOG.md index a15709a..528129a 100644 --- a/CHAGELOG.md +++ b/CHAGELOG.md @@ -18,11 +18,11 @@ All notable changes to this project will be documented in this file. ### Added -- New condition functionality between fields. [Check it out](src/flask_inputfilter/Condition/README.md) +- New condition functionality between fields. [Check it out](flask_inputfilter/Condition/README.md) ### Changed -- Switched external_api config from dict to class. [Check it out](src/flask_inputfilter/Model/ExternalApiConfig.py) +- Switched external_api config from dict to class. [Check it out](flask_inputfilter/Model/ExternalApiConfig.py) ## [0.0.4] - 2025-01-09 diff --git a/CREATE_OWN.md b/CREATE_OWN.md new file mode 100644 index 0000000..467646a --- /dev/null +++ b/CREATE_OWN.md @@ -0,0 +1 @@ +# Create own \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0752203..32f64ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.7-slim WORKDIR /app -RUN apt-get update && apt-get install -y gcc python3-dev +RUN apt-get update && apt-get install -y gcc python3-dev git RUN pip install --upgrade pip diff --git a/README.md b/README.md index 6d91b23..ae91362 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,30 @@ -from src.flask_inputfilter.Validator import IsStringValidator - # flask-inputfilter 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. +:Test Status: + + .. image:: https://img.shields.io/github/actions/workflow/status/LeanderCS/flask-inputfilter/test.yaml?branch=main&style=flat-square&label=Github%20Actions + :target: https://github.com/LeanderCS/flask-inputfilter/actions + .. image:: https://img.shields.io/coveralls/LeanderCS/flask-inputfilter/main.svg?style=flat-square&label=Coverage + :target: https://coveralls.io/r/LeanderCS/flask-inputfilter + +:Version Info: + + .. image:: https://img.shields.io/pypi/v/flask-inputfilter?style=flat-square&label=PyPI + :target: https://pypi.org/project/flask-inputfilter/ + +:Compatibility: + + .. image:: https://img.shields.io/pypi/pyversions/flask-inputfilter?style=flat-square&label=PyPI + :target: https://pypi.org/project/flask-inputfilter/ + +:Downloads: + + .. image:: https://img.shields.io/pypi/dm/flask-inputfilter?style=flat-square&label=PyPI + :target: https://pypi.org/project/flask-inputfilter/ + --- ## Installation @@ -19,7 +39,7 @@ pip install flask-inputfilter 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. -There are lots of different filters and validators available to use, but it is also possible to create your own. +There are lots of different filters and validators available to use, but it is also possible to create your [own](CREATE_OWN.md). ### Definition @@ -40,7 +60,7 @@ class UpdateZipcodeInputFilter(InputFilter): self.add( 'id', required=True, - filters=[ToNullFilter()], + filters=[ToIntegerFilter(), ToNullFilter()], validators=[ IsIntegerValidator() ] @@ -102,8 +122,8 @@ def updateZipcode(): The `add` method takes the following options: - [`Required`](#required) -- [`Filter`](src/flask_inputfilter/Filter/README.md) -- [`Validator`](src/flask_inputfilter/Validator/README.md) +- [`Filter`](flask_inputfilter/Filter/README.md) +- [`Validator`](flask_inputfilter/Validator/README.md) - [`Default`](#default) - [`Fallback`](#fallback) - [`ExternalApi`](EXTERNAL_API.md) diff --git a/flask_inputfilter/Condition/ArrayLengthEqualCondition.py b/flask_inputfilter/Condition/ArrayLengthEqualCondition.py new file mode 100644 index 0000000..90d88cc --- /dev/null +++ b/flask_inputfilter/Condition/ArrayLengthEqualCondition.py @@ -0,0 +1,20 @@ +from typing import Any, Dict + +from .BaseCondition import BaseCondition + + +class ArrayLengthEqualCondition(BaseCondition): + """ + Condition that checks if the array is of the specified length. + """ + + def __init__( + self, first_array_field: str, second_array_field: str + ) -> None: + self.first_array_field = first_array_field + self.second_array_field = second_array_field + + def check(self, data: Dict[str, Any]) -> bool: + return len(data.get(self.first_array_field) or []) == len( + data.get(self.second_array_field) or [] + ) diff --git a/src/flask_inputfilter/Condition/ArrayLongerThanCondition.py b/flask_inputfilter/Condition/ArrayLongerThanCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/ArrayLongerThanCondition.py rename to flask_inputfilter/Condition/ArrayLongerThanCondition.py diff --git a/src/flask_inputfilter/Condition/BaseCondition.py b/flask_inputfilter/Condition/BaseCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/BaseCondition.py rename to flask_inputfilter/Condition/BaseCondition.py diff --git a/src/flask_inputfilter/Condition/CustomCondition.py b/flask_inputfilter/Condition/CustomCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/CustomCondition.py rename to flask_inputfilter/Condition/CustomCondition.py diff --git a/src/flask_inputfilter/Condition/EqualCondition.py b/flask_inputfilter/Condition/EqualCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/EqualCondition.py rename to flask_inputfilter/Condition/EqualCondition.py diff --git a/src/flask_inputfilter/Condition/ExactlyNOfCondition.py b/flask_inputfilter/Condition/ExactlyNOfCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/ExactlyNOfCondition.py rename to flask_inputfilter/Condition/ExactlyNOfCondition.py diff --git a/src/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py b/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py rename to flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py diff --git a/src/flask_inputfilter/Condition/ExactlyOneOfCondition.py b/flask_inputfilter/Condition/ExactlyOneOfCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/ExactlyOneOfCondition.py rename to flask_inputfilter/Condition/ExactlyOneOfCondition.py diff --git a/src/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py b/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py rename to flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py diff --git a/src/flask_inputfilter/Condition/IntegerBiggerThanCondition.py b/flask_inputfilter/Condition/IntegerBiggerThanCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/IntegerBiggerThanCondition.py rename to flask_inputfilter/Condition/IntegerBiggerThanCondition.py diff --git a/src/flask_inputfilter/Condition/NOfCondition.py b/flask_inputfilter/Condition/NOfCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/NOfCondition.py rename to flask_inputfilter/Condition/NOfCondition.py diff --git a/src/flask_inputfilter/Condition/NOfMatchesCondition.py b/flask_inputfilter/Condition/NOfMatchesCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/NOfMatchesCondition.py rename to flask_inputfilter/Condition/NOfMatchesCondition.py diff --git a/src/flask_inputfilter/Condition/NotEqualCondition.py b/flask_inputfilter/Condition/NotEqualCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/NotEqualCondition.py rename to flask_inputfilter/Condition/NotEqualCondition.py diff --git a/src/flask_inputfilter/Condition/OneOfCondition.py b/flask_inputfilter/Condition/OneOfCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/OneOfCondition.py rename to flask_inputfilter/Condition/OneOfCondition.py diff --git a/src/flask_inputfilter/Condition/OneOfMatchesCondition.py b/flask_inputfilter/Condition/OneOfMatchesCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/OneOfMatchesCondition.py rename to flask_inputfilter/Condition/OneOfMatchesCondition.py diff --git a/src/flask_inputfilter/Condition/README.md b/flask_inputfilter/Condition/README.md similarity index 82% rename from src/flask_inputfilter/Condition/README.md rename to flask_inputfilter/Condition/README.md index df3e52c..16aa7e3 100644 --- a/src/flask_inputfilter/Condition/README.md +++ b/flask_inputfilter/Condition/README.md @@ -7,30 +7,33 @@ The `Condition` module contains the conditions that can be used to validate the The `addCondition` method is used to add a condition between fields. ```python + from flask_inputfilter import InputFilter -from flask_inputfilter.Condition import ExactlyOneOfCondition -from flask_inputfilter.Filter import ToIntegerFilter -from flask_inputfilter.Validator import IsIntegerValidator +from flask_inputfilter.Condition import OneOfCondition +from flask_inputfilter.Filter import StringTrimFilter +from flask_inputfilter.Validator import IsStringValidator + class TestInputFilter(InputFilter): def __init__(self): super().__init__() self.add( - 'id', - required=True, - filters=[ToIntegerFilter()], - validators=[IsIntegerValidator()] + 'username', + filters=[StringTrimFilter()], + validators=[IsStringValidator()] ) self.add( 'name', - required=True + filters=[StringTrimFilter()], + validators=[IsStringValidator()] ) self.addCondition( - ExactlyOneOfCondition('id', 'name') + OneOfCondition(['id', 'name']) ) + ``` ## Available conditions @@ -51,4 +54,4 @@ The following conditions are available in the `Condition` module: 12. [`OneOfMatchesCondition`](OneOfMatchesCondition.py) - Validates that at least one of the given matches is true. 13. [`RequiredIfCondition`](RequiredIfCondition.py) - Validates that the input is required if the given condition is true. 14. [`StringLongerThanCondition`](StringLongerThanCondition.py) - Validates that the string is longer than the given value. -15. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is a temporal value. +15. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is in correct temporal order. diff --git a/src/flask_inputfilter/Condition/RequiredIfCondition.py b/flask_inputfilter/Condition/RequiredIfCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/RequiredIfCondition.py rename to flask_inputfilter/Condition/RequiredIfCondition.py diff --git a/src/flask_inputfilter/Condition/StringLongerThanCondition.py b/flask_inputfilter/Condition/StringLongerThanCondition.py similarity index 100% rename from src/flask_inputfilter/Condition/StringLongerThanCondition.py rename to flask_inputfilter/Condition/StringLongerThanCondition.py diff --git a/src/flask_inputfilter/Condition/TemporalOrderCondition.py b/flask_inputfilter/Condition/TemporalOrderCondition.py similarity index 67% rename from src/flask_inputfilter/Condition/TemporalOrderCondition.py rename to flask_inputfilter/Condition/TemporalOrderCondition.py index 7789ad5..807ac4b 100644 --- a/src/flask_inputfilter/Condition/TemporalOrderCondition.py +++ b/flask_inputfilter/Condition/TemporalOrderCondition.py @@ -11,23 +11,20 @@ class TemporalOrderCondition(BaseCondition): Supports datetime, date, and ISO 8601 formatted strings. """ - def __init__(self, first_field: str, second_field: str) -> None: - self.first_field = first_field - self.second_field = second_field + def __init__( + self, smaller_date_field: str, larger_date_field: str + ) -> None: + self.smaller_date_field = smaller_date_field + self.larger_date_field = larger_date_field def check(self, data: Dict[str, Any]) -> bool: - first_date = self._parse_date(data.get(self.first_field)) - second_date = self._parse_date(data.get(self.second_field)) + smaller_date = self._parse_date(data.get(self.smaller_date_field)) + larger_date = self._parse_date(data.get(self.larger_date_field)) - return first_date < second_date + return smaller_date < larger_date @staticmethod def _parse_date(value: Any) -> datetime: - """ - Converts a value to a datetime object if possible. - Supports datetime, date, and ISO 8601 strings. - """ - if isinstance(value, datetime): return value diff --git a/src/flask_inputfilter/Condition/__init__.py b/flask_inputfilter/Condition/__init__.py similarity index 100% rename from src/flask_inputfilter/Condition/__init__.py rename to flask_inputfilter/Condition/__init__.py diff --git a/src/flask_inputfilter/Enum/RegexEnum.py b/flask_inputfilter/Enum/RegexEnum.py similarity index 100% rename from src/flask_inputfilter/Enum/RegexEnum.py rename to flask_inputfilter/Enum/RegexEnum.py diff --git a/src/flask_inputfilter/Enum/__init__.py b/flask_inputfilter/Enum/__init__.py similarity index 100% rename from src/flask_inputfilter/Enum/__init__.py rename to flask_inputfilter/Enum/__init__.py diff --git a/src/flask_inputfilter/Exception/ValidationError.py b/flask_inputfilter/Exception/ValidationError.py similarity index 100% rename from src/flask_inputfilter/Exception/ValidationError.py rename to flask_inputfilter/Exception/ValidationError.py diff --git a/src/flask_inputfilter/Exception/__init__.py b/flask_inputfilter/Exception/__init__.py similarity index 100% rename from src/flask_inputfilter/Exception/__init__.py rename to flask_inputfilter/Exception/__init__.py diff --git a/src/flask_inputfilter/Filter/ArrayExplodeFilter.py b/flask_inputfilter/Filter/ArrayExplodeFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ArrayExplodeFilter.py rename to flask_inputfilter/Filter/ArrayExplodeFilter.py diff --git a/src/flask_inputfilter/Filter/BaseFilter.py b/flask_inputfilter/Filter/BaseFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/BaseFilter.py rename to flask_inputfilter/Filter/BaseFilter.py diff --git a/flask_inputfilter/Filter/BlacklistFilter.py b/flask_inputfilter/Filter/BlacklistFilter.py new file mode 100644 index 0000000..cfca125 --- /dev/null +++ b/flask_inputfilter/Filter/BlacklistFilter.py @@ -0,0 +1,28 @@ +from typing import Any, List + +from .BaseFilter import BaseFilter + + +class BlacklistFilter(BaseFilter): + """Filter that filters out values that are in the blacklist.""" + + def __init__(self, blacklist: List[str]) -> None: + self.blacklist = blacklist + + def apply(self, value: Any) -> Any: + if isinstance(value, str): + for item in self.blacklist: + value = value.replace(item, "") + return value.strip() + + if isinstance(value, list): + return [item for item in value if item not in self.blacklist] + + if isinstance(value, dict): + return { + key: value + for key, value in value.items() + if key not in self.blacklist + } + + return value diff --git a/src/flask_inputfilter/Filter/README.md b/flask_inputfilter/Filter/README.md similarity index 100% rename from src/flask_inputfilter/Filter/README.md rename to flask_inputfilter/Filter/README.md diff --git a/src/flask_inputfilter/Filter/RemoveEmojisFilter.py b/flask_inputfilter/Filter/RemoveEmojisFilter.py similarity index 99% rename from src/flask_inputfilter/Filter/RemoveEmojisFilter.py rename to flask_inputfilter/Filter/RemoveEmojisFilter.py index 15899c7..6773405 100644 --- a/src/flask_inputfilter/Filter/RemoveEmojisFilter.py +++ b/flask_inputfilter/Filter/RemoveEmojisFilter.py @@ -21,7 +21,6 @@ class RemoveEmojisFilter(BaseFilter): """ def apply(self, value: Any) -> Union[Optional[str], Any]: - if not isinstance(value, str): return value diff --git a/src/flask_inputfilter/Filter/SlugifyFilter.py b/flask_inputfilter/Filter/SlugifyFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/SlugifyFilter.py rename to flask_inputfilter/Filter/SlugifyFilter.py diff --git a/src/flask_inputfilter/Filter/StringTrimFilter.py b/flask_inputfilter/Filter/StringTrimFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/StringTrimFilter.py rename to flask_inputfilter/Filter/StringTrimFilter.py diff --git a/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py b/flask_inputfilter/Filter/ToAlphaNumericFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToAlphaNumericFilter.py rename to flask_inputfilter/Filter/ToAlphaNumericFilter.py diff --git a/src/flask_inputfilter/Filter/ToBooleanFilter.py b/flask_inputfilter/Filter/ToBooleanFilter.py similarity index 69% rename from src/flask_inputfilter/Filter/ToBooleanFilter.py rename to flask_inputfilter/Filter/ToBooleanFilter.py index 9a094a5..8ed49e1 100644 --- a/src/flask_inputfilter/Filter/ToBooleanFilter.py +++ b/flask_inputfilter/Filter/ToBooleanFilter.py @@ -9,8 +9,4 @@ class ToBooleanFilter(BaseFilter): """ def apply(self, value: Any) -> Union[Optional[bool], Any]: - try: - return bool(value) - - except (ValueError, TypeError): - return value + return bool(value) diff --git a/src/flask_inputfilter/Filter/ToCamelCaseFilter.py b/flask_inputfilter/Filter/ToCamelCaseFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToCamelCaseFilter.py rename to flask_inputfilter/Filter/ToCamelCaseFilter.py diff --git a/src/flask_inputfilter/Filter/ToDateFilter.py b/flask_inputfilter/Filter/ToDateFilter.py similarity index 88% rename from src/flask_inputfilter/Filter/ToDateFilter.py rename to flask_inputfilter/Filter/ToDateFilter.py index 2723f4b..db5f35e 100644 --- a/src/flask_inputfilter/Filter/ToDateFilter.py +++ b/flask_inputfilter/Filter/ToDateFilter.py @@ -11,12 +11,12 @@ class ToDateFilter(BaseFilter): """ def apply(self, value: Any) -> Union[date, Any]: - if isinstance(value, date): - return value - - elif isinstance(value, datetime): + if isinstance(value, datetime): return value.date() + elif isinstance(value, date): + return value + elif isinstance(value, str): try: return datetime.fromisoformat(value).date() diff --git a/src/flask_inputfilter/Filter/ToDateTimeFilter.py b/flask_inputfilter/Filter/ToDateTimeFilter.py similarity index 79% rename from src/flask_inputfilter/Filter/ToDateTimeFilter.py rename to flask_inputfilter/Filter/ToDateTimeFilter.py index 19e9dd3..d646793 100644 --- a/src/flask_inputfilter/Filter/ToDateTimeFilter.py +++ b/flask_inputfilter/Filter/ToDateTimeFilter.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import date, datetime from typing import Any, Union from .BaseFilter import BaseFilter @@ -14,6 +14,9 @@ def apply(self, value: Any) -> Union[datetime, Any]: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/src/flask_inputfilter/Filter/ToEnumFilter.py b/flask_inputfilter/Filter/ToEnumFilter.py similarity index 89% rename from src/flask_inputfilter/Filter/ToEnumFilter.py rename to flask_inputfilter/Filter/ToEnumFilter.py index a376b18..f4fc5af 100644 --- a/src/flask_inputfilter/Filter/ToEnumFilter.py +++ b/flask_inputfilter/Filter/ToEnumFilter.py @@ -16,6 +16,9 @@ def apply(self, value: Any) -> Union[Enum, Any]: if not isinstance(value, (str, int)): return value + if isinstance(value, Enum): + return value + try: return self.enum_class(value) diff --git a/src/flask_inputfilter/Filter/ToFloatFilter.py b/flask_inputfilter/Filter/ToFloatFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToFloatFilter.py rename to flask_inputfilter/Filter/ToFloatFilter.py diff --git a/src/flask_inputfilter/Filter/ToIntegerFilter.py b/flask_inputfilter/Filter/ToIntegerFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToIntegerFilter.py rename to flask_inputfilter/Filter/ToIntegerFilter.py diff --git a/src/flask_inputfilter/Filter/ToIsoFilter.py b/flask_inputfilter/Filter/ToIsoFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToIsoFilter.py rename to flask_inputfilter/Filter/ToIsoFilter.py diff --git a/src/flask_inputfilter/Filter/ToLowerFilter.py b/flask_inputfilter/Filter/ToLowerFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToLowerFilter.py rename to flask_inputfilter/Filter/ToLowerFilter.py diff --git a/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py b/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py rename to flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py diff --git a/src/flask_inputfilter/Filter/ToNullFilter.py b/flask_inputfilter/Filter/ToNullFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToNullFilter.py rename to flask_inputfilter/Filter/ToNullFilter.py diff --git a/src/flask_inputfilter/Filter/ToPascaleCaseFilter.py b/flask_inputfilter/Filter/ToPascaleCaseFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToPascaleCaseFilter.py rename to flask_inputfilter/Filter/ToPascaleCaseFilter.py diff --git a/src/flask_inputfilter/Filter/ToSnakeCaseFilter.py b/flask_inputfilter/Filter/ToSnakeCaseFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToSnakeCaseFilter.py rename to flask_inputfilter/Filter/ToSnakeCaseFilter.py diff --git a/src/flask_inputfilter/Filter/ToStringFilter.py b/flask_inputfilter/Filter/ToStringFilter.py similarity index 67% rename from src/flask_inputfilter/Filter/ToStringFilter.py rename to flask_inputfilter/Filter/ToStringFilter.py index 26c7fdc..3249cf4 100644 --- a/src/flask_inputfilter/Filter/ToStringFilter.py +++ b/flask_inputfilter/Filter/ToStringFilter.py @@ -9,8 +9,4 @@ class ToStringFilter(BaseFilter): """ def apply(self, value: Any) -> Union[str, Any]: - try: - return str(value) - - except (ValueError, TypeError): - return value + return str(value) diff --git a/src/flask_inputfilter/Filter/ToUpperFilter.py b/flask_inputfilter/Filter/ToUpperFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/ToUpperFilter.py rename to flask_inputfilter/Filter/ToUpperFilter.py diff --git a/src/flask_inputfilter/Filter/TruncateFilter.py b/flask_inputfilter/Filter/TruncateFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/TruncateFilter.py rename to flask_inputfilter/Filter/TruncateFilter.py diff --git a/flask_inputfilter/Filter/WhitelistFilter.py b/flask_inputfilter/Filter/WhitelistFilter.py new file mode 100644 index 0000000..490e73d --- /dev/null +++ b/flask_inputfilter/Filter/WhitelistFilter.py @@ -0,0 +1,28 @@ +from typing import Any, List + +from .BaseFilter import BaseFilter + + +class WhitelistFilter(BaseFilter): + """Filter that filters out values that are not in the whitelist.""" + + def __init__(self, whitelist: List[str] = None) -> None: + self.whitelist = whitelist + + def apply(self, value: Any) -> Any: + if isinstance(value, str): + return " ".join( + [word for word in value.split() if word in self.whitelist] + ) + + if isinstance(value, list): + return [item for item in value if item in self.whitelist] + + if isinstance(value, dict): + return { + key: value + for key, value in value.items() + if key in self.whitelist + } + + return value diff --git a/src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py b/flask_inputfilter/Filter/WhitespaceCollapseFilter.py similarity index 100% rename from src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py rename to flask_inputfilter/Filter/WhitespaceCollapseFilter.py diff --git a/src/flask_inputfilter/Filter/__init__.py b/flask_inputfilter/Filter/__init__.py similarity index 92% rename from src/flask_inputfilter/Filter/__init__.py rename to flask_inputfilter/Filter/__init__.py index a60cdd3..7223626 100644 --- a/src/flask_inputfilter/Filter/__init__.py +++ b/flask_inputfilter/Filter/__init__.py @@ -1,5 +1,6 @@ from .ArrayExplodeFilter import ArrayExplodeFilter from .BaseFilter import BaseFilter +from .BlacklistFilter import BlacklistFilter from .RemoveEmojisFilter import RemoveEmojisFilter from .SlugifyFilter import SlugifyFilter from .StringTrimFilter import StringTrimFilter @@ -20,4 +21,5 @@ from .ToStringFilter import ToStringFilter from .ToUpperFilter import ToUpperFilter from .TruncateFilter import TruncateFilter +from .WhitelistFilter import WhitelistFilter from .WhitespaceCollapseFilter import WhitespaceCollapseFilter diff --git a/src/flask_inputfilter/InputFilter.py b/flask_inputfilter/InputFilter.py similarity index 96% rename from src/flask_inputfilter/InputFilter.py rename to flask_inputfilter/InputFilter.py index 9f74c65..5c04c3a 100644 --- a/src/flask_inputfilter/InputFilter.py +++ b/flask_inputfilter/InputFilter.py @@ -93,12 +93,15 @@ def _callExternalApi( der im Antwortkörper zu finden ist. """ - requestData = {} + requestData = { + "headers": {}, + "params": {}, + } if config.api_key: - requestData["headers"][ - "Authorization" - ] = f"Bearer {config.api_key}" + requestData["headers"]["Authorization"] = ( + f"Bearer " f"{config.api_key}" + ) if config.headers: requestData["headers"].update(config.headers) @@ -117,8 +120,8 @@ def _callExternalApi( if response.status_code != 200: raise ValidationError( - f"External API call failed with status code " - f"{response.status_code}" + f"External API call failed with " + f"status code {response.status_code}" ) result = response.json() diff --git a/src/flask_inputfilter/Model/ExternalApiConfig.py b/flask_inputfilter/Model/ExternalApiConfig.py similarity index 100% rename from src/flask_inputfilter/Model/ExternalApiConfig.py rename to flask_inputfilter/Model/ExternalApiConfig.py diff --git a/src/flask_inputfilter/Model/__init__.py b/flask_inputfilter/Model/__init__.py similarity index 100% rename from src/flask_inputfilter/Model/__init__.py rename to flask_inputfilter/Model/__init__.py diff --git a/src/flask_inputfilter/Validator/ArrayElementValidator.py b/flask_inputfilter/Validator/ArrayElementValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/ArrayElementValidator.py rename to flask_inputfilter/Validator/ArrayElementValidator.py diff --git a/src/flask_inputfilter/Validator/ArrayLengthValidator.py b/flask_inputfilter/Validator/ArrayLengthValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/ArrayLengthValidator.py rename to flask_inputfilter/Validator/ArrayLengthValidator.py diff --git a/src/flask_inputfilter/Validator/BaseValidator.py b/flask_inputfilter/Validator/BaseValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/BaseValidator.py rename to flask_inputfilter/Validator/BaseValidator.py diff --git a/src/flask_inputfilter/Validator/DateAfterValidator.py b/flask_inputfilter/Validator/DateAfterValidator.py similarity index 66% rename from src/flask_inputfilter/Validator/DateAfterValidator.py rename to flask_inputfilter/Validator/DateAfterValidator.py index bde84d1..54b1055 100644 --- a/src/flask_inputfilter/Validator/DateAfterValidator.py +++ b/flask_inputfilter/Validator/DateAfterValidator.py @@ -1,10 +1,11 @@ -from datetime import datetime -from typing import Any +from datetime import date, datetime +from typing import Any, Union from ..Exception import ValidationError +from .BaseValidator import BaseValidator -class DateAfterValidator: +class DateAfterValidator(BaseValidator): """ Validator that checks if a date is after a specific date. Supports datetime and ISO 8601 formatted strings. @@ -12,19 +13,20 @@ class DateAfterValidator: def __init__( self, - reference_date: str, + reference_date: Union[str, date, datetime], error_message: str = "Date '{}' is not after '{}'.", ) -> None: - self.reference_date = datetime.fromisoformat(reference_date) + self.reference_date = reference_date self.error_message = error_message def validate(self, value: Any) -> None: + value_reference_date = self._parse_date(self.reference_date) value_datetime = self._parse_date(value) - if value_datetime <= self.reference_date: + if value_datetime <= value_reference_date: if "{}" in self.error_message: raise ValidationError( - self.error_message.format(value, self.reference_date) + self.error_message.format(value, value_reference_date) ) raise ValidationError(self.error_message) @@ -33,6 +35,9 @@ def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/src/flask_inputfilter/Validator/DateBeforeValidator.py b/flask_inputfilter/Validator/DateBeforeValidator.py similarity index 66% rename from src/flask_inputfilter/Validator/DateBeforeValidator.py rename to flask_inputfilter/Validator/DateBeforeValidator.py index 1397bb7..dda4fd9 100644 --- a/src/flask_inputfilter/Validator/DateBeforeValidator.py +++ b/flask_inputfilter/Validator/DateBeforeValidator.py @@ -1,10 +1,11 @@ -from datetime import datetime -from typing import Any +from datetime import date, datetime +from typing import Any, Union from ..Exception import ValidationError +from .BaseValidator import BaseValidator -class DateBeforeValidator: +class DateBeforeValidator(BaseValidator): """ Validator that checks if a date is before a specific date. Supports datetime and ISO 8601 formatted strings. @@ -12,19 +13,20 @@ class DateBeforeValidator: def __init__( self, - reference_date: str, + reference_date: Union[str, date, datetime], error_message: str = "Date '{}' is not before '{}'.", ) -> None: - self.reference_date = datetime.fromisoformat(reference_date) + self.reference_date = reference_date self.error_message = error_message def validate(self, value: Any) -> None: + value_reference_date = self._parse_date(self.reference_date) value_datetime = self._parse_date(value) - if value_datetime >= self.reference_date: + if value_datetime >= value_reference_date: if "{}" in self.error_message: raise ValidationError( - self.error_message.format(value, self.reference_date) + self.error_message.format(value, value_reference_date) ) raise ValidationError(self.error_message) @@ -33,6 +35,9 @@ def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/src/flask_inputfilter/Validator/DateRangeValidator.py b/flask_inputfilter/Validator/DateRangeValidator.py similarity index 85% rename from src/flask_inputfilter/Validator/DateRangeValidator.py rename to flask_inputfilter/Validator/DateRangeValidator.py index 336f64c..4a0b550 100644 --- a/src/flask_inputfilter/Validator/DateRangeValidator.py +++ b/flask_inputfilter/Validator/DateRangeValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any, Optional +from typing import Any, Optional, Union from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -12,8 +12,8 @@ class DateRangeValidator(BaseValidator): def __init__( self, - min_date: Optional[date] = None, - max_date: Optional[date] = None, + min_date: Optional[Union[str, date, datetime]] = None, + max_date: Optional[Union[str, date, datetime]] = None, error_message="Date '{}' is not in the range from '{}' to '{}'.", ) -> None: self.min_date = min_date @@ -56,7 +56,6 @@ def _parse_date(self, value: Any) -> datetime: elif isinstance(value, date): return datetime.combine(value, datetime.min.time()) - else: - raise ValidationError( - f"Unsupported type for past date validation: {type(value)}" - ) + raise ValidationError( + f"Unsupported type for past date validation: {type(value)}" + ) diff --git a/src/flask_inputfilter/Validator/FloatPrecisionValidator.py b/flask_inputfilter/Validator/FloatPrecisionValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/FloatPrecisionValidator.py rename to flask_inputfilter/Validator/FloatPrecisionValidator.py diff --git a/src/flask_inputfilter/Validator/InArrayValidator.py b/flask_inputfilter/Validator/InArrayValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/InArrayValidator.py rename to flask_inputfilter/Validator/InArrayValidator.py diff --git a/src/flask_inputfilter/Validator/InEnumValidator.py b/flask_inputfilter/Validator/InEnumValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/InEnumValidator.py rename to flask_inputfilter/Validator/InEnumValidator.py diff --git a/src/flask_inputfilter/Validator/IsArrayValidator.py b/flask_inputfilter/Validator/IsArrayValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsArrayValidator.py rename to flask_inputfilter/Validator/IsArrayValidator.py diff --git a/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py b/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py rename to flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py diff --git a/src/flask_inputfilter/Validator/IsBase64ImageValidator.py b/flask_inputfilter/Validator/IsBase64ImageValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsBase64ImageValidator.py rename to flask_inputfilter/Validator/IsBase64ImageValidator.py diff --git a/src/flask_inputfilter/Validator/IsBooleanValidator.py b/flask_inputfilter/Validator/IsBooleanValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsBooleanValidator.py rename to flask_inputfilter/Validator/IsBooleanValidator.py diff --git a/src/flask_inputfilter/Validator/IsFloatValidator.py b/flask_inputfilter/Validator/IsFloatValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsFloatValidator.py rename to flask_inputfilter/Validator/IsFloatValidator.py diff --git a/src/flask_inputfilter/Validator/IsFutureDateValidator.py b/flask_inputfilter/Validator/IsFutureDateValidator.py similarity index 90% rename from src/flask_inputfilter/Validator/IsFutureDateValidator.py rename to flask_inputfilter/Validator/IsFutureDateValidator.py index 6aae2a1..30124da 100644 --- a/src/flask_inputfilter/Validator/IsFutureDateValidator.py +++ b/flask_inputfilter/Validator/IsFutureDateValidator.py @@ -33,6 +33,9 @@ def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) @@ -40,10 +43,6 @@ def _parse_date(self, value: Any) -> datetime: except ValueError: raise ValidationError(f"Invalid ISO 8601 format: {value}") - elif isinstance(value, date): - return datetime.combine(value, datetime.min.time()) - - else: - raise ValidationError( - f"Unsupported type for past date validation: {type(value)}" - ) + raise ValidationError( + f"Unsupported type for past date validation: {type(value)}" + ) diff --git a/src/flask_inputfilter/Validator/IsHexadecimalValidator.py b/flask_inputfilter/Validator/IsHexadecimalValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsHexadecimalValidator.py rename to flask_inputfilter/Validator/IsHexadecimalValidator.py diff --git a/src/flask_inputfilter/Validator/IsInstanceValidator.py b/flask_inputfilter/Validator/IsInstanceValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsInstanceValidator.py rename to flask_inputfilter/Validator/IsInstanceValidator.py diff --git a/src/flask_inputfilter/Validator/IsIntegerValidator.py b/flask_inputfilter/Validator/IsIntegerValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsIntegerValidator.py rename to flask_inputfilter/Validator/IsIntegerValidator.py diff --git a/src/flask_inputfilter/Validator/IsJsonValidator.py b/flask_inputfilter/Validator/IsJsonValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsJsonValidator.py rename to flask_inputfilter/Validator/IsJsonValidator.py diff --git a/src/flask_inputfilter/Validator/IsPastDateValidator.py b/flask_inputfilter/Validator/IsPastDateValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsPastDateValidator.py rename to flask_inputfilter/Validator/IsPastDateValidator.py diff --git a/src/flask_inputfilter/Validator/IsStringValidator.py b/flask_inputfilter/Validator/IsStringValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsStringValidator.py rename to flask_inputfilter/Validator/IsStringValidator.py diff --git a/src/flask_inputfilter/Validator/IsUUIDValidator.py b/flask_inputfilter/Validator/IsUUIDValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/IsUUIDValidator.py rename to flask_inputfilter/Validator/IsUUIDValidator.py diff --git a/src/flask_inputfilter/Validator/IsWeekdayValidator.py b/flask_inputfilter/Validator/IsWeekdayValidator.py similarity index 83% rename from src/flask_inputfilter/Validator/IsWeekdayValidator.py rename to flask_inputfilter/Validator/IsWeekdayValidator.py index 61109af..26a01dc 100644 --- a/src/flask_inputfilter/Validator/IsWeekdayValidator.py +++ b/flask_inputfilter/Validator/IsWeekdayValidator.py @@ -1,10 +1,11 @@ -from datetime import datetime +from datetime import date, datetime from typing import Any from ..Exception import ValidationError +from .BaseValidator import BaseValidator -class IsWeekdayValidator: +class IsWeekdayValidator(BaseValidator): """ Validator that checks if a date is on a weekday (Monday to Friday). Supports datetime and ISO 8601 formatted strings. @@ -28,6 +29,9 @@ def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) diff --git a/src/flask_inputfilter/Validator/IsWeekendValidator.py b/flask_inputfilter/Validator/IsWeekendValidator.py similarity index 75% rename from src/flask_inputfilter/Validator/IsWeekendValidator.py rename to flask_inputfilter/Validator/IsWeekendValidator.py index dab759f..1162dfa 100644 --- a/src/flask_inputfilter/Validator/IsWeekendValidator.py +++ b/flask_inputfilter/Validator/IsWeekendValidator.py @@ -1,10 +1,11 @@ -from datetime import datetime +from datetime import date, datetime from typing import Any from ..Exception import ValidationError +from .BaseValidator import BaseValidator -class IsWeekendValidator: +class IsWeekendValidator(BaseValidator): """ Validator that checks if a date is on a weekend (Saturday or Sunday). Supports datetime and ISO 8601 formatted strings. @@ -28,6 +29,9 @@ def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): return value + elif isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + elif isinstance(value, str): try: return datetime.fromisoformat(value) @@ -35,7 +39,6 @@ def _parse_date(self, value: Any) -> datetime: except ValueError: raise ValidationError(f"Invalid ISO 8601 format: {value}") - else: - raise ValidationError( - f"Unsupported type for weekend validation: {type(value)}" - ) + raise ValidationError( + f"Unsupported type for weekend validation: {type(value)}" + ) diff --git a/src/flask_inputfilter/Validator/LengthValidator.py b/flask_inputfilter/Validator/LengthValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/LengthValidator.py rename to flask_inputfilter/Validator/LengthValidator.py diff --git a/src/flask_inputfilter/Validator/README.md b/flask_inputfilter/Validator/README.md similarity index 100% rename from src/flask_inputfilter/Validator/README.md rename to flask_inputfilter/Validator/README.md diff --git a/src/flask_inputfilter/Validator/RangeValidator.py b/flask_inputfilter/Validator/RangeValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/RangeValidator.py rename to flask_inputfilter/Validator/RangeValidator.py diff --git a/src/flask_inputfilter/Validator/RegexValidator.py b/flask_inputfilter/Validator/RegexValidator.py similarity index 100% rename from src/flask_inputfilter/Validator/RegexValidator.py rename to flask_inputfilter/Validator/RegexValidator.py diff --git a/src/flask_inputfilter/Validator/__init__.py b/flask_inputfilter/Validator/__init__.py similarity index 100% rename from src/flask_inputfilter/Validator/__init__.py rename to flask_inputfilter/Validator/__init__.py diff --git a/src/flask_inputfilter/__init__.py b/flask_inputfilter/__init__.py similarity index 100% rename from src/flask_inputfilter/__init__.py rename to flask_inputfilter/__init__.py diff --git a/requirements.txt b/requirements.txt index c3ed8a5..a9f07e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,4 @@ isort pillow==8.0.0 pytest requests==2.22.0 -setuptools -twine +coveralls diff --git a/setup.py b/setup.py index baf2e8c..980b194 100644 --- a/setup.py +++ b/setup.py @@ -10,15 +10,20 @@ long_description=open("README.md").read(), long_description_content_type="text/markdown", url="https://github.com/LeanderCS/flask-inputfilter", - packages=find_packages(where="src"), - package_dir={"": "src"}, + packages=find_packages(), install_requires=[ "Flask>=2.1", "pillow>=8.0.0", "requests>=2.22.0", ], classifiers=[ - "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.7", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/flask_inputfilter/Condition/ArrayLengthEqualCondition.py b/src/flask_inputfilter/Condition/ArrayLengthEqualCondition.py deleted file mode 100644 index 736cb47..0000000 --- a/src/flask_inputfilter/Condition/ArrayLengthEqualCondition.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any, Dict - -from .BaseCondition import BaseCondition - - -class ArrayLengthEqualCondition(BaseCondition): - """ - Condition that checks if the array is of the specified length. - """ - - def __init__(self, first_field: str, second_field: str) -> None: - self.first_field = first_field - self.second_field = second_field - - def check(self, data: Dict[str, Any]) -> bool: - return len(data.get(self.first_field) or []) == len( - data.get(self.second_field) or [] - ) diff --git a/test/test_condition.py b/test/test_condition.py index 9880919..6d7007b 100644 --- a/test/test_condition.py +++ b/test/test_condition.py @@ -1,9 +1,11 @@ import unittest +from datetime import date, datetime -from src.flask_inputfilter import InputFilter -from src.flask_inputfilter.Condition import ( +from flask_inputfilter import InputFilter +from flask_inputfilter.Condition import ( ArrayLengthEqualCondition, ArrayLongerThanCondition, + BaseCondition, CustomCondition, EqualCondition, ExactlyNOfCondition, @@ -19,7 +21,8 @@ StringLongerThanCondition, TemporalOrderCondition, ) -from src.flask_inputfilter.Exception import ValidationError +from flask_inputfilter.Condition.NotEqualCondition import NotEqualCondition +from flask_inputfilter.Exception import ValidationError class TestConditions(unittest.TestCase): @@ -44,12 +47,19 @@ def test_array_length_equal_condition(self) -> None: self.inputFilter.validateData({"field1": [1, 2], "field2": [1, 2]}) + self.inputFilter.validateData({"field1": [], "field2": []}) + with self.assertRaises(ValidationError): self.inputFilter.validateData({"field1": [1, 2]}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"field1": [1, 2], "field2": [1]}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": [1, 2], "field2": [1, 2, 3]} + ) + def test_array_longer_than_condition(self) -> None: """ Test ArrayLongerThanCondition. @@ -67,6 +77,14 @@ def test_array_longer_than_condition(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"field1": [1, 2], "field2": [1, 2]}) + def test_base_condition(self) -> None: + """ + Test BaseCondition. + """ + + with self.assertRaises(NotImplementedError): + BaseCondition().check({}) + def test_custom_condition(self) -> None: """ Test CustomCondition. @@ -261,6 +279,36 @@ def test_nth_of_matches_condition(self) -> None: {"field1": "value", "field2": "value"} ) + def test_not_equal_condition(self) -> None: + """ + Test NotEqualCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition(NotEqualCondition("field1", "field2")) + + self.inputFilter.validateData( + {"field1": "value", "field2": "not value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": True, "field2": True}) + def test_one_of_condition(self) -> None: """ Test OneOfCondition when at least one field is present. @@ -355,6 +403,13 @@ def test_temporal_order_condition(self) -> None: {"field1": "2021-01-01", "field2": "2021-01-02"} ) + self.inputFilter.validateData( + { + "field1": datetime(2021, 1, 1, 12, 0, 0), + "field2": datetime(2021, 1, 2, 12, 0, 0), + } + ) + with self.assertRaises(ValidationError): self.inputFilter.validateData( {"field1": "2021-01-02", "field2": "2021-01-01"} @@ -362,12 +417,23 @@ def test_temporal_order_condition(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData( - {"field1": "2021-01-01", "field2": "2021-01-01"} + {"field1": date(2023, 1, 1), "field2": "2021-01-01"} ) with self.assertRaises(ValidationError): self.inputFilter.validateData({"field1": "2021-01-01"}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + { + "field1": datetime(2021, 1, 1, 12, 0, 0), + "field2": datetime(2020, 1, 1, 12, 0, 0), + } + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": "not a datetime"}) + if __name__ == "__main__": unittest.main() diff --git a/test/test_filter.py b/test/test_filter.py index 158a08a..d4c304f 100644 --- a/test/test_filter.py +++ b/test/test_filter.py @@ -2,8 +2,11 @@ from datetime import date, datetime from enum import Enum -from src.flask_inputfilter.Filter import ( +from flask_inputfilter import InputFilter +from flask_inputfilter.Filter import ( ArrayExplodeFilter, + BaseFilter, + BlacklistFilter, RemoveEmojisFilter, SlugifyFilter, StringTrimFilter, @@ -24,9 +27,9 @@ ToStringFilter, ToUpperFilter, TruncateFilter, + WhitelistFilter, WhitespaceCollapseFilter, ) -from src.flask_inputfilter.InputFilter import InputFilter class TestInputFilter(unittest.TestCase): @@ -51,7 +54,6 @@ def test_array_explode_filter(self) -> None: validated_data = self.inputFilter.validateData( {"tags": "tag1,tag2,tag3"} ) - self.assertEqual(validated_data["tags"], ["tag1", "tag2", "tag3"]) self.inputFilter.add( @@ -61,9 +63,50 @@ def test_array_explode_filter(self) -> None: validated_data = self.inputFilter.validateData( {"items": "item1;item2;item3"} ) - self.assertEqual(validated_data["items"], ["item1", "item2", "item3"]) + def test_base_filter(self) -> None: + """ + Test that BaseFilter raises NotImplementedError when apply + method is called. + """ + + with self.assertRaises(NotImplementedError): + BaseFilter().apply("test") + + def test_blacklist_filter(self) -> None: + """ + Test that BlacklistFilter filters out values that are in the blacklist. + """ + + self.inputFilter.add( + "blacklisted_field", + required=False, + filters=[BlacklistFilter(["test", "user"])], + ) + + validated_data = self.inputFilter.validateData( + {"blacklisted_field": "test user"} + ) + self.assertEqual(validated_data["blacklisted_field"], "") + + validated_data = self.inputFilter.validateData( + {"blacklisted_field": ["test", "user", "admin"]} + ) + self.assertEqual(validated_data["blacklisted_field"], ["admin"]) + + validated_data = self.inputFilter.validateData( + {"blacklisted_field": {"test": "user", "admin": "admin"}} + ) + self.assertEqual( + validated_data["blacklisted_field"], {"admin": "admin"} + ) + + validated_data = self.inputFilter.validateData( + {"blacklisted_field": 123} + ) + self.assertEqual(validated_data["blacklisted_field"], 123) + def test_remove_emojis_filter(self) -> None: """ Test that RemoveEmojisFilter removes emojis from a string. @@ -78,9 +121,11 @@ def test_remove_emojis_filter(self) -> None: validated_data = self.inputFilter.validateData( {"text": "Hello World! 😊"} ) - self.assertEqual(validated_data["text"], "Hello World! ") + validated_data = self.inputFilter.validateData({"text": 123}) + self.assertEqual(validated_data["text"], 123) + def test_slugify_filter(self) -> None: """ Test that SlugifyFilter slugifies a string. @@ -95,9 +140,11 @@ def test_slugify_filter(self) -> None: validated_data = self.inputFilter.validateData( {"slug": "Hello World!"} ) - self.assertEqual(validated_data["slug"], "hello-world") + validated_data = self.inputFilter.validateData({"slug": 123}) + self.assertEqual(validated_data["slug"], 123) + def test_string_trim_filter(self) -> None: """ Test that StringTrimFilter trims whitespace. @@ -110,7 +157,6 @@ def test_string_trim_filter(self) -> None: validated_data = self.inputFilter.validateData( {"trimmed_field": " Hello World "} ) - self.assertEqual(validated_data["trimmed_field"], "Hello World") def test_to_alphanumeric_filter(self) -> None: @@ -127,10 +173,14 @@ def test_to_alphanumeric_filter(self) -> None: validated_data = self.inputFilter.validateData( {"alphanumeric_field": "Hello World!123"} ) - self.assertEqual(validated_data["alphanumeric_field"], "HelloWorld123") - def test_to_bool_filter(self) -> None: + validated_data = self.inputFilter.validateData( + {"alphanumeric_field": 123} + ) + self.assertEqual(validated_data["alphanumeric_field"], 123) + + def test_to_boolean_filter(self) -> None: """ Test that ToBooleanFilter converts string to boolean. """ @@ -140,7 +190,6 @@ def test_to_bool_filter(self) -> None: ) validated_data = self.inputFilter.validateData({"is_active": "true"}) - self.assertTrue(validated_data["is_active"]) def test_to_camel_case_filter(self) -> None: @@ -155,9 +204,11 @@ def test_to_camel_case_filter(self) -> None: validated_data = self.inputFilter.validateData( {"username": "test user"} ) - self.assertEqual(validated_data["username"], "testUser") + validated_data = self.inputFilter.validateData({"username": 123}) + self.assertEqual(validated_data["username"], 123) + def test_to_date_filter(self) -> None: """ Test that ToDateFilter converts string to date. @@ -166,9 +217,24 @@ def test_to_date_filter(self) -> None: self.inputFilter.add("dob", required=True, filters=[ToDateFilter()]) validated_data = self.inputFilter.validateData({"dob": "1996-12-01"}) + self.assertEqual(validated_data["dob"], date(1996, 12, 1)) + validated_data = self.inputFilter.validateData( + {"dob": date(1996, 12, 1)} + ) self.assertEqual(validated_data["dob"], date(1996, 12, 1)) + validated_data = self.inputFilter.validateData( + {"dob": datetime(1996, 12, 1, 12, 0, 0)} + ) + self.assertEqual(validated_data["dob"], date(1996, 12, 1)) + + validated_data = self.inputFilter.validateData({"dob": "no date"}) + self.assertEqual(validated_data["dob"], "no date") + + validated_data = self.inputFilter.validateData({"dob": 123}) + self.assertEqual(validated_data["dob"], 123) + def test_to_datetime_filter(self) -> None: """ Test that ToDateTimeFilter converts string to datetime. @@ -181,12 +247,35 @@ def test_to_datetime_filter(self) -> None: validated_data = self.inputFilter.validateData( {"created_at": "2021-01-01T12:00:00"} ) + self.assertEqual( + validated_data["created_at"], + datetime(2021, 1, 1, 12, 0, 0), + ) + + validated_data = self.inputFilter.validateData( + {"created_at": date(2021, 1, 1)} + ) + self.assertEqual( + validated_data["created_at"], + datetime(2021, 1, 1, 0, 0, 0), + ) + validated_data = self.inputFilter.validateData( + {"created_at": datetime(2021, 1, 1, 12, 0, 0)} + ) self.assertEqual( validated_data["created_at"], datetime(2021, 1, 1, 12, 0, 0), ) + validated_data = self.inputFilter.validateData( + {"created_at": "no date"} + ) + self.assertEqual(validated_data["created_at"], "no date") + + validated_data = self.inputFilter.validateData({"created_at": 123}) + self.assertEqual(validated_data["created_at"], 123) + def test_to_enum_filter(self) -> None: """ Test that EnumFilter validates a string against a list of values. @@ -204,7 +293,17 @@ class ColorEnum(Enum): ) validated_data = self.inputFilter.validateData({"color": "red"}) + self.assertEqual(validated_data["color"], ColorEnum.RED) + + validated_data = self.inputFilter.validateData({"color": "yellow"}) + self.assertEqual(validated_data["color"], "yellow") + validated_data = self.inputFilter.validateData({"color": 123}) + self.assertEqual(validated_data["color"], 123) + + validated_data = self.inputFilter.validateData( + {"color": ColorEnum.RED} + ) self.assertEqual(validated_data["color"], ColorEnum.RED) def test_to_float_filter(self) -> None: @@ -215,9 +314,14 @@ def test_to_float_filter(self) -> None: self.inputFilter.add("price", required=True, filters=[ToFloatFilter()]) validated_data = self.inputFilter.validateData({"price": "19.99"}) - self.assertEqual(validated_data["price"], 19.99) + validated_data = self.inputFilter.validateData({"price": False}) + self.assertEqual(validated_data["price"], False) + + validated_data = self.inputFilter.validateData({"price": "no float"}) + self.assertEqual(validated_data["price"], "no float") + def test_to_integer_filter(self) -> None: """ Test that ToIntegerFilter converts string to integer. @@ -226,9 +330,17 @@ def test_to_integer_filter(self) -> None: self.inputFilter.add("age", required=True, filters=[ToIntegerFilter()]) validated_data = self.inputFilter.validateData({"age": "25"}) + self.assertEqual(validated_data["age"], 25) + validated_data = self.inputFilter.validateData({"age": 25.3}) self.assertEqual(validated_data["age"], 25) + validated_data = self.inputFilter.validateData({"age": False}) + self.assertEqual(validated_data["age"], False) + + validated_data = self.inputFilter.validateData({"age": "no integer"}) + self.assertEqual(validated_data["age"], "no integer") + def test_to_iso_filter(self) -> None: """ Test that ToIsoFilter converts date or datetime to @@ -240,7 +352,6 @@ def test_to_iso_filter(self) -> None: validated_data = self.inputFilter.validateData( {"date": date(2021, 1, 1)} ) - self.assertEqual(validated_data["date"], "2021-01-01") self.inputFilter.add("datetime", filters=[ToIsoFilter()]) @@ -248,7 +359,6 @@ def test_to_iso_filter(self) -> None: validated_data = self.inputFilter.validateData( {"datetime": datetime(2021, 1, 1, 12, 0, 0)} ) - self.assertEqual(validated_data["datetime"], "2021-01-01T12:00:00") def test_to_lower_filter(self) -> None: @@ -263,9 +373,11 @@ def test_to_lower_filter(self) -> None: validated_data = self.inputFilter.validateData( {"username": "TESTUSER"} ) - self.assertEqual(validated_data["username"], "testuser") + validated_data = self.inputFilter.validateData({"username": 123}) + self.assertEqual(validated_data["username"], 123) + def test_to_normalized_unicode_filter(self) -> None: """ Test that NormalizeUnicodeFilter normalizes Unicode characters. @@ -280,9 +392,11 @@ def test_to_normalized_unicode_filter(self) -> None: validated_data = self.inputFilter.validateData( {"unicode_field": "Héllô Wôrld"} ) - self.assertEqual(validated_data["unicode_field"], "Hello World") + validated_data = self.inputFilter.validateData({"unicode_field": 123}) + self.assertEqual(validated_data["unicode_field"], 123) + def test_to_null_filter(self) -> None: """ Test that ToNullFilter transforms empty string to None. @@ -293,7 +407,6 @@ def test_to_null_filter(self) -> None: ) validated_data = self.inputFilter.validateData({"optional_field": ""}) - self.assertIsNone(validated_data["optional_field"]) def test_to_pascal_case_filter(self) -> None: @@ -308,9 +421,11 @@ def test_to_pascal_case_filter(self) -> None: validated_data = self.inputFilter.validateData( {"username": "test user"} ) - self.assertEqual(validated_data["username"], "TestUser") + validated_data = self.inputFilter.validateData({"username": 123}) + self.assertEqual(validated_data["username"], 123) + def test_snake_case_filter(self) -> None: """ Test that SnakeCaseFilter converts string to snake case. @@ -323,9 +438,11 @@ def test_snake_case_filter(self) -> None: validated_data = self.inputFilter.validateData( {"username": "TestUser"} ) - self.assertEqual(validated_data["username"], "test_user") + validated_data = self.inputFilter.validateData({"username": 123}) + self.assertEqual(validated_data["username"], 123) + def test_to_string_filter(self) -> None: """ Test that ToStringFilter converts any type to string. @@ -334,7 +451,6 @@ def test_to_string_filter(self) -> None: self.inputFilter.add("age", required=True, filters=[ToStringFilter()]) validated_data = self.inputFilter.validateData({"age": 25}) - self.assertEqual(validated_data["age"], "25") def test_to_upper_filter(self) -> None: @@ -349,9 +465,11 @@ def test_to_upper_filter(self) -> None: validated_data = self.inputFilter.validateData( {"username": "testuser"} ) - self.assertEqual(validated_data["username"], "TESTUSER") + validated_data = self.inputFilter.validateData({"username": 123}) + self.assertEqual(validated_data["username"], 123) + def test_truncate_filter(self) -> None: """ Test that TruncateFilter truncates a string. @@ -364,9 +482,45 @@ def test_truncate_filter(self) -> None: validated_data = self.inputFilter.validateData( {"truncated_field": "Hello World"} ) - self.assertEqual(validated_data["truncated_field"], "Hello") + validated_data = self.inputFilter.validateData( + {"truncated_field": 123} + ) + self.assertEqual(validated_data["truncated_field"], 123) + + def test_whitelist_filter(self) -> None: + """ + Test that WhitelistFilter filters out values that are + not in the whitelist. + """ + + self.inputFilter.add( + "whitelisted_field", + required=False, + filters=[WhitelistFilter(["test", "user"])], + ) + + validated_data = self.inputFilter.validateData( + {"whitelisted_field": "test user admin"} + ) + self.assertEqual(validated_data["whitelisted_field"], "test user") + + validated_data = self.inputFilter.validateData( + {"whitelisted_field": ["test", "user", "admin"]} + ) + self.assertEqual(validated_data["whitelisted_field"], ["test", "user"]) + + validated_data = self.inputFilter.validateData( + {"whitelisted_field": {"test": "user", "admin": "admin"}} + ) + self.assertEqual(validated_data["whitelisted_field"], {"test": "user"}) + + validated_data = self.inputFilter.validateData( + {"whitelisted_field": 123} + ) + self.assertEqual(validated_data["whitelisted_field"], 123) + def test_whitespace_collapse_filter(self) -> None: """ Test that WhitespaceCollapseFilter collapses whitespace. @@ -381,9 +535,13 @@ def test_whitespace_collapse_filter(self) -> None: validated_data = self.inputFilter.validateData( {"collapsed_field": "Hello World"} ) - self.assertEqual(validated_data["collapsed_field"], "Hello World") + validated_data = self.inputFilter.validateData( + {"collapsed_field": 123} + ) + self.assertEqual(validated_data["collapsed_field"], 123) + if __name__ == "__main__": unittest.main() diff --git a/test/test_input_filter.py b/test/test_input_filter.py index 983da92..3890466 100644 --- a/test/test_input_filter.py +++ b/test/test_input_filter.py @@ -1,9 +1,10 @@ import unittest from unittest.mock import Mock, patch -from src.flask_inputfilter.Exception import ValidationError -from src.flask_inputfilter.InputFilter import ExternalApiConfig, InputFilter -from src.flask_inputfilter.Validator import InArrayValidator +from flask_inputfilter import InputFilter +from flask_inputfilter.Exception import ValidationError +from flask_inputfilter.Model import ExternalApiConfig +from flask_inputfilter.Validator import InArrayValidator class TestInputFilter(unittest.TestCase): @@ -100,8 +101,7 @@ def test_external_api(self, mock_request: Mock) -> None: 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, + headers={}, method="GET", url=expected_url, params={} ) # API returns invalid result @@ -134,6 +134,8 @@ def test_external_api_params(self, mock_request: Mock) -> None: method="GET", params={"hash": "{{hash}}"}, data_key="is_valid", + headers={"custom_header": "value"}, + api_key="1234", ), ) @@ -145,7 +147,10 @@ def test_external_api_params(self, mock_request: Mock) -> None: 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"} + headers={"Authorization": "Bearer 1234", "custom_header": "value"}, + method="GET", + url=expected_url, + params={"hash": "1234"}, ) # API returns invalid status code diff --git a/test/test_validator.py b/test/test_validator.py index 90d083b..dc06575 100644 --- a/test/test_validator.py +++ b/test/test_validator.py @@ -2,12 +2,15 @@ from datetime import date, datetime, timedelta from enum import Enum -from src.flask_inputfilter.Enum import RegexEnum -from src.flask_inputfilter.Exception import ValidationError -from src.flask_inputfilter.InputFilter import InputFilter -from src.flask_inputfilter.Validator import ( +from flask_inputfilter import InputFilter +from flask_inputfilter.Enum import RegexEnum +from flask_inputfilter.Exception import ValidationError +from flask_inputfilter.Validator import ( ArrayElementValidator, ArrayLengthValidator, + BaseValidator, + DateAfterValidator, + DateBeforeValidator, DateRangeValidator, FloatPrecisionValidator, InArrayValidator, @@ -25,6 +28,8 @@ IsPastDateValidator, IsStringValidator, IsUUIDValidator, + IsWeekdayValidator, + IsWeekendValidator, LengthValidator, RangeValidator, RegexValidator, @@ -47,20 +52,17 @@ def test_array_element_validator(self) -> None: elementFilter = InputFilter() elementFilter.add( "id", - required=True, validators=[IsIntegerValidator()], ) self.inputFilter.add( "items", - required=True, validators=[ArrayElementValidator(elementFilter)], ) validated_data = self.inputFilter.validateData( {"items": [{"id": 1}, {"id": 2}]} ) - self.assertEqual(validated_data["items"], [{"id": 1}, {"id": 2}]) with self.assertRaises(ValidationError): @@ -68,6 +70,22 @@ def test_array_element_validator(self) -> None: {"items": [{"id": 1}, {"id": "invalid"}]} ) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"items": "not an array"}) + + # test custom error message + self.inputFilter.add( + "items", + validators=[ + ArrayElementValidator(elementFilter, "Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"items": [{"id": 1}, {"id": "invalid"}]} + ) + def test_array_length_validator(self) -> None: """ Test ArrayLengthValidator. @@ -75,7 +93,6 @@ def test_array_length_validator(self) -> None: self.inputFilter.add( "items", - required=True, validators=[ArrayLengthValidator(min_length=2, max_length=5)], ) @@ -87,6 +104,31 @@ def test_array_length_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"items": [1, 2, 3, 4, 5, 6]}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"items": "not an array"}) + + self.inputFilter.add( + "items", + validators=[ + ArrayLengthValidator( + max_length=10, error_message="custom error message" + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"items": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]} + ) + + def test_base_validator(self) -> None: + """ + Test BaseValidator. + """ + + with self.assertRaises(NotImplementedError): + BaseValidator().validate("value") + def test_date_after_validator(self) -> None: """ Test DateAfterValidator. @@ -94,46 +136,73 @@ def test_date_after_validator(self) -> None: self.inputFilter.add( "date", - validators=[DateRangeValidator(min_date=date(2021, 1, 1))], + validators=[DateAfterValidator(reference_date=date(2021, 1, 1))], ) - self.inputFilter.validateData({"date": date(2021, 6, 1)}) + self.inputFilter.validateData({"datetime": datetime(2021, 6, 1, 0, 0)}) + self.inputFilter.validateData({"isodate": "2021-06-02T10:00:55"}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"date": date(2020, 6, 1)}) + self.inputFilter.validateData({"date": date(2020, 12, 31)}) + self.inputFilter.validateData( + {"datetime": datetime(2020, 12, 31, 23, 59)} + ) + self.inputFilter.validateData({"isodate": "2020-12-31T23:59:59"}) self.inputFilter.add( "datetime", validators=[ - DateRangeValidator( - min_date=datetime(2021, 1, 1, 0, 0), + DateAfterValidator( + reference_date=datetime(2021, 1, 1, 0, 0), ) ], ) - - self.inputFilter.validateData({"datetime": date(2021, 6, 1)}) + self.inputFilter.validateData( + {"datetime": datetime(2021, 6, 1, 12, 0)} + ) + self.inputFilter.validateData({"isodatetime": "2021-06-01T12:00:00"}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"datetime": date(2020, 6, 1)}) + self.inputFilter.validateData( + {"datetime": datetime(2020, 12, 31, 23, 59)} + ) + self.inputFilter.validateData( + {"isodatetime": "2020-12-31T23:59:59"} + ) self.inputFilter.add( - "iso_date", + "isodatetime", validators=[ - DateRangeValidator( - min_date="2021-01-12T22:26:08.542945", - max_date="2021-01-24T22:26:08.542945", + DateAfterValidator( + reference_date="2021-01-01T00:00:00", ) ], ) + self.inputFilter.validateData({"isodatetime": "2021-06-01T00:00:00"}) - self.inputFilter.validateData( - {"iso_date": "2021-01-15T22:26:08.542945"} + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"isodatetime": "2020-12-31T23:59:59"} + ) + + self.inputFilter.add( + "custom_error", + validators=[ + DateAfterValidator( + reference_date="2021-01-01T00:00:00", + error_message="Custom error message", + ) + ], ) - with self.assertRaises(ValidationError): + with self.assertRaises(ValidationError) as context: self.inputFilter.validateData( - {"iso_date": "2021-01-10T22:26:08.542945"} + {"custom_error": "2020-12-31T23:59:59"} ) + self.assertEqual(str(context.exception), "Custom error message") + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": "unparseable date"}) def test_date_before_validator(self) -> None: """ @@ -142,47 +211,88 @@ def test_date_before_validator(self) -> None: self.inputFilter.add( "date", - validators=[DateRangeValidator(min_date=date(2021, 1, 1))], + validators=[ + DateBeforeValidator(reference_date=date(2021, 12, 31)) + ], ) self.inputFilter.validateData({"date": date(2021, 6, 1)}) + self.inputFilter.validateData({"datetime": datetime(2021, 6, 1, 0, 0)}) + self.inputFilter.validateData({"isodate": "2022-06-00T10:00:55"}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"date": date(2020, 6, 1)}) + self.inputFilter.validateData({"date": date(2022, 6, 1)}) + self.inputFilter.validateData( + {"datetime": datetime(2022, 6, 1, 0, 54)} + ) + self.inputFilter.validateData({"isodatetime": "20"}) self.inputFilter.add( "datetime", validators=[ - DateRangeValidator( - max_date=datetime(2021, 12, 31, 0, 0), + DateBeforeValidator( + reference_date=datetime(2021, 12, 31, 0, 0), ) ], ) - self.inputFilter.validateData({"datetime": date(2021, 6, 1)}) + self.inputFilter.validateData({"date": date(2021, 6, 1)}) + self.inputFilter.validateData( + {"datetime": datetime(2021, 6, 1, 12, 0)} + ) + self.inputFilter.validateData({"isodatetime": "2021-06-01T00:00:00"}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"datetime": date(2022, 6, 1)}) + self.inputFilter.validateData({"date": date(2022, 6, 1)}) + self.inputFilter.validateData( + {"datetime": datetime(2022, 6, 1, 0, 0)} + ) + self.inputFilter.validateData( + {"isodatetime": "2022-06-01T00:00:00"} + ) self.inputFilter.add( - "iso_date", + "isodatetime", validators=[ - DateRangeValidator( - max_date="2021-01-24T22:26:08.542945", - min_date="2021-01-12T22:26:08.542945", + DateBeforeValidator( + reference_date="2021-12-31T00:00:00", ) ], ) + self.inputFilter.validateData({"date": date(2021, 6, 1)}) self.inputFilter.validateData( - {"iso_date": "2021-01-15T22:26:08.542945"} + {"datetime": datetime(2021, 6, 1, 12, 0)} ) + self.inputFilter.validateData({"isodatetime": "2021-06-01T00:00:00"}) with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": date(2022, 6, 1)}) self.inputFilter.validateData( - {"iso_date": "2021-01-25T22:26:08.542945"} + {"datetime": datetime(2022, 6, 1, 10, 0)} + ) + self.inputFilter.validateData( + {"isodatetime": "2022-06-01T00:00:00"} ) + self.inputFilter.add( + "custom_error", + validators=[ + DateBeforeValidator( + reference_date="2021-12-31T00:00:00", + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"custom_error": "2022-06-01T00:00:00"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": "unparseable date"}) + def test_date_range_validator(self) -> None: """ Test DateRangeValidator. @@ -231,6 +341,27 @@ def test_date_range_validator(self) -> None: {"iso_date": "2022-01-15T22:26:08.542945"} ) + self.inputFilter.add( + "custom_error", + validators=[ + DateRangeValidator( + max_date="2021-01-24T22:26:08.542945", + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"custom_error": "2022-12-31T23:59:59"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": "unparseable date"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": 123}) + def test_float_precision_validator(self) -> None: """ Test FloatPrecisionValidator. @@ -238,7 +369,6 @@ def test_float_precision_validator(self) -> None: self.inputFilter.add( "price", - required=True, validators=[FloatPrecisionValidator(precision=5, scale=2)], ) @@ -247,6 +377,24 @@ def test_float_precision_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"price": 19.999}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"price": 1999.99}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"price": "not a float"}) + + self.inputFilter.add( + "custom_message2", + validators=[ + FloatPrecisionValidator( + precision=5, scale=2, error_message="Custom error message" + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_message2": 19.999}) + def test_in_array_validator(self) -> None: """ Test InArrayValidator. @@ -254,7 +402,6 @@ def test_in_array_validator(self) -> None: self.inputFilter.add( "color", - required=True, validators=[InArrayValidator(["red", "green", "blue"])], ) @@ -263,6 +410,29 @@ def test_in_array_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"color": "yellow"}) + self.inputFilter.add( + "color_strict", + validators=[InArrayValidator(["red", "green", "blue"], True)], + ) + + self.inputFilter.validateData({"color_strict": "red"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"color_strict": 1}) + + self.inputFilter.add( + "custom_error2", + validators=[ + InArrayValidator( + ["red", "green", "blue"], + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error2": "yellow"}) + def test_in_enum_validator(self) -> None: """ Test InEnumValidator. @@ -273,29 +443,45 @@ class Color(Enum): GREEN = "green" BLUE = "blue" - self.inputFilter.add( - "color", required=True, validators=[InEnumValidator(Color)] - ) + self.inputFilter.add("color", validators=[InEnumValidator(Color)]) self.inputFilter.validateData({"color": "red"}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"color": "yellow"}) + self.inputFilter.add( + "custom_error2", + validators=[ + InEnumValidator(Color, error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error2": "yellow"}) + 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", validators=[IsArrayValidator()]) self.inputFilter.validateData({"tags": ["tag1", "tag2"]}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"tags": "not_an_array"}) + self.inputFilter.add( + "tags2", + validators=[ + IsArrayValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"tags2": "not_an_array"}) + def test_is_base64_image_correct_size_validator(self) -> None: """ Test IsBase64ImageCorrectSizeValidator. @@ -303,7 +489,6 @@ def test_is_base64_image_correct_size_validator(self) -> None: self.inputFilter.add( "image", - required=True, validators=[ IsBase64ImageCorrectSizeValidator(minSize=10, maxSize=50) ], @@ -318,6 +503,22 @@ def test_is_base64_image_correct_size_validator(self) -> None: {"image": "iVBORw0KGgoAAAANSUhEUgAAAAU"} ) + self.inputFilter.add( + "image2", + validators=[ + IsBase64ImageCorrectSizeValidator( + minSize=10, + maxSize=50, + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"image2": "iVBORw0KGgoAAAANSUhEUgAAAAU"} + ) + def test_is_base64_image_validator(self) -> None: """ Test IsBase64ImageValidator. @@ -338,37 +539,51 @@ def test_is_boolean_validator(self) -> None: Test IsBooleanValidator. """ - self.inputFilter.add( - "is_active", required=True, validators=[IsBooleanValidator()] - ) + self.inputFilter.add("is_active", validators=[IsBooleanValidator()]) self.inputFilter.validateData({"is_active": True}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"is_active": "yes"}) + self.inputFilter.add( + "is_active2", + validators=[ + IsBooleanValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"is_active2": "yes"}) + 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", validators=[IsFloatValidator()]) self.inputFilter.validateData({"price": 19.99}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"price": "not_a_float"}) + self.inputFilter.add( + "price2", + validators=[ + IsFloatValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"price2": "not_a_float"}) + def test_is_future_date_validator(self) -> None: """ Test IsFutureDateValidator. """ - self.inputFilter.add( - "date", required=True, validators=[IsFutureDateValidator()] - ) + self.inputFilter.add("date", validators=[IsFutureDateValidator()]) future_date = datetime.now() + timedelta(days=10) self.inputFilter.validateData({"date": future_date}) @@ -381,70 +596,118 @@ def test_is_future_date_validator(self) -> None: past_date = date.today() - timedelta(days=10) self.inputFilter.validateData({"date": past_date}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "not a date"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": 123}) + + self.inputFilter.add( + "date2", + validators=[ + IsFutureDateValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + past_date = date.today() - timedelta(days=10) + self.inputFilter.validateData({"date2": past_date}) + 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", validators=[IsHexadecimalValidator()]) self.inputFilter.validateData({"hex": "0x1234"}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"hex": "not_a_hex"}) + self.inputFilter.add( + "hex2", + validators=[ + IsHexadecimalValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"hex2": "not_a_hex"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"hex2": 123}) + def test_is_instance_validator(self) -> None: """ Test IsInstanceValidator. """ - self.inputFilter.add( - "user", required=True, validators=[IsInstanceValidator(dict)] - ) + self.inputFilter.add("user", validators=[IsInstanceValidator(dict)]) self.inputFilter.validateData({"user": {"name": "Alice"}}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"user": "Alice"}) + self.inputFilter.add( + "user2", + validators=[ + IsInstanceValidator(dict, error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"user2": "Alice"}) + 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", validators=[IsIntegerValidator()]) self.inputFilter.validateData({"age": 25}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"age": "obviously not an integer"}) + self.inputFilter.add( + "age2", + validators=[ + IsIntegerValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"age2": "obviously not an integer"}) + 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", validators=[IsJsonValidator()]) self.inputFilter.validateData({"data": '{"name": "Alice"}'}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"data": "not_a_json"}) + self.inputFilter.add( + "data2", + validators=[IsJsonValidator(error_message="Custom error message")], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"data2": "not_a_json"}) + def test_is_past_date_validator(self) -> None: """ Test IsPastDateValidator. """ - self.inputFilter.add( - "date", required=True, validators=[IsPastDateValidator()] - ) + self.inputFilter.add("date", validators=[IsPastDateValidator()]) self.inputFilter.validateData({"date": date(2021, 1, 1)}) self.inputFilter.validateData({"date": datetime(2021, 1, 1, 0, 0)}) @@ -455,28 +718,51 @@ def test_is_past_date_validator(self) -> None: future_date = date.today() + timedelta(days=10) self.inputFilter.validateData({"date": future_date}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "not a date"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": 123}) + + self.inputFilter.add( + "date2", + validators=[ + IsPastDateValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + future_date = date.today() + timedelta(days=10) + self.inputFilter.validateData({"date2": future_date}) + 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", validators=[IsStringValidator()]) self.inputFilter.validateData({"name": "obviously an string"}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"name": 123}) + self.inputFilter.add( + "name", + validators=[ + IsStringValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"name": 123}) + 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", validators=[IsUUIDValidator()]) self.inputFilter.validateData( {"uuid": "550e8400-e29b-41d4-a716-446655440000"} @@ -485,41 +771,76 @@ def test_is_uuid_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"uuid": "not_a_uuid"}) + self.inputFilter.add( + "uuid", + validators=[IsUUIDValidator(error_message="Custom error message")], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"uuid": "not_a_uuid"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"uuid": 123}) + def test_is_weekday_validator(self) -> None: """ Test IsWeekdayValidator. """ - self.inputFilter.add( - "date", required=True, validators=[IsPastDateValidator()] - ) + self.inputFilter.add("date", validators=[IsWeekdayValidator()]) self.inputFilter.validateData({"date": date(2021, 1, 1)}) - self.inputFilter.validateData({"date": datetime(2021, 1, 1, 0, 0)}) - past_date = (datetime.now() - timedelta(days=10)).isoformat() - self.inputFilter.validateData({"date": past_date}) + self.inputFilter.validateData({"date": datetime(2021, 1, 1, 11, 11)}) + self.inputFilter.validateData({"date": "2021-01-01T11:11:11"}) with self.assertRaises(ValidationError): - future_date = date.today() + timedelta(days=10) - self.inputFilter.validateData({"date": future_date}) + self.inputFilter.validateData({"date": date(2021, 1, 2)}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "not a date"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": False}) + + self.inputFilter.add( + "date", + validators=[ + IsWeekdayValidator(error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": date(2021, 1, 2)}) def test_is_weekend_validator(self) -> None: """ Test IsWeekendValidator. """ + self.inputFilter.add("date", validators=[IsWeekendValidator()]) + + self.inputFilter.validateData({"date": date(2021, 1, 2)}) + self.inputFilter.validateData({"date": datetime(2021, 1, 2, 11, 11)}) + self.inputFilter.validateData({"date": "2021-01-02T11:11:11"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": date(2021, 1, 1)}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "not a date"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": False}) + self.inputFilter.add( - "date", required=True, validators=[IsPastDateValidator()] + "date", + validators=[ + IsWeekendValidator(error_message="Custom error message") + ], ) - self.inputFilter.validateData({"date": date(2021, 1, 1)}) - self.inputFilter.validateData({"date": datetime(2021, 1, 1, 0, 0)}) - past_date = (datetime.now() - timedelta(days=10)).isoformat() - self.inputFilter.validateData({"date": past_date}) - with self.assertRaises(ValidationError): - future_date = date.today() + timedelta(days=10) - self.inputFilter.validateData({"date": future_date}) + self.inputFilter.validateData({"date": date(2021, 1, 1)}) def test_length_validator(self) -> None: """ @@ -528,7 +849,6 @@ def test_length_validator(self) -> None: self.inputFilter.add( "name", - required=False, validators=[LengthValidator(min_length=2, max_length=5)], ) @@ -540,15 +860,30 @@ def test_length_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"name": "this_is_too_long"}) + self.inputFilter.add( + "name", + validators=[ + LengthValidator( + min_length=2, + max_length=5, + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"name": "this_is_too_long"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"name": "a"}) + def test_range_validator(self) -> None: """ Test that RangeValidator validates numeric values within a specified range. """ - self.inputFilter.add( - "range_field", required=False, validators=[RangeValidator(2, 5)] - ) + self.inputFilter.add("range_field", validators=[RangeValidator(2, 5)]) self.inputFilter.validateData({"name": "test", "range_field": 3.76}) @@ -562,6 +897,18 @@ def test_range_validator(self) -> None: {"name": "test", "range_field": 7.89} ) + self.inputFilter.add( + "range_field", + validators=[ + RangeValidator(2, 5, error_message="Custom error message") + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"name": "test", "range_field": 7.89} + ) + def test_regex_validator(self) -> None: """ Test successful validation of a valid regex format. @@ -569,7 +916,6 @@ def test_regex_validator(self) -> None: self.inputFilter.add( "email", - required=False, validators=[ RegexValidator( RegexEnum.EMAIL.value, @@ -586,6 +932,19 @@ def test_regex_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"email": "invalid_email"}) + self.inputFilter.add( + "email", + validators=[ + RegexValidator( + RegexEnum.EMAIL.value, + error_message="Custom error message", + ) + ], + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"email": "invalid_email"}) + if __name__ == "__main__": unittest.main()