diff --git a/.flake8 b/.flake8 index 8b8914f..66c296d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] exclude = __init__.py, venv, *.md, .* -max-line-length = 90 +max-line-length = 79 diff --git a/CHAGELOG.md b/CHAGELOG.md new file mode 100644 index 0000000..1b9440c --- /dev/null +++ b/CHAGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.0.5] - 2025-01-12 + +### Added + +- New condition functionality between fields. [Check it out](src/flask_inputfilter/Condition/README.md) + +### Changed + +- Switched external_api config from dict to class. [Check it out](src/flask_inputfilter/Model/ExternalApiConfig.py) diff --git a/LICENSE b/LICENSE index 064470a..6728fe1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Leander Cain Slotosch +Copyright (c) 2025 Leander Cain Slotosch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8ff2d15..6d91b23 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +from src.flask_inputfilter.Validator import IsStringValidator + # flask-inputfilter The `InputFilter` class is used to validate and filter input data in Flask applications. @@ -16,15 +18,18 @@ 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. -There are lots of different filters and validators available to use, and you can also create your own custom filters and validators. + +There are lots of different filters and validators available to use, but it is also possible to create your own. ### Definition ```python + from flask_inputfilter import InputFilter +from flask_inputfilter.Condition import ExactlyOneOfCondition from flask_inputfilter.Enum import RegexEnum from flask_inputfilter.Filter import StringTrimFilter, ToIntegerFilter, ToNullFilter -from flask_inputfilter.Validator import IsIntegerValidator, RegexValidator +from flask_inputfilter.Validator import IsIntegerValidator, IsStringValidator, RegexValidator class UpdateZipcodeInputFilter(InputFilter): @@ -35,7 +40,7 @@ class UpdateZipcodeInputFilter(InputFilter): self.add( 'id', required=True, - filters=[ToIntegerFilter(), ToNullFilter()], + filters=[ToNullFilter()], validators=[ IsIntegerValidator() ] @@ -43,7 +48,6 @@ class UpdateZipcodeInputFilter(InputFilter): self.add( 'zipcode', - required=True, filters=[StringTrimFilter()], validators=[ RegexValidator( @@ -52,6 +56,19 @@ class UpdateZipcodeInputFilter(InputFilter): ) ] ) + + self.add( + 'city', + filters=[StringTrimFilter()], + validators=[ + IsStringValidator() + ] + ) + + self.addCondition( + ExactlyOneOfCondition(['zipcode', 'city']) + ) + ``` ### Usage @@ -61,6 +78,7 @@ After calling the `validate` method, the validated data will be available in the If the data is not valid, the `validate` method will return a 400 response with the error message. ```python + from flask import Flask, g from your-path import UpdateZipcodeInputFilter @@ -74,6 +92,7 @@ def updateZipcode(): # Do something with validatedData id = data.get('id') zipcode = data.get('zipcode') + ``` --- diff --git a/docker-compose.yaml b/docker-compose.yaml index 648929f..4e98fa4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ services: - app: + + flask-inputfilter: build: context: . dockerfile: Dockerfile diff --git a/setup.py b/setup.py index 0c9c8c8..31c950d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="flask_inputfilter", - version="0.0.4", + version="0.0.5", author="Leander Cain Slotosch", author_email="slotosch.leander@outlook.de", description="A library to filter and validate input data in" diff --git a/src/flask_inputfilter/Condition/ArrayLengthEqualCondition.py b/src/flask_inputfilter/Condition/ArrayLengthEqualCondition.py new file mode 100644 index 0000000..74c8515 --- /dev/null +++ b/src/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_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/src/flask_inputfilter/Condition/ArrayLongerThanCondition.py b/src/flask_inputfilter/Condition/ArrayLongerThanCondition.py new file mode 100644 index 0000000..6a5a0e4 --- /dev/null +++ b/src/flask_inputfilter/Condition/ArrayLongerThanCondition.py @@ -0,0 +1,20 @@ +from typing import Any, Dict + +from .BaseCondition import BaseCondition + + +class ArrayLongerThanCondition(BaseCondition): + """ + Condition that checks if the array is longer than the specified length. + """ + + def __init__(self, longer_field: str, shorter_field: str) -> None: + + self.longer_field = longer_field + self.shorter_field = shorter_field + + def check(self, data: Dict[str, Any]) -> bool: + + return len(data.get(self.longer_field) or []) > len( + data.get(self.shorter_field) or [] + ) diff --git a/src/flask_inputfilter/Condition/BaseCondition.py b/src/flask_inputfilter/Condition/BaseCondition.py new file mode 100644 index 0000000..cdbd024 --- /dev/null +++ b/src/flask_inputfilter/Condition/BaseCondition.py @@ -0,0 +1,12 @@ +from typing import Any, Dict + + +class BaseCondition: + """ + Base class for defining conditions. + Each condition should implement the `check` method. + """ + + def check(self, data: Dict[str, Any]) -> bool: + + raise NotImplementedError("Condition must implement 'check' method.") diff --git a/src/flask_inputfilter/Condition/CustomCondition.py b/src/flask_inputfilter/Condition/CustomCondition.py new file mode 100644 index 0000000..f778199 --- /dev/null +++ b/src/flask_inputfilter/Condition/CustomCondition.py @@ -0,0 +1,17 @@ +from typing import Any, Callable, Dict + +from .BaseCondition import BaseCondition + + +class CustomCondition(BaseCondition): + """ + Allows users to define their own condition as a callable. + """ + + def __init__(self, condition: Callable[[Dict[str, Any]], bool]) -> None: + + self.condition = condition + + def check(self, data: Dict[str, Any]) -> bool: + + return self.condition(data) diff --git a/src/flask_inputfilter/Condition/EqualCondition.py b/src/flask_inputfilter/Condition/EqualCondition.py new file mode 100644 index 0000000..00c0e55 --- /dev/null +++ b/src/flask_inputfilter/Condition/EqualCondition.py @@ -0,0 +1,18 @@ +from typing import Any, Dict + +from .BaseCondition import BaseCondition + + +class EqualCondition(BaseCondition): + """ + Condition that checks if two fields are equal. + """ + + 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 data.get(self.first_field) == data.get(self.second_field) diff --git a/src/flask_inputfilter/Condition/ExactlyNOfCondition.py b/src/flask_inputfilter/Condition/ExactlyNOfCondition.py new file mode 100644 index 0000000..3de8029 --- /dev/null +++ b/src/flask_inputfilter/Condition/ExactlyNOfCondition.py @@ -0,0 +1,22 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class ExactlyNOfCondition(BaseCondition): + """ + Condition that checks if exactly n of the given + fields are present in the data. + """ + + def __init__(self, fields: List[str], n: int) -> None: + + self.fields = fields + self.n = n + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) is not None) + == self.n + ) diff --git a/src/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py b/src/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py new file mode 100644 index 0000000..1b04d95 --- /dev/null +++ b/src/flask_inputfilter/Condition/ExactlyNOfMatchesCondition.py @@ -0,0 +1,22 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class ExactlyNOfMatchesCondition(BaseCondition): + """ + Condition that checks if exactly n of the given fields + match with the value. + """ + + def __init__(self, fields: List[str], n: int, value: Any) -> None: + self.fields = fields + self.n = n + self.value = value + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) == self.value) + == self.n + ) diff --git a/src/flask_inputfilter/Condition/ExactlyOneOfCondition.py b/src/flask_inputfilter/Condition/ExactlyOneOfCondition.py new file mode 100644 index 0000000..892e810 --- /dev/null +++ b/src/flask_inputfilter/Condition/ExactlyOneOfCondition.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class ExactlyOneOfCondition(BaseCondition): + """ + Condition that ensures exactly one of the specified fields is present. + """ + + def __init__(self, fields: List[str]) -> None: + + self.fields = fields + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) is not None) == 1 + ) diff --git a/src/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py b/src/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py new file mode 100644 index 0000000..4cf373f --- /dev/null +++ b/src/flask_inputfilter/Condition/ExactlyOneOfMatchesCondition.py @@ -0,0 +1,22 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class ExactlyOneOfMatchesCondition(BaseCondition): + """ + Condition that ensures exactly one of the specified + fields matches the value. + """ + + def __init__(self, fields: List[str], value: Any) -> None: + + self.fields = fields + self.value = value + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) == self.value) + == 1 + ) diff --git a/src/flask_inputfilter/Condition/IntegerBiggerThanCondition.py b/src/flask_inputfilter/Condition/IntegerBiggerThanCondition.py new file mode 100644 index 0000000..304319c --- /dev/null +++ b/src/flask_inputfilter/Condition/IntegerBiggerThanCondition.py @@ -0,0 +1,24 @@ +from typing import Dict + +from .BaseCondition import BaseCondition + + +class IntegerBiggerThanCondition(BaseCondition): + """ + Condition that ensures an integer is bigger than the specified value. + """ + + def __init__(self, bigger_field: str, smaller_field: str) -> None: + + self.bigger_field = bigger_field + self.smaller_field = smaller_field + + def check(self, data: Dict[str, int]) -> bool: + + if ( + data.get(self.bigger_field) is None + or data.get(self.smaller_field) is None + ): + return False + + return data.get(self.bigger_field) > data.get(self.smaller_field) diff --git a/src/flask_inputfilter/Condition/NOfCondition.py b/src/flask_inputfilter/Condition/NOfCondition.py new file mode 100644 index 0000000..1ed0696 --- /dev/null +++ b/src/flask_inputfilter/Condition/NOfCondition.py @@ -0,0 +1,21 @@ +from typing import Any, List + +from .BaseCondition import BaseCondition + + +class NOfCondition(BaseCondition): + """ + Condition that ensures at least N of the specified fields are present. + """ + + def __init__(self, fields: List[str], n: int) -> None: + + self.fields = fields + self.n = n + + def check(self, data: Any) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) is not None) + >= self.n + ) diff --git a/src/flask_inputfilter/Condition/NOfMatchesCondition.py b/src/flask_inputfilter/Condition/NOfMatchesCondition.py new file mode 100644 index 0000000..7bbf935 --- /dev/null +++ b/src/flask_inputfilter/Condition/NOfMatchesCondition.py @@ -0,0 +1,23 @@ +from typing import Any, Dict, List + +from src.flask_inputfilter.Condition import BaseCondition + + +class NOfMatchesCondition(BaseCondition): + """ + Condition that ensures at least N of the specified + fields matches the value. + """ + + def __init__(self, fields: List[str], n: int, value: Any) -> None: + + self.fields = fields + self.n = n + self.value = value + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + sum(1 for field in self.fields if data.get(field) == self.value) + == self.n + ) diff --git a/src/flask_inputfilter/Condition/NotEqualCondition.py b/src/flask_inputfilter/Condition/NotEqualCondition.py new file mode 100644 index 0000000..1b7a74d --- /dev/null +++ b/src/flask_inputfilter/Condition/NotEqualCondition.py @@ -0,0 +1,18 @@ +from typing import Any, Dict + +from .BaseCondition import BaseCondition + + +class NotEqualCondition(BaseCondition): + """ + Condition that checks if two fields are not equal. + """ + + 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 data.get(self.first_field) != data.get(self.second_field) diff --git a/src/flask_inputfilter/Condition/OneOfCondition.py b/src/flask_inputfilter/Condition/OneOfCondition.py new file mode 100644 index 0000000..85339df --- /dev/null +++ b/src/flask_inputfilter/Condition/OneOfCondition.py @@ -0,0 +1,20 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class OneOfCondition(BaseCondition): + """ + Condition that ensures at least one of the specified fields is present. + """ + + def __init__(self, fields: List[str]) -> None: + + self.fields = fields + + def check(self, data: Dict[str, Any]) -> bool: + + return any( + field in data and data.get(field) is not None + for field in self.fields + ) diff --git a/src/flask_inputfilter/Condition/OneOfMatchesCondition.py b/src/flask_inputfilter/Condition/OneOfMatchesCondition.py new file mode 100644 index 0000000..beaeb86 --- /dev/null +++ b/src/flask_inputfilter/Condition/OneOfMatchesCondition.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, List + +from .BaseCondition import BaseCondition + + +class OneOfMatchesCondition(BaseCondition): + """ + Condition that ensures at least one of the specified + fields matches the value. + """ + + def __init__(self, fields: List[str], value: Any) -> None: + + self.fields = fields + self.value = value + + def check(self, data: Dict[str, Any]) -> bool: + + return any(data.get(field) == self.value for field in self.fields) diff --git a/src/flask_inputfilter/Condition/README.md b/src/flask_inputfilter/Condition/README.md new file mode 100644 index 0000000..8feb491 --- /dev/null +++ b/src/flask_inputfilter/Condition/README.md @@ -0,0 +1,53 @@ +# Condition + +The `Condition` module contains the conditions that can be used to validate the input data. + +## Conditions + +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 + +class TestInputFilter(InputFilter): + def __init__(self): + super().__init__() + + self.add( + 'id', + required=True, + filters=[ToIntegerFilter()], + validators=[IsIntegerValidator()] + ) + + self.add( + 'name', + required=True + ) + + self.addCondition( + ExactlyOneOfCondition('id', 'name') + ) +``` + +## Available conditions + +The following conditions are available in the `Condition` module: + +1. [`CustomCondition`](CustomCondition.py) - A custom condition that can be used to validate the input data. +2. [`EqualCondition`](EqualCondition.py) - Validates that the input is equal to the given value. +3. [`ExactlyNOfCondition`](ExactlyNOfCondition.py) - Validates that exactly `n` of the given conditions are true. +4. [`ExactlyNOfMatchesCondition`](ExactlyNOfMatchesCondition.py) - Validates that exactly `n` of the given matches are true. +5. [`ExactlyOneOfCondition`](ExactlyOneOfCondition.py) - Validates that exactly one of the given conditions is true. +6. [`ExactlyOneOfMatchesCondition`](ExactlyOneOfMatchesCondition.py) - Validates that exactly one of the given matches is true. +7. [`IntegerBiggerThanCondition`](IntegerBiggerThanCondition.py) - Validates that the integer is bigger than the given value. +8. [`NOfCondition`](NOfCondition.py) - Validates that at least `n` of the given conditions are true. +9. [`NOfMatchesCondition`](NOfMatchesCondition.py) - Validates that at least `n` of the given matches are true. +10. [`NotEqualCondition`](NotEqualCondition.py) - Validates that the input is not equal to the given value. +11. [`OneOfCondition`](OneOfCondition.py) - Validates that at least one of the given conditions is true. +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. diff --git a/src/flask_inputfilter/Condition/RequiredIfCondition.py b/src/flask_inputfilter/Condition/RequiredIfCondition.py new file mode 100644 index 0000000..50be411 --- /dev/null +++ b/src/flask_inputfilter/Condition/RequiredIfCondition.py @@ -0,0 +1,25 @@ +from typing import Any, Dict + +from .BaseCondition import BaseCondition + + +class RequiredIfCondition(BaseCondition): + """ + Condition that ensures a field is required if another + field has a specific value. + """ + + def __init__( + self, condition_field: str, value: Any, required_field: str + ) -> None: + + self.condition_field = condition_field + self.value = value + self.required_field = required_field + + def check(self, data: Dict[str, Any]) -> bool: + + return ( + data.get(self.condition_field) != self.value + or data.get(self.required_field) is not None + ) diff --git a/src/flask_inputfilter/Condition/StringLongerThanCondition.py b/src/flask_inputfilter/Condition/StringLongerThanCondition.py new file mode 100644 index 0000000..bbf5e9e --- /dev/null +++ b/src/flask_inputfilter/Condition/StringLongerThanCondition.py @@ -0,0 +1,21 @@ +from typing import Dict + +from .BaseCondition import BaseCondition + + +class StringLongerThanCondition(BaseCondition): + """ + Condition that checks if the length of the string is longer + than the given length. + """ + + def __init__(self, longer_field: str, shorter_field: str) -> None: + + self.longer_field = longer_field + self.shorter_field = shorter_field + + def check(self, value: Dict[str, str]) -> bool: + + return len(value.get(self.longer_field) or 0) > len( + value.get(self.shorter_field) or 0 + ) diff --git a/src/flask_inputfilter/Condition/__init__.py b/src/flask_inputfilter/Condition/__init__.py new file mode 100644 index 0000000..853d9c3 --- /dev/null +++ b/src/flask_inputfilter/Condition/__init__.py @@ -0,0 +1,16 @@ +from .ArrayLengthEqualCondition import ArrayLengthEqualCondition +from .ArrayLongerThanCondition import ArrayLongerThanCondition +from .BaseCondition import BaseCondition +from .CustomCondition import CustomCondition +from .EqualCondition import EqualCondition +from .ExactlyNOfCondition import ExactlyNOfCondition +from .ExactlyNOfMatchesCondition import ExactlyNOfMatchesCondition +from .ExactlyOneOfCondition import ExactlyOneOfCondition +from .ExactlyOneOfMatchesCondition import ExactlyOneOfMatchesCondition +from .IntegerBiggerThanCondition import IntegerBiggerThanCondition +from .NOfCondition import NOfCondition +from .NOfMatchesCondition import NOfMatchesCondition +from .OneOfCondition import OneOfCondition +from .OneOfMatchesCondition import OneOfMatchesCondition +from .RequiredIfCondition import RequiredIfCondition +from .StringLongerThanCondition import StringLongerThanCondition diff --git a/src/flask_inputfilter/Enum/RegexEnum.py b/src/flask_inputfilter/Enum/RegexEnum.py index a05e613..c060ad5 100644 --- a/src/flask_inputfilter/Enum/RegexEnum.py +++ b/src/flask_inputfilter/Enum/RegexEnum.py @@ -22,3 +22,16 @@ class RegexEnum(Enum): POSTAL_CODE = r"^\d{4,10}$" URL = r"^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$" + + UUID = ( + r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + ) + + CREDIT_CARD = ( + r"^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3" + r"(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|" + r"(?:2131|1800|35\d{3})\d{11})$" + ) + + HEX_COLOR = r"^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$" diff --git a/src/flask_inputfilter/Enum/__init__.py b/src/flask_inputfilter/Enum/__init__.py index b6e690f..ab16559 100644 --- a/src/flask_inputfilter/Enum/__init__.py +++ b/src/flask_inputfilter/Enum/__init__.py @@ -1 +1 @@ -from . import * +from .RegexEnum import RegexEnum diff --git a/src/flask_inputfilter/Filter/ArrayExplodeFilter.py b/src/flask_inputfilter/Filter/ArrayExplodeFilter.py index b398739..fb69a82 100644 --- a/src/flask_inputfilter/Filter/ArrayExplodeFilter.py +++ b/src/flask_inputfilter/Filter/ArrayExplodeFilter.py @@ -1,6 +1,6 @@ from typing import Any, List, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ArrayExplodeFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/README.md b/src/flask_inputfilter/Filter/README.md index 0121c9a..6fe94bf 100644 --- a/src/flask_inputfilter/Filter/README.md +++ b/src/flask_inputfilter/Filter/README.md @@ -7,18 +7,20 @@ 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`](ArrayExplodeFilter.py) - Explodes the input string into an array. -2. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug. +2. [`RemoveEmojisFilter`](RemoveEmojisFilter.py) - Removes the emojis from the string. +3. [`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. +7. [`ToEnumFilter`](ToEnumFilter.py) - Converts a string or integer to an enum value. +8. [`ToFloatFilter`](ToFloatFilter.py) - Converts a string to a float value. +9. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts a string to an integer value. +10. [`ToLowerFilter`](ToLowerFilter.py) - Converts a string to lowercase. +11. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes a unicode string. +12. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string). +13. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case. +14. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case. +15. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value. +16. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase. +17. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string. diff --git a/src/flask_inputfilter/Filter/RemoveEmojisFilter.py b/src/flask_inputfilter/Filter/RemoveEmojisFilter.py new file mode 100644 index 0000000..2c9e026 --- /dev/null +++ b/src/flask_inputfilter/Filter/RemoveEmojisFilter.py @@ -0,0 +1,29 @@ +import re +from typing import Any + +from .BaseFilter import BaseFilter + +emoji_pattern = re.compile( + "[" + "\U0001F600-\U0001F64F" + "\U0001F300-\U0001F5FF" + "\U0001F680-\U0001F6FF" + "\U0001F1E0-\U0001F1FF" + "\U00002702-\U000027B0" + "\U000024C2-\U0001F251" + "]+", + flags=re.UNICODE, +) + + +class RemoveEmojisFilter(BaseFilter): + """ + Filter that removes emojis from a string. + """ + + def apply(self, value: Any) -> str: + + if not isinstance(value, str): + return value + + return emoji_pattern.sub("", value) diff --git a/src/flask_inputfilter/Filter/SlugifyFilter.py b/src/flask_inputfilter/Filter/SlugifyFilter.py index 190fbab..d4372bb 100644 --- a/src/flask_inputfilter/Filter/SlugifyFilter.py +++ b/src/flask_inputfilter/Filter/SlugifyFilter.py @@ -1,7 +1,7 @@ import re from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class SlugifyFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/StringTrimFilter.py b/src/flask_inputfilter/Filter/StringTrimFilter.py index 1547391..7a31c6f 100644 --- a/src/flask_inputfilter/Filter/StringTrimFilter.py +++ b/src/flask_inputfilter/Filter/StringTrimFilter.py @@ -1,4 +1,4 @@ -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class StringTrimFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py b/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py index 8c5841b..51abd06 100644 --- a/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py +++ b/src/flask_inputfilter/Filter/ToAlphaNumericFilter.py @@ -1,7 +1,7 @@ import re from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToAlphaNumericFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToBooleanFilter.py b/src/flask_inputfilter/Filter/ToBooleanFilter.py index fcfbce0..c211790 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.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToBooleanFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToCamelCaseFilter.py b/src/flask_inputfilter/Filter/ToCamelCaseFilter.py index 29938d7..c328a20 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.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToCamelCaseFilter(BaseFilter): @@ -14,7 +14,7 @@ def apply(self, value: Any) -> Optional[str]: if not isinstance(value, str): return None - value = re.sub(r"[\s-_]+", " ", value).strip() + value = re.sub(r"[\s_-]+", " ", value).strip() value = "".join(word.capitalize() for word in value.split()) diff --git a/src/flask_inputfilter/Filter/ToEnumFilter.py b/src/flask_inputfilter/Filter/ToEnumFilter.py new file mode 100644 index 0000000..c142b08 --- /dev/null +++ b/src/flask_inputfilter/Filter/ToEnumFilter.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Any, Optional, Type + +from .BaseFilter import BaseFilter + + +class ToEnumFilter(BaseFilter): + """ + Filter that converts a value to an Enum instance. + """ + + def __init__(self, enum_class: Type[Enum]) -> None: + + self.enum_class = enum_class + + def apply(self, value: Any) -> Optional[Enum]: + + if not isinstance(value, (str, int)): + return None + + try: + return self.enum_class(value) + + except ValueError: + return None diff --git a/src/flask_inputfilter/Filter/ToFloatFilter.py b/src/flask_inputfilter/Filter/ToFloatFilter.py index 9871e80..02d0381 100644 --- a/src/flask_inputfilter/Filter/ToFloatFilter.py +++ b/src/flask_inputfilter/Filter/ToFloatFilter.py @@ -1,4 +1,4 @@ -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToFloatFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToIntegerFilter.py b/src/flask_inputfilter/Filter/ToIntegerFilter.py index eddc674..53993dd 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.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToIntegerFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToLowerFilter.py b/src/flask_inputfilter/Filter/ToLowerFilter.py index 0d04774..90a3b6b 100644 --- a/src/flask_inputfilter/Filter/ToLowerFilter.py +++ b/src/flask_inputfilter/Filter/ToLowerFilter.py @@ -1,4 +1,4 @@ -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToLowerFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py b/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py index a70c8ea..af7ed77 100644 --- a/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py +++ b/src/flask_inputfilter/Filter/ToNormalizedUnicodeFilter.py @@ -3,7 +3,7 @@ from typing_extensions import Literal -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToNormalizedUnicodeFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToNullFilter.py b/src/flask_inputfilter/Filter/ToNullFilter.py index c59732b..c5e5178 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.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToNullFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToPascaleCaseFilter.py b/src/flask_inputfilter/Filter/ToPascaleCaseFilter.py index 283c900..0fc5367 100644 --- a/src/flask_inputfilter/Filter/ToPascaleCaseFilter.py +++ b/src/flask_inputfilter/Filter/ToPascaleCaseFilter.py @@ -1,7 +1,7 @@ import re from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToPascaleCaseFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToSnakeCaseFilter.py b/src/flask_inputfilter/Filter/ToSnakeCaseFilter.py index 22606a7..93ba606 100644 --- a/src/flask_inputfilter/Filter/ToSnakeCaseFilter.py +++ b/src/flask_inputfilter/Filter/ToSnakeCaseFilter.py @@ -1,7 +1,7 @@ import re from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToSnakeCaseFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToStringFilter.py b/src/flask_inputfilter/Filter/ToStringFilter.py index bd0b311..e5a1ef3 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.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToStringFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/ToUpperFilter.py b/src/flask_inputfilter/Filter/ToUpperFilter.py index a2dcaba..746da8e 100644 --- a/src/flask_inputfilter/Filter/ToUpperFilter.py +++ b/src/flask_inputfilter/Filter/ToUpperFilter.py @@ -1,4 +1,4 @@ -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class ToUpperFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/TruncateFilter.py b/src/flask_inputfilter/Filter/TruncateFilter.py index 972f7a7..3d8a700 100644 --- a/src/flask_inputfilter/Filter/TruncateFilter.py +++ b/src/flask_inputfilter/Filter/TruncateFilter.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class TruncateFilter(BaseFilter): diff --git a/src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py b/src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py index 805a02d..835fdff 100644 --- a/src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py +++ b/src/flask_inputfilter/Filter/WhitespaceCollapseFilter.py @@ -1,12 +1,13 @@ import re from typing import Any, Optional -from ..Filter.BaseFilter import BaseFilter +from .BaseFilter import BaseFilter class WhitespaceCollapseFilter(BaseFilter): """ - Filter that collapses multiple consecutive whitespace characters into a single space. + Filter that collapses multiple consecutive whitespace + characters into a single space. """ def apply(self, value: Any) -> Optional[str]: diff --git a/src/flask_inputfilter/Filter/__init__.py b/src/flask_inputfilter/Filter/__init__.py index f351bb3..8d154ed 100644 --- a/src/flask_inputfilter/Filter/__init__.py +++ b/src/flask_inputfilter/Filter/__init__.py @@ -1,10 +1,12 @@ from .ArrayExplodeFilter import ArrayExplodeFilter from .BaseFilter import BaseFilter +from .RemoveEmojisFilter import RemoveEmojisFilter from .SlugifyFilter import SlugifyFilter from .StringTrimFilter import StringTrimFilter from .ToAlphaNumericFilter import ToAlphaNumericFilter from .ToBooleanFilter import ToBooleanFilter from .ToCamelCaseFilter import ToCamelCaseFilter +from .ToEnumFilter import ToEnumFilter from .ToFloatFilter import ToFloatFilter from .ToIntegerFilter import ToIntegerFilter from .ToLowerFilter import ToLowerFilter diff --git a/src/flask_inputfilter/InputFilter.py b/src/flask_inputfilter/InputFilter.py index 0388b59..d264a4f 100644 --- a/src/flask_inputfilter/InputFilter.py +++ b/src/flask_inputfilter/InputFilter.py @@ -3,30 +3,14 @@ import requests from flask import Response, g, request -from typing_extensions import TypedDict +from .Condition.BaseCondition import BaseCondition from .Exception import ValidationError from .Filter.BaseFilter import BaseFilter +from .Model import ExternalApiConfig 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: """ Base class for input filters. @@ -35,6 +19,7 @@ class InputFilter: def __init__(self) -> None: self.fields = {} + self.conditions = [] def add( self, @@ -44,7 +29,7 @@ def add( fallback: Any = None, filters: Optional[List[BaseFilter]] = None, validators: Optional[List[BaseValidator]] = None, - external_api: Optional[Dict[str, Union[str, Dict[str, str]]]] = None, + external_api: Optional[ExternalApiConfig] = None, ) -> None: """ Add the field to the input filter. @@ -67,6 +52,12 @@ def add( "external_api": external_api, } + def addCondition(self, condition: BaseCondition) -> None: + """ + Add a condition to the input filter. + """ + self.conditions.append(condition) + def _applyFilters(self, field_name: str, value: Any) -> Any: """ Apply filters to the field value. @@ -96,7 +87,7 @@ def _validateField(self, field_name: str, value: Any) -> None: validator.validate(value) def _callExternalApi( - self, config: dict, validated_data: dict + self, config: ExternalApiConfig, validated_data: dict ) -> Optional[Any]: """ Führt den API-Aufruf durch und gibt den Wert zurück, @@ -105,34 +96,35 @@ def _callExternalApi( requestData = {} - if "api_key" in config: + if config.api_key: requestData["headers"][ "Authorization" - ] = f"Bearer {config['api_key']}" + ] = f"Bearer {config.api_key}" - if "headers" in config: - requestData["headers"].update(config["headers"]) + if config.headers: + requestData["headers"].update(config.headers) - if "params" in config: + if config.params: requestData["params"] = self.__replacePlaceholdersInParams( - config["params"], validated_data + config.params, validated_data ) requestData["url"] = self.__replacePlaceholders( - config["url"], validated_data + config.url, validated_data ) - requestData["method"] = config["method"] + 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}" + f"External API call failed with status code " + f"{response.status_code}" ) result = response.json() - data_key = config.get("data_key", None) + data_key = config.data_key if data_key: return result.get(data_key) @@ -195,61 +187,66 @@ def validateData( # Check for required field if value is None: if ( - field_info["required"] - and field_info["external_api"] is None + field_info.get("required") + and field_info.get("external_api") is None ): - if field_info["fallback"] is None: + if field_info.get("fallback") is None: raise ValidationError( f"Field '{field_name}' is required." ) - value = field_info["fallback"] + value = field_info.get("fallback") - if field_info["default"] is not None: - value = field_info["default"] + if field_info.get("default") is not None: + value = field_info.get("default") # Validate field if value is not None: try: self._validateField(field_name, value) except ValidationError: - if field_info["fallback"] is not None: - value = field_info["fallback"] + if field_info.get("fallback") is not None: + value = field_info.get("fallback") else: raise # External API call - if field_info["external_api"]: - external_api_config = field_info["external_api"] + if field_info.get("external_api"): + external_api_config = field_info.get("external_api") try: value = self._callExternalApi( external_api_config, validated_data ) - except ValidationError as e: - if field_info["fallback"] is None: - print(e) + except ValidationError: + if field_info.get("fallback") is None: raise ValidationError( - f"External API call failed for field '{field_name}'." + f"External API call failed for field " + f"'{field_name}'." ) - value = field_info["fallback"] + value = field_info.get("fallback") if value is None: - if field_info["required"]: - if field_info["fallback"] is None: + if field_info.get("required"): + if field_info.get("fallback") is None: raise ValidationError( f"Field '{field_name}' is required." ) - value = field_info["fallback"] + value = field_info.get("fallback") - if field_info["default"] is not None: - value = field_info["default"] + if field_info.get("default") is not None: + value = field_info.get("default") validated_data[field_name] = value + # Check conditions + for condition in self.conditions: + if not condition.check(validated_data): + raise ValidationError(f"Condition '{condition}' not met.") + return validated_data @classmethod diff --git a/src/flask_inputfilter/Model/ExternalApiConfig.py b/src/flask_inputfilter/Model/ExternalApiConfig.py new file mode 100644 index 0000000..ee8534d --- /dev/null +++ b/src/flask_inputfilter/Model/ExternalApiConfig.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Dict, Optional + + +@dataclass +class ExternalApiConfig: + """ + 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]] = None + data_key: Optional[str] = None + api_key: Optional[str] = None + headers: Optional[Dict[str, str]] = None diff --git a/src/flask_inputfilter/Model/__init__.py b/src/flask_inputfilter/Model/__init__.py new file mode 100644 index 0000000..3dafed5 --- /dev/null +++ b/src/flask_inputfilter/Model/__init__.py @@ -0,0 +1 @@ +from .ExternalApiConfig import ExternalApiConfig diff --git a/src/flask_inputfilter/Validator/ArrayElementValidator.py b/src/flask_inputfilter/Validator/ArrayElementValidator.py index 908f427..f9f9acc 100644 --- a/src/flask_inputfilter/Validator/ArrayElementValidator.py +++ b/src/flask_inputfilter/Validator/ArrayElementValidator.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator if TYPE_CHECKING: from ..InputFilter import InputFilter diff --git a/src/flask_inputfilter/Validator/ArrayLengthValidator.py b/src/flask_inputfilter/Validator/ArrayLengthValidator.py index dff3f0e..ff36b0d 100644 --- a/src/flask_inputfilter/Validator/ArrayLengthValidator.py +++ b/src/flask_inputfilter/Validator/ArrayLengthValidator.py @@ -1,12 +1,13 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class ArrayLengthValidator(BaseValidator): """ - Validator that checks if the length of an array is within the specified range. + Validator that checks if the length of an array is within + the specified range. """ def __init__( diff --git a/src/flask_inputfilter/Validator/FloatPrecisionValidator.py b/src/flask_inputfilter/Validator/FloatPrecisionValidator.py index f85049b..3792980 100644 --- a/src/flask_inputfilter/Validator/FloatPrecisionValidator.py +++ b/src/flask_inputfilter/Validator/FloatPrecisionValidator.py @@ -2,7 +2,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class FloatPrecisionValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/InArrayValidator.py b/src/flask_inputfilter/Validator/InArrayValidator.py index 2873255..8d0742c 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class InArrayValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/InEnumValidator.py b/src/flask_inputfilter/Validator/InEnumValidator.py index e0c431d..9f1ac27 100644 --- a/src/flask_inputfilter/Validator/InEnumValidator.py +++ b/src/flask_inputfilter/Validator/InEnumValidator.py @@ -2,7 +2,7 @@ from typing import Any, Type from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class InEnumValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsArrayValidator.py b/src/flask_inputfilter/Validator/IsArrayValidator.py index b5bda4a..8038d03 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsArrayValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py b/src/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py index 45f3aba..8a4ef44 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsBase64ImageCorrectSizeValidator(BaseValidator): @@ -15,7 +15,8 @@ def __init__( self, minSize: int = 1, maxSize: int = 4 * 1024 * 1024, - error_message: str = "The image is invalid or does not have an allowed size.", + error_message: str = "The image is invalid or does not " + "have an allowed size.", ) -> None: self.min_size = minSize diff --git a/src/flask_inputfilter/Validator/IsBase64ImageValidator.py b/src/flask_inputfilter/Validator/IsBase64ImageValidator.py index 2bcb460..87eb6fc 100644 --- a/src/flask_inputfilter/Validator/IsBase64ImageValidator.py +++ b/src/flask_inputfilter/Validator/IsBase64ImageValidator.py @@ -5,7 +5,7 @@ from PIL import Image from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsBase64ImageValidator(BaseValidator): @@ -15,7 +15,8 @@ class IsBase64ImageValidator(BaseValidator): def __init__( self, - error_message: str = "The image is invalid or does not have an allowed size.", + error_message: str = "The image is invalid or does not " + "have an allowed size.", ) -> None: self.error_message = error_message diff --git a/src/flask_inputfilter/Validator/IsBooleanValidator.py b/src/flask_inputfilter/Validator/IsBooleanValidator.py index 834c9be..f3dbb47 100644 --- a/src/flask_inputfilter/Validator/IsBooleanValidator.py +++ b/src/flask_inputfilter/Validator/IsBooleanValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsBooleanValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsFloatValidator.py b/src/flask_inputfilter/Validator/IsFloatValidator.py index 3b2d08e..15d4895 100644 --- a/src/flask_inputfilter/Validator/IsFloatValidator.py +++ b/src/flask_inputfilter/Validator/IsFloatValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsFloatValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsHexadecimalValidator.py b/src/flask_inputfilter/Validator/IsHexadecimalValidator.py index dc616f1..2931fcf 100644 --- a/src/flask_inputfilter/Validator/IsHexadecimalValidator.py +++ b/src/flask_inputfilter/Validator/IsHexadecimalValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsHexadecimalValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsInstanceValidator.py b/src/flask_inputfilter/Validator/IsInstanceValidator.py index 14bd37c..a015f9c 100644 --- a/src/flask_inputfilter/Validator/IsInstanceValidator.py +++ b/src/flask_inputfilter/Validator/IsInstanceValidator.py @@ -1,7 +1,7 @@ from typing import Any, Type from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsInstanceValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsIntegerValidator.py b/src/flask_inputfilter/Validator/IsIntegerValidator.py index 0c1e687..bdb2be8 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsIntegerValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsJsonValidator.py b/src/flask_inputfilter/Validator/IsJsonValidator.py index a693f1e..65eed95 100644 --- a/src/flask_inputfilter/Validator/IsJsonValidator.py +++ b/src/flask_inputfilter/Validator/IsJsonValidator.py @@ -2,7 +2,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsJsonValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsStringValidator.py b/src/flask_inputfilter/Validator/IsStringValidator.py index 51eceda..5ba32ca 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsStringValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/IsUUIDValidator.py b/src/flask_inputfilter/Validator/IsUUIDValidator.py index b28a0e3..cfb151d 100644 --- a/src/flask_inputfilter/Validator/IsUUIDValidator.py +++ b/src/flask_inputfilter/Validator/IsUUIDValidator.py @@ -2,7 +2,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class IsUUIDValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/LengthValidator.py b/src/flask_inputfilter/Validator/LengthValidator.py index acc6207..c39df4f 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class LengthEnum(Enum): diff --git a/src/flask_inputfilter/Validator/RangeValidator.py b/src/flask_inputfilter/Validator/RangeValidator.py index 7f80bc8..f54f861 100644 --- a/src/flask_inputfilter/Validator/RangeValidator.py +++ b/src/flask_inputfilter/Validator/RangeValidator.py @@ -1,7 +1,7 @@ from typing import Any from ..Exception import ValidationError -from ..Validator.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class RangeValidator(BaseValidator): diff --git a/src/flask_inputfilter/Validator/RegexValidator.py b/src/flask_inputfilter/Validator/RegexValidator.py index 8f1e102..352acfd 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.BaseValidator import BaseValidator +from .BaseValidator import BaseValidator class RegexValidator(BaseValidator): @@ -13,7 +13,8 @@ class RegexValidator(BaseValidator): def __init__( self, pattern: str, - error_message: str = "Value '{}' does not match the required pattern '{}'.", + error_message: str = "Value '{}' does not match the " + "required pattern '{}'.", ) -> None: self.pattern = pattern diff --git a/src/flask_inputfilter/Validator/__init__.py b/src/flask_inputfilter/Validator/__init__.py index 1cbbfb3..46c4f35 100644 --- a/src/flask_inputfilter/Validator/__init__.py +++ b/src/flask_inputfilter/Validator/__init__.py @@ -1,6 +1,7 @@ from .ArrayElementValidator import ArrayElementValidator from .ArrayLengthValidator import ArrayLengthValidator from .BaseValidator import BaseValidator +from .FloatPrecisionValidator import FloatPrecisionValidator from .InArrayValidator import InArrayValidator from .InEnumValidator import InEnumValidator from .IsArrayValidator import IsArrayValidator diff --git a/test/test_condition.py b/test/test_condition.py new file mode 100644 index 0000000..07d2fbe --- /dev/null +++ b/test/test_condition.py @@ -0,0 +1,343 @@ +import unittest + +from src.flask_inputfilter import InputFilter +from src.flask_inputfilter.Condition import ( + ArrayLengthEqualCondition, + ArrayLongerThanCondition, + CustomCondition, + EqualCondition, + ExactlyNOfCondition, + ExactlyNOfMatchesCondition, + ExactlyOneOfCondition, + ExactlyOneOfMatchesCondition, + IntegerBiggerThanCondition, + NOfCondition, + NOfMatchesCondition, + OneOfCondition, + OneOfMatchesCondition, + RequiredIfCondition, + StringLongerThanCondition, +) +from src.flask_inputfilter.Exception import ValidationError + + +class TestConditions(unittest.TestCase): + def setUp(self): + """ + Set up test data. + """ + + self.inputFilter = InputFilter() + + def test_array_length_equal_condition(self) -> None: + """ + Test ArrayLengthEqualCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + ArrayLengthEqualCondition("field1", "field2") + ) + + self.inputFilter.validateData({"field1": [1, 2], "field2": [1, 2]}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": [1, 2]}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": [1, 2], "field2": [1]}) + + def test_array_longer_than_condition(self) -> None: + """ + Test ArrayLongerThanCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + ArrayLongerThanCondition("field1", "field2") + ) + + self.inputFilter.validateData({"field1": [1, 2], "field2": [1]}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": [1, 2], "field2": [1, 2]}) + + def test_custom_condition(self) -> None: + """ + Test CustomCondition. + """ + + self.inputFilter.add("field") + + self.inputFilter.addCondition( + CustomCondition( + lambda data: "field" in data and data["field"] == "value" + ) + ) + + self.inputFilter.validateData({"field": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + def test_equal_condition(self) -> None: + """ + Test EqualCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition(EqualCondition("field1", "field2")) + + self.inputFilter.validateData({}) + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + self.inputFilter.validateData({"field1": True, "field2": True}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "not value"} + ) + + def test_exactly_nth_of_condition(self) -> None: + """ + Test NthOfCondition when exactly one field is present. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + ExactlyNOfCondition(["field1", "field2", "field3"], 1) + ) + + self.inputFilter.validateData({"field1": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + def test_exactly_nth_of_matches_condition(self) -> None: + """ + Test NthOfMatchesCondition when exactly one field matches the value. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + self.inputFilter.add("field3") + + self.inputFilter.addCondition( + ExactlyNOfMatchesCondition( + ["field1", "field2", "field3"], 2, "value" + ) + ) + + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value", "field3": "value"} + ) + + def test_exactly_one_of_condition(self) -> None: + """ + Test OneOfCondition when exactly one field is present. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + ExactlyOneOfCondition(["field1", "field2", "field3"]) + ) + + self.inputFilter.validateData({"field1": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + def test_exactly_one_of_matches_condition(self) -> None: + """ + Test OneOfMatchesCondition when exactly one field matches the value. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + ExactlyOneOfMatchesCondition(["field1", "field2"], "value") + ) + + self.inputFilter.validateData({"field1": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + def test_integer_bigger_than_condition(self) -> None: + """ + Test IntegerBiggerThanCondition. + """ + + self.inputFilter.add("field") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + IntegerBiggerThanCondition("field", "field2") + ) + + self.inputFilter.validateData({"field": 11, "field2": 10}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field": 10, "field2": 10}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field": 10, "field2": 11}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field": 10}) + + def test_nth_of_condition(self) -> None: + """ + Test NthOfCondition when exactly one field is present. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + NOfCondition(["field1", "field2", "field3"], 2) + ) + + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + self.inputFilter.validateData( + {"field1": "value", "field2": "value", "field3": "value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": "value"}) + + def test_nth_of_matches_condition(self) -> None: + """ + Test NthOfMatchesCondition when exactly one field matches the value. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + self.inputFilter.add("field3") + self.inputFilter.add("field4") + + self.inputFilter.addCondition( + NOfMatchesCondition(["field1", "field2", "field3"], 3, "value") + ) + + self.inputFilter.validateData( + {"field1": "value", "field2": "value", "field3": "value"} + ) + + self.inputFilter.validateData( + { + "field1": "value", + "field2": "value", + "field3": "value", + "field4": "value", + } + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + def test_one_of_condition(self) -> None: + """ + Test OneOfCondition when at least one field is present. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + OneOfCondition(["field1", "field2", "field3"]) + ) + + self.inputFilter.validateData({"field1": "value"}) + self.inputFilter.validateData({"field2": "value"}) + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + self.inputFilter.validateData( + {"field": "not value", "field2": "value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + def test_one_of_matches_condition(self) -> None: + """ + Test OneOfMatchesCondition when at least one field matches the value. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + OneOfMatchesCondition(["field1", "field2"], "value") + ) + + self.inputFilter.validateData({"field1": "value"}) + self.inputFilter.validateData({"field2": "value"}) + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field": "not value"}) + + def test_required_if_condition(self) -> None: + """ + Test RequiredIfCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + RequiredIfCondition("field1", "value", "field2") + ) + + self.inputFilter.validateData({"field2": "value"}) + self.inputFilter.validateData({"field1": "value", "field2": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": "value"}) + + def test_string_longer_than_condition(self) -> None: + """ + Test StringLongerThanCondition. + """ + + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addCondition( + StringLongerThanCondition("field1", "field2") + ) + + self.inputFilter.validateData({"field1": "value", "field2": "val"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData( + {"field1": "value", "field2": "value"} + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_filter.py b/test/test_filter.py index db4cdd3..06a03b0 100644 --- a/test/test_filter.py +++ b/test/test_filter.py @@ -1,11 +1,15 @@ import unittest +from enum import Enum from src.flask_inputfilter.Filter import ( ArrayExplodeFilter, + RemoveEmojisFilter, SlugifyFilter, StringTrimFilter, ToAlphaNumericFilter, ToBooleanFilter, + ToCamelCaseFilter, + ToEnumFilter, ToFloatFilter, ToIntegerFilter, ToLowerFilter, @@ -56,6 +60,23 @@ def test_array_explode_filter(self) -> None: self.assertEqual(validated_data["items"], ["item1", "item2", "item3"]) + def test_remove_emojis_filter(self) -> None: + """ + Test that RemoveEmojisFilter removes emojis from a string. + """ + + self.inputFilter.add( + "text", + required=False, + filters=[RemoveEmojisFilter()], + ) + + validated_data = self.inputFilter.validateData( + {"text": "Hello World! 😊"} + ) + + self.assertEqual(validated_data["text"], "Hello World! ") + def test_slugify_filter(self) -> None: """ Test that SlugifyFilter slugifies a string. @@ -118,6 +139,41 @@ def test_to_bool_filter(self) -> None: self.assertTrue(validated_data["is_active"]) + def test_to_camel_case_filter(self) -> None: + """ + Test that CamelCaseFilter converts string to camel case. + """ + + self.inputFilter.add( + "username", required=True, filters=[ToCamelCaseFilter()] + ) + + validated_data = self.inputFilter.validateData( + {"username": "test user"} + ) + + self.assertEqual(validated_data["username"], "testUser") + + def test_to_enum_filter(self) -> None: + """ + Test that EnumFilter validates a string against a list of values. + """ + + class ColorEnum(Enum): + RED = "red" + GREEN = "green" + BLUE = "blue" + + self.inputFilter.add( + "color", + required=True, + filters=[ToEnumFilter(ColorEnum)], + ) + + validated_data = self.inputFilter.validateData({"color": "red"}) + + self.assertEqual(validated_data["color"], ColorEnum.RED) + def test_to_float_filter(self) -> None: """ Test that ToFloatFilter converts string to float. @@ -272,3 +328,7 @@ def test_whitespace_collapse_filter(self) -> None: ) self.assertEqual(validated_data["collapsed_field"], "Hello World") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_input_filter.py b/test/test_input_filter.py index 5101bc9..3e1786b 100644 --- a/test/test_input_filter.py +++ b/test/test_input_filter.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch from src.flask_inputfilter.Exception import ValidationError -from src.flask_inputfilter.InputFilter import InputFilter +from src.flask_inputfilter.InputFilter import ExternalApiConfig, InputFilter from src.flask_inputfilter.Validator import InArrayValidator @@ -87,11 +87,11 @@ def test_external_api(self, mock_request: Mock) -> None: # 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", - }, + external_api=ExternalApiConfig( + url="https://api.example.com/validate_user/{{name}}", + method="GET", + data_key="is_valid", + ), ) # API returns valid result @@ -129,12 +129,12 @@ def test_external_api_params(self, mock_request: Mock) -> None: 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", - }, + external_api=ExternalApiConfig( + url="https://api.example.com/validate_user/{{name}}", + method="GET", + params={"hash": "{{hash}}"}, + data_key="is_valid", + ), ) # API returns valid result @@ -176,12 +176,12 @@ def test_external_api_fallback(self, mock_request: Mock) -> None: "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", - }, + external_api=ExternalApiConfig( + url="https://api.example.com/validate_user", + method="GET", + params={"user": "{{value}}"}, + data_key="name", + ), ) validated_data = self.inputFilter.validateData( @@ -190,3 +190,7 @@ def test_external_api_fallback(self, mock_request: Mock) -> None: self.assertEqual( validated_data["username_with_fallback"], "fallback_user" ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_validator.py b/test/test_validator.py index 3cb1d41..22ccd80 100644 --- a/test/test_validator.py +++ b/test/test_validator.py @@ -1,11 +1,13 @@ import unittest 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 ( ArrayElementValidator, ArrayLengthValidator, + FloatPrecisionValidator, InArrayValidator, InEnumValidator, IsArrayValidator, @@ -23,9 +25,6 @@ RangeValidator, RegexValidator, ) -from src.flask_inputfilter.Validator.FloatPrecisionValidator import ( - FloatPrecisionValidator, -) class TestInputFilter(unittest.TestCase): @@ -321,7 +320,8 @@ def test_length_validator(self) -> None: def test_range_validator(self) -> None: """ - Test that RangeValidator validates numeric values within a specified range. + Test that RangeValidator validates numeric values + within a specified range. """ self.inputFilter.add( @@ -348,7 +348,11 @@ def test_regex_validator(self) -> None: self.inputFilter.add( "email", required=False, - validators=[RegexValidator(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")], + validators=[ + RegexValidator( + RegexEnum.EMAIL.value, + ) + ], ) validated_data = self.inputFilter.validateData( @@ -359,3 +363,7 @@ def test_regex_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"email": "invalid_email"}) + + +if __name__ == "__main__": + unittest.main()