85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Album evidence and naming-proposal API (US03-01/US03-03).
|
|
|
|
Proposals are read, edited, and approved by album label. Edits and approvals carry an
|
|
``expected_version``; a stale version returns 409 without mutating anything. Approval
|
|
records intent only — no rename happens in this phase.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from photo_pipeline.schemas import (
|
|
ApproveProposalRequest,
|
|
EditProposalRequest,
|
|
GenerateProposalsRequest,
|
|
)
|
|
from photo_pipeline.services.albums import AlbumService
|
|
from photo_pipeline.services.proposals import ConflictError, ProposalError, ProposalService
|
|
|
|
router = APIRouter(tags=["albums"])
|
|
|
|
|
|
def _roots(request: Request) -> tuple:
|
|
return tuple(request.app.state.config.library_roots)
|
|
|
|
|
|
def _albums(request: Request) -> AlbumService:
|
|
return AlbumService(request.app.state.session_factory, library_roots=_roots(request))
|
|
|
|
|
|
def _proposals(request: Request) -> ProposalService:
|
|
return ProposalService(request.app.state.session_factory, library_roots=_roots(request))
|
|
|
|
|
|
def _error(status: int, code: str, message: str) -> JSONResponse:
|
|
return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}})
|
|
|
|
|
|
@router.get("/albums/evidence")
|
|
def evidence(
|
|
request: Request,
|
|
offset: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
) -> dict:
|
|
return _albums(request).aggregate_evidence(offset=offset, limit=limit)
|
|
|
|
|
|
@router.post("/albums/proposals")
|
|
def generate(body: GenerateProposalsRequest, request: Request) -> dict:
|
|
return _proposals(request).generate(body.albums)
|
|
|
|
|
|
@router.get("/albums/proposals")
|
|
def list_proposals(request: Request, status: str | None = None) -> dict:
|
|
return _proposals(request).list(status=status)
|
|
|
|
|
|
@router.get("/albums/proposals/{album:path}")
|
|
def get_proposal(album: str, request: Request):
|
|
data = _proposals(request).get(album)
|
|
if data is None:
|
|
return _error(404, "not_found", f"no proposal for album {album!r}")
|
|
return data
|
|
|
|
|
|
@router.post("/albums/proposals/{album:path}/edit")
|
|
def edit_proposal(album: str, body: EditProposalRequest, request: Request):
|
|
try:
|
|
return _proposals(request).edit(album, body.name, expected_version=body.expected_version)
|
|
except ConflictError as error:
|
|
return _error(409, "version_conflict", str(error))
|
|
except ProposalError as error:
|
|
return _error(422, "invalid_proposal", str(error))
|
|
|
|
|
|
@router.post("/albums/proposals/{album:path}/approve")
|
|
def approve_proposal(album: str, body: ApproveProposalRequest, request: Request):
|
|
try:
|
|
return _proposals(request).approve(album, expected_version=body.expected_version)
|
|
except ConflictError as error:
|
|
return _error(409, "version_conflict", str(error))
|
|
except ProposalError as error:
|
|
return _error(422, "invalid_proposal", str(error))
|