fire-planner/fire_planner/api/examples.py

62 lines
2.1 KiB
Python
Raw Normal View History

"""GET /api/examples and /api/examples/summary.
`/examples` returns the raw FireExample rows (filterable by country,
fi_status, with a sane limit). `/examples/summary` is the aggregated
view the UI / simulator overlay actually wants.
"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fire_planner.api.dependencies import get_session
from fire_planner.db import FireExample
from fire_planner.examples.models import Summary
from fire_planner.examples.service import summary_for_country
router = APIRouter(prefix="/examples", tags=["examples"])
@router.get("")
async def list_examples(
country: Annotated[str | None, Query()] = None,
fi_status: Annotated[str | None, Query()] = None,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
session: AsyncSession = Depends(get_session),
) -> list[dict[str, object]]:
stmt = select(FireExample)
if country is not None:
stmt = stmt.where(FireExample.country == country)
if fi_status is not None:
stmt = stmt.where(FireExample.fi_status == fi_status)
stmt = stmt.order_by(FireExample.post_date.desc()).limit(limit)
rows = (await session.execute(stmt)).scalars().all()
return [
{
"reddit_id": r.reddit_id,
"source_sub": r.source_sub,
"post_url": r.post_url,
"post_date": r.post_date.isoformat(),
"country": r.country,
"city": r.city,
"portfolio_gbp": float(r.portfolio_gbp) if r.portfolio_gbp else None,
"annual_exp_gbp": float(r.annual_exp_gbp) if r.annual_exp_gbp else None,
"age": r.age,
"family_size": r.family_size,
"fi_status": r.fi_status,
"is_retired": r.is_retired,
}
for r in rows
]
@router.get("/summary", response_model=Summary)
async def get_summary(
country: Annotated[str, Query(min_length=2)],
session: AsyncSession = Depends(get_session),
) -> Summary:
return await summary_for_country(session, country)