From 0cab115fb601af80c4c61de4ee4521223bd6abaf Mon Sep 17 00:00:00 2001 From: Akash Patel Date: Fri, 26 May 2023 10:15:56 +0530 Subject: [PATCH] added contains express support --- .gitignore | 1 + docs/index.md | 1 + examples/fastapi_filter_sqlalchemy.py | 1 + fastapi_filter/contrib/sqlalchemy/filter.py | 1 + tests/test_sqlalchemy/conftest.py | 1 + tests/test_sqlalchemy/test_filter.py | 7 ++++++- 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d831690e..633d004d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ fastapi_filter.sqlite poetry.toml .pytest_cache/ .ruff_cache/ +__pycache__ diff --git a/docs/index.md b/docs/index.md index dff7f32c..9f7a4223 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,7 @@ By default, **fastapi_filter** supports the following operators: - `not`/`ne` - `not_in`/`nin` - `like`/`ilike` + - `contains` _**Note:** Mysql excludes `None` values when using `in` filter_ diff --git a/examples/fastapi_filter_sqlalchemy.py b/examples/fastapi_filter_sqlalchemy.py index 822df56d..ee2f8289 100644 --- a/examples/fastapi_filter_sqlalchemy.py +++ b/examples/fastapi_filter_sqlalchemy.py @@ -95,6 +95,7 @@ class UserFilter(Filter): name: Optional[str] name__ilike: Optional[str] name__like: Optional[str] + name__contains: Optional[str] name__neq: Optional[str] address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter)) age__lt: Optional[int] diff --git a/fastapi_filter/contrib/sqlalchemy/filter.py b/fastapi_filter/contrib/sqlalchemy/filter.py index 01d54bdf..5804a6bf 100644 --- a/fastapi_filter/contrib/sqlalchemy/filter.py +++ b/fastapi_filter/contrib/sqlalchemy/filter.py @@ -44,6 +44,7 @@ def _backward_compatible_value_for_like_and_ilike(value: str): # XXX(arthurio): Mysql excludes None values when using `in` or `not in` filters. "not": lambda value: ("is_not", value), "not_in": lambda value: ("not_in", value), + "contains": lambda value: ("contains", value), } """Operators à la Django. diff --git a/tests/test_sqlalchemy/conftest.py b/tests/test_sqlalchemy/conftest.py index 17be0aa3..eaa137b9 100644 --- a/tests/test_sqlalchemy/conftest.py +++ b/tests/test_sqlalchemy/conftest.py @@ -265,6 +265,7 @@ class UserFilter(Filter): # type: ignore[misc, valid-type] name: Optional[str] name__neq: Optional[str] name__like: Optional[str] + name__contains: Optional[str] name__ilike: Optional[str] name__in: Optional[List[str]] name__not: Optional[str] diff --git a/tests/test_sqlalchemy/test_filter.py b/tests/test_sqlalchemy/test_filter.py index 691b4524..7b697023 100644 --- a/tests/test_sqlalchemy/test_filter.py +++ b/tests/test_sqlalchemy/test_filter.py @@ -9,6 +9,8 @@ "filter_,expected_count", [ [{"name": "Mr Praline"}, 1], + [{"name__contains": "Mr"}, 2], + [{"name__contains": "mR"}, 2], [{"name__neq": "Mr Praline"}, 4], [{"name__in": "Mr Praline,Mr Creosote,Gumbys,Knight"}, 3], [{"name__like": "%Mr%"}, 2], @@ -60,6 +62,8 @@ async def test_filter_deprecation_like_and_ilike(session, Address, User, UserFil "filter_,expected_count", [ [{"name": "Mr Praline"}, 1], + [{"name__contains": "Mr"}, 2], + [{"name__contains": "mR"}, 2], [{"name__in": "Mr Praline,Mr Creosote,Gumbys,Knight"}, 3], [{"name__isnull": True}, 1], [{"name__isnull": False}, 5], @@ -89,7 +93,8 @@ async def test_api(test_client, users, filter_, expected_count): [{"is_individual": False}, status.HTTP_200_OK], [{}, status.HTTP_422_UNPROCESSABLE_ENTITY], [{"is_individual": None}, status.HTTP_422_UNPROCESSABLE_ENTITY], - [{"is_individual": True, "bogus_filter": "bad"}, status.HTTP_422_UNPROCESSABLE_ENTITY], + [{"is_individual": True, "bogus_filter": "bad"}, + status.HTTP_422_UNPROCESSABLE_ENTITY], ], ) @pytest.mark.asyncio