Serverside sort and limit for vendor and filaments

This commit is contained in:
Donkie
2023-08-25 10:23:12 +02:00
parent b9f1b71c1f
commit 9d51ee49ed
6 changed files with 119 additions and 25 deletions

View File

@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.api.v1.models import Filament, Message from spoolman.api.v1.models import Filament, Message
from spoolman.database import filament from spoolman.database import filament
from spoolman.database.database import get_db_session from spoolman.database.database import get_db_session
from spoolman.database.utils import SortOrder
from spoolman.exceptions import ItemDeleteError from spoolman.exceptions import ItemDeleteError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -137,7 +138,31 @@ async def find(
title="Filament Article Number", title="Filament Article Number",
description="Partial case-insensitive search term for the filament article number.", description="Partial case-insensitive search term for the filament article number.",
), ),
sort: Optional[str] = Query(
default=None,
title="Sort",
description=(
'Sort the results by the given field. Should be a comma-separate string with "field:direction" items.'
),
example="vendor.name:asc,spool_weight:desc",
),
limit: Optional[int] = Query(
default=None,
title="Limit",
description="Maximum number of items in the response.",
),
offset: int = Query(
default=0,
title="Offset",
description="Offset in the full result set if a limit is set.",
),
) -> list[Filament]: ) -> list[Filament]:
sort_by: dict[str, SortOrder] = {}
if sort is not None:
for sort_item in sort.split(","):
field, direction = sort_item.split(":")
sort_by[field] = SortOrder[direction.upper()]
db_items = await filament.find( db_items = await filament.find(
db=db, db=db,
vendor_name=vendor_name, vendor_name=vendor_name,
@@ -145,6 +170,9 @@ async def find(
name=name, name=name,
material=material, material=material,
article_number=article_number, article_number=article_number,
sort_by=sort_by,
limit=limit,
offset=offset,
) )
return [Filament.from_db(db_item) for db_item in db_items] return [Filament.from_db(db_item) for db_item in db_items]

View File

@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.api.v1.models import Message, Vendor from spoolman.api.v1.models import Message, Vendor
from spoolman.database import vendor from spoolman.database import vendor
from spoolman.database.database import get_db_session from spoolman.database.database import get_db_session
from spoolman.database.utils import SortOrder
router = APIRouter( router = APIRouter(
prefix="/vendor", prefix="/vendor",
@@ -51,10 +52,37 @@ async def find(
title="Vendor Name", title="Vendor Name",
description="Partial case-insensitive search term for the vendor name.", description="Partial case-insensitive search term for the vendor name.",
), ),
sort: Optional[str] = Query(
default=None,
title="Sort",
description=(
'Sort the results by the given field. Should be a comma-separate string with "field:direction" items.'
),
example="name:asc,id:desc",
),
limit: Optional[int] = Query(
default=None,
title="Limit",
description="Maximum number of items in the response.",
),
offset: int = Query(
default=0,
title="Offset",
description="Offset in the full result set if a limit is set.",
),
) -> list[Vendor]: ) -> list[Vendor]:
sort_by: dict[str, SortOrder] = {}
if sort is not None:
for sort_item in sort.split(","):
field, direction = sort_item.split(":")
sort_by[field] = SortOrder[direction.upper()]
db_items = await vendor.find( db_items = await vendor.find(
db=db, db=db,
name=name, name=name,
sort_by=sort_by,
limit=limit,
offset=offset,
) )
return [Vendor.from_db(db_item) for db_item in db_items] return [Vendor.from_db(db_item) for db_item in db_items]

View File

@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
from spoolman.database import models, vendor from spoolman.database import models, vendor
from spoolman.database.utils import SortOrder, parse_nested_field
from spoolman.exceptions import ItemDeleteError, ItemNotFoundError from spoolman.exceptions import ItemDeleteError, ItemNotFoundError
@@ -65,7 +66,7 @@ async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament:
return filament return filament
async def find( async def find( # noqa: C901
*, *,
db: AsyncSession, db: AsyncSession,
vendor_name: Optional[str] = None, vendor_name: Optional[str] = None,
@@ -73,8 +74,15 @@ async def find(
name: Optional[str] = None, name: Optional[str] = None,
material: Optional[str] = None, material: Optional[str] = None,
article_number: Optional[str] = None, article_number: Optional[str] = None,
sort_by: Optional[dict[str, SortOrder]] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> list[models.Filament]: ) -> list[models.Filament]:
"""Find a list of filament objects by search criteria.""" """Find a list of filament objects by search criteria.
Sort by a field by passing a dict with the field name as key and the sort order as value.
The field name can contain nested fields, e.g. vendor.name.
"""
stmt = ( stmt = (
select(models.Filament) select(models.Filament)
.options(contains_eager(models.Filament.vendor)) .options(contains_eager(models.Filament.vendor))
@@ -91,6 +99,17 @@ async def find(
if article_number is not None: if article_number is not None:
stmt = stmt.where(models.Filament.article_number.ilike(f"%{article_number}%")) stmt = stmt.where(models.Filament.article_number.ilike(f"%{article_number}%"))
if sort_by is not None:
for fieldstr, order in sort_by.items():
field = parse_nested_field(models.Filament, fieldstr)
if order == SortOrder.ASC:
stmt = stmt.order_by(field.asc())
elif order == SortOrder.DESC:
stmt = stmt.order_by(field.desc())
if limit is not None:
stmt = stmt.offset(offset).limit(limit)
rows = await db.execute(stmt) rows = await db.execute(stmt)
return list(rows.scalars().all()) return list(rows.scalars().all())

View File

@@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
from spoolman.database import filament, models from spoolman.database import filament, models
from spoolman.database.utils import SortOrder from spoolman.database.utils import SortOrder, parse_nested_field
from spoolman.exceptions import ItemCreateError, ItemNotFoundError from spoolman.exceptions import ItemCreateError, ItemNotFoundError
from spoolman.math import weight_from_length from spoolman.math import weight_from_length
@@ -76,28 +76,6 @@ async def get_by_id(db: AsyncSession, spool_id: int) -> models.Spool:
return spool return spool
def parse_nested_field(base_obj: type[models.Base], field: str) -> sqlalchemy.Column:
"""Parse a nested field string into a sqlalchemy field object."""
fields = field.split(".")
if not hasattr(base_obj, fields[0]):
raise ValueError(f"Invalid field name '{field}'")
if fields[0] == "filament" and len(fields) == 1:
raise ValueError("No field specified for filament")
if fields[0] == "filament":
return parse_nested_field(models.Filament, ".".join(fields[1:]))
if fields[0] == "vendor" and len(fields) == 1:
raise ValueError("No field specified for vendor")
if fields[0] == "vendor":
return parse_nested_field(models.Vendor, ".".join(fields[1:]))
if len(fields) > 1:
raise ValueError(f"Field '{fields[0]}' does not have any nested fields")
return getattr(base_obj, fields[0])
async def find( # noqa: C901, PLR0912 async def find( # noqa: C901, PLR0912
*, *,
db: AsyncSession, db: AsyncSession,

View File

@@ -2,7 +2,33 @@
from enum import Enum from enum import Enum
import sqlalchemy
from spoolman.database import models
class SortOrder(Enum): class SortOrder(Enum):
ASC = 1 ASC = 1
DESC = 2 DESC = 2
def parse_nested_field(base_obj: type[models.Base], field: str) -> sqlalchemy.Column:
"""Parse a nested field string into a sqlalchemy field object."""
fields = field.split(".")
if not hasattr(base_obj, fields[0]):
raise ValueError(f"Invalid field name '{field}'")
if fields[0] == "filament" and len(fields) == 1:
raise ValueError("No field specified for filament")
if fields[0] == "filament":
return parse_nested_field(models.Filament, ".".join(fields[1:]))
if fields[0] == "vendor" and len(fields) == 1:
raise ValueError("No field specified for vendor")
if fields[0] == "vendor":
return parse_nested_field(models.Vendor, ".".join(fields[1:]))
if len(fields) > 1:
raise ValueError(f"Field '{fields[0]}' does not have any nested fields")
return getattr(base_obj, fields[0])

View File

@@ -6,6 +6,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import models from spoolman.database import models
from spoolman.database.utils import SortOrder
from spoolman.exceptions import ItemNotFoundError from spoolman.exceptions import ItemNotFoundError
@@ -37,12 +38,26 @@ async def find(
*, *,
db: AsyncSession, db: AsyncSession,
name: Optional[str] = None, name: Optional[str] = None,
sort_by: Optional[dict[str, SortOrder]] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> list[models.Vendor]: ) -> list[models.Vendor]:
"""Find a list of vendor objects by search criteria.""" """Find a list of vendor objects by search criteria."""
stmt = select(models.Vendor) stmt = select(models.Vendor)
if name is not None: if name is not None:
stmt = stmt.where(models.Vendor.name.ilike(f"%{name}%")) stmt = stmt.where(models.Vendor.name.ilike(f"%{name}%"))
if sort_by is not None:
for fieldstr, order in sort_by.items():
field = getattr(models.Vendor, fieldstr)
if order == SortOrder.ASC:
stmt = stmt.order_by(field.asc())
elif order == SortOrder.DESC:
stmt = stmt.order_by(field.desc())
if limit is not None:
stmt = stmt.offset(offset).limit(limit)
rows = await db.execute(stmt) rows = await db.execute(stmt)
return list(rows.scalars().all()) return list(rows.scalars().all())