PMFlow/backend/app/services/ai_pmo_cadence/mixin_snapshots.py

81 lines
2.9 KiB
Python

"""Snapshot persistence APIs for AI PMO cadence service."""
from __future__ import annotations
from .shared import Any, Optional, PortfolioSnapshot, and_, select
class CadenceSnapshotsMixin:
"""Generates and retrieves cadence snapshots."""
async def generate_snapshot(
self,
*,
cadence_type: str,
project_id: Optional[int] = None,
) -> dict[str, Any]:
normalized_type = str(cadence_type or "").strip().lower()
if normalized_type not in self.SUPPORTED_CADENCE_TYPES:
raise ValueError("unsupported cadence_type")
if normalized_type == "daily_standup":
payload = await self._build_daily_standup(project_id=project_id)
elif normalized_type == "weekly_review":
payload = await self._build_weekly_review(project_id=project_id)
elif normalized_type == "risk_review":
payload = await self._build_risk_review(project_id=project_id)
else:
payload = await self._build_milestone_check(project_id=project_id)
scope_ref = f"project:{int(project_id)}" if project_id is not None else f"tenant:{self.tenant_id}"
row = PortfolioSnapshot(
tenant_id=self.tenant_id,
scope_type=normalized_type[:20],
scope_ref=scope_ref[:64],
summary=payload.get("summary"),
payload=payload,
)
self.db.add(row)
await self.db.flush()
return {
"id": int(row.id),
"scope_type": row.scope_type,
"scope_ref": row.scope_ref,
"summary": row.summary,
"payload": row.payload,
"created_at": row.created_at.isoformat() if row.created_at else None,
}
async def get_latest_snapshot(
self,
*,
cadence_type: str,
project_id: Optional[int] = None,
) -> Optional[dict[str, Any]]:
normalized_type = str(cadence_type or "").strip().lower()
scope_ref = f"project:{int(project_id)}" if project_id is not None else f"tenant:{self.tenant_id}"
row = (
await self.db.execute(
select(PortfolioSnapshot)
.where(
and_(
PortfolioSnapshot.tenant_id == self.tenant_id,
PortfolioSnapshot.scope_type == normalized_type,
PortfolioSnapshot.scope_ref == scope_ref,
)
)
.order_by(PortfolioSnapshot.id.desc())
.limit(1)
)
).scalar_one_or_none()
if row is None:
return None
return {
"id": int(row.id),
"scope_type": row.scope_type,
"scope_ref": row.scope_ref,
"summary": row.summary,
"payload": row.payload,
"created_at": row.created_at.isoformat() if row.created_at else None,
}