Sheetly / app /api /documents.py
nvikou's picture
initial commit
da6ee5e verified
Raw History Blame Contribute Delete
4.12 kB
"""Document CRUD and validation API."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.serializers import document_out
from app.db import get_session
from app.models import Document
from app.schemas import DocStatus, DocumentOut, DocumentUpdate
from app.services.cleanup import cleanup_orphan_documents
from app.services.paths import resolve_stored_path
from app.services.validation import (
apply_duplicate_flags,
apply_post_extract_checks,
average_from_detail,
mark_fields_confirmed,
)
router = APIRouter(prefix="/api/documents", tags=["documents"])
@router.get("", response_model=list[DocumentOut])
async def list_documents(
status: str | None = None,
doc_type: str | None = None,
limit: int = Query(100, ge=1, le=500),
session: AsyncSession = Depends(get_session),
) -> list[DocumentOut]:
"""List processed documents (files that still exist)."""
stmt = select(Document).order_by(Document.created_at.desc())
if status:
stmt = stmt.where(Document.status == status)
if doc_type:
stmt = stmt.where(Document.doc_type == doc_type)
stmt = stmt.limit(limit * 5)
result = await session.execute(stmt)
docs = []
for d in result.scalars().all():
if len(docs) >= limit:
break
try:
if resolve_stored_path(d.file_path).is_file():
docs.append(document_out(d))
except FileNotFoundError:
continue
return docs
@router.post("/cleanup")
async def cleanup_documents(
session: AsyncSession = Depends(get_session),
) -> dict[str, int]:
"""Remove orphan DB rows whose files were deleted."""
return await cleanup_orphan_documents(session)
@router.get("/{doc_id}", response_model=DocumentOut)
async def get_document(
doc_id: str,
session: AsyncSession = Depends(get_session),
) -> DocumentOut:
"""Fetch one document."""
doc = await session.get(Document, doc_id)
if not doc:
raise HTTPException(404, "Document not found")
return document_out(doc)
@router.get("/{doc_id}/file")
async def get_document_file(
doc_id: str,
session: AsyncSession = Depends(get_session),
) -> FileResponse:
"""Serve original uploaded file for validation UI."""
doc = await session.get(Document, doc_id)
if not doc:
raise HTTPException(404, "Document not found")
try:
path = resolve_stored_path(doc.file_path)
except FileNotFoundError as exc:
raise HTTPException(
404,
"Original file is no longer available",
) from exc
if not path.is_file():
raise HTTPException(
404,
"Original file is no longer available",
)
return FileResponse(
path,
filename=doc.filename,
media_type=doc.mime_type or "application/octet-stream",
content_disposition_type="inline",
)
@router.patch("/{doc_id}", response_model=DocumentOut)
async def update_document(
doc_id: str,
body: DocumentUpdate,
session: AsyncSession = Depends(get_session),
) -> DocumentOut:
"""Apply manual corrections and re-run checks."""
doc = await session.get(Document, doc_id)
if not doc:
raise HTTPException(404, "Document not found")
if body.doc_type:
doc.doc_type = body.doc_type
doc.extracted = body.extracted
issues, is_dup, dup_of = await apply_post_extract_checks(
session,
document_id=doc.id,
doc_type=doc.doc_type,
data=body.extracted,
)
apply_duplicate_flags(
doc,
is_duplicate=is_dup,
duplicate_of=dup_of,
)
conf = mark_fields_confirmed(body.extracted, doc.confidence)
doc.confidence = conf
doc.avg_confidence = average_from_detail(conf) or 1.0
doc.issues = issues
doc.status = DocStatus.VALIDATED.value
await session.commit()
await session.refresh(doc)
return document_out(doc)