Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

- MongoEngine: >=0.24.1, <0.28.0
- SQLAlchemy: >=1.4.36, <2.1.0
- tortoise-orm: >=0.22.1

## Installation

Expand All @@ -30,6 +31,7 @@ pip install fastapi-filter[all]
# More selective
pip install fastapi-filter[sqlalchemy]
pip install fastapi-filter[mongoengine]
pip install fastapi-filter[tortoise-orm]
```

## Documentation
Expand Down
15 changes: 15 additions & 0 deletions examples/fastapi_filter_tortoise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TODO
import logging
from collections.abc import AsyncIterator
from typing import Any, Optional

import click
import uvicorn
from faker import Faker
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel, ConfigDict, Field

from fastapi_filter import FilterDepends, with_prefix
from fastapi_filter.contrib.tortoise import Filter

logger = logging.getLogger("uvicorn")
3 changes: 3 additions & 0 deletions fastapi_filter/contrib/tortoise/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .filter import Filter

__all__ = ("Filter",)
110 changes: 110 additions & 0 deletions fastapi_filter/contrib/tortoise/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from functools import reduce
from typing import Union
from warnings import warn
from operator import or_

from pydantic import ValidationInfo, field_validator
from tortoise.queryset import QuerySet, Q


from ...base.filter import BaseFilterModel




_orm_operator_transformer = {
"neq": lambda value: ("__not", value),
"gt": lambda value: ("__gt", value),
"gte": lambda value: ("__gte", value),
"in": lambda value: ("__in", value),
"isnull": lambda value: ("__isnull", True),
"lt": lambda value: ("__lt", value),
"lte": lambda value: ("__lte", value),
"like": lambda value: ("__contains", value),
"ilike": lambda value: ("__icontains", value),
"not": lambda value: ("__not", value),
"not_in": lambda value: ("__not_in", value),
}
"""Operators à la Django.

Examples:
my_datetime__gte
count__lt
name__isnull
user_id__in
"""


class Filter(BaseFilterModel):
"""Base filter for orm related filters.

All children must set:
```python
class Constants(Filter.Constants):
model = MyModel
```

It can handle regular field names and Django style operators.

Example:
```python
class MyModel:
id: PrimaryKey()
name: StringField(nullable=True)
count: IntegerField()
created_at: DatetimeField()

class MyModelFilter(Filter):
id: Optional[int]
id__in: Optional[str]
count: Optional[int]
count__lte: Optional[int]
created_at__gt: Optional[datetime]
name__isnull: Optional[bool]
"""

@field_validator("*", mode="before")
def split_str(cls, value, field: ValidationInfo):
if (
field.field_name is not None
and (
field.field_name == cls.Constants.ordering_field_name
or field.field_name.endswith("__in")
or field.field_name.endswith("__not_in")
)
and isinstance(value, str)
):
if not value:
# Empty string should return [] not ['']
return []
return list(value.split(","))
return value

def filter(self, query: QuerySet):
for field_name, value in self.filtering_fields:
field_value = getattr(self, field_name)
if isinstance(field_value, Filter):
query = field_value.filter(query)
else:
if "__" in field_name:
field_name, operator = field_name.split("__")
operator, value = _orm_operator_transformer[operator](value)
else:
operator = ""

if field_name == self.Constants.search_field_name and hasattr(self.Constants, "search_model_fields"):
search_filters = [
{f'{field}__icontains': value}
for field in self.Constants.search_model_fields
]
query = query.filter(reduce(or_, [Q(**filt) for filt in search_filters]))
else:
query = query.filter(**{f'{field_name}{operator}': value})

return query

def sort(self, query: QuerySet):
if not self.ordering_values:
return query

return query.order_by(*self.ordering_values)
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ classifiers = [
SQLAlchemy = {version = ">=1.4.36,<2.1.0", optional = true}
fastapi = ">=0.100.0,<1.0"
mongoengine = {version = ">=0.24.1,<0.28.0", optional = true}
tortoise-orm = {version = ">=0.22.1", optional = true}
pydantic = ">=2.0.0,<3.0.0"
python = ">=3.9,<4.0"

Expand Down Expand Up @@ -145,7 +146,8 @@ pydantic = {extras = ["email"], version="^2.7.1"}
[tool.poetry.extras]
mongoengine = ["mongoengine"]
sqlalchemy = ["SQLAlchemy"]
all = ["mongoengine", "SQLAlchemy"]
tortoise-orm = ["tortoise-orm"]
all = ["mongoengine", "SQLAlchemy", "tortoise-orm"]

[tool.poetry.group.dev.dependencies]
nox = "^2024.0.0"
Expand Down
Empty file added tests/test_tortoise/__init__.py
Empty file.
Loading