"""Live generation progress for ``GET /reports/{id}/status`` polling.""" from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import case, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Report, ReportSection, ReportStatus async def count_report_section_progress( db: AsyncSession, report_id: str, ) -> tuple[int, int]: """Return ``(sections_saved, sections_with_text)`` for a report.""" row = await db.execute( select( func.count(ReportSection.id), func.coalesce( func.sum( case( (func.length(func.trim(ReportSection.text)) > 0, 1), else_=0, ) ), 0, ), ).where(ReportSection.report_id == report_id) ) saved, with_text = row.one() return int(saved or 0), int(with_text or 0) def generation_elapsed_seconds(report: Report) -> float | None: """Seconds since ``generation_started_at`` while the job is active.""" if report.status != ReportStatus.generating or report.generation_started_at is None: return None started = report.generation_started_at if started.tzinfo is None: started = started.replace(tzinfo=UTC) return max(0.0, (datetime.now(UTC) - started).total_seconds()) async def build_status_progress_fields( db: AsyncSession, report: Report, ) -> dict[str, int | float | None]: """Fields merged into :class:`~app.models.schemas.ReportStatusResponse`.""" saved, with_text = await count_report_section_progress(db, report.id) elapsed = generation_elapsed_seconds(report) return { "sections_saved": saved, "sections_with_text": with_text, "generation_section_total": report.generation_section_total, "generation_elapsed_seconds": round(elapsed, 1) if elapsed is not None else None, }