diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d26f4aa..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 8ac79b2..6378e30 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,4 @@ cython_debug/ .idea/ .vscode/ +.DS_Store diff --git a/flask_inputfilter/InputFilter.py b/flask_inputfilter/InputFilter.py index 4e75498..a854d96 100644 --- a/flask_inputfilter/InputFilter.py +++ b/flask_inputfilter/InputFilter.py @@ -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: """ @@ -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. """ @@ -52,6 +54,7 @@ def add( "fallback": fallback, "filters": filters or [], "validators": validators or [], + "steps": steps or [], "external_api": external_api, } @@ -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) @@ -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": {}, @@ -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: @@ -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]: @@ -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 diff --git a/flask_inputfilter/Validator/DateAfterValidator.py b/flask_inputfilter/Validator/DateAfterValidator.py index 3acb0da..18c4028 100644 --- a/flask_inputfilter/Validator/DateAfterValidator.py +++ b/flask_inputfilter/Validator/DateAfterValidator.py @@ -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}'." @@ -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)}'." + ) diff --git a/flask_inputfilter/Validator/DateBeforeValidator.py b/flask_inputfilter/Validator/DateBeforeValidator.py index e8088ec..0be119d 100644 --- a/flask_inputfilter/Validator/DateBeforeValidator.py +++ b/flask_inputfilter/Validator/DateBeforeValidator.py @@ -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)}'." + ) diff --git a/flask_inputfilter/Validator/IsPastDateValidator.py b/flask_inputfilter/Validator/IsPastDateValidator.py index 9ee4e49..1596857 100644 --- a/flask_inputfilter/Validator/IsPastDateValidator.py +++ b/flask_inputfilter/Validator/IsPastDateValidator.py @@ -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)}'." + ) diff --git a/flask_inputfilter/Validator/IsWeekdayValidator.py b/flask_inputfilter/Validator/IsWeekdayValidator.py index fd0e7ca..de57176 100644 --- a/flask_inputfilter/Validator/IsWeekdayValidator.py +++ b/flask_inputfilter/Validator/IsWeekdayValidator.py @@ -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)}'." + ) diff --git a/setup.py b/setup.py index 5f960e1..6a15aad 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import find_packages, setup +from setuptools import setup setup( name="flask_inputfilter", @@ -10,13 +10,15 @@ 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", @@ -24,8 +26,6 @@ "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", ) diff --git a/test/test_input_filter.py b/test/test_input_filter.py index c1a1660..1c36e58 100644 --- a/test/test_input_filter.py +++ b/test/test_input_filter.py @@ -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()], ) @@ -107,7 +104,6 @@ def __init__(self): name="age", required=False, default=18, - filters=[], validators=[IsIntegerValidator()], ) @@ -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()], ) @@ -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: """ diff --git a/test/test_validator.py b/test/test_validator.py index 78acbdc..41c116f 100644 --- a/test/test_validator.py +++ b/test/test_validator.py @@ -141,15 +141,19 @@ def test_date_after_validator(self) -> None: validators=[DateAfterValidator(reference_date=date(2021, 1, 1))], ) self.inputFilter.validateData({"date": date(2021, 6, 1)}) - self.inputFilter.validateData({"datetime": datetime(2021, 6, 1, 0, 0)}) - self.inputFilter.validateData({"isodate": "2021-06-02T10:00:55"}) + self.inputFilter.validateData({"date": datetime(2021, 6, 1, 0, 0)}) + self.inputFilter.validateData({"date": "2021-06-02T10:00:55"}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"date": date(2020, 12, 31)}) + + with self.assertRaises(ValidationError): self.inputFilter.validateData( - {"datetime": datetime(2020, 12, 31, 23, 59)} + {"date": datetime(2020, 12, 31, 23, 59)} ) - self.inputFilter.validateData({"isodate": "2020-12-31T23:59:59"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "2020-12-31T23:59:59"}) self.inputFilter.add( "datetime", @@ -162,15 +166,15 @@ def test_date_after_validator(self) -> None: self.inputFilter.validateData( {"datetime": datetime(2021, 6, 1, 12, 0)} ) - self.inputFilter.validateData({"isodatetime": "2021-06-01T12:00:00"}) + self.inputFilter.validateData({"datetime": "2021-06-01T12:00:00"}) with self.assertRaises(ValidationError): self.inputFilter.validateData( {"datetime": datetime(2020, 12, 31, 23, 59)} ) - self.inputFilter.validateData( - {"isodatetime": "2020-12-31T23:59:59"} - ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"datetime": "2020-12-31T23:59:59"}) self.inputFilter.add( "isodatetime", @@ -206,6 +210,9 @@ def test_date_after_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"custom_error": "unparseable date"}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": 123}) + def test_date_before_validator(self) -> None: """ Test DateBeforeValidator. @@ -219,15 +226,19 @@ def test_date_before_validator(self) -> None: ) self.inputFilter.validateData({"date": date(2021, 6, 1)}) - self.inputFilter.validateData({"datetime": datetime(2021, 6, 1, 0, 0)}) - self.inputFilter.validateData({"isodate": "2022-06-00T10:00:55"}) + self.inputFilter.validateData({"date": datetime(2021, 6, 1, 0, 0)}) + self.inputFilter.validateData({"date": "2021-06-01T10:00:55"}) with self.assertRaises(ValidationError): self.inputFilter.validateData({"date": date(2022, 6, 1)}) + + with self.assertRaises(ValidationError): self.inputFilter.validateData( - {"datetime": datetime(2022, 6, 1, 0, 54)} + {"date": datetime(2022, 6, 1, 0, 54)} ) - self.inputFilter.validateData({"isodatetime": "20"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"date": "20"}) self.inputFilter.add( "datetime", @@ -238,20 +249,22 @@ def test_date_before_validator(self) -> None: ], ) - self.inputFilter.validateData({"date": date(2021, 6, 1)}) + self.inputFilter.validateData({"datetime": date(2021, 6, 1)}) self.inputFilter.validateData( {"datetime": datetime(2021, 6, 1, 12, 0)} ) - self.inputFilter.validateData({"isodatetime": "2021-06-01T00:00:00"}) + self.inputFilter.validateData({"datetime": "2021-06-01T00:00:00"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"datetime": date(2022, 6, 1)}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"date": date(2022, 6, 1)}) self.inputFilter.validateData( {"datetime": datetime(2022, 6, 1, 0, 0)} ) - self.inputFilter.validateData( - {"isodatetime": "2022-06-01T00:00:00"} - ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"datetime": "2022-06-01T00:00:00"}) self.inputFilter.add( "isodatetime", @@ -262,17 +275,21 @@ def test_date_before_validator(self) -> None: ], ) - self.inputFilter.validateData({"date": date(2021, 6, 1)}) + self.inputFilter.validateData({"isodatetime": date(2021, 6, 1)}) self.inputFilter.validateData( - {"datetime": datetime(2021, 6, 1, 12, 0)} + {"isodatetime": datetime(2021, 6, 1, 12, 0)} ) self.inputFilter.validateData({"isodatetime": "2021-06-01T00:00:00"}) with self.assertRaises(ValidationError): - self.inputFilter.validateData({"date": date(2022, 6, 1)}) + self.inputFilter.validateData({"isodatetime": date(2022, 6, 1)}) + + with self.assertRaises(ValidationError): self.inputFilter.validateData( - {"datetime": datetime(2022, 6, 1, 10, 0)} + {"isodatetime": datetime(2022, 6, 1, 10, 0)} ) + + with self.assertRaises(ValidationError): self.inputFilter.validateData( {"isodatetime": "2022-06-01T00:00:00"} ) @@ -295,6 +312,9 @@ def test_date_before_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"custom_error": "unparseable date"}) + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": 123}) + def test_date_range_validator(self) -> None: """ Test DateRangeValidator. @@ -510,7 +530,7 @@ def test_is_base64_image_correct_size_validator(self) -> None: validators=[ IsBase64ImageCorrectSizeValidator( minSize=10, - maxSize=50, + maxSize=5, error_message="Custom error message", ) ],