mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-04-16 06:36:26 +02:00
Compare commits
6 Commits
74d15e13bc
...
v3.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
0ed93d62c8
|
|||
|
|
2a49814818 | ||
|
|
f8e090c7c3 | ||
|
|
54decaf3e1 | ||
|
6b127d9645
|
|||
|
|
8bed96f4bf |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -6,6 +6,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "3.0.1"
|
version = "3.0.3"
|
||||||
description = "Production-ready utilities for FastAPI applications"
|
description = "Production-ready utilities for FastAPI applications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ Example usage:
|
|||||||
return Response(data={"user": user.username}, message="Success")
|
return Response(data={"user": user.username}, message="Success")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "3.0.1"
|
__version__ = "3.0.3"
|
||||||
|
|||||||
@@ -170,6 +170,18 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
return load_options
|
return load_options
|
||||||
return cls.default_load_options
|
return cls.default_load_options
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _reload_with_options(
|
||||||
|
cls: type[Self], session: AsyncSession, instance: ModelType
|
||||||
|
) -> ModelType:
|
||||||
|
"""Re-query instance by PK with default_load_options applied."""
|
||||||
|
mapper = cls.model.__mapper__
|
||||||
|
pk_filters = [
|
||||||
|
getattr(cls.model, col.key) == getattr(instance, col.key)
|
||||||
|
for col in mapper.primary_key
|
||||||
|
]
|
||||||
|
return await cls.get(session, filters=pk_filters)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _resolve_m2m(
|
async def _resolve_m2m(
|
||||||
cls: type[Self],
|
cls: type[Self],
|
||||||
@@ -705,6 +717,8 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
|
|
||||||
session.add(db_model)
|
session.add(db_model)
|
||||||
await session.refresh(db_model)
|
await session.refresh(db_model)
|
||||||
|
if cls.default_load_options:
|
||||||
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
result = cast(ModelType, db_model)
|
result = cast(ModelType, db_model)
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(result))
|
return Response(data=schema.model_validate(result))
|
||||||
@@ -1060,6 +1074,8 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
for rel_attr, related_instances in m2m_resolved.items():
|
for rel_attr, related_instances in m2m_resolved.items():
|
||||||
setattr(db_model, rel_attr, related_instances)
|
setattr(db_model, rel_attr, related_instances)
|
||||||
await session.refresh(db_model)
|
await session.refresh(db_model)
|
||||||
|
if cls.default_load_options:
|
||||||
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(db_model))
|
return Response(data=schema.model_validate(db_model))
|
||||||
return db_model
|
return db_model
|
||||||
|
|||||||
@@ -265,7 +265,15 @@ async def build_facets(
|
|||||||
else:
|
else:
|
||||||
q = q.order_by(column)
|
q = q.order_by(column)
|
||||||
result = await session.execute(q)
|
result = await session.execute(q)
|
||||||
values = [row[0] for row in result.all() if row[0] is not None]
|
col_type = column.property.columns[0].type
|
||||||
|
enum_class = getattr(col_type, "enum_class", None)
|
||||||
|
values = [
|
||||||
|
row[0].name
|
||||||
|
if (enum_class is not None and isinstance(row[0], enum_class))
|
||||||
|
else row[0]
|
||||||
|
for row in result.all()
|
||||||
|
if row[0] is not None
|
||||||
|
]
|
||||||
return key, values
|
return key, values
|
||||||
|
|
||||||
pairs = await asyncio.gather(
|
pairs = await asyncio.gather(
|
||||||
@@ -347,6 +355,24 @@ def build_filter_by(
|
|||||||
filters.append(column.overlap(value))
|
filters.append(column.overlap(value))
|
||||||
else:
|
else:
|
||||||
filters.append(column.any(value))
|
filters.append(column.any(value))
|
||||||
|
elif isinstance(col_type, Enum):
|
||||||
|
enum_class = col_type.enum_class
|
||||||
|
if enum_class is not None:
|
||||||
|
|
||||||
|
def _coerce_enum(v: Any) -> Any:
|
||||||
|
if isinstance(v, enum_class):
|
||||||
|
return v
|
||||||
|
return enum_class[v] # lookup by name: "PENDING", "RED"
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
filters.append(column.in_([_coerce_enum(v) for v in value]))
|
||||||
|
else:
|
||||||
|
filters.append(column == _coerce_enum(value))
|
||||||
|
else: # pragma: no cover
|
||||||
|
if isinstance(value, list):
|
||||||
|
filters.append(column.in_(value))
|
||||||
|
else:
|
||||||
|
filters.append(column == value)
|
||||||
elif isinstance(col_type, _EQUALITY_TYPES):
|
elif isinstance(col_type, _EQUALITY_TYPES):
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
filters.append(column.in_(value))
|
filters.append(column.in_(value))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -12,6 +13,7 @@ from sqlalchemy import (
|
|||||||
Column,
|
Column,
|
||||||
Date,
|
Date,
|
||||||
DateTime,
|
DateTime,
|
||||||
|
Enum as SAEnum,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
JSON,
|
JSON,
|
||||||
@@ -139,6 +141,35 @@ class Post(Base):
|
|||||||
tags: Mapped[list[Tag]] = relationship(secondary=post_tags)
|
tags: Mapped[list[Tag]] = relationship(secondary=post_tags)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatus(int, Enum):
|
||||||
|
"""Integer-backed enum for order status."""
|
||||||
|
|
||||||
|
PENDING = 1
|
||||||
|
PROCESSING = 2
|
||||||
|
SHIPPED = 3
|
||||||
|
CANCELLED = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Color(str, Enum):
|
||||||
|
"""String-backed enum for color."""
|
||||||
|
|
||||||
|
RED = "red"
|
||||||
|
GREEN = "green"
|
||||||
|
BLUE = "blue"
|
||||||
|
|
||||||
|
|
||||||
|
class Order(Base):
|
||||||
|
"""Test model with an IntEnum column (Enum(int, Enum)) and a raw Integer column."""
|
||||||
|
|
||||||
|
__tablename__ = "orders"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
|
||||||
|
name: Mapped[str] = mapped_column(String(100))
|
||||||
|
status: Mapped[OrderStatus] = mapped_column(SAEnum(OrderStatus))
|
||||||
|
priority: Mapped[int] = mapped_column(Integer)
|
||||||
|
color: Mapped[Color] = mapped_column(SAEnum(Color))
|
||||||
|
|
||||||
|
|
||||||
class Transfer(Base):
|
class Transfer(Base):
|
||||||
"""Test model with two FKs to the same table (users)."""
|
"""Test model with two FKs to the same table (users)."""
|
||||||
|
|
||||||
@@ -311,6 +342,26 @@ class ArticleRead(PydanticBase):
|
|||||||
labels: list[str]
|
labels: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCreate(BaseModel):
|
||||||
|
"""Schema for creating an order."""
|
||||||
|
|
||||||
|
id: uuid.UUID | None = None
|
||||||
|
name: str
|
||||||
|
status: OrderStatus
|
||||||
|
priority: int = 0
|
||||||
|
color: Color = Color.RED
|
||||||
|
|
||||||
|
|
||||||
|
class OrderRead(PydanticBase):
|
||||||
|
"""Schema for reading an order."""
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
status: OrderStatus
|
||||||
|
priority: int
|
||||||
|
color: Color
|
||||||
|
|
||||||
|
|
||||||
class TransferCreate(BaseModel):
|
class TransferCreate(BaseModel):
|
||||||
"""Schema for creating a transfer."""
|
"""Schema for creating a transfer."""
|
||||||
|
|
||||||
@@ -327,6 +378,7 @@ class TransferRead(PydanticBase):
|
|||||||
amount: str
|
amount: str
|
||||||
|
|
||||||
|
|
||||||
|
OrderCrud = CrudFactory(Order)
|
||||||
TransferCrud = CrudFactory(Transfer)
|
TransferCrud = CrudFactory(Transfer)
|
||||||
ArticleCrud = CrudFactory(Article)
|
ArticleCrud = CrudFactory(Article)
|
||||||
RoleCrud = CrudFactory(Role)
|
RoleCrud = CrudFactory(Role)
|
||||||
|
|||||||
@@ -380,6 +380,43 @@ class TestDefaultLoadOptionsIntegration:
|
|||||||
assert result.data[0].role is not None
|
assert result.data[0].role is not None
|
||||||
assert result.data[0].role.name == "admin"
|
assert result.data[0].role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_default_load_options_applied_to_create(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""default_load_options loads relationships after create()."""
|
||||||
|
UserWithDefaultLoad = CrudFactory(
|
||||||
|
User, default_load_options=[selectinload(User.role)]
|
||||||
|
)
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
user = await UserWithDefaultLoad.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="alice", email="alice@test.com", role_id=role.id),
|
||||||
|
)
|
||||||
|
assert user.role is not None
|
||||||
|
assert user.role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_default_load_options_applied_to_update(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""default_load_options loads relationships after update()."""
|
||||||
|
UserWithDefaultLoad = CrudFactory(
|
||||||
|
User, default_load_options=[selectinload(User.role)]
|
||||||
|
)
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
user = await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="alice", email="alice@test.com"),
|
||||||
|
)
|
||||||
|
updated = await UserWithDefaultLoad.update(
|
||||||
|
db_session,
|
||||||
|
UserUpdate(role_id=role.id),
|
||||||
|
filters=[User.id == user.id],
|
||||||
|
)
|
||||||
|
assert updated.role is not None
|
||||||
|
assert updated.role.name == "admin"
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_load_options_overrides_default_load_options(
|
async def test_load_options_overrides_default_load_options(
|
||||||
self, db_session: AsyncSession
|
self, db_session: AsyncSession
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ from .conftest import (
|
|||||||
ArticleCreate,
|
ArticleCreate,
|
||||||
ArticleCrud,
|
ArticleCrud,
|
||||||
ArticleRead,
|
ArticleRead,
|
||||||
|
Color,
|
||||||
|
Order,
|
||||||
|
OrderCreate,
|
||||||
|
OrderCrud,
|
||||||
|
OrderRead,
|
||||||
|
OrderStatus,
|
||||||
Role,
|
Role,
|
||||||
RoleCreate,
|
RoleCreate,
|
||||||
RoleCrud,
|
RoleCrud,
|
||||||
@@ -1121,6 +1127,253 @@ class TestFilterBy:
|
|||||||
assert "JSON" in exc_info.value.col_type
|
assert "JSON" in exc_info.value.col_type
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterByIntEnum:
|
||||||
|
"""Tests for filter_by on columns typed as (int, Enum) / IntEnum."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_intenum_member(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with an IntEnum member value filters correctly."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-1", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-2", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-3", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"status": OrderStatus.PENDING},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 2
|
||||||
|
names = {o.name for o in result.data}
|
||||||
|
assert names == {"order-1", "order-3"}
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_plain_int_value_raises(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a plain int on an IntEnum column raises KeyError — use name or member."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"status": 1},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_intenum_list(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a list of IntEnum members produces an IN filter."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-1", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-2", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-3", status=OrderStatus.CANCELLED)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"status": [OrderStatus.PENDING, OrderStatus.SHIPPED]},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 2
|
||||||
|
names = {o.name for o in result.data}
|
||||||
|
assert names == {"order-1", "order-2"}
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_plain_int_list_raises(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a list of plain ints on an IntEnum column raises KeyError — use names or members."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"status": [1, 3]},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_intenum_name_string(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with the enum member name as a string filters correctly."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-1", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-2", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={
|
||||||
|
"status": "PENDING"
|
||||||
|
}, # name as string, e.g. from HTTP query param
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].name == "order-1"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_intenum_name_string_list(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a list of enum name strings produces an IN filter."""
|
||||||
|
OrderFacetCrud = CrudFactory(Order, facet_fields=[Order.status])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-1", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-2", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="order-3", status=OrderStatus.CANCELLED)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderFacetCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"status": ["PENDING", "SHIPPED"]},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 2
|
||||||
|
names = {o.name for o in result.data}
|
||||||
|
assert names == {"order-1", "order-2"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterByStrEnum:
|
||||||
|
"""Tests for filter_by on columns typed as (str, Enum) / StrEnum (lines 364-367)."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_strenum_member(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a StrEnum member on a string Enum column filters correctly."""
|
||||||
|
OrderColorCrud = CrudFactory(Order, facet_fields=[Order.color])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="red-order", status=OrderStatus.PENDING, color=Color.RED),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(
|
||||||
|
name="blue-order", status=OrderStatus.PENDING, color=Color.BLUE
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderColorCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"color": Color.RED},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].name == "red-order"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_strenum_list(self, db_session: AsyncSession):
|
||||||
|
"""filter_by with a list of StrEnum members produces an IN filter."""
|
||||||
|
OrderColorCrud = CrudFactory(Order, facet_fields=[Order.color])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="red-order", status=OrderStatus.PENDING, color=Color.RED),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(
|
||||||
|
name="green-order", status=OrderStatus.PENDING, color=Color.GREEN
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(
|
||||||
|
name="blue-order", status=OrderStatus.PENDING, color=Color.BLUE
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderColorCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"color": [Color.RED, Color.BLUE]},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 2
|
||||||
|
names = {o.name for o in result.data}
|
||||||
|
assert names == {"red-order", "blue-order"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterByIntegerColumn:
|
||||||
|
"""Tests for filter_by on plain Integer columns with IntEnum values."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_integer_column_with_intenum_member(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""filter_by with an IntEnum member on an Integer column works correctly."""
|
||||||
|
OrderPriorityCrud = CrudFactory(Order, facet_fields=[Order.priority])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(
|
||||||
|
name="order-1", status=OrderStatus.PENDING, priority=OrderStatus.PENDING
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(
|
||||||
|
name="order-2", status=OrderStatus.SHIPPED, priority=OrderStatus.SHIPPED
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderPriorityCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={
|
||||||
|
"priority": OrderStatus.PENDING
|
||||||
|
}, # IntEnum member on Integer col
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].name == "order-1"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_by_integer_column_with_plain_int(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""filter_by with a plain int on an Integer column works correctly."""
|
||||||
|
OrderPriorityCrud = CrudFactory(Order, facet_fields=[Order.priority])
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="order-1", status=OrderStatus.PENDING, priority=1),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="order-2", status=OrderStatus.SHIPPED, priority=3),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderPriorityCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
filter_by={"priority": 1},
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result.pagination, OffsetPagination)
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].name == "order-1"
|
||||||
|
|
||||||
|
|
||||||
class TestFilterParamsViaConsolidated:
|
class TestFilterParamsViaConsolidated:
|
||||||
"""Tests for filter params via consolidated offset_paginate_params()."""
|
"""Tests for filter params via consolidated offset_paginate_params()."""
|
||||||
|
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -251,7 +251,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "3.0.1"
|
version = "3.0.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
|
|||||||
Reference in New Issue
Block a user