language-learning-app/api/app/routers/api/jobs.py
wilson fecb5839ea
Some checks failed
/ test (push) Has been cancelled
feats: use Procrastinate for persistant jobs; try using Gemini for text
generation
2026-05-27 18:45:52 +01:00

111 lines
3.5 KiB
Python

import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import require_admin
from ...outbound.postgres.database import get_db
from ...outbound.postgres.entities.translated_article_entity import TranslatedArticleEntity
from ...outbound.postgres.repositories import summarise_job_repository
from ...tasks import regenerate_audio_for_job
router = APIRouter(prefix="/jobs", dependencies=[Depends(require_admin)])
class JobResponse(BaseModel):
id: uuid.UUID
status: str
translated_article_id: uuid.UUID | None = None
created_at: datetime
started_at: datetime | None = None
completed_at: datetime | None = None
error_message: str | None = None
model_config = {"from_attributes": True}
class JobSummary(BaseModel):
id: uuid.UUID
status: str
created_at: datetime
completed_at: datetime | None = None
error_message: str | None = None
model_config = {"from_attributes": True}
class JobListResponse(BaseModel):
jobs: list[JobSummary]
model_config = {"from_attributes": True}
@router.get("/", response_model=JobListResponse)
async def get_jobs(
db: AsyncSession = Depends(get_db),
) -> JobListResponse:
try:
jobs = await summarise_job_repository.list_all(db)
return {"jobs": jobs}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{job_id}", response_model=JobResponse)
async def get_job(
job_id: str,
db: AsyncSession = Depends(get_db),
) -> JobResponse:
try:
uid = uuid.UUID(job_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid job ID format")
job = await summarise_job_repository.get_by_id(db, uid)
if job is None:
raise HTTPException(status_code=404, detail="Job not found")
return JobResponse(
id=job.id,
status=job.status,
translated_article_id=job.translated_article_id,
created_at=job.created_at,
started_at=job.started_at,
completed_at=job.completed_at,
error_message=job.error_message,
)
@router.post("/{job_id}/regenerate-audio", status_code=202)
async def regenerate_audio(
job_id: str,
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(require_admin),
) -> dict:
try:
uid = uuid.UUID(job_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid job ID format")
job = await summarise_job_repository.get_by_id(db, uid)
if job is None:
raise HTTPException(status_code=404, detail="Job not found")
if str(job.user_id) != token_data["sub"]:
raise HTTPException(status_code=403, detail="Not authorized to modify this job")
if job.translated_article_id is None:
raise HTTPException(status_code=400, detail="Job has no associated article")
article_entity = await db.get(TranslatedArticleEntity, job.translated_article_id)
if not article_entity or not article_entity.target_body:
raise HTTPException(status_code=400, detail="Job has no generated text to synthesize")
if article_entity.audio_url:
raise HTTPException(status_code=409, detail="Job already has audio")
if job.status == "processing":
raise HTTPException(status_code=409, detail="Job is already processing")
await regenerate_audio_for_job.defer_async(job_id=str(uid))
return {"job_id": job_id}