You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
"""Review queue: CRUD operations for ReviewItem."""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from libs.common.logging import get_logger
|
|
from libs.common.time_utils import utc_now
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
_PRIORITY_ORDER = {"P0": 0, "P1": 1, "P2": 2}
|
|
|
|
|
|
async def create_review_item(
|
|
session: AsyncSession,
|
|
entity_type: str,
|
|
entity_id: str,
|
|
priority: str,
|
|
reason_codes: list[str],
|
|
snapshot_refs: dict[str, Any],
|
|
suggested_overrides: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
"""Create a ReviewItem, or update an existing open one (deduplicated).
|
|
|
|
Args:
|
|
session: Async DB session.
|
|
entity_type: "parser_event" | "feature_record" | "label_record".
|
|
entity_id: ID of the entity to review.
|
|
priority: "P0" | "P1" | "P2".
|
|
reason_codes: List of reason code strings.
|
|
snapshot_refs: Dict of snapshot references.
|
|
suggested_overrides: Optional suggested field overrides.
|
|
|
|
Returns:
|
|
ReviewItem ORM instance.
|
|
"""
|
|
from libs.db.models import ReviewItem
|
|
|
|
# Check for existing open item
|
|
result = await session.execute(
|
|
select(ReviewItem).where(
|
|
ReviewItem.entity_type == entity_type,
|
|
ReviewItem.entity_id == entity_id,
|
|
ReviewItem.status == "open",
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing is not None:
|
|
# Update existing: escalate priority if needed, merge reason_codes
|
|
existing_priority_rank = _PRIORITY_ORDER.get(existing.priority, 99)
|
|
new_priority_rank = _PRIORITY_ORDER.get(priority, 99)
|
|
if new_priority_rank < existing_priority_rank:
|
|
existing.priority = priority # type: ignore[assignment]
|
|
|
|
existing_codes: list[str] = list(existing.reason_codes or [])
|
|
for code in reason_codes:
|
|
if code not in existing_codes:
|
|
existing_codes.append(code)
|
|
existing.reason_codes = existing_codes # type: ignore[assignment]
|
|
|
|
if suggested_overrides:
|
|
existing.suggested_overrides = suggested_overrides # type: ignore[assignment]
|
|
|
|
existing.updated_at_utc = utc_now() # type: ignore[assignment]
|
|
await session.flush()
|
|
logger.info("review_item_updated", entity_id=entity_id, entity_type=entity_type)
|
|
return existing
|
|
|
|
item = ReviewItem(
|
|
review_id=uuid.uuid4(),
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
priority=priority,
|
|
reason_codes=reason_codes,
|
|
status="open",
|
|
snapshot_refs=snapshot_refs,
|
|
suggested_overrides=suggested_overrides,
|
|
created_at_utc=utc_now(),
|
|
updated_at_utc=utc_now(),
|
|
)
|
|
session.add(item)
|
|
await session.flush()
|
|
logger.info("review_item_created", entity_id=entity_id, priority=priority, reasons=reason_codes)
|
|
return item
|
|
|
|
|
|
async def resolve_review_item(
|
|
session: AsyncSession,
|
|
review_id: str | uuid.UUID,
|
|
reviewer_id: str,
|
|
resolution_type: str,
|
|
root_cause: str,
|
|
notes: str,
|
|
field_overrides: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
"""Resolve a ReviewItem.
|
|
|
|
Args:
|
|
session: Async DB session.
|
|
review_id: UUID of the ReviewItem to resolve.
|
|
reviewer_id: ID/name of the reviewer.
|
|
resolution_type: E.g. "accepted", "corrected", "wont_fix".
|
|
root_cause: Short description of root cause.
|
|
notes: Free-text reviewer notes.
|
|
field_overrides: Optional field overrides applied.
|
|
|
|
Returns:
|
|
Resolved ReviewItem.
|
|
|
|
Raises:
|
|
ValueError: If item not found or already resolved.
|
|
"""
|
|
from libs.db.models import ReviewItem
|
|
|
|
if isinstance(review_id, str):
|
|
review_id = uuid.UUID(review_id)
|
|
|
|
result = await session.execute(
|
|
select(ReviewItem).where(ReviewItem.review_id == review_id)
|
|
)
|
|
item = result.scalar_one_or_none()
|
|
|
|
if item is None:
|
|
raise ValueError(f"ReviewItem {review_id} not found")
|
|
if item.status != "open": # type: ignore[comparison-overlap]
|
|
raise ValueError(f"ReviewItem {review_id} is already {item.status}")
|
|
|
|
item.status = "resolved" # type: ignore[assignment]
|
|
item.reviewer_id = reviewer_id # type: ignore[assignment]
|
|
item.resolution_type = resolution_type # type: ignore[assignment]
|
|
item.root_cause = root_cause # type: ignore[assignment]
|
|
item.notes = notes # type: ignore[assignment]
|
|
item.field_overrides = field_overrides # type: ignore[assignment]
|
|
item.resolved_at = utc_now() # type: ignore[assignment]
|
|
item.updated_at_utc = utc_now() # type: ignore[assignment]
|
|
|
|
await session.flush()
|
|
logger.info("review_item_resolved", review_id=str(review_id), resolution_type=resolution_type)
|
|
return item
|
|
|
|
|
|
async def list_review_items(
|
|
session: AsyncSession,
|
|
status: str | None = None,
|
|
priority: str | None = None,
|
|
entity_type: str | None = None,
|
|
) -> list[Any]:
|
|
"""List ReviewItems with optional filters.
|
|
|
|
Args:
|
|
session: Async DB session.
|
|
status: Filter by status ("open", "resolved", etc.).
|
|
priority: Filter by priority ("P0", "P1", "P2").
|
|
entity_type: Filter by entity_type.
|
|
|
|
Returns:
|
|
List of ReviewItem ORM instances.
|
|
"""
|
|
from libs.db.models import ReviewItem
|
|
|
|
stmt = select(ReviewItem)
|
|
if status is not None:
|
|
stmt = stmt.where(ReviewItem.status == status)
|
|
if priority is not None:
|
|
stmt = stmt.where(ReviewItem.priority == priority)
|
|
if entity_type is not None:
|
|
stmt = stmt.where(ReviewItem.entity_type == entity_type)
|
|
|
|
stmt = stmt.order_by(ReviewItem.created_at_utc.desc())
|
|
result = await session.execute(stmt)
|
|
return list(result.scalars().all())
|