From b51d0a31835bdfc1cb1a982de03458ef030dfddf Mon Sep 17 00:00:00 2001 From: Donkie Date: Wed, 5 Apr 2023 21:29:39 +0200 Subject: [PATCH] Implemented find queries --- spoolman/api/v1/filament.py | 47 ++++++++++++++++++++++----- spoolman/api/v1/spool.py | 61 +++++++++++++++++++++++++++++------ spoolman/api/v1/vendor.py | 21 +++++++++--- spoolman/database/filament.py | 34 ++++++++++++++++++- spoolman/database/spool.py | 46 +++++++++++++++++++++++++- spoolman/database/vendor.py | 15 +++++++++ 6 files changed, 199 insertions(+), 25 deletions(-) diff --git a/spoolman/api/v1/filament.py b/spoolman/api/v1/filament.py index bea135b..114e957 100644 --- a/spoolman/api/v1/filament.py +++ b/spoolman/api/v1/filament.py @@ -1,8 +1,8 @@ """Filament related endpoints.""" -from typing import Annotated, Optional, Union +from typing import Annotated, Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel, Field @@ -19,7 +19,7 @@ router = APIRouter( tags=["filament"], ) -# ruff: noqa: D103 +# ruff: noqa: D103, B008 class FilamentParameters(BaseModel): @@ -74,12 +74,43 @@ class FilamentUpdateParameters(FilamentParameters): response_model_exclude_none=True, ) async def find( - vendor_id: Optional[int] = None, - name: Optional[str] = None, - material: Optional[str] = None, - article_number: Optional[str] = None, + *, + db: Annotated[AsyncSession, Depends(get_db_session)], + vendor_name: Optional[str] = Query( + default=None, + title="Vendor Name", + description="Partial case-insensitive search term for the filament vendor name.", + ), + vendor_id: Optional[int] = Query( + default=None, + title="Vendor ID", + description="Match an exact vendor ID.", + ), + name: Optional[str] = Query( + default=None, + title="Filament Name", + description="Partial case-insensitive search term for the filament name.", + ), + material: Optional[str] = Query( + default=None, + title="Filament Material", + description="Partial case-insensitive search term for the filament material.", + ), + article_number: Optional[str] = Query( + default=None, + title="Filament Article Number", + description="Partial case-insensitive search term for the filament article number.", + ), ) -> list[Filament]: - return [] # TODO: Implement + db_items = await filament.find( + db=db, + vendor_name=vendor_name, + vendor_id=vendor_id, + name=name, + material=material, + article_number=article_number, + ) + return [Filament.from_db(db_item) for db_item in db_items] @router.get( diff --git a/spoolman/api/v1/spool.py b/spoolman/api/v1/spool.py index b9e11a3..1c60523 100644 --- a/spoolman/api/v1/spool.py +++ b/spoolman/api/v1/spool.py @@ -1,9 +1,9 @@ """Spool related endpoints.""" from datetime import datetime -from typing import Annotated, Optional, Union +from typing import Annotated, Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel, Field @@ -20,7 +20,7 @@ router = APIRouter( tags=["spool"], ) -# ruff: noqa: D103 +# ruff: noqa: D103,B008 class SpoolParameters(BaseModel): @@ -64,14 +64,55 @@ class SpoolUseParameters(BaseModel): response_model_exclude_none=True, ) async def find( - filament_id: Optional[int] = None, - name: Optional[str] = None, - material: Optional[str] = None, - vendor: Optional[str] = None, - location: Optional[str] = None, - lot_nr: Optional[str] = None, + *, + db: Annotated[AsyncSession, Depends(get_db_session)], + filament_name: Optional[int] = Query( + default=None, + title="Filament Name", + description="Partial case-insensitive search term for the filament name.", + ), + filament_id: Optional[int] = Query( + default=None, + title="Filament ID", + description="Match an exact filament ID.", + ), + filament_material: Optional[str] = Query( + default=None, + title="Filament Material", + description="Partial case-insensitive search term for the filament material.", + ), + vendor_name: Optional[str] = Query( + default=None, + title="Vendor Name", + description="Partial case-insensitive search term for the filament vendor name.", + ), + vendor_id: Optional[int] = Query( + default=None, + title="Vendor ID", + description="Match an exact vendor ID.", + ), + location: Optional[str] = Query( + default=None, + title="Location", + description="Partial case-insensitive search term for the spool location.", + ), + lot_nr: Optional[str] = Query( + default=None, + title="Lot/Batch Number", + description="Partial case-insensitive search term for the spool lot number.", + ), ) -> list[Spool]: - return [] # TODO: Implement + db_items = await spool.find( + db=db, + filament_name=filament_name, + filament_id=filament_id, + filament_material=filament_material, + vendor_name=vendor_name, + vendor_id=vendor_id, + location=location, + lot_nr=lot_nr, + ) + return [Spool.from_db(db_item) for db_item in db_items] @router.get( diff --git a/spoolman/api/v1/vendor.py b/spoolman/api/v1/vendor.py index 5e261f4..c92bedf 100644 --- a/spoolman/api/v1/vendor.py +++ b/spoolman/api/v1/vendor.py @@ -1,8 +1,8 @@ """Vendor related endpoints.""" -from typing import Annotated, Optional, Union +from typing import Annotated, Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from fastapi.exceptions import RequestValidationError from pydantic import BaseModel, Field from pydantic.error_wrappers import ErrorWrapper @@ -17,7 +17,7 @@ router = APIRouter( tags=["vendor"], ) -# ruff: noqa: D103 +# ruff: noqa: D103,B008 class VendorParameters(BaseModel): @@ -44,8 +44,19 @@ class VendorUpdateParameters(VendorParameters): description="Get a list of vendors that matches the search query.", response_model_exclude_none=True, ) -async def find(name: Optional[str] = None) -> list[Vendor]: - return [] # TODO: Implement +async def find( + db: Annotated[AsyncSession, Depends(get_db_session)], + name: Optional[str] = Query( + default=None, + title="Vendor Name", + description="Partial case-insensitive search term for the vendor name.", + ), +) -> list[Vendor]: + db_items = await vendor.find( + db=db, + name=name, + ) + return [Vendor.from_db(db_item) for db_item in db_items] @router.get( diff --git a/spoolman/database/filament.py b/spoolman/database/filament.py index 38732f2..1017203 100644 --- a/spoolman/database/filament.py +++ b/spoolman/database/filament.py @@ -2,8 +2,10 @@ from typing import Optional +from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import contains_eager, joinedload from spoolman.database import models, vendor from spoolman.exceptions import ItemDeleteError, ItemNotFoundError @@ -47,12 +49,42 @@ async def create( async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament: """Get a filament object from the database by the unique ID.""" - filament = await db.get(models.Filament, filament_id) + filament = await db.get( + models.Filament, + filament_id, + options=[joinedload("*")], # Load all nested objects as well + ) if filament is None: raise ItemNotFoundError(f"No filament with ID {filament_id} found.") return filament +async def find( + *, + db: AsyncSession, + vendor_name: Optional[str] = None, + vendor_id: Optional[int] = None, + name: Optional[str] = None, + material: Optional[str] = None, + article_number: Optional[str] = None, +) -> list[models.Filament]: + """Find a list of filament objects by search criteria.""" + stmt = select(models.Filament).options(contains_eager(models.Filament.vendor)).join(models.Filament.vendor) + if vendor_name is not None: + stmt = stmt.where(models.Vendor.name.ilike(f"%{vendor_name}%")) + if vendor_id is not None: + stmt = stmt.where(models.Filament.vendor_id == vendor_id) + if name is not None: + stmt = stmt.where(models.Filament.name.ilike(f"%{name}%")) + if material is not None: + stmt = stmt.where(models.Filament.material.ilike(f"%{material}%")) + if article_number is not None: + stmt = stmt.where(models.Filament.article_number.ilike(f"%{article_number}%")) + + rows = await db.execute(stmt) + return list(rows.scalars().all()) + + async def update( *, db: AsyncSession, diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index b57488f..77b28aa 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -3,7 +3,9 @@ from datetime import datetime from typing import Optional +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import contains_eager, joinedload from spoolman.database import filament, models from spoolman.exceptions import ItemCreateError, ItemNotFoundError @@ -44,12 +46,54 @@ async def create( async def get_by_id(db: AsyncSession, spool_id: int, with_for_update: Optional[bool] = None) -> models.Spool: """Get a spool object from the database by the unique ID.""" - spool = await db.get(models.Spool, spool_id, with_for_update=with_for_update) # type: ignore # noqa: PGH003 + spool = await db.get( + models.Spool, + spool_id, + with_for_update=with_for_update, # type: ignore # noqa: PGH003 + options=[joinedload("*")], # Load all nested objects as well + ) if spool is None: raise ItemNotFoundError(f"No spool with ID {spool_id} found.") return spool +async def find( + *, + db: AsyncSession, + filament_name: Optional[int] = None, + filament_id: Optional[int] = None, + filament_material: Optional[str] = None, + vendor_name: Optional[str] = None, + vendor_id: Optional[int] = None, + location: Optional[str] = None, + lot_nr: Optional[str] = None, +) -> list[models.Spool]: + """Find a list of spool objects by search criteria.""" + stmt = ( + select(models.Spool) + .join(models.Spool.filament) + .join(models.Filament.vendor) + .options(contains_eager(models.Spool.filament).contains_eager(models.Filament.vendor)) + ) + if filament_name is not None: + stmt = stmt.where(models.Filament.name.ilike(f"%{filament_name}%")) + if filament_id is not None: + stmt = stmt.where(models.Spool.filament_id == filament_id) + if filament_material is not None: + stmt = stmt.where(models.Filament.material.ilike(f"%{filament_material}%")) + if vendor_name is not None: + stmt = stmt.where(models.Vendor.name.ilike(f"%{vendor_name}%")) + if vendor_id is not None: + stmt = stmt.where(models.Filament.vendor_id == vendor_id) + if location is not None: + stmt = stmt.where(models.Spool.location.ilike(f"%{location}%")) + if lot_nr is not None: + stmt = stmt.where(models.Spool.lot_nr.ilike(f"%{lot_nr}%")) + + rows = await db.execute(stmt) + return list(rows.scalars().all()) + + async def update( *, db: AsyncSession, diff --git a/spoolman/database/vendor.py b/spoolman/database/vendor.py index bfdddb6..0829536 100644 --- a/spoolman/database/vendor.py +++ b/spoolman/database/vendor.py @@ -2,6 +2,7 @@ from typing import Optional +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from spoolman.database import models @@ -32,6 +33,20 @@ async def get_by_id(db: AsyncSession, vendor_id: int) -> models.Vendor: return vendor +async def find( + *, + db: AsyncSession, + name: Optional[str] = None, +) -> list[models.Vendor]: + """Find a list of vendor objects by search criteria.""" + stmt = select(models.Vendor) + if name is not None: + stmt = stmt.where(models.Vendor.name.ilike(f"%{name}%")) + + rows = await db.execute(stmt) + return list(rows.scalars().all()) + + async def update( *, db: AsyncSession,