80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""Duplicate cluster review API: list, detail, and decision.
|
|
|
|
Decisions carry an ``expected_version``; a stale version returns 409 without
|
|
mutating anything, so a browser acting on out-of-date state cannot overwrite a
|
|
newer decision.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from photo_pipeline.schemas import DecisionRequest
|
|
from photo_pipeline.services.duplicates import (
|
|
MAX_MEMBER_PAGE,
|
|
MEMBER_PAGE,
|
|
ConflictError,
|
|
DuplicateError,
|
|
DuplicateService,
|
|
)
|
|
|
|
router = APIRouter(tags=["duplicates"])
|
|
|
|
|
|
def _service(request: Request) -> DuplicateService:
|
|
return DuplicateService(request.app.state.session_factory)
|
|
|
|
|
|
def _error(status: int, code: str, message: str) -> JSONResponse:
|
|
return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}})
|
|
|
|
|
|
@router.post("/duplicates/detect")
|
|
def detect(request: Request) -> dict:
|
|
"""Run duplicate detection over the current assets (synchronous in Phase A)."""
|
|
report = _service(request).detect()
|
|
return {"clusters": len(report.clusters), "counts": report.counts}
|
|
|
|
|
|
@router.get("/duplicates/clusters")
|
|
def list_clusters(
|
|
request: Request,
|
|
state: str | None = None,
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
) -> dict:
|
|
return _service(request).list_clusters(state=state, limit=limit, offset=offset)
|
|
|
|
|
|
@router.get("/duplicates/clusters/{cluster_id}")
|
|
def get_cluster(
|
|
cluster_id: str,
|
|
request: Request,
|
|
limit: int = Query(MEMBER_PAGE, ge=1, le=MAX_MEMBER_PAGE),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
detail = _service(request).get_cluster(cluster_id, limit=limit, offset=offset)
|
|
if detail is None:
|
|
return _error(404, "not_found", f"unknown cluster {cluster_id}")
|
|
return detail
|
|
|
|
|
|
@router.post("/duplicates/clusters/{cluster_id}/decision")
|
|
def decide(cluster_id: str, body: DecisionRequest, request: Request):
|
|
service = _service(request)
|
|
if service.get_cluster(cluster_id) is None:
|
|
return _error(404, "not_found", f"unknown cluster {cluster_id}")
|
|
try:
|
|
return service.decide(
|
|
cluster_id,
|
|
body.decision,
|
|
canonical_asset_id=body.canonical_asset_id,
|
|
expected_version=body.expected_version,
|
|
reason=body.reason,
|
|
)
|
|
except ConflictError as error:
|
|
return _error(409, "version_conflict", str(error))
|
|
except DuplicateError as error:
|
|
return _error(422, "invalid_decision", str(error))
|