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>
158 lines
4.5 KiB
Python
158 lines
4.5 KiB
Python
"""Database operations for print jobs."""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from spoolman.database.models import PrintJob, Spool, SpoolAdjustment
|
|
from spoolman.exceptions import ItemNotFoundError
|
|
|
|
|
|
async def create(
|
|
*,
|
|
db: AsyncSession,
|
|
spool_id: int,
|
|
filename: str,
|
|
filament_used_g: float,
|
|
filament_used_mm: Optional[float] = None,
|
|
) -> PrintJob:
|
|
"""Create a new pending print job."""
|
|
# Verify spool exists
|
|
spool = await db.get(Spool, spool_id)
|
|
if spool is None:
|
|
raise ItemNotFoundError(f"Spool with ID {spool_id} not found.")
|
|
|
|
job = PrintJob(
|
|
spool_id=spool_id,
|
|
created=datetime.utcnow(),
|
|
filename=filename,
|
|
filament_used_g=filament_used_g,
|
|
filament_used_mm=filament_used_mm,
|
|
status="pending",
|
|
)
|
|
db.add(job)
|
|
await db.commit()
|
|
await db.refresh(job)
|
|
|
|
# Load spool relationship
|
|
result = await db.execute(
|
|
select(PrintJob).where(PrintJob.id == job.id).options(joinedload(PrintJob.spool))
|
|
)
|
|
return result.scalar_one()
|
|
|
|
|
|
async def get_by_id(db: AsyncSession, job_id: int) -> PrintJob:
|
|
"""Get a print job by ID."""
|
|
result = await db.execute(
|
|
select(PrintJob).where(PrintJob.id == job_id).options(joinedload(PrintJob.spool))
|
|
)
|
|
job = result.scalar_one_or_none()
|
|
if job is None:
|
|
raise ItemNotFoundError(f"Print job with ID {job_id} not found.")
|
|
return job
|
|
|
|
|
|
async def find(
|
|
*,
|
|
db: AsyncSession,
|
|
status: Optional[str] = None,
|
|
spool_id: Optional[int] = None,
|
|
limit: Optional[int] = None,
|
|
offset: int = 0,
|
|
) -> tuple[list[PrintJob], int]:
|
|
"""Find print jobs with optional filters."""
|
|
query = select(PrintJob).options(joinedload(PrintJob.spool))
|
|
|
|
if status:
|
|
query = query.where(PrintJob.status == status)
|
|
if spool_id:
|
|
query = query.where(PrintJob.spool_id == spool_id)
|
|
|
|
# Get total count
|
|
count_query = select(PrintJob)
|
|
if status:
|
|
count_query = count_query.where(PrintJob.status == status)
|
|
if spool_id:
|
|
count_query = count_query.where(PrintJob.spool_id == spool_id)
|
|
count_result = await db.execute(count_query)
|
|
total = len(count_result.all())
|
|
|
|
# Apply ordering and pagination
|
|
query = query.order_by(PrintJob.created.desc())
|
|
if offset:
|
|
query = query.offset(offset)
|
|
if limit:
|
|
query = query.limit(limit)
|
|
|
|
result = await db.execute(query)
|
|
return list(result.scalars().all()), total
|
|
|
|
|
|
async def complete(db: AsyncSession, job_id: int, comment: Optional[str] = None) -> PrintJob:
|
|
"""Complete a print job - deduct filament from spool."""
|
|
job = await get_by_id(db, job_id)
|
|
if job.status != "pending":
|
|
raise ValueError(f"Job {job_id} is not pending (status: {job.status}).")
|
|
|
|
# Deduct filament from spool
|
|
spool = await db.get(Spool, job.spool_id)
|
|
if spool is None:
|
|
raise ItemNotFoundError(f"Spool with ID {job.spool_id} not found.")
|
|
|
|
spool.used_weight += job.filament_used_g
|
|
if spool.first_used is None:
|
|
spool.first_used = datetime.utcnow()
|
|
spool.last_used = datetime.utcnow()
|
|
|
|
# Record adjustment
|
|
adjustment = SpoolAdjustment(
|
|
spool_id=spool.id,
|
|
timestamp=datetime.utcnow(),
|
|
adjustment_type="weight",
|
|
value=-job.filament_used_g,
|
|
comment=comment or f"Print job: {job.filename}",
|
|
)
|
|
db.add(adjustment)
|
|
|
|
# Update job status
|
|
job.status = "completed"
|
|
job.finished = datetime.utcnow()
|
|
|
|
await db.commit()
|
|
|
|
# Reload with relationships
|
|
return await get_by_id(db, job_id)
|
|
|
|
|
|
async def cancel(db: AsyncSession, job_id: int) -> PrintJob:
|
|
"""Cancel a print job - flag spool for weighing."""
|
|
job = await get_by_id(db, job_id)
|
|
if job.status != "pending":
|
|
raise ValueError(f"Job {job_id} is not pending (status: {job.status}).")
|
|
|
|
# Flag spool for weighing
|
|
spool = await db.get(Spool, job.spool_id)
|
|
if spool is None:
|
|
raise ItemNotFoundError(f"Spool with ID {job.spool_id} not found.")
|
|
|
|
spool.needs_weighing = True
|
|
|
|
# Update job status
|
|
job.status = "cancelled"
|
|
job.finished = datetime.utcnow()
|
|
|
|
await db.commit()
|
|
|
|
# Reload with relationships
|
|
return await get_by_id(db, job_id)
|
|
|
|
|
|
async def delete(db: AsyncSession, job_id: int) -> None:
|
|
"""Delete a print job."""
|
|
job = await get_by_id(db, job_id)
|
|
await db.delete(job)
|
|
await db.commit()
|