Files
photoanalyzer/photo_pipeline/api/routes/albums.py

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