Type safety filtering and ordering for SQLAlchemy
Project description
SQLAlchemy Filter
Package for convenient filtering functionality in SQLAlchemy
Quite often, optional filtering functionality is required. To facilitate the implementation of filtering functionality, this package was written
Filter example
# db/models.py
class Review(Base):
__tablename__ = "review"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
content: Mapped[str]
book_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("book.id"))
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user.id"))
class Author(Base):
__tablename__ = "author"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
class BookAuthor(Base):
__tablename__ = "book__author"
book_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("book.id"), primary_key=True)
author_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("author.id"),
primary_key=True,
)
class Book(Base):
__tablename__ = "book"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
authors: Mapped[list[Author]] = relationship(secondary=BookAuthor.__table__)
reviews: Mapped[list[Review]] = relationship()
created_at: Mapped[datetime]
This is how it was before:
# core/domain/book/dto.py
@dataclass(frozen=True, slots=True)
class BookFilter:
ident: UUID | None = None
created_at_from: datetime | None = None
created_at_to: datetime | None = None
author_ids: Sequence[UUID] | None = None
review_ids: Sequence[UUID] | None = None
review_content_contains: str | None = None
# core/domain/book/repository.py
class BookRepository:
...
async def get_list(self, filter_: BookFilter) -> Sequence[Book]:
stmt = select(Book)
if filter_.ident is not None:
stmt = stmt.where(Book.id == filter_.ident)
if filter_.created_at_from is not None:
stmt = stmt.where(Book.created_at >= filter_.created_at_from)
if filter_.created_at_to is not None:
stmt = stmt.where(Book.created_at <= filter_.created_at_to)
if filter_.author_ids is not None:
stmt = stmt.join(Book.authors).where(Author.id.in_(filter_.author_ids))
if filter_.review_ids is not None:
stmt = stmt.join(Book.reviews).where(Review.id.in_(filter_.review_ids))
return (await self._session.scalars(stmt)).all()
And here's how after using the package:
# core/domain/book/dto.py
from sqlalchemy.sql.operators import eq, ge, icontains_op, in_op, le
from sqla_filter import (
BaseFilter,
Unset,
UNSET,
FilterField,
RelationshipInfo,
)
class BookFilter(BaseFilter):
ident: Annotated[UUID | Unset, FilterField(Book.id, operator=eq)] = UNSET
created_at_from: Annotated[
datetime | Unset,
FilterField(Book.created_at, operator=ge),
] = UNSET
created_at_to: Annotated[
datetime | Unset,
FilterField(Book.created_at, operator=le),
] = UNSET
author_ids: Annotated[
Sequence[UUID] | Unset,
FilterField(
Author.id,
operator=in_op,
relationship=RelationshipInfo(field=Book.authors),
),
] = UNSET
review_ids: Annotated[
list[UUID] | Unset,
FilterField(
Review.id,
operator=in_op,
relationship=RelationshipInfo(field=Book.reviews),
),
] = UNSET
review_content_contains: Annotated[
str | Unset,
FilterField(
Review.content,
operator=icontains_op,
relationship=RelationshipInfo(field=Book.reviews),
),
] = UNSET
author_user_id: Annotated[
UUID | Unset,
FilterField(
User.id,
operator=eq,
relationships=[
RelationshipInfo(field=Book.authors),
RelationshipInfo(field=Author.user),
],
),
] = UNSET
# core/domain/book/repository.py
class BookRepository:
...
async def get_list(self, filter_: BookFilter) -> Sequence[Book]:
stmt = select(Book)
stmt = filter_.apply(stmt)
return (await self._session.scalars(stmt)).all()
Ordering example
from typing import Annotated, Any
from sqlalchemy import Select, select
from examples.models import Author, Book, User
from sqla_filter import (
UNSET,
BaseSorter,
OrderingEnum,
OrderingField,
RelationshipInfo,
Unset,
)
from sqla_filter.ordering import build_priorities
class BookSorter(BaseSorter):
created_at: Annotated[OrderingEnum | Unset, OrderingField(Book.created_at)] = UNSET
author_user_first_name: Annotated[
OrderingEnum | Unset,
OrderingField(
User.first_name,
relationships=[
RelationshipInfo(field=Book.authors),
RelationshipInfo(field=Author.user),
],
),
] = UNSET
author_user_last_name: Annotated[
OrderingEnum | Unset,
OrderingField(
User.last_name,
relationships=[
RelationshipInfo(field=Book.authors),
RelationshipInfo(field=Author.user),
],
),
] = UNSET
author_user_last_name_nulls_last: Annotated[
OrderingEnum | Unset,
OrderingField(
User.last_name,
relationships=[
RelationshipInfo(field=Book.authors),
RelationshipInfo(field=Author.user),
],
),
nulls="last",
] = UNSET
author_user_is_deleted: Annotated[
OrderingEnum | Unset,
OrderingField(
User.is_deleted,
relationships=[
RelationshipInfo(field=Book.authors),
RelationshipInfo(field=Author.user),
],
),
] = UNSET
def main() -> None:
stmt = select(Book)
print_stmt(stmt)
sorter = BookSorter(
author_user_last_name=OrderingEnum.desc,
created_at=OrderingEnum.desc,
author_user_is_deleted=OrderingEnum.desc,
)
stmt = sorter.apply(
stmt,
fields_priority=build_priorities(
BookSorter.author_user_is_deleted,
BookSorter.author_user_last_name,
),
)
print_stmt(stmt)
def print_stmt(stmt: Select[tuple[Any, ...]]) -> None:
from sqlalchemy.dialects import postgresql
print( # noqa: T201
stmt.compile(
dialect=postgresql.dialect(), # type: ignore[no-untyped-call]
compile_kwargs={
"literal_binds": True,
},
),
"\n",
)
if __name__ == "__main__":
main()
Important note: Only pass fields of a class that extends the BaseSorter class to build_priorities.
If the field is set to "NOT_SET", the filter will not be applied.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqla_filter-0.3.2.tar.gz.
File metadata
- Download URL: sqla_filter-0.3.2.tar.gz
- Upload date:
- Size: 10.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: pdm/2.22.0 CPython/3.11.9 Windows/10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0cc0c0daee07dcf43826ad9486366f6105dbf66ba28b77831cad50770d61b315
|
|
| MD5 |
f56b20781bbb4ce1b6c2a5649e22f486
|
|
| BLAKE2b-256 |
ed864e381b32f1472887f50dd0ca525d4a6e37e530ffbe6bb6ca0d7e9b0972e8
|
File details
Details for the file sqla_filter-0.3.2-py3-none-any.whl.
File metadata
- Download URL: sqla_filter-0.3.2-py3-none-any.whl
- Upload date:
- Size: 9.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: pdm/2.22.0 CPython/3.11.9 Windows/10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3638f8bd4136ec938ade59e5c1a2680bbc10d2d7ef6388fb38a5ca4a04f2735c
|
|
| MD5 |
e0fe1c9511b2ae598348fb71c0b6146f
|
|
| BLAKE2b-256 |
8c3ac97ee1e72b587eef5124da6628d1d1d6f88611f319e592b96a420f0aab98
|