"""Location related endpoints.""" from typing import Annotated, Optional from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from spoolman.api.v1.models import Message from spoolman.database import location from spoolman.database.database import get_db_session from spoolman.database import models router = APIRouter( prefix="/location", tags=["location"], ) class LocationResponse(BaseModel): """Location response model.""" id: int name: str parent_id: Optional[int] = None location_type: Optional[str] = None description: Optional[str] = None full_path: Optional[str] = None # Only populated for single-item requests spool_count: Optional[int] = None # Only populated when requested @staticmethod def from_db(item: models.Location, full_path: Optional[str] = None) -> "LocationResponse": return LocationResponse( id=item.id, name=item.name, parent_id=item.parent_id, location_type=item.location_type, description=item.description, full_path=full_path, ) class LocationTreeNode(BaseModel): """Location tree node for nested structure.""" id: int name: str location_type: Optional[str] = None description: Optional[str] = None children: list["LocationTreeNode"] = [] class LocationParameters(BaseModel): """Parameters for creating a location.""" name: str = Field(max_length=64, description="Location name.") parent_id: Optional[int] = Field(None, description="Parent location ID.") location_type: Optional[str] = Field(None, max_length=32, description="Type: room, shelf, bin, etc.") description: Optional[str] = Field(None, max_length=256, description="Optional description.") class LocationUpdateParameters(BaseModel): """Parameters for updating a location.""" name: Optional[str] = Field(None, max_length=64, description="Location name.") parent_id: Optional[int] = Field(None, description="Parent location ID.") location_type: Optional[str] = Field(None, max_length=32, description="Type: room, shelf, bin, etc.") description: Optional[str] = Field(None, max_length=256, description="Optional description.") @router.get( "", name="Find locations", description="Get all locations as a flat list.", response_model=list[LocationResponse], ) async def find_locations( db: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[LocationResponse]: db_items = await location.find_all(db=db) return [LocationResponse.from_db(item) for item in db_items] @router.get( "/tree", name="Get location tree", description="Get all locations as a nested tree structure.", response_model=list[LocationTreeNode], ) async def get_location_tree( db: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[dict]: return await location.get_tree(db=db) @router.get( "/{location_id}", name="Get location", description="Get a specific location by ID.", response_model=LocationResponse, responses={404: {"model": Message}}, ) async def get_location( db: Annotated[AsyncSession, Depends(get_db_session)], location_id: int, ) -> LocationResponse: db_item = await location.get_by_id(db=db, location_id=location_id) full_path = await location.get_full_path(db=db, location_id=location_id) spool_count = await location.get_spool_count(db=db, location_id=location_id) response = LocationResponse.from_db(db_item, full_path=full_path) response.spool_count = spool_count return response @router.post( "", name="Create location", description="Create a new location.", response_model=LocationResponse, responses={400: {"model": Message}, 404: {"model": Message}}, ) async def create_location( db: Annotated[AsyncSession, Depends(get_db_session)], body: LocationParameters, ) -> LocationResponse: db_item = await location.create( db=db, name=body.name, parent_id=body.parent_id, location_type=body.location_type, description=body.description, ) return LocationResponse.from_db(db_item) @router.patch( "/{location_id}", name="Update location", description="Update a location.", response_model=LocationResponse, responses={400: {"model": Message}, 404: {"model": Message}}, ) async def update_location( db: Annotated[AsyncSession, Depends(get_db_session)], location_id: int, body: LocationUpdateParameters, ) -> LocationResponse: data = body.model_dump(exclude_unset=True) db_item = await location.update(db=db, location_id=location_id, data=data) return LocationResponse.from_db(db_item) @router.delete( "/{location_id}", name="Delete location", description="Delete a location. Spools using this location will have their location_id set to null.", responses={404: {"model": Message}}, ) async def delete_location( db: Annotated[AsyncSession, Depends(get_db_session)], location_id: int, ) -> Message: await location.delete(db=db, location_id=location_id) return Message(message="Success!")