diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index cba5eb5..c0830c2 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -3,6 +3,18 @@ Changelog All notable changes to this project will be documented in this file. +[0.6.3] - 2025-09-24 +-------------------- + +Added +^^^^^ +- Added default timeout of 30s for external api requests. + +Changed +^^^^^^^ +- Switched to more strict exception types for image filter. + + [0.6.2] - 2025-07-03 -------------------- diff --git a/flask_inputfilter/filters/to_base64_image_filter.py b/flask_inputfilter/filters/to_base64_image_filter.py index 33c89b0..9bbe116 100644 --- a/flask_inputfilter/filters/to_base64_image_filter.py +++ b/flask_inputfilter/filters/to_base64_image_filter.py @@ -70,7 +70,7 @@ def apply(self, value: Any) -> Any: try: Image.open(io.BytesIO(base64.b64decode(value))).verify() return value - except Exception: + except (ValueError, OSError, base64.binascii.Error): pass # Try to open as raw bytes diff --git a/flask_inputfilter/filters/to_image_filter.py b/flask_inputfilter/filters/to_image_filter.py index c5d4231..badf929 100644 --- a/flask_inputfilter/filters/to_image_filter.py +++ b/flask_inputfilter/filters/to_image_filter.py @@ -51,7 +51,7 @@ def apply(self, value: Any) -> Any: # Try to decode as base64 try: return Image.open(io.BytesIO(base64.b64decode(value))) - except Exception: + except (ValueError, OSError, base64.binascii.Error): pass # Try to open as raw bytes diff --git a/flask_inputfilter/filters/to_typed_dict_filter.py b/flask_inputfilter/filters/to_typed_dict_filter.py index ae9deaf..4288c60 100644 --- a/flask_inputfilter/filters/to_typed_dict_filter.py +++ b/flask_inputfilter/filters/to_typed_dict_filter.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any +from typing import Any, Type from flask_inputfilter.models import BaseFilter @@ -34,7 +34,7 @@ def __init__(self): __slots__ = ("typed_dict",) - def __init__(self, typed_dict) -> None: + def __init__(self, typed_dict: Type) -> None: """ Parameters: typed_dict (Type[TypedDict]): The TypedDict class diff --git a/flask_inputfilter/input_filter.py b/flask_inputfilter/input_filter.py index 8e61183..cb7bc06 100644 --- a/flask_inputfilter/input_filter.py +++ b/flask_inputfilter/input_filter.py @@ -104,7 +104,7 @@ def decorator( """ def wrapper( - *args, **kwargs + *args: Any, **kwargs: Any ) -> Union[Response, tuple[Any, dict[str, Any]]]: """ Wrapper function to handle input validation and error handling diff --git a/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx index 8db0a98..532a072 100644 --- a/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx +++ b/flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx @@ -79,7 +79,7 @@ cdef class ExternalApiMixin: requestData["method"] = config.method try: - response = requests.request(**requestData) + response = requests.request(timeout=30, **requestData) result = response.json() except requests.exceptions.RequestException: if fallback is None: diff --git a/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py b/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py index a093054..f1bac5f 100644 --- a/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py +++ b/flask_inputfilter/mixins/external_api_mixin/external_api_mixin.py @@ -83,7 +83,7 @@ def call_external_api( request_data["method"] = config.method try: - response = requests.request(**request_data) + response = requests.request(timeout=30, **request_data) result = response.json() except requests.exceptions.RequestException: if fallback is None: diff --git a/flask_inputfilter/validators/is_dataclass_validator.py b/flask_inputfilter/validators/is_dataclass_validator.py index 9887d50..7644580 100644 --- a/flask_inputfilter/validators/is_dataclass_validator.py +++ b/flask_inputfilter/validators/is_dataclass_validator.py @@ -113,7 +113,7 @@ def __init__( ) ) - def _format_error(self, error_type: str, **kwargs) -> str: + def _format_error(self, error_type: str, **kwargs: Any) -> str: """Format error message using template or custom message.""" if self.error_message: return self.error_message diff --git a/flask_inputfilter/validators/is_horizontal_image_validator.py b/flask_inputfilter/validators/is_horizontal_image_validator.py index 7bd54a9..7635af6 100644 --- a/flask_inputfilter/validators/is_horizontal_image_validator.py +++ b/flask_inputfilter/validators/is_horizontal_image_validator.py @@ -3,6 +3,7 @@ import base64 import binascii import io +from typing import Any, Optional from PIL import Image from PIL.Image import Image as ImageType @@ -41,10 +42,10 @@ def __init__(self): __slots__ = ("error_message",) - def __init__(self, error_message=None): + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message - def validate(self, value): + def validate(self, value: Any) -> None: if not isinstance(value, (str, ImageType)): raise ValidationError( "The value is not an image or its base 64 representation." diff --git a/flask_inputfilter/validators/is_vertical_image_validator.py b/flask_inputfilter/validators/is_vertical_image_validator.py index d8a1f8c..35a32c7 100644 --- a/flask_inputfilter/validators/is_vertical_image_validator.py +++ b/flask_inputfilter/validators/is_vertical_image_validator.py @@ -42,7 +42,7 @@ def __init__(self): __slots__ = ("error_message",) - def __init__(self, error_message: Optional[str] = None): + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = ( error_message or "The image is not vertically oriented." ) diff --git a/pyproject.toml b/pyproject.toml index 690f8c0..5c99f89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "flask_inputfilter" -version = "0.6.2" +version = "0.6.3" description = "A library to easily filter and validate input data in Flask applications" readme = "README.md" keywords = [ @@ -125,6 +125,16 @@ select = [ "TCH", # flake8-type-checking "PTH", # flake8-use-pathlib "RUF", # Ruff-specific rules + "S", # flake8-bandit (Security) + "BLE", # flake8-blind-except + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "ERA", # eradicate + "PERF", # Perflint + "FURB", # refurb + "TRY", # tryceratops + "SLF", # flake8-self + "A", # flake8-builtins ] fixable = ["ALL"] unfixable = [] @@ -136,11 +146,35 @@ ignore = [ "UP006", # Use `list` instead of `List` (Python 3.9+) "UP007", # Use `X | Y` for unions (Python 3.10+) "UP035", # Import from collections.abc (Python 3.9+) + "ANN101", # Missing type annotation for self + "ANN102", # Missing type annotation for cls + "ANN401", # Dynamically typed expressions (Any) - OK for flexible library + "TRY003", # Long exception messages (ok for Libraries) + "TRY300", # Consider moving to else block - not always applicable + "TRY301", # Abstract raise to inner function - not needed for simple cases + "PERF203", # try-except in loop - acceptable for validation patterns + "A001", # Variable shadowing builtin - OK in specific contexts (e.g. copyright) + "A002", # Variable shadowing builtin ] pyupgrade = [ true ] +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "S101", # Assert usage ist OK in Tests + "ANN", # Type annotations optional in Tests + "ARG", # Unused arguments OK in Test fixtures +] +"examples/*" = [ + "ANN201", # Missing return type annotation in examples + "ANN204", # Missing return type annotation for __init__ in examples + "S201", # debug=True is OK in examples +] +"docs/*" = [ + "A001", # Variable shadowing builtin (copyright) is OK in docs +] + [tool.ruff.lint.isort] force-single-line = false split-on-trailing-comma = true diff --git a/tests/test_input_filter.py b/tests/test_input_filter.py index fc44c8e..5948c9c 100644 --- a/tests/test_input_filter.py +++ b/tests/test_input_filter.py @@ -859,7 +859,7 @@ def test_external_api(self, mock_request: Mock) -> None: self.assertTrue(validated_data["is_valid"]) expected_url = "https://api.example.com/validate_user/test_user" mock_request.assert_called_with( - headers={}, method="GET", url=expected_url, params={} + headers={}, method="GET", url=expected_url, params={}, timeout=30 ) # API returns invalid result @@ -904,6 +904,7 @@ def test_external_api_params(self, mock_request: Mock) -> None: method="GET", url=expected_url, params={"hash": "1234", "id": 123}, + timeout=30, ) mock_response.status_code = 500