diff --git a/app/api/v1/jobs.py b/app/api/v1/jobs.py index 2f452b7..d2e80fa 100644 --- a/app/api/v1/jobs.py +++ b/app/api/v1/jobs.py @@ -13,6 +13,10 @@ async def upload_csv( file: UploadFile = File(...), service: JobService = Depends(get_job_service), ): + """ + Accepts a CSV file upload. Validates it, creates a job record in the database + with status=pending, enqueues the processing task, and returns the job_id immediately. + """ if not file.filename.endswith(".csv"): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -35,6 +39,10 @@ async def get_job_status( job_id: str, service: JobService = Depends(get_job_service), ): + """ + Returns the current status of the job: pending, processing, completed, or failed. + If completed, also includes a summary field with high-level stats. + """ try: job = await service.get_job_status(job_id) return job @@ -50,6 +58,10 @@ async def get_job_result( job_id: str, service: JobService = Depends(get_job_service), ): + """ + Returns the full structured output: cleaned transactions list, flagged anomalies, + per-category spend breakdown, and the LLM-generated narrative summary. + """ try: job = await service.get_job_results(job_id) return job @@ -65,4 +77,8 @@ async def get_all_jobs( status: Optional[str] = Query(None, description="Filter jobs by status"), service: JobService = Depends(get_job_service), ): + """ + Lists all jobs with their status, filename, row count, and created_at timestamp. + Supports filtering via ?status= query parameter. + """ return await service.list_jobs(status=status) diff --git a/app/db/models/job.py b/app/db/models/job.py index 5ed7da1..73dc2d2 100644 --- a/app/db/models/job.py +++ b/app/db/models/job.py @@ -33,6 +33,7 @@ class Job(Base): onupdate=func.now(), nullable=False, ) + # JSON summaries and results summary: Mapped[Optional[Dict[str, Any]]] = mapped_column( JSON, nullable=True, diff --git a/app/db/session.py b/app/db/session.py index fa7cbc4..f0a14fc 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -2,6 +2,7 @@ from typing import AsyncGenerator from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from app.core.config import settings +# Create async database engine engine = create_async_engine( settings.DATABASE_URL, echo=False, @@ -10,6 +11,7 @@ engine = create_async_engine( max_overflow=20, ) +# Async session factory AsyncSessionLocal = async_sessionmaker( bind=engine, class_=AsyncSession, @@ -18,6 +20,7 @@ AsyncSessionLocal = async_sessionmaker( async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Dependency for obtaining an asynchronous database session""" async with AsyncSessionLocal() as session: try: yield session diff --git a/app/main.py b/app/main.py index 61b9e88..84136df 100644 --- a/app/main.py +++ b/app/main.py @@ -9,16 +9,21 @@ from app.db.session import engine @asynccontextmanager async def lifespan(app: FastAPI): + # Startup: Create tables if they don't exist async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield # Shutdown: Clean up pool await engine.dispose() + + app = FastAPI( title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json", lifespan=lifespan, ) + +# CORS Middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -26,7 +31,12 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) + +# Include API Router app.include_router(jobs_router, prefix=settings.API_V1_STR) + + +# Root healthcheck endpoint @app.get("/health", tags=["health"]) def health_check(): return {"status": "ok", "project": settings.PROJECT_NAME}