Sheetly / app /api /batches.py
nvikou's picture
initial commit
da6ee5e verified
Raw History Blame Contribute Delete
3.14 kB
"""Batch upload API."""
from __future__ import annotations
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.serializers import batch_out, document_out
from app.db import get_session
from app.models import Batch, Document
from app.schemas import BatchOut, DocStatus, DocumentOut
from app.services.ingest import ensure_upload_dir, save_upload
from app.services.paths import resolve_stored_path, to_stored_path
from app.services.queue import enqueue_batch
router = APIRouter(prefix="/api/batches", tags=["batches"])
@router.post("", response_model=BatchOut)
async def create_batch(
files: list[UploadFile] = File(...),
session: AsyncSession = Depends(get_session),
) -> BatchOut:
"""Upload one or more files / ZIPs as a batch."""
if not files:
raise HTTPException(400, "No files uploaded")
batch_id = str(uuid.uuid4())
root = ensure_upload_dir()
batch_dir = root / batch_id
batch_dir.mkdir(parents=True, exist_ok=True)
saved: list[tuple[str, Path, str]] = []
errors: list[str] = []
for upload in files:
try:
saved.extend(await save_upload(upload, batch_dir))
except ValueError as exc:
errors.append(str(exc))
if not saved:
raise HTTPException(
400,
"; ".join(errors) or "No valid files",
)
batch = Batch(
id=batch_id,
status="pending",
total=len(saved),
processed=0,
failed=0,
)
session.add(batch)
for name, path, mime in saved:
session.add(
Document(
id=str(uuid.uuid4()),
batch_id=batch_id,
filename=name,
file_path=to_stored_path(path),
mime_type=mime,
status=DocStatus.QUEUED.value,
)
)
await session.commit()
await session.refresh(batch)
enqueue_batch(batch_id)
return batch_out(batch)
@router.get("/{batch_id}", response_model=BatchOut)
async def get_batch(
batch_id: str,
session: AsyncSession = Depends(get_session),
) -> BatchOut:
"""Batch status and progress."""
batch = await session.get(Batch, batch_id)
if not batch:
raise HTTPException(404, "Batch not found")
return batch_out(batch)
@router.get(
"/{batch_id}/documents",
response_model=list[DocumentOut],
)
async def list_batch_documents(
batch_id: str,
session: AsyncSession = Depends(get_session),
) -> list[DocumentOut]:
"""Documents belonging to a batch."""
batch = await session.get(Batch, batch_id)
if not batch:
raise HTTPException(404, "Batch not found")
result = await session.execute(
select(Document).where(Document.batch_id == batch_id)
)
out = []
for d in result.scalars().all():
try:
if resolve_stored_path(d.file_path).is_file():
out.append(document_out(d))
except FileNotFoundError:
continue
return out