130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
"""Query helpers (part A) for AI PMO cadence service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .shared import (
|
|
Any,
|
|
Optional,
|
|
ProjectSignalLink,
|
|
Signal,
|
|
Task,
|
|
WorkItem,
|
|
and_,
|
|
func,
|
|
or_,
|
|
select,
|
|
utc_now_naive,
|
|
)
|
|
|
|
|
|
class CadenceQueriesAMixin:
|
|
"""Work item/task/signal query helpers."""
|
|
|
|
async def _list_recent_work_items(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(WorkItem).where(
|
|
and_(
|
|
WorkItem.tenant_id == self.tenant_id,
|
|
WorkItem.created_at >= since,
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(WorkItem.project_id == int(project_id))
|
|
rows = (await self.db.execute(stmt.order_by(WorkItem.created_at.desc()).limit(50))).scalars().all()
|
|
return [
|
|
{
|
|
"id": int(row.id),
|
|
"state": row.state,
|
|
"intent": row.intent,
|
|
"project_id": row.project_id,
|
|
"signal_id": row.signal_id,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
async def _count_work_items(self, *, state: str, project_id: Optional[int]) -> int:
|
|
stmt = select(func.count(WorkItem.id)).where(
|
|
and_(
|
|
WorkItem.tenant_id == self.tenant_id,
|
|
WorkItem.state == state,
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(WorkItem.project_id == int(project_id))
|
|
return int((await self.db.execute(stmt)).scalar_one() or 0)
|
|
|
|
async def _list_blocked_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(Task).where(
|
|
and_(
|
|
Task.tenant_id == self.tenant_id,
|
|
Task.status == "blocked",
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(Task.project_id == int(project_id))
|
|
rows = (await self.db.execute(stmt.order_by(Task.updated_at.desc()).limit(50))).scalars().all()
|
|
return [
|
|
{
|
|
"id": int(row.id),
|
|
"project_id": int(row.project_id),
|
|
"title": str(row.title or ""),
|
|
"priority": str(row.priority or ""),
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
async def _list_overdue_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
now = utc_now_naive()
|
|
stmt = select(Task).where(
|
|
and_(
|
|
Task.tenant_id == self.tenant_id,
|
|
Task.due_date.is_not(None),
|
|
Task.due_date < now,
|
|
Task.status.not_in(["done", "completed", "closed"]),
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(Task.project_id == int(project_id))
|
|
rows = (await self.db.execute(stmt.order_by(Task.due_date.asc()).limit(50))).scalars().all()
|
|
return [
|
|
{
|
|
"id": int(row.id),
|
|
"project_id": int(row.project_id),
|
|
"title": str(row.title or ""),
|
|
"due_date": row.due_date.isoformat() if row.due_date else None,
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
async def _list_recent_signals(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]:
|
|
stmt = select(Signal).where(
|
|
and_(
|
|
Signal.tenant_id == self.tenant_id,
|
|
Signal.created_at >= since,
|
|
)
|
|
)
|
|
if project_id is not None:
|
|
stmt = stmt.where(
|
|
or_(
|
|
Signal.matched_project_id == int(project_id),
|
|
Signal.id.in_(
|
|
select(ProjectSignalLink.signal_id).where(
|
|
and_(
|
|
ProjectSignalLink.tenant_id == self.tenant_id,
|
|
ProjectSignalLink.project_id == int(project_id),
|
|
)
|
|
)
|
|
),
|
|
)
|
|
)
|
|
rows = (await self.db.execute(stmt.order_by(Signal.created_at.desc()).limit(50))).scalars().all()
|
|
return [
|
|
{
|
|
"id": int(row.id),
|
|
"title": str(row.title or ""),
|
|
"status": str(row.status or ""),
|
|
"signal_type": str(row.signal_type or ""),
|
|
}
|
|
for row in rows
|
|
]
|