generated from coulomb/repo-seed
Add hourly RecentlyOnScope batch endpoint
This commit is contained in:
@@ -22,8 +22,11 @@ from api.models.task import Task, TaskStatus
|
||||
from api.models.topic import Topic
|
||||
from api.models.workstream import Workstream
|
||||
from api.schemas.recently_on_scope import (
|
||||
RecentlyOnScopeFailedDomain,
|
||||
RecentlyOnScopeHourlyRun,
|
||||
RecentlyOnScopeReportMetadata,
|
||||
RecentlyOnScopeSourceCounts,
|
||||
RecentlyOnScopeSkippedDomain,
|
||||
)
|
||||
from api.services.markitect_templates import inspect_markdown_template, render_markdown_template
|
||||
|
||||
@@ -157,6 +160,68 @@ async def generate_report(
|
||||
window: DigestWindow,
|
||||
) -> tuple[RecentlyOnScopeReportMetadata, str]:
|
||||
data = await collect_domain_activity(session, domain_slug, window)
|
||||
return _render_report_from_data(data, window)
|
||||
|
||||
|
||||
async def generate_hourly_reports(
|
||||
session: AsyncSession,
|
||||
window: DigestWindow,
|
||||
*,
|
||||
active_only: bool = True,
|
||||
include_attention: bool = False,
|
||||
) -> RecentlyOnScopeHourlyRun:
|
||||
generated: list[RecentlyOnScopeReportMetadata] = []
|
||||
skipped: list[RecentlyOnScopeSkippedDomain] = []
|
||||
failed: list[RecentlyOnScopeFailedDomain] = []
|
||||
|
||||
domains = await _list_domains(session, active_only=active_only)
|
||||
for domain in domains:
|
||||
try:
|
||||
data = await collect_domain_activity(session, domain.slug, window)
|
||||
counts = RecentlyOnScopeSourceCounts(**data["source_counts"])
|
||||
if not _has_qualifying_activity(counts, include_attention=include_attention):
|
||||
skipped.append(
|
||||
RecentlyOnScopeSkippedDomain(
|
||||
domain_slug=domain.slug,
|
||||
reason="no qualifying activity in window",
|
||||
source_counts=counts,
|
||||
)
|
||||
)
|
||||
continue
|
||||
metadata, _markdown = _render_report_from_data(data, window)
|
||||
generated.append(metadata)
|
||||
except Exception as exc: # pragma: no cover - exercised via router tests
|
||||
failed.append(RecentlyOnScopeFailedDomain(domain_slug=domain.slug, error=str(exc)))
|
||||
|
||||
generated_at = datetime.now(tz=UTC)
|
||||
progress_event_id = await _log_hourly_progress(
|
||||
session,
|
||||
window,
|
||||
generated_at=generated_at,
|
||||
active_only=active_only,
|
||||
include_attention=include_attention,
|
||||
generated=generated,
|
||||
skipped=skipped,
|
||||
failed=failed,
|
||||
)
|
||||
return RecentlyOnScopeHourlyRun(
|
||||
range=window.range,
|
||||
since=window.since,
|
||||
until=window.until,
|
||||
generated_at=generated_at,
|
||||
active_only=active_only,
|
||||
include_attention=include_attention,
|
||||
generated=generated,
|
||||
skipped=skipped,
|
||||
failed=failed,
|
||||
progress_event_id=progress_event_id,
|
||||
)
|
||||
|
||||
|
||||
def _render_report_from_data(
|
||||
data: dict[str, Any],
|
||||
window: DigestWindow,
|
||||
) -> tuple[RecentlyOnScopeReportMetadata, str]:
|
||||
tmpl = template_path()
|
||||
inspect_markdown_template(tmpl)
|
||||
markdown = render_markdown_template(tmpl, data)
|
||||
@@ -207,6 +272,65 @@ def metadata_from_report(path: Path) -> RecentlyOnScopeReportMetadata | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _list_domains(session: AsyncSession, *, active_only: bool) -> list[Domain]:
|
||||
stmt = select(Domain).order_by(Domain.slug)
|
||||
if active_only:
|
||||
stmt = stmt.where(Domain.status == "active")
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
def _has_qualifying_activity(
|
||||
counts: RecentlyOnScopeSourceCounts,
|
||||
*,
|
||||
include_attention: bool,
|
||||
) -> bool:
|
||||
if (
|
||||
counts.progress_events
|
||||
or counts.decisions
|
||||
or counts.workstreams
|
||||
or counts.tasks
|
||||
):
|
||||
return True
|
||||
return include_attention and counts.attention_items > 0
|
||||
|
||||
|
||||
async def _log_hourly_progress(
|
||||
session: AsyncSession,
|
||||
window: DigestWindow,
|
||||
*,
|
||||
generated_at: datetime,
|
||||
active_only: bool,
|
||||
include_attention: bool,
|
||||
generated: list[RecentlyOnScopeReportMetadata],
|
||||
skipped: list[RecentlyOnScopeSkippedDomain],
|
||||
failed: list[RecentlyOnScopeFailedDomain],
|
||||
) -> uuid.UUID:
|
||||
event = ProgressEvent(
|
||||
event_type="recently_on_scope_hourly",
|
||||
summary=(
|
||||
"RecentlyOnScope hourly batch completed: "
|
||||
f"{len(generated)} generated, {len(skipped)} skipped, {len(failed)} failed"
|
||||
),
|
||||
detail={
|
||||
"range": window.range,
|
||||
"since": _iso(window.since),
|
||||
"until": _iso(window.until),
|
||||
"generated_at": _iso(generated_at),
|
||||
"active_only": active_only,
|
||||
"include_attention": include_attention,
|
||||
"generated": [item.model_dump(mode="json") for item in generated],
|
||||
"skipped": [item.model_dump(mode="json") for item in skipped],
|
||||
"failed": [item.model_dump(mode="json") for item in failed],
|
||||
},
|
||||
author="state-hub",
|
||||
)
|
||||
session.add(event)
|
||||
await session.commit()
|
||||
await session.refresh(event)
|
||||
return event.id
|
||||
|
||||
|
||||
async def _get_domain(domain_slug: str, session: AsyncSession) -> Domain:
|
||||
result = await session.execute(select(Domain).where(Domain.slug == domain_slug))
|
||||
domain = result.scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user