import json from pathlib import Path from fastapi import APIRouter, HTTPException, status from fastapi.responses import FileResponse, JSONResponse from app.config import get_settings from app.models.jobs import JobResponse from app.services.jobs import get_job, get_job_record router = APIRouter() def _artifact_path(job_id: str) -> Path: record = get_job_record(job_id) if record is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"code": "JOB_NOT_FOUND", "detail": "Job not found"}, ) if record.get("status") != "done": raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail={"code": "JOB_NOT_READY", "detail": "Job is not finished yet"}, ) result = record.get("result") or {} data = result.get("data") if isinstance(result.get("data"), dict) else {} uri = result.get("uri") or data.get("uri") if not uri: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"code": "ARTIFACT_NOT_FOUND", "detail": "Job has no artifact URI"}, ) path = Path(uri).resolve() root = get_settings().jobs_output_path.resolve() if root not in path.parents and path != root: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"code": "ARTIFACT_FORBIDDEN", "detail": "Artifact path is outside jobs output"}, ) if not path.is_file(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"code": "ARTIFACT_MISSING", "detail": f"Artifact file not found: {path.name}"}, ) return path @router.get("/{job_id}", response_model=JobResponse) def read(job_id: str) -> JobResponse: job = get_job(job_id) if job is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"code": "JOB_NOT_FOUND", "detail": "Job not found"}, ) return job @router.get("/{job_id}/artifact") def read_artifact(job_id: str): path = _artifact_path(job_id) if path.suffix.lower() == ".json": return JSONResponse(content=json.loads(path.read_text(encoding="utf-8"))) return FileResponse(path)