Added x-total-count response header

This commit is contained in:
Donkie
2023-08-26 11:58:06 +02:00
parent 557ea82171
commit e2d937aaf1
7 changed files with 94 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import logging
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
@@ -109,6 +110,7 @@ class FilamentUpdateParameters(FilamentParameters):
name="Find filaments", name="Find filaments",
description="Get a list of filaments that matches the search query.", description="Get a list of filaments that matches the search query.",
response_model_exclude_none=True, response_model_exclude_none=True,
response_model=list[Filament],
) )
async def find( async def find(
*, *,
@@ -156,14 +158,14 @@ async def find(
title="Offset", title="Offset",
description="Offset in the full result set if a limit is set.", description="Offset in the full result set if a limit is set.",
), ),
) -> list[Filament]: ) -> JSONResponse:
sort_by: dict[str, SortOrder] = {} sort_by: dict[str, SortOrder] = {}
if sort is not None: if sort is not None:
for sort_item in sort.split(","): for sort_item in sort.split(","):
field, direction = sort_item.split(":") field, direction = sort_item.split(":")
sort_by[field] = SortOrder[direction.upper()] sort_by[field] = SortOrder[direction.upper()]
db_items = await filament.find( db_items, total_count = await filament.find(
db=db, db=db,
vendor_name=vendor_name, vendor_name=vendor_name,
vendor_id=vendor_id, vendor_id=vendor_id,
@@ -174,7 +176,15 @@ async def find(
limit=limit, limit=limit,
offset=offset, offset=offset,
) )
return [Filament.from_db(db_item) for db_item in db_items]
# Set x-total-count header for pagination
return JSONResponse(
content=jsonable_encoder(
(Filament.from_db(db_item) for db_item in db_items),
exclude_none=True,
),
headers={"x-total-count": str(total_count)},
)
@router.get( @router.get(

View File

@@ -5,6 +5,7 @@ from datetime import datetime
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -67,6 +68,7 @@ class SpoolUseParameters(BaseModel):
name="Find spool", name="Find spool",
description="Get a list of spools that matches the search query.", description="Get a list of spools that matches the search query.",
response_model_exclude_none=True, response_model_exclude_none=True,
response_model=list[Spool],
) )
async def find( async def find(
*, *,
@@ -129,14 +131,14 @@ async def find(
title="Offset", title="Offset",
description="Offset in the full result set if a limit is set.", description="Offset in the full result set if a limit is set.",
), ),
) -> list[Spool]: ) -> JSONResponse:
sort_by: dict[str, SortOrder] = {} sort_by: dict[str, SortOrder] = {}
if sort is not None: if sort is not None:
for sort_item in sort.split(","): for sort_item in sort.split(","):
field, direction = sort_item.split(":") field, direction = sort_item.split(":")
sort_by[field] = SortOrder[direction.upper()] sort_by[field] = SortOrder[direction.upper()]
db_items = await spool.find( db_items, total_count = await spool.find(
db=db, db=db,
filament_name=filament_name, filament_name=filament_name,
filament_id=filament_id, filament_id=filament_id,
@@ -150,7 +152,15 @@ async def find(
limit=limit, limit=limit,
offset=offset, offset=offset,
) )
return [Spool.from_db(db_item) for db_item in db_items]
# Set x-total-count header for pagination
return JSONResponse(
content=jsonable_encoder(
(Spool.from_db(db_item) for db_item in db_items),
exclude_none=True,
),
headers={"x-total-count": str(total_count)},
)
@router.get( @router.get(

View File

@@ -3,7 +3,9 @@
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic.error_wrappers import ErrorWrapper from pydantic.error_wrappers import ErrorWrapper
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -44,6 +46,7 @@ class VendorUpdateParameters(VendorParameters):
name="Find vendor", name="Find vendor",
description="Get a list of vendors that matches the search query.", description="Get a list of vendors that matches the search query.",
response_model_exclude_none=True, response_model_exclude_none=True,
response_model=list[Vendor],
) )
async def find( async def find(
db: Annotated[AsyncSession, Depends(get_db_session)], db: Annotated[AsyncSession, Depends(get_db_session)],
@@ -70,21 +73,28 @@ async def find(
title="Offset", title="Offset",
description="Offset in the full result set if a limit is set.", description="Offset in the full result set if a limit is set.",
), ),
) -> list[Vendor]: ) -> JSONResponse:
sort_by: dict[str, SortOrder] = {} sort_by: dict[str, SortOrder] = {}
if sort is not None: if sort is not None:
for sort_item in sort.split(","): for sort_item in sort.split(","):
field, direction = sort_item.split(":") field, direction = sort_item.split(":")
sort_by[field] = SortOrder[direction.upper()] sort_by[field] = SortOrder[direction.upper()]
db_items = await vendor.find( db_items, total_count = await vendor.find(
db=db, db=db,
name=name, name=name,
sort_by=sort_by, sort_by=sort_by,
limit=limit, limit=limit,
offset=offset, offset=offset,
) )
return [Vendor.from_db(db_item) for db_item in db_items] # Set x-total-count header for pagination
return JSONResponse(
content=jsonable_encoder(
(Vendor.from_db(db_item) for db_item in db_items),
exclude_none=True,
),
headers={"x-total-count": str(total_count)},
)
@router.get( @router.get(

View File

@@ -3,7 +3,7 @@
import logging import logging
from typing import Optional from typing import Optional
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
@@ -78,11 +78,13 @@ async def find( # noqa: C901
sort_by: Optional[dict[str, SortOrder]] = None, sort_by: Optional[dict[str, SortOrder]] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
offset: int = 0, offset: int = 0,
) -> list[models.Filament]: ) -> tuple[list[models.Filament], int]:
"""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. 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. The field name can contain nested fields, e.g. vendor.name.
Returns a tuple containing the list of items and the total count of matching items.
""" """
stmt = ( stmt = (
select(models.Filament) select(models.Filament)
@@ -100,6 +102,14 @@ async def find( # noqa: C901
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}%"))
total_count = None
if limit is not None:
total_count_stmt = stmt.with_only_columns(func.count())
total_count = (await db.execute(total_count_stmt)).scalar()
stmt = stmt.offset(offset).limit(limit)
if sort_by is not None: if sort_by is not None:
for fieldstr, order in sort_by.items(): for fieldstr, order in sort_by.items():
field = parse_nested_field(models.Filament, fieldstr) field = parse_nested_field(models.Filament, fieldstr)
@@ -108,11 +118,12 @@ async def find( # noqa: C901
elif order == SortOrder.DESC: elif order == SortOrder.DESC:
stmt = stmt.order_by(field.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()) result = list(rows.scalars().all())
if total_count is None:
total_count = len(result)
return result, total_count
async def update( async def update(

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timezone
from typing import Optional from typing import Optional
import sqlalchemy import sqlalchemy
from sqlalchemy import case from sqlalchemy import case, func
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import contains_eager, joinedload from sqlalchemy.orm import contains_eager, joinedload
@@ -90,11 +90,13 @@ async def find( # noqa: C901, PLR0912
sort_by: Optional[dict[str, SortOrder]] = None, sort_by: Optional[dict[str, SortOrder]] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
offset: int = 0, offset: int = 0,
) -> list[models.Spool]: ) -> tuple[list[models.Spool], int]:
"""Find a list of spool objects by search criteria. """Find a list of spool objects by search criteria.
Sort by a field by passing a dict with the field name as key and the sort order as value. 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. filament.name. The field name can contain nested fields, e.g. filament.name.
Returns a tuple containing the list of items and the total count of matching items.
""" """
stmt = ( stmt = (
sqlalchemy.select(models.Spool) sqlalchemy.select(models.Spool)
@@ -125,6 +127,14 @@ async def find( # noqa: C901, PLR0912
), ),
) )
total_count = None
if limit is not None:
total_count_stmt = stmt.with_only_columns(func.count())
total_count = (await db.execute(total_count_stmt)).scalar()
stmt = stmt.offset(offset).limit(limit)
if sort_by is not None: if sort_by is not None:
for fieldstr, order in sort_by.items(): for fieldstr, order in sort_by.items():
field = parse_nested_field(models.Spool, fieldstr) field = parse_nested_field(models.Spool, fieldstr)
@@ -133,11 +143,12 @@ async def find( # noqa: C901, PLR0912
elif order == SortOrder.DESC: elif order == SortOrder.DESC:
stmt = stmt.order_by(field.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()) result = list(rows.scalars().all())
if total_count is None:
total_count = len(result)
return result, total_count
async def update( async def update(

View File

@@ -1,6 +1,7 @@
"""Utility functions for the database module.""" """Utility functions for the database module."""
from enum import Enum from enum import Enum
from typing import Any
import sqlalchemy import sqlalchemy
@@ -12,7 +13,7 @@ class SortOrder(Enum):
DESC = 2 DESC = 2
def parse_nested_field(base_obj: type[models.Base], field: str) -> sqlalchemy.Column: def parse_nested_field(base_obj: type[models.Base], field: str) -> sqlalchemy.Column[Any]:
"""Parse a nested field string into a sqlalchemy field object.""" """Parse a nested field string into a sqlalchemy field object."""
fields = field.split(".") fields = field.split(".")
if not hasattr(base_obj, fields[0]): if not hasattr(base_obj, fields[0]):

View File

@@ -2,7 +2,7 @@
from typing import Optional from typing import Optional
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import models from spoolman.database import models
@@ -41,12 +41,23 @@ async def find(
sort_by: Optional[dict[str, SortOrder]] = None, sort_by: Optional[dict[str, SortOrder]] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
offset: int = 0, offset: int = 0,
) -> list[models.Vendor]: ) -> tuple[list[models.Vendor], int]:
"""Find a list of vendor objects by search criteria.""" """Find a list of vendor objects by search criteria.
Returns a tuple containing the list of items and the total count of matching items.
"""
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}%"))
total_count = None
if limit is not None:
total_count_stmt = stmt.with_only_columns(func.count())
total_count = (await db.execute(total_count_stmt)).scalar()
stmt = stmt.offset(offset).limit(limit)
if sort_by is not None: if sort_by is not None:
for fieldstr, order in sort_by.items(): for fieldstr, order in sort_by.items():
field = getattr(models.Vendor, fieldstr) field = getattr(models.Vendor, fieldstr)
@@ -55,11 +66,12 @@ async def find(
elif order == SortOrder.DESC: elif order == SortOrder.DESC:
stmt = stmt.order_by(field.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()) result = list(rows.scalars().all())
if total_count is None:
total_count = len(result)
return result, total_count
async def update( async def update(