Some checks failed
CI / lint (pull_request) Successful in 33s
CI / typecheck (pull_request) Failing after 1m42s
CI / test (pull_request) Failing after 2m33s
CI / security (pull_request) Failing after 39s
CI / gitleaks (pull_request) Successful in 36s
CI / commitlint (pull_request) Failing after 10s
152 lines
4.7 KiB
Python
152 lines
4.7 KiB
Python
"""Pry — Review router (remaining api.py routes).
|
|
|
|
Auto-extracted from api.py during the router-split refactor.
|
|
"""
|
|
|
|
# SPDX-License-Identifier: MIT
|
|
# Copyright (c) 2026 Rug Munch Media LLC
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Body
|
|
|
|
from deps import scraper
|
|
from errors import NotFoundError, ScrapeError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["Review"])
|
|
|
|
|
|
@router.post("/v1/review/submit", tags=["Review"], summary="Submit extracted data for human review")
|
|
async def review_submit(
|
|
data: dict[str, Any] = Body(...),
|
|
url: str = Body(...),
|
|
schema_name: str | None = Body(None),
|
|
confidence_score: float = Body(0.0),
|
|
flagged_fields: list[dict[str, Any]] | None = Body(None),
|
|
) -> dict[str, Any]:
|
|
"""Submit extracted data for human review before delivery.
|
|
|
|
Use this when extraction confidence is low or anomalies were detected.
|
|
Data will be held in the review queue until approved or rejected.
|
|
"""
|
|
from review import submit_for_review
|
|
|
|
result = await submit_for_review(
|
|
data=data,
|
|
extraction_url=url,
|
|
schema_name=schema_name,
|
|
confidence_score=confidence_score,
|
|
flagged_fields=flagged_fields,
|
|
)
|
|
return {"success": True, "data": result}
|
|
|
|
|
|
@router.post("/v1/review/{review_id}/approve", tags=["Review"], summary="Approve a review item")
|
|
async def review_approve(
|
|
review_id: str,
|
|
reviewer: str = Body("api"),
|
|
notes: str = Body(""),
|
|
) -> dict[str, Any]:
|
|
"""Approve a review item, allowing data to proceed to delivery."""
|
|
from review import approve_review
|
|
|
|
result = await approve_review(review_id, reviewer, notes)
|
|
if "error" in result:
|
|
raise NotFoundError(result["error"])
|
|
return {"success": True, "data": result}
|
|
|
|
|
|
@router.post("/v1/review/{review_id}/reject", tags=["Review"], summary="Reject a review item")
|
|
async def review_reject(
|
|
review_id: str,
|
|
reviewer: str = Body("api"),
|
|
notes: str = Body(""),
|
|
) -> dict[str, Any]:
|
|
"""Reject a review item, blocking data delivery."""
|
|
from review import reject_review
|
|
|
|
result = await reject_review(review_id, reviewer, notes)
|
|
if "error" in result:
|
|
raise NotFoundError(result["error"])
|
|
return {"success": True, "data": result}
|
|
|
|
|
|
@router.get("/v1/reviews", tags=["Review"], summary="List reviews in the queue")
|
|
async def list_reviews(status: str | None = None) -> dict[str, Any]:
|
|
"""List reviews, optionally filtered by status (pending/approved/rejected)."""
|
|
from review import get_review_queue
|
|
|
|
reviews = get_review_queue(status)
|
|
return {"success": True, "data": {"reviews": reviews, "total": len(reviews)}}
|
|
|
|
|
|
@router.get("/v1/review/{review_id}", tags=["Review"], summary="Get review details")
|
|
async def get_review(review_id: str) -> dict[str, Any]:
|
|
"""Get full details of a review item including the data payload."""
|
|
from review import get_review_detail
|
|
|
|
result = get_review_detail(review_id)
|
|
if not result:
|
|
raise NotFoundError(f"Review not found: {review_id}")
|
|
return {"success": True, "data": result}
|
|
|
|
|
|
@router.post(
|
|
"/v1/extract-with-review",
|
|
tags=["Review"],
|
|
summary="Extract with automatic human review routing",
|
|
)
|
|
async def extract_with_review(
|
|
url: str = Body(...),
|
|
schema: dict[str, Any] | None = Body(None),
|
|
expected_types: dict[str, str] | None = Body(None),
|
|
slack_webhook: str = Body(""),
|
|
auto_approve_threshold: float = Body(0.8),
|
|
auto_reject_threshold: float = Body(0.2),
|
|
) -> dict[str, Any]:
|
|
"""Extract data with automatic quality check and human review routing.
|
|
|
|
High-confidence results are auto-approved.
|
|
Low-confidence results are auto-rejected.
|
|
Medium-confidence results go to the human review queue with Slack notification.
|
|
"""
|
|
from quality import run_quality_check
|
|
from review import auto_review_threshold
|
|
|
|
# Scrape
|
|
scrape_result = await scraper.scrape(url, {"bypass_cloudflare": True})
|
|
if scrape_result.get("status") != "ok":
|
|
raise ScrapeError(scrape_result.get("error") or "Scrape failed")
|
|
|
|
data = scrape_result
|
|
|
|
# Quality check
|
|
quality = await run_quality_check(
|
|
url=url,
|
|
data=data,
|
|
schema=schema,
|
|
expected_types=None,
|
|
)
|
|
|
|
# Auto-route
|
|
decision = await auto_review_threshold(
|
|
data=data,
|
|
extraction_url=url,
|
|
quality_result=quality,
|
|
slack_webhook=slack_webhook,
|
|
auto_approve_threshold=auto_approve_threshold,
|
|
auto_reject_threshold=auto_reject_threshold,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"decision": decision,
|
|
"quality": {k: v for k, v in quality.items() if k != "url"},
|
|
},
|
|
}
|