Repository: igorbenav/fastcrud (python)

Issue:
Missing type parsing to dynamic filter
**Describe the bug or question**
I have a type issue with the filter created from FilterConfig. The filter does not convert the query param before querying the database.


**To Reproduce**
Configure a filter with a UUID that is not a primary key


**Description**
I noticed that the function _create_dynamic_filters from fastcrud/endpoint/helper.py take column_types has parameter but never use it.
When I try to work with UUID object I got error because the id I pass in the query param of the request is always a string and is never parsed as an object.
I was able to resolve my problem with the following code (which is the filter function from _create_dynamic_filters function):
```python
def filters(
        **kwargs: Any,
    ) -> dict[str, Any]:
        filtered_params = {}
        for key, value in kwargs.items():
            if value is not None:
                parse_func = column_types.get(key)
                filtered_params[key] = parse_func(value) if parse_func else value
        return filtered_params
```


Pull request:
Some fixes
Closes #126, #123

- Get multi with return_as_model is now properly typed
- Filter with a UUID that is not a primary key now working
- Update with not found record now raise error as previously defined by warning

Patch:
diff --git a/fastcrud/crud/fast_crud.py b/fastcrud/crud/fast_crud.py
index 5f3a6dc..a200ea7 100644
--- a/fastcrud/crud/fast_crud.py
+++ b/fastcrud/crud/fast_crud.py
@@ -1,6 +1,5 @@
 from typing import Any, Dict, Generic, Union, Optional, Callable
 from datetime import datetime, timezone
-import warnings
 
 from pydantic import ValidationError
 from sqlalchemy import (
@@ -33,6 +32,8 @@ from fastcrud.types import (
     SelectSchemaType,
     UpdateSchemaInternalType,
     UpdateSchemaType,
+    GetMultiResponseModel,
+    GetMultiResponseDict,
 )
 
 from .helper import (
@@ -1149,7 +1150,7 @@ class FastCRUD(
         return_as_model: bool = False,
         return_total_count: bool = True,
         **kwargs: Any,
-    ) -> dict[str, Any]:
+    ) -> Union[GetMultiResponseModel[SelectSchemaType], GetMultiResponseDict]:
         """
         Fetches multiple records based on filters, supporting sorting, pagination.
 
@@ -1167,7 +1168,10 @@ class FastCRUD(
             **kwargs: Filters to apply to the query, including advanced comparison operators for more detailed querying.
 
         Returns:
-            A dictionary containing `"data"` with fetched records and `"total_count"` indicating the total number of records matching the filters.
+            A dictionary containing the data list and optionally the total count:
+            - With return_as_model=True: Dict with "data": List[SelectSchemaType]
+            - With return_as_model=False: Dict with "data": List[Dict[str, Any]]
+            - If return_total_count=True, includes "total_count": int
 
         Raises:
             ValueError: If `limit` or `offset` is negative, or if `schema_to_select` is required but not provided or invalid.
@@ -2206,12 +2210,7 @@ class FastCRUD(
         """
         total_count = await self.count(db, **kwargs)
         if total_count == 0:
-            warnings.warn(
-                "Passing non-existing records to `update` will raise NoResultFound on version 0.15.3.",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            # raise NoResultFound("No record found to update.")
+            raise NoResultFound("No record found to update.")
         if not allow_multiple and total_count > 1:
             raise MultipleResultsFound(
                 f"Expected exactly one record to update, found {total_count}."
diff --git a/fastcrud/endpoint/helper.py b/fastcrud/endpoint/helper.py
index 0e5c01e..84dcda2 100644
--- a/fastcrud/endpoint/helper.py
+++ b/fastcrud/endpoint/helper.py
@@ -138,7 +138,10 @@ def _get_column_types(
     column_types = {}
     for column in inspector_result.mapper.columns:
         column_type = _get_python_type(column)
-        if hasattr(column.type, "__visit_name__") and column.type.__visit_name__ == "uuid":
+        if (
+            hasattr(column.type, "__visit_name__")
+            and column.type.__visit_name__ == "uuid"
+        ):
             column_type = UUID
         column_types[column.name] = column_type
     return column_types
@@ -191,15 +194,13 @@ def _apply_model_pk(**pkeys: dict[str, type]):
                     inspect.Parameter(
                         name=k,
                         annotation=Annotated[UUID, Path(...)],
-                        kind=inspect.Parameter.POSITIONAL_ONLY
+                        kind=inspect.Parameter.POSITIONAL_ONLY,
                     )
                 )
             else:
                 extra_positional_params.append(
                     inspect.Parameter(
-                        name=k,
-                        annotation=v,
-                        kind=inspect.Parameter.POSITIONAL_ONLY
+                        name=k, annotation=v, kind=inspect.Parameter.POSITIONAL_ONLY
                     )
                 )
 
@@ -223,7 +224,14 @@ def _create_dynamic_filters(
         filtered_params = {}
         for key, value in kwargs.items():
             if value is not None:
-                filtered_params[key] = value
+                parse_func = column_types.get(key)
+                if parse_func:
+                    try:
+                        filtered_params[key] = parse_func(value)
+                    except (ValueError, TypeError):
+                        filtered_params[key] = value
+                else:
+                    filtered_params[key] = value
         return filtered_params
 
     params = []
diff --git a/fastcrud/types.py b/fastcrud/types.py
index 548af9c..24af6ac 100644
--- a/fastcrud/types.py
+++ b/fastcrud/types.py
@@ -1,4 +1,4 @@
-from typing import TypeVar, Any
+from typing import TypeVar, Any, Dict, Union, List
 
 from pydantic import BaseModel
 
@@ -9,3 +9,6 @@ CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
 UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
 UpdateSchemaInternalType = TypeVar("UpdateSchemaInternalType", bound=BaseModel)
 DeleteSchemaType = TypeVar("DeleteSchemaType", bound=BaseModel)
+
+GetMultiResponseDict = Dict[str, Union[List[Dict[str, Any]], int]]
+GetMultiResponseModel = Dict[str, Union[List[SelectSchemaType], int]]


Test patch:
diff --git a/tests/sqlalchemy/core/test_uuid.py b/tests/sqlalchemy/core/test_uuid.py
index ca6a439..8c4028b 100644
--- a/tests/sqlalchemy/core/test_uuid.py
+++ b/tests/sqlalchemy/core/test_uuid.py
@@ -8,6 +8,8 @@ from fastapi import FastAPI
 from fastapi.testclient import TestClient
 
 from fastcrud import crud_router, FastCRUD
+from fastcrud import FilterConfig
+from fastcrud.endpoint.helper import _create_dynamic_filters
 from pydantic import BaseModel
 
 from ..conftest import Base
@@ -202,3 +204,43 @@ async def test_uuid_list_endpoint(uuid_client):
             UUID(item["id"])
         except ValueError:  # pragma: no cover
             pytest.fail("Invalid UUID format in list response")
+
+
+def test_create_dynamic_filters_type_conversion():
+    filter_config = FilterConfig(uuid_field=None, int_field=None, str_field=None)
+    column_types = {
+        "uuid_field": UUID,
+        "int_field": int,
+        "str_field": str,
+    }
+
+    filters_func = _create_dynamic_filters(filter_config, column_types)
+
+    test_uuid = "123e4567-e89b-12d3-a456-426614174000"
+    result = filters_func(uuid_field=test_uuid, int_field="123", str_field=456)
+
+    assert isinstance(result["uuid_field"], UUID)
+    assert result["uuid_field"] == UUID(test_uuid)
+    assert isinstance(result["int_field"], int)
+    assert result["int_field"] == 123
+    assert isinstance(result["str_field"], str)
+    assert result["str_field"] == "456"
+
+    result = filters_func(
+        uuid_field="not-a-uuid", int_field="not-an-int", str_field=456
+    )
+
+    assert result["uuid_field"] == "not-a-uuid"
+    assert result["int_field"] == "not-an-int"
+    assert isinstance(result["str_field"], str)
+
+    result = filters_func(uuid_field=None, int_field="123", str_field=None)
+    assert "uuid_field" not in result
+    assert result["int_field"] == 123
+    assert "str_field" not in result
+
+    result = filters_func(unknown_field="test")
+    assert result["unknown_field"] == "test"
+
+    empty_filters_func = _create_dynamic_filters(None, {})
+    assert empty_filters_func() == {}
diff --git a/tests/sqlalchemy/crud/test_update.py b/tests/sqlalchemy/crud/test_update.py
index 9680320..8997a82 100644
--- a/tests/sqlalchemy/crud/test_update.py
+++ b/tests/sqlalchemy/crud/test_update.py
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
 import pytest
 
 from sqlalchemy import select
-from sqlalchemy.exc import MultipleResultsFound
+from sqlalchemy.exc import MultipleResultsFound, NoResultFound
 
 from fastcrud.crud.fast_crud import FastCRUD
 from ...sqlalchemy.conftest import ModelTest, UpdateSchemaTest, ModelTestWithTimestamp
@@ -51,24 +51,11 @@ async def test_update_non_existent_record(async_session, test_data):
     crud = FastCRUD(ModelTest)
     non_existent_id = 99999
     updated_data = {"name": "New Name"}
-    """
-    In version 0.15.3, the `update` method will raise a `NoResultFound` exception:
 
-    ```
     with pytest.raises(NoResultFound) as exc_info:
         await crud.update(db=async_session, object=updated_data, id=non_existent_id)
 
     assert "No record found to update" in str(exc_info.value)
-    ```
-
-    For 0.15.2, the test will check if the record is not updated.
-    """
-    await crud.update(db=async_session, object=updated_data, id=non_existent_id)
-
-    record = await async_session.execute(
-        select(ModelTest).where(ModelTest.id == non_existent_id)
-    )
-    assert record.scalar_one_or_none() is None
 
 
 @pytest.mark.asyncio
@@ -81,26 +68,10 @@ async def test_update_invalid_filters(async_session, test_data):
     updated_data = {"name": "New Name"}
 
     non_matching_filter = {"name": "NonExistingName"}
-    """
-    In version 0.15.3, the `update` method will raise a `NoResultFound` exception:
-
-    ```
     with pytest.raises(NoResultFound) as exc_info:
         await crud.update(db=async_session, object=updated_data, **non_matching_filter)
 
     assert "No record found to update" in str(exc_info.value)
-    ```
-
-    For 0.15.2, the test will check if the record is not updated.
-    """
-    await crud.update(db=async_session, object=updated_data, **non_matching_filter)
-
-    for item in test_data:
-        record = await async_session.execute(
-            select(ModelTest).where(ModelTest.id == item["id"])
-        )
-        fetched_record = record.scalar_one()
-        assert fetched_record.name != "New Name"
 
 
 @pytest.mark.asyncio
diff --git a/tests/sqlmodel/core/test_uuid.py b/tests/sqlmodel/core/test_uuid.py
index 0378305..cf525a8 100644
--- a/tests/sqlmodel/core/test_uuid.py
+++ b/tests/sqlmodel/core/test_uuid.py
@@ -9,6 +9,8 @@ from fastapi.testclient import TestClient
 from sqlmodel import Field, SQLModel
 
 from fastcrud import crud_router, FastCRUD
+from fastcrud import FilterConfig
+from fastcrud.endpoint.helper import _create_dynamic_filters
 from pydantic import ConfigDict
 
 
@@ -204,3 +206,43 @@ async def test_uuid_list_endpoint(uuid_client):
             UUID(item["id"])
         except ValueError:  # pragma: no cover
             pytest.fail("Invalid UUID format in list response")
+
+
+def test_create_dynamic_filters_type_conversion():
+    filter_config = FilterConfig(uuid_field=None, int_field=None, str_field=None)
+    column_types = {
+        "uuid_field": UUID,
+        "int_field": int,
+        "str_field": str,
+    }
+
+    filters_func = _create_dynamic_filters(filter_config, column_types)
+
+    test_uuid = "123e4567-e89b-12d3-a456-426614174000"
+    result = filters_func(uuid_field=test_uuid, int_field="123", str_field=456)
+
+    assert isinstance(result["uuid_field"], UUID)
+    assert result["uuid_field"] == UUID(test_uuid)
+    assert isinstance(result["int_field"], int)
+    assert result["int_field"] == 123
+    assert isinstance(result["str_field"], str)
+    assert result["str_field"] == "456"
+
+    result = filters_func(
+        uuid_field="not-a-uuid", int_field="not-an-int", str_field=456
+    )
+
+    assert result["uuid_field"] == "not-a-uuid"
+    assert result["int_field"] == "not-an-int"
+    assert isinstance(result["str_field"], str)
+
+    result = filters_func(uuid_field=None, int_field="123", str_field=None)
+    assert "uuid_field" not in result
+    assert result["int_field"] == 123
+    assert "str_field" not in result
+
+    result = filters_func(unknown_field="test")
+    assert result["unknown_field"] == "test"
+
+    empty_filters_func = _create_dynamic_filters(None, {})
+    assert empty_filters_func() == {}
diff --git a/tests/sqlmodel/crud/test_update.py b/tests/sqlmodel/crud/test_update.py
index 2968674..0209f05 100644
--- a/tests/sqlmodel/crud/test_update.py
+++ b/tests/sqlmodel/crud/test_update.py
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
 import pytest
 
 from sqlalchemy import select
-from sqlalchemy.exc import MultipleResultsFound
+from sqlalchemy.exc import MultipleResultsFound, NoResultFound
 
 from fastcrud.crud.fast_crud import FastCRUD
 from ...sqlmodel.conftest import ModelTest, UpdateSchemaTest, ModelTestWithTimestamp
@@ -51,24 +51,11 @@ async def test_update_non_existent_record(async_session, test_data):
     crud = FastCRUD(ModelTest)
     non_existent_id = 99999
     updated_data = {"name": "New Name"}
-    """
-    In version 0.15.3, the `update` method will raise a `NoResultFound` exception:
 
-    ```
     with pytest.raises(NoResultFound) as exc_info:
         await crud.update(db=async_session, object=updated_data, id=non_existent_id)
 
     assert "No record found to update" in str(exc_info.value)
-    ```
-
-    For 0.15.2, the test will check if the record is not updated.
-    """
-    await crud.update(db=async_session, object=updated_data, id=non_existent_id)
-
-    record = await async_session.execute(
-        select(ModelTest).where(ModelTest.id == non_existent_id)
-    )
-    assert record.scalar_one_or_none() is None
 
 
 @pytest.mark.asyncio
@@ -81,26 +68,11 @@ async def test_update_invalid_filters(async_session, test_data):
     updated_data = {"name": "New Name"}
 
     non_matching_filter = {"name": "NonExistingName"}
-    """
-    In version 0.15.3, the `update` method will raise a `NoResultFound` exception:
 
-    ```
     with pytest.raises(NoResultFound) as exc_info:
         await crud.update(db=async_session, object=updated_data, **non_matching_filter)
 
     assert "No record found to update" in str(exc_info.value)
-    ```
-
-    For 0.15.2, the test will check if the record is not updated.
-    """
-    await crud.update(db=async_session, object=updated_data, **non_matching_filter)
-
-    for item in test_data:
-        record = await async_session.execute(
-            select(ModelTest).where(ModelTest.id == item["id"])
-        )
-        fetched_record = record.scalar_one()
-        assert fetched_record.name != "New Name"
 
 
 @pytest.mark.asyncio