125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
"""Query helpers (part B) for AI PMO cadence service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .shared import (
|
|
Any,
|
|
Budget,
|
|
Notification,
|
|
Optional,
|
|
SignalSourceRegistry,
|
|
UserProject,
|
|
UserWorkload,
|
|
and_,
|
|
select,
|
|
)
|
|
|
|
|
|
class CadenceQueriesBMixin:
|
|
"""Budget/capacity/signal-source/notification query helpers."""
|
|
|
|
async def _list_budget_hotspots(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(Budget).where(Budget.tenant_id == self.tenant_id)
|
|
if project_id is not None:
|
|
stmt = stmt.where(Budget.project_id == int(project_id))
|
|
rows = (await self.db.execute(stmt)).scalars().all()
|
|
hotspots = []
|
|
for row in rows:
|
|
amount = float(row.budget_amount_usd or 0.0)
|
|
usage = float(row.current_usage_usd or 0.0)
|
|
ratio = (usage / amount) if amount > 0 else 0.0
|
|
if ratio >= 0.8:
|
|
hotspots.append(
|
|
{
|
|
"budget_id": int(row.id),
|
|
"project_id": row.project_id,
|
|
"usage_ratio": round(ratio, 4),
|
|
"hard_limit_enabled": bool(row.hard_limit_enabled),
|
|
}
|
|
)
|
|
return sorted(hotspots, key=lambda item: item["usage_ratio"], reverse=True)
|
|
|
|
async def _list_capacity_hotspots(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(UserWorkload).where(
|
|
and_(
|
|
UserWorkload.tenant_id == self.tenant_id,
|
|
UserWorkload.workload_ratio >= 0.85,
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(
|
|
UserWorkload.user_id.in_(
|
|
select(UserProject.user_id).where(
|
|
and_(
|
|
UserProject.tenant_id == self.tenant_id,
|
|
UserProject.project_id == int(project_id),
|
|
)
|
|
)
|
|
)
|
|
)
|
|
rows = (await self.db.execute(stmt.limit(50))).scalars().all()
|
|
return [
|
|
{
|
|
"user_id": int(row.user_id),
|
|
"workload_ratio": float(row.workload_ratio or 0.0),
|
|
"current_assigned_hours": float(row.current_assigned_hours or 0.0),
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
async def _list_pending_signal_sources(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(SignalSourceRegistry).where(
|
|
and_(
|
|
SignalSourceRegistry.tenant_id == self.tenant_id,
|
|
SignalSourceRegistry.onboarding_status == "pending_manual",
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(SignalSourceRegistry.project_id == int(project_id))
|
|
rows = (
|
|
await self.db.execute(
|
|
stmt.order_by(SignalSourceRegistry.updated_at.desc()).limit(50)
|
|
)
|
|
).scalars().all()
|
|
return [
|
|
{
|
|
"registry_id": int(row.id),
|
|
"project_id": int(row.project_id),
|
|
"source_kind": str(row.source_kind or ""),
|
|
"source_name": str(row.source_name or ""),
|
|
"status": str(row.status or ""),
|
|
"onboarding_status": str(row.onboarding_status or ""),
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
async def _list_unread_risk_notifications(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(Notification).where(
|
|
and_(
|
|
Notification.tenant_id == self.tenant_id,
|
|
Notification.is_read.is_(False),
|
|
Notification.kind.in_(["risk", "alert", "action_required"]),
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(
|
|
and_(
|
|
Notification.entity_type == "project",
|
|
Notification.entity_id == int(project_id),
|
|
)
|
|
)
|
|
rows = (
|
|
await self.db.execute(
|
|
stmt.order_by(Notification.updated_at.desc()).limit(50)
|
|
)
|
|
).scalars().all()
|
|
return [
|
|
{
|
|
"id": int(row.id),
|
|
"title": str(row.title or ""),
|
|
"status": str(row.status or ""),
|
|
"priority": str(row.priority or ""),
|
|
}
|
|
for row in rows
|
|
]
|