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
17 changes: 17 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ Changelog

All notable changes to this project will be documented in this file.

[0.3.0] - 2025-04-10
--------------------

Added
^^^^^


Changed
^^^^^^^
- Updated ``IsTypedDictValidator` and ``IsDataclassValidator`` to require a specific model and
checks if the input json is in the defined format.

Removed
^^^^^^^
- ``RemoveEmojisFilter``
- ``ToPascaleCaseFilter``


[0.2.0] - 2025-04-07
--------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
project = "flask-inputfilter"
copyright = "2025, Leander Cain Slotosch"
author = "Leander Cain Slotosch"
release = "0.2.0"
release = "0.3.0"

extensions = ["sphinx_rtd_theme"]

Expand Down
109 changes: 109 additions & 0 deletions docs/source/options/deserialization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
Deserialization
===============

Deserialization in Flask-InputFilter allows you to convert validated data
into custom model objects or maintain it as a dictionary. This feature is
particularly useful when you want to work with strongly-typed objects in
your application.

Overview
--------

The deserialization process is handled through two main methods:

- ``setModel()``: Sets the model class to be used for deserialization
- ``serialize()``: Converts the validated data into an instance of the
specified model class or returns the raw data as a dictionary

Configuration
-------------

The ``validate()`` method will automatically deserialize the validated data
into an instance of the model class, if there is a model class set.

.. code-block:: python

from flask_inputfilter import InputFilter
from dataclasses import dataclass


@dataclass
class User:
username: str
email: str


class UserInputFilter(InputFilter):
def __init__(self):
super().__init__()

self.setModel(User)

Examples
--------

Usage with Flask Routes
^^^^^^^^^^^^^^^^^^^^^^^

You can also use deserialization in your Flask routes:

.. code-block:: python

from flask import Flask, jsonify, g
from flask_inputfilter import InputFilter


class User:
def __init__(self, username: str):
self.username = username


class MyInputFilter(InputFilter):
def __init__(self):
super().__init__(methods=["GET"])
self.add("username")
self.setModel(User)


app = Flask(__name__)

@app.route("/test", methods=["GET"])
@MyInputFilter.validate()
def test_route():
# g.validated_data will contain the deserialized User instance

validated_data: User = g.validated_data

Usage outside of Flask Routes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can also use deserialization outside of Flask routes:

.. code-block:: python

from flask import Flask, jsonify, g
from flask_inputfilter import InputFilter


class User:
def __init__(self, username: str):
self.username = username


class MyInputFilter(InputFilter):
def __init__(self):
super().__init__(methods=["GET"])
self.add("username")
self.setModel(User)

app = Flask(__name__)

@app.route("/test", methods=["GET"])
def test_route():
input_filter = MyInputFilter()
input_filter.setData({"username": "test user"})

if not input_filter.isValid():
return jsonify({"error": "Invalid data"}), 400

validated_data: User = input_filter.serialize()
1 change: 1 addition & 0 deletions docs/source/options/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Options
condition
copy
external_api
deserialization
12 changes: 6 additions & 6 deletions docs/source/options/validator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -504,16 +504,16 @@ IsDataclassValidator
~~~~~~~~~~~~~~~~~~~~
**Description:**

Validates that the provided value is an instance of a dataclass. Optionally checks whether it matches a specific dataclass type.
Validates that the provided value conforms to a specific dataclass type.

**Parameters:**

- **dataclass_type** (*Optional[Type]*): The expected dataclass type.
- **dataclass_type** (*Type[dict]*): The expected dataclass type.
- **error_message** (*Optional[str]*): Custom error message if validation fails.

**Expected Behavior:**

Ensures the input is a dataclass (using Python’s dataclass mechanism) and, if specified, that it is an instance of the provided type. Raises a ``ValidationError`` otherwise.
Ensures the input is a dictionary and, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.

**Example Usage:**

Expand Down Expand Up @@ -974,16 +974,16 @@ IsTypedDictValidator
~~~~~~~~~~~~~~~~~~~~
**Description:**

Validates that the provided value is a TypedDict instance. Optionally, it checks whether the dictionary conforms to a specified TypedDict structure.
Validates that the provided value conforms to a specified TypedDict structure.

**Parameters:**

- **typed_dict_type** (*Optional[Type[TypedDict]]*): The TypedDict class that defines the expected structure.
- **typed_dict_type** (*Type[TypedDict]*): The TypedDict class that defines the expected structure.
- **error_message** (*Optional[str]*): Custom error message if the validation fails.

**Expected Behavior:**

Ensures the input is a dictionary and, if a specific TypedDict type is provided, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.
Ensures the input is a dictionary and, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.

**Example Usage:**

Expand Down
50 changes: 47 additions & 3 deletions flask_inputfilter/InputFilter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import json
import re
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
)

from flask import Response, g, request
from typing_extensions import final
Expand All @@ -13,6 +23,8 @@

API_PLACEHOLDER_PATTERN = re.compile(r"{{(.*?)}}")

T = TypeVar("T")


class InputFilter:
"""
Expand All @@ -28,6 +40,7 @@ class InputFilter:
"__data",
"__validated_data",
"__errors",
"__model_class",
)

def __init__(self, methods: Optional[List[str]] = None) -> None:
Expand All @@ -39,6 +52,7 @@ def __init__(self, methods: Optional[List[str]] = None) -> None:
self.__data: Dict[str, Any] = {}
self.__validated_data: Dict[str, Any] = {}
self.__errors: Dict[str, str] = {}
self.__model_class: Optional[Type[T]] = None

@final
def add(
Expand Down Expand Up @@ -333,7 +347,7 @@ def getValues(self) -> Dict[str, Any]:
Retrieves a dictionary of key-value pairs from the current object.
This method provides access to the internal state or configuration of
the object in a dictionary format, where keys are strings and values
can be of various types depending on the objects design.
can be of various types depending on the object's design.

Returns:
Dict[str, Any]: A dictionary containing string keys and their
Expand Down Expand Up @@ -518,6 +532,31 @@ def merge(self, other: "InputFilter") -> None:
else:
self.__global_validators.append(validator)

@final
def setModel(self, model_class: Type[T]) -> None:
"""
Set the model class for serialization.

Args:
model_class: The class to use for serialization.
"""
self.__model_class = model_class

@final
def serialize(self) -> Union[Dict[str, Any], T]:
"""
Serialize the validated data. If a model class is set,
returns an instance of that class, otherwise returns the
raw validated data.

Returns:
Union[Dict[str, Any], T]: The serialized data.
"""
if self.__model_class is None:
return self.__validated_data

return self.__model_class(**self.__validated_data)

@final
def isValid(self) -> bool:
"""
Expand Down Expand Up @@ -649,7 +688,12 @@ def wrapper(

input_filter.__data = {**data, **kwargs}

g.validated_data = input_filter.validateData()
validated_data = input_filter.validateData()

if input_filter.__model_class is not None:
validated_data = input_filter.serialize()

g.validated_data = validated_data

except ValidationError as e:
return Response(
Expand Down
14 changes: 8 additions & 6 deletions flask_inputfilter/Validator/IsDataclassValidator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from dataclasses import is_dataclass
from typing import Any, Optional, Type
from typing import Any, Optional, Type, TypeVar

from flask_inputfilter.Exception import ValidationError
from flask_inputfilter.Validator import BaseValidator

T = TypeVar("T")


class IsDataclassValidator(BaseValidator):
"""
Expand All @@ -14,20 +15,21 @@ class IsDataclassValidator(BaseValidator):

def __init__(
self,
dataclass_type: Optional[Type[dict]] = None,
dataclass_type: Type[T],
error_message: Optional[str] = None,
) -> None:
self.dataclass_type = dataclass_type
self.error_message = error_message

def validate(self, value: Any) -> None:
if not is_dataclass(value):
if not isinstance(value, dict):
raise ValidationError(
self.error_message
or "The provided value is not a dataclass instance."
or "The provided value is not a dict instance."
)

if self.dataclass_type and not isinstance(value, self.dataclass_type):
expected_keys = self.dataclass_type.__annotations__.keys()
if any(key not in value for key in expected_keys):
raise ValidationError(
self.error_message
or f"'{value}' is not an instance "
Expand Down
19 changes: 9 additions & 10 deletions flask_inputfilter/Validator/IsTypedDictValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class IsTypedDictValidator(BaseValidator):

def __init__(
self,
typed_dict_type: Optional[Type[TypedDict]] = None,
typed_dict_type: Type[TypedDict],
error_message: Optional[str] = None,
) -> None:
self.typed_dict_type = typed_dict_type
Expand All @@ -25,14 +25,13 @@ def validate(self, value: Any) -> None:
if not isinstance(value, dict):
raise ValidationError(
self.error_message
or "The provided value is not a TypedDict instance."
or "The provided value is not a dict instance."
)

if self.typed_dict_type:
expected_keys = self.typed_dict_type.__annotations__.keys()
if not all(key in value for key in expected_keys):
raise ValidationError(
self.error_message
or f"'{value}' does not match "
f"{self.typed_dict_type.__name__} structure."
)
expected_keys = self.typed_dict_type.__annotations__.keys()
if any(key not in value for key in expected_keys):
raise ValidationError(
self.error_message
or f"'{value}' does not match "
f"{self.typed_dict_type.__name__} structure."
)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name="flask_inputfilter",
version="0.2.0",
version="0.3.0",
license="MIT",
author="Leander Cain Slotosch",
author_email="[email protected]",
Expand Down
Loading