import uuid from datetime import UTC, datetime from fastapi import APIRouter, HTTPException, status from sqlalchemy import select from sqlalchemy.exc import IntegrityError from app.db import SessionDep from app.models import Job, JobStatus, Sample from app.schemas import JobOut, SampleCreate, SampleOut from app.services import events router = APIRouter() @router.get("", response_model=list[SampleOut]) async def list_samples(session: SessionDep): result = await session.scalars(select(Sample).order_by(Sample.created_at.desc())) return result.all() @router.post("", response_model=SampleOut, status_code=status.HTTP_201_CREATED) async def create_sample(payload: SampleCreate, session: SessionDep): sample = Sample(**payload.model_dump()) session.add(sample) try: await session.commit() except IntegrityError: # samples.name is unique await session.rollback() raise HTTPException(409, f"a sample named {payload.name!r} already exists") from None await session.refresh(sample) return sample @router.get("/{sample_id}/jobs", response_model=list[JobOut]) async def list_jobs(sample_id: uuid.UUID, session: SessionDep): result = await session.scalars( select(Job).where(Job.sample_id == sample_id).order_by(Job.created_at.desc()) ) return result.all() @router.post("/{sample_id}/annotate", response_model=JobOut, status_code=status.HTTP_202_ACCEPTED) async def annotate(sample_id: uuid.UUID, session: SessionDep): sample = await session.get(Sample, sample_id) if sample is None: raise HTTPException(404, "sample not found") # Commit `running` before launching: a local run that dies instantly is marked failed by its # watcher, and a later status write here would overwrite that. job = Job(sample_id=sample.id, status=JobStatus.running) session.add(job) await session.commit() try: job.workflow_ref = await events.launch(job.id, sample.vcf_uri, sample.assembly) except events.LaunchError as e: job.status = JobStatus.failed job.log = str(e) job.finished_at = datetime.now(UTC) await session.commit() await session.refresh(job) return job