Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions CHAGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,46 @@
All notable changes to this project will be documented in this file.


## [0.0.7] - 2025-01-14
# [0.0.7] - 2025-01-14

### Added
## Added

- Workflow to run tests on all supported python versions. [Check it out](.github/workflows/test_env.yaml)
- Added more test coverage for validators and filters.
- Added tracking of coverage in tests.
- Added tracking of coverage in tests. [Check it out](https://coveralls.io/github/LeanderCS/flask-inputfilter)
- New functionality for global filters and validators in InputFilters.
- New functionality to define custom supported methods.

### Changed
### Validator

- Updated root README.md to include badges.
- New `NotInArrayValidator` to check if a value is not in a list. [Check it out](flask_inputfilter/Validator/NotInArrayValidator.py)
- New `NotValidator` to invert the result of another validator. [Check it out](flask_inputfilter/Validator/NotValidator.py)


## [0.0.6] - 2025-01-12
# [0.0.6] - 2025-01-12

### Added
## Added

- New date validators and filters.

### Changed
## Removed

- Dropped support for Python 3.6.


## [0.0.5] - 2025-01-12
# [0.0.5] - 2025-01-12

### Added
## Added

- New condition functionality between fields. [Check it out](flask_inputfilter/Condition/README.md)

### Changed
## Changed

- Switched external_api config from dict to class. [Check it out](flask_inputfilter/Model/ExternalApiConfig.py)


## [0.0.4] - 2025-01-09
# [0.0.4] - 2025-01-09

### Added
## Added

- New external api functionality. [Check it out](EXTERNAL_API.md)
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Contributing

Thank you for considering contributing to the project. To make the process as easy and as effective as possible,
please follow the guidelines below.

## Reporting Issues

If you find a bug or have a feature request, please open an issue on the project's GitHub repository.
Before submitting an issue, please search the existing issues to see if your issue has already been reported.
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ Options
The `add` method supports several options:

- `Required`_
- `Filter` (see `Filter` documentation in :file:`flask_inputfilter/Filter/README.md`)
- `Validator` (see `Validator` documentation in :file:`flask_inputfilter/Validator/README.md`)
- `Filter <flask_inputfilter/Filter/README.md>`_
- `Validator <flask_inputfilter/Validator/README.md>`_
- `Default`_
- `Fallback`_
- `ExternalApi` (see :file:`EXTERNAL_API.md`)
- `ExternalApi <EXTERNAL_API.md>`_

Required
--------
Expand Down
32 changes: 17 additions & 15 deletions flask_inputfilter/Condition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,20 @@ class TestInputFilter(InputFilter):

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.
15. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is in correct temporal order.
1. [`ArrayLengthEqualCondition`](ArrayLengthEqualCondition.py) - Validates that the length of the array is equal to the given value.
2. [`ArrayLongerThanCondition`](ArrayLongerThanCondition.py) - Validates that the length of the array is longer than the given value.
3. [`CustomCondition`](CustomCondition.py) - A custom condition that can be used to validate the input data.
4. [`EqualCondition`](EqualCondition.py) - Validates that the input is equal to the given value.
5. [`ExactlyNOfCondition`](ExactlyNOfCondition.py) - Validates that exactly `n` of the given conditions are true.
6. [`ExactlyNOfMatchesCondition`](ExactlyNOfMatchesCondition.py) - Validates that exactly `n` of the given matches are true.
7. [`ExactlyOneOfCondition`](ExactlyOneOfCondition.py) - Validates that exactly one of the given conditions is true.
8. [`ExactlyOneOfMatchesCondition`](ExactlyOneOfMatchesCondition.py) - Validates that exactly one of the given matches is true.
9. [`IntegerBiggerThanCondition`](IntegerBiggerThanCondition.py) - Validates that the integer is bigger than the given value.
10. [`NOfCondition`](NOfCondition.py) - Validates that at least `n` of the given conditions are true.
11. [`NOfMatchesCondition`](NOfMatchesCondition.py) - Validates that at least `n` of the given matches are true.
12. [`NotEqualCondition`](NotEqualCondition.py) - Validates that the input is not equal to the given value.
13. [`OneOfCondition`](OneOfCondition.py) - Validates that at least one of the given conditions is true.
14. [`OneOfMatchesCondition`](OneOfMatchesCondition.py) - Validates that at least one of the given matches is true.
15. [`RequiredIfCondition`](RequiredIfCondition.py) - Validates that the input is required if the given field has a specific value.
16. [`StringLongerThanCondition`](StringLongerThanCondition.py) - Validates that the string is longer than the given value.
17. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is in correct temporal order.
26 changes: 20 additions & 6 deletions flask_inputfilter/Condition/RequiredIfCondition.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any, Dict, List, Optional, Union

from .BaseCondition import BaseCondition

Expand All @@ -10,14 +10,28 @@ class RequiredIfCondition(BaseCondition):
"""

def __init__(
self, condition_field: str, value: Any, required_field: str
self,
condition_field: str,
value: Optional[Union[Any, List[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
)
condition_value = data.get(self.condition_field)

if self.value is not None:
if isinstance(self.value, list):
if condition_value in self.value:
return data.get(self.required_field) is not None
else:
if condition_value == self.value:
return data.get(self.required_field) is not None

else:
if condition_value is not None:
return data.get(self.required_field) is not None

return True
43 changes: 23 additions & 20 deletions flask_inputfilter/Filter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ 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. [`RemoveEmojisFilter`](RemoveEmojisFilter.py) - Removes the emojis from the string.
3. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug.
4. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string.
5. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string.
6. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value.
7. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case.
8. [`ToDateFilter`](ToDateFilter.py) - Converts a string to a date value.
9. [`ToDateTimeFilter`](ToDateTimeFilter.py) - Converts a string to a datetime value.
10. [`ToEnumFilter`](ToEnumFilter.py) - Converts a string or integer to an enum value.
11. [`ToFloatFilter`](ToFloatFilter.py) - Converts a string to a float value.
12. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts a string to an integer value.
13. [`ToIsoFilter`](ToIsoFilter.py) - Converts a string to an ISO8601 date time value.
14. [`ToLowerFilter`](ToLowerFilter.py) - Converts a string to lowercase.
15. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes a unicode string.
16. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string).
17. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case.
18. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case.
19. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value.
20. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase.
21. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string.
2. [`BlacklistFilter`](BlacklistFilter.py) - Filters the string based on the blacklist.
3. [`RemoveEmojisFilter`](RemoveEmojisFilter.py) - Removes the emojis from the string.
4. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug.
5. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string.
6. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string.
7. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value.
8. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case.
9. [`ToDateFilter`](ToDateFilter.py) - Converts a string to a date value.
10. [`ToDateTimeFilter`](ToDateTimeFilter.py) - Converts a string to a datetime value.
11. [`ToEnumFilter`](ToEnumFilter.py) - Converts a string or integer to an enum value.
12. [`ToFloatFilter`](ToFloatFilter.py) - Converts a string to a float value.
13. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts a string to an integer value.
14. [`ToIsoFilter`](ToIsoFilter.py) - Converts a string to an ISO8601 date time value.
15. [`ToLowerFilter`](ToLowerFilter.py) - Converts a string to lowercase.
16. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes a unicode string.
17. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string).
18. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case.
19. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case.
20. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value.
21. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase.
22. [`TruncateFilter`](TruncateFilter.py) - Truncates the string to the specified length.
23. [`WhitelistFilter`](WhitelistFilter.py) - Filters the string based on the whitelist.
24. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string.
83 changes: 40 additions & 43 deletions flask_inputfilter/InputFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ class InputFilter:
Base class for input filters.
"""

def __init__(self) -> None:
def __init__(self, methods: Optional[List[str]] = None) -> None:
self.methods = methods or ["GET", "POST", "PATCH", "PUT", "DELETE"]
self.fields = {}
self.conditions = []
self.global_filters = []
self.global_validators = []

def add(
self,
Expand All @@ -36,7 +39,8 @@ def add(
:param name: The name of the field.
:param required: Whether the field is required.
:param default: The default value of the field.
:param fallback: The fallback value of the field.
:param fallback: The fallback value of the field, if validations fails
or field None, although it is required .
:param filters: The filters to apply to the field value.
:param validators: The validators to apply to the field value.
:param external_api: Configuration for an external API call.
Expand All @@ -57,15 +61,27 @@ def addCondition(self, condition: BaseCondition) -> None:
"""
self.conditions.append(condition)

def addGlobalFilter(self, filter_: BaseFilter) -> None:
"""
Add a global filter to be applied to all fields.
"""
self.global_filters.append(filter_)

def addGlobalValidator(self, validator: BaseValidator) -> None:
"""
Add a global validator to be applied to all fields.
"""
self.global_validators.append(validator)

def _applyFilters(self, field_name: str, value: Any) -> Any:
"""
Apply filters to the field value.
"""

field = self.fields.get(field_name)
for filter_ in self.global_filters:
value = filter_.apply(value)

if not field:
return value
field = self.fields.get(field_name)

for filter_ in field["filters"]:
value = filter_.apply(value)
Expand All @@ -77,10 +93,10 @@ def _validateField(self, field_name: str, value: Any) -> None:
Validate the field value.
"""

field = self.fields.get(field_name)
for validator in self.global_validators:
validator.validate(value)

if not field:
return
field = self.fields.get(field_name)

for validator in field["validators"]:
validator.validate(value)
Expand Down Expand Up @@ -133,38 +149,31 @@ def _callExternalApi(
return result

@staticmethod
def __replacePlaceholders(url: str, validated_data: dict) -> str:
def __replacePlaceholders(value: str, validated_data: dict) -> str:
"""
Ersetzt alle Platzhalter in der URL, die mit {{}} definiert sind,
durch die entsprechenden Werte aus den Parametern.
Replace all placeholders, marked with '{{ }}' in value
with the corresponding values from validated_data.
"""

return re.sub(
r"{{(.*?)}}",
lambda match: str(validated_data.get(match.group(1))),
url,
value,
)

@staticmethod
def __replacePlaceholdersInParams(
params: dict, validated_data: dict
self, params: dict, validated_data: dict
) -> dict:
"""
Replace all placeholders in params with the
corresponding values from validated_data.
"""
replaced_params = {}
for key, value in params.items():
if isinstance(value, str):
replaced_value = re.sub(
r"{{(.*?)}}",
lambda match: str(validated_data.get(match.group(1), "")),
value,
)
replaced_params[key] = replaced_value
else:
replaced_params[key] = value
return replaced_params
return {
key: self.__replacePlaceholders(value, validated_data)
if isinstance(value, str)
else value
for key, value in params.items()
}

def validateData(
self, data: Dict[str, Any], kwargs: Dict[str, Any] = None
Expand Down Expand Up @@ -221,7 +230,7 @@ def validateData(
external_api_config, validated_data
)

except ValidationError:
except Exception:
if field_info.get("fallback") is None:
raise ValidationError(
f"External API call failed for field "
Expand Down Expand Up @@ -274,25 +283,13 @@ def decorator(
def wrapper(
*args, **kwargs
) -> Union[Response, Tuple[Any, Dict[str, Any]]]:
if request.method == "GET":
data = request.args

elif request.method in ["POST", "PUT", "DELETE"]:
if not request.is_json:
data = request.args

else:
data = request.json

else:
return Response(
status=415, response="Unsupported method Type"
)
if request.method not in cls().methods:
return Response(status=405, response="Method Not Allowed")

inputFilter = cls()
data = request.json if request.is_json else request.args

try:
g.validated_data = inputFilter.validateData(data, kwargs)
g.validated_data = cls().validateData(data, kwargs)

except ValidationError as e:
return Response(status=400, response=str(e))
Expand Down
Loading
Loading