File size: 3,772 Bytes
42a0d15 37b5223 42a0d15 37b5223 42a0d15 37b5223 42a0d15 37b5223 42a0d15 37b5223 42a0d15 37b5223 42a0d15 | 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 | """
Ukweli — Document Catalog Endpoint
GET /documents — Browse and filter the document registry.
Public tier (IP rate-limited) per Endpoint Security Matrix.
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.auth import AuthContext, require_auth
from app.db.session import get_db_session
from app.models.database import Document
from app.models.schemas import DocumentListResponse, DocumentOut
logger = logging.getLogger("ukweli.api.documents")
router = APIRouter(tags=["Documents"])
@router.get("/documents", response_model=DocumentListResponse)
async def list_documents(
auditee: str | None = Query(None, description="Filter by auditee name (partial match)"),
fy: str | None = Query(None, description="Filter by fiscal year, e.g. 2024/25"),
report_type: str | None = Query(None, description="Filter by report type enum value"),
status: str | None = Query(None, description="Filter by processing status"),
limit: int = Query(20, ge=1, le=100, description="Number of results to return"),
offset: int = Query(0, ge=0, description="Pagination offset"),
db: AsyncSession = Depends(get_db_session),
_auth: AuthContext = Depends(require_auth), # public tier rate limit
) -> DocumentListResponse:
"""
List documents from the catalog with optional filters.
Supports pagination via limit/offset.
No authentication required, but public-tier rate limits apply.
"""
query = select(Document)
count_query = select(func.count(Document.id))
# Apply filters
if auditee:
query = query.where(Document.auditee.ilike(f"%{auditee}%"))
count_query = count_query.where(Document.auditee.ilike(f"%{auditee}%"))
if fy:
query = query.where(Document.fiscal_year == fy)
count_query = count_query.where(Document.fiscal_year == fy)
if report_type:
query = query.where(Document.report_type == report_type)
count_query = count_query.where(Document.report_type == report_type)
if status:
query = query.where(Document.status == status)
count_query = count_query.where(Document.status == status)
# Get total count
total_result = await db.execute(count_query)
total = total_result.scalar_one()
# Apply pagination and ordering
query = query.order_by(Document.published_date.desc().nullslast(), Document.created_at.desc())
query = query.limit(limit).offset(offset)
result = await db.execute(query)
documents = result.scalars().all()
return DocumentListResponse(
documents=[
DocumentOut(
id=doc.id,
title=doc.title,
fiscal_year=doc.fiscal_year,
auditee=doc.auditee,
report_type=doc.report_type,
page_count=doc.page_count,
download_url=doc.pdf_url,
published_date=doc.published_date,
status=doc.status,
)
for doc in documents
],
total=total,
limit=limit,
offset=offset,
)
@router.post("/documents/trigger-scraper", status_code=202)
async def trigger_scraper(
max_pages: int | None = Query(None, description="Max pages to crawl"),
_auth: AuthContext = Depends(require_auth), # public tier rate limit
) -> dict[str, str]:
"""
Trigger the OAG Kenya spider to discover and crawl new reports.
Runs asynchronously in the background.
"""
from app.scraper.workers.pdf_processor import run_scraper
run_scraper.delay(max_pages=max_pages)
return {"status": "triggered", "message": "Scraper task has been queued."}
|