File size: 4,116 Bytes
da6ee5e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """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)
|