"""Print job API endpoints.""" from datetime import datetime from typing import Annotated, Optional from fastapi import APIRouter, Depends, Query from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from spoolman.api.v1.models import Message, Spool from spoolman.database import print_job from spoolman.database.database import get_db_session from spoolman.database.models import PrintJob as DBPrintJob router = APIRouter( prefix="/print-job", tags=["print-job"], ) class PrintJobResponse(BaseModel): """Print job response model.""" id: int spool_id: int spool: Optional[Spool] = None created: datetime finished: Optional[datetime] = None filename: str filament_used_g: float filament_used_mm: Optional[float] = None status: str @classmethod def from_db(cls, db_item: DBPrintJob) -> "PrintJobResponse": """Create from database model.""" return cls( id=db_item.id, spool_id=db_item.spool_id, spool=Spool.from_db(db_item.spool) if db_item.spool else None, created=db_item.created, finished=db_item.finished, filename=db_item.filename, filament_used_g=db_item.filament_used_g, filament_used_mm=db_item.filament_used_mm, status=db_item.status, ) class CreatePrintJobRequest(BaseModel): """Request to create a print job.""" spool_id: int = Field(..., description="ID of the spool to use.") filename: str = Field(..., max_length=256, description="G-code filename.") filament_used_g: float = Field(..., ge=0, description="Estimated filament usage in grams.") filament_used_mm: Optional[float] = Field(None, ge=0, description="Estimated filament usage in mm.") class CompletePrintJobRequest(BaseModel): """Request to complete a print job.""" comment: Optional[str] = Field(None, max_length=256, description="Optional comment for adjustment history.") @router.get( "", name="List print jobs", description="Get a list of print jobs.", response_model_exclude_none=True, responses={200: {"model": list[PrintJobResponse]}}, ) async def find( *, db: Annotated[AsyncSession, Depends(get_db_session)], status: Annotated[ Optional[str], Query(description="Filter by status: 'pending', 'completed', 'cancelled'."), ] = None, spool_id: Annotated[ Optional[int], Query(description="Filter by spool ID."), ] = None, limit: Annotated[ Optional[int], Query(description="Maximum number of items to return."), ] = None, offset: Annotated[ int, Query(description="Offset for pagination."), ] = 0, ) -> JSONResponse: """Find print jobs.""" db_items, total_count = await print_job.find( db=db, status=status, spool_id=spool_id, limit=limit, offset=offset, ) return JSONResponse( content=jsonable_encoder( [PrintJobResponse.from_db(item) for item in db_items], exclude_none=True, ), headers={"x-total-count": str(total_count)}, ) @router.get( "/{job_id}", name="Get print job", description="Get a specific print job.", response_model_exclude_none=True, responses={404: {"model": Message}}, ) async def get( db: Annotated[AsyncSession, Depends(get_db_session)], job_id: int, ) -> PrintJobResponse: """Get a print job by ID.""" db_item = await print_job.get_by_id(db, job_id) return PrintJobResponse.from_db(db_item) @router.post( "", name="Create print job", description="Create a new pending print job (called by slicer post-processing script).", response_model_exclude_none=True, responses={400: {"model": Message}, 404: {"model": Message}}, ) async def create( db: Annotated[AsyncSession, Depends(get_db_session)], body: CreatePrintJobRequest, ) -> PrintJobResponse: """Create a new print job.""" db_item = await print_job.create( db=db, spool_id=body.spool_id, filename=body.filename, filament_used_g=body.filament_used_g, filament_used_mm=body.filament_used_mm, ) return PrintJobResponse.from_db(db_item) @router.post( "/{job_id}/complete", name="Complete print job", description="Mark a print job as completed - deducts filament from spool.", response_model_exclude_none=True, responses={400: {"model": Message}, 404: {"model": Message}}, ) async def complete( db: Annotated[AsyncSession, Depends(get_db_session)], job_id: int, body: Optional[CompletePrintJobRequest] = None, ) -> PrintJobResponse: """Complete a print job.""" db_item = await print_job.complete( db=db, job_id=job_id, comment=body.comment if body else None, ) return PrintJobResponse.from_db(db_item) @router.post( "/{job_id}/cancel", name="Cancel print job", description="Mark a print job as cancelled - flags spool for manual weigh-in.", response_model_exclude_none=True, responses={400: {"model": Message}, 404: {"model": Message}}, ) async def cancel( db: Annotated[AsyncSession, Depends(get_db_session)], job_id: int, ) -> PrintJobResponse: """Cancel a print job.""" db_item = await print_job.cancel(db=db, job_id=job_id) return PrintJobResponse.from_db(db_item) @router.delete( "/{job_id}", name="Delete print job", description="Delete a print job.", response_model=Message, responses={404: {"model": Message}}, ) async def delete( db: Annotated[AsyncSession, Depends(get_db_session)], job_id: int, ) -> Message: """Delete a print job.""" await print_job.delete(db=db, job_id=job_id) return Message(message="Success!")