Benard John
feat: implement full-stack architecture with database models, authentication services, and web dashboard components
37b5223 | """ | |
| 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"]) | |
| 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, | |
| ) | |
| 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."} | |