RICS / app /services /generation_progress.py
StormShadow308's picture
Speed up generation and ingestion; add live report progress on /status.
c893230
Raw
History Blame Contribute Delete
1.98 kB
"""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,
}