Backend for tracking print jobs from Elegoo/Orca Slicer: - PrintJob model: tracks pending/completed/cancelled jobs - API endpoints: create, list, complete, cancel jobs - needs_weighing flag on Spool: for cancelled print recalibration - Database migration for new table and column Workflow: 1. Slicer post-processing script creates pending job 2. User completes job → auto-deducts filament 3. User cancels job → flags spool for manual weigh-in Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
200 lines
5.8 KiB
Python
200 lines
5.8 KiB
Python
"""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!")
|