"""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())