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
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ cython_debug/

.idea/
.vscode/
.DS_Store
162 changes: 84 additions & 78 deletions flask_inputfilter/InputFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def add(
fallback: Any = None,
filters: Optional[List[BaseFilter]] = None,
validators: Optional[List[BaseValidator]] = None,
steps: Optional[List[Union[BaseFilter, BaseValidator]]] = None,
external_api: Optional[ExternalApiConfig] = None,
) -> None:
"""
Expand All @@ -43,6 +44,7 @@ def add(
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 steps:
:param external_api: Configuration for an external API call.
"""

Expand All @@ -52,6 +54,7 @@ def add(
"fallback": fallback,
"filters": filters or [],
"validators": validators or [],
"steps": steps or [],
"external_api": external_api,
}

Expand All @@ -73,11 +76,14 @@ def addGlobalValidator(self, validator: BaseValidator) -> None:
"""
self.global_validators.append(validator)

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

if value is None:
return value

for filter_ in self.global_filters:
value = filter_.apply(value)

Expand All @@ -88,27 +94,38 @@ def _applyFilters(self, field_name: str, value: Any) -> Any:

return value

def _validateField(self, field_name: str, value: Any) -> None:
def __validateField(self, field_name: str, field_info, value: Any) -> None:
"""
Validate the field value.
"""

for validator in self.global_validators:
validator.validate(value)
if value is None:
return

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

field = self.fields.get(field_name)

for validator in field["validators"]:
validator.validate(value)
except ValidationError:
if field_info.get("fallback") is None:
raise

for validator in field["validators"]:
validator.validate(value)
return field_info.get("fallback")

def _callExternalApi(
self, config: ExternalApiConfig, validated_data: dict
def __callExternalApi(
self, field_info, validated_data: dict
) -> Optional[Any]:
"""
Führt den API-Aufruf durch und gibt den Wert zurück,
der im Antwortkörper zu finden ist.
"""

config: ExternalApiConfig = field_info.get("external_api")

requestData = {
"headers": {},
"params": {},
Expand All @@ -132,21 +149,30 @@ def _callExternalApi(
)
requestData["method"] = config.method

response = requests.request(**requestData)
try:
response = requests.request(**requestData)

if response.status_code != 200:
raise ValidationError(
f"External API call failed with "
f"status code {response.status_code}"
)
if response.status_code != 200:
raise ValidationError(
f"External API call failed with "
f"status code {response.status_code}"
)

result = response.json()
result = response.json()

data_key = config.data_key
if data_key:
return result.get(data_key)
data_key = config.data_key
if data_key:
return result.get(data_key)

return result
return result
except Exception:
if field_info and field_info.get("fallback") is None:
raise ValidationError(
f"External API call failed for field "
f"'{config.data_key}'."
)

return field_info.get("fallback")

@staticmethod
def __replacePlaceholders(value: str, validated_data: dict) -> str:
Expand Down Expand Up @@ -175,6 +201,37 @@ def __replacePlaceholdersInParams(
for key, value in params.items()
}

@staticmethod
def __checkForRequired(
field_name: str, field_info: dict, value: Any
) -> Any:
"""
Determine the value of the field, considering the required and
fallback attributes.

If the field is not required and no value is provided, the default
value is returned.
If the field is required and no value is provided, the fallback
value is returned.
If no of the above conditions are met, a ValidationError is raised.
"""

if value is not None:
return value

if not field_info.get("required"):
return field_info.get("default")

if field_info.get("fallback") is not None:
return field_info.get("fallback")

raise ValidationError(f"Field '{field_name}' is required.")

def __checkConditions(self, validated_data: dict) -> None:
for condition in self.conditions:
if not condition.check(validated_data):
raise ValidationError(f"Condition '{condition}' not met.")

def validateData(
self, data: Dict[str, Any], kwargs: Dict[str, Any] = None
) -> Dict[str, Any]:
Expand All @@ -192,71 +249,20 @@ def validateData(
for field_name, field_info in self.fields.items():
value = combined_data.get(field_name)

# Apply filters
value = self._applyFilters(field_name, value)

# Check for required field
if value is None:
if (
field_info.get("required")
and field_info.get("external_api") is None
):
if field_info.get("fallback") is None:
raise ValidationError(
f"Field '{field_name}' is required."
)
value = self.__applyFilters(field_name, value)

value = field_info.get("fallback")

if field_info.get("default") is not None:
value = field_info.get("default")
value = (
self.__validateField(field_name, field_info, value) or value
)

# Validate field
if value is not None:
try:
self._validateField(field_name, value)
except ValidationError:
if field_info.get("fallback") is not None:
value = field_info.get("fallback")
else:
raise

# External API call
if field_info.get("external_api"):
external_api_config = field_info.get("external_api")

try:
value = self._callExternalApi(
external_api_config, validated_data
)

except Exception:
if field_info.get("fallback") is None:
raise ValidationError(
f"External API call failed for field "
f"'{field_name}'."
)

value = field_info.get("fallback")

if value is None:
if field_info.get("required"):
if field_info.get("fallback") is None:
raise ValidationError(
f"Field '{field_name}' is required."
)
value = self.__callExternalApi(field_info, validated_data)

value = field_info.get("fallback")

if field_info.get("default") is not None:
value = field_info.get("default")
value = self.__checkForRequired(field_name, field_info, value)

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.")
self.__checkConditions(validated_data)

return validated_data

Expand Down
9 changes: 4 additions & 5 deletions flask_inputfilter/Validator/DateAfterValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def validate(self, value: Any) -> None:
value_reference_date = self._parse_date(self.reference_date)
value_datetime = self._parse_date(value)

if value_datetime <= value_reference_date:
if value_datetime < value_reference_date:
raise ValidationError(
self.error_message
or f"Date '{value}' is not after '{value_reference_date}'."
Expand All @@ -43,7 +43,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 date comparison '{type(value)}'."
)
raise ValidationError(
f"Unsupported type for date comparison '{type(value)}'."
)
7 changes: 3 additions & 4 deletions flask_inputfilter/Validator/DateBeforeValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,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 date comparison '{type(value)}'."
)
raise ValidationError(
f"Unsupported type for date comparison '{type(value)}'."
)
7 changes: 3 additions & 4 deletions flask_inputfilter/Validator/IsPastDateValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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)}'."
)
7 changes: 3 additions & 4 deletions flask_inputfilter/Validator/IsWeekdayValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,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 weekday validation '{type(value)}'."
)
raise ValidationError(
f"Unsupported type for weekday validation '{type(value)}'."
)
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from setuptools import find_packages, setup
from setuptools import setup

setup(
name="flask_inputfilter",
Expand All @@ -10,22 +10,22 @@
long_description=open("README.rst").read(),
long_description_content_type="text/markdown",
url="https://github.com/LeanderCS/flask-inputfilter",
packages=find_packages(),
packages=["flask_inputfilter"],
install_requires=[
"Flask>=2.1",
"pillow>=8.0.0",
"requests>=2.22.0",
],
classifiers=[
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"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",
],
python_requires=">=3.7",
)
14 changes: 6 additions & 8 deletions test/test_input_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ def __init__(self):
self.add(
name="username",
required=True,
filters=[],
validators=[],
)
self.add(
name="age",
required=False,
default=18,
filters=[],
validators=[IsIntegerValidator()],
)

Expand Down Expand Up @@ -107,7 +104,6 @@ def __init__(self):
name="age",
required=False,
default=18,
filters=[],
validators=[IsIntegerValidator()],
)

Expand Down Expand Up @@ -135,14 +131,11 @@ def __init__(self):
self.add(
name="username",
required=True,
filters=[],
validators=[],
)
self.add(
name="age",
required=False,
default=18,
filters=[],
validators=[IsIntegerValidator()],
)

Expand Down Expand Up @@ -237,13 +230,18 @@ def test_fallback_with_default(self) -> None:

validated_data = self.inputFilter.validateData({})

self.assertEqual(validated_data["available"], True)
self.assertEqual(validated_data["available"], False)
self.assertEqual(validated_data["color"], "red")

validated_data = self.inputFilter.validateData({"available": False})

self.assertEqual(validated_data["available"], False)

self.inputFilter.add("required_without_fallback", required=True)

with self.assertRaises(ValidationError):
self.inputFilter.validateData({})

@patch("requests.request")
def test_external_api(self, mock_request: Mock) -> None:
"""
Expand Down
Loading
Loading