examples: FireExample ORM class + round-trip test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-28 22:09:23 +00:00
parent b5bfe8b73c
commit 8f2a80f563
2 changed files with 81 additions and 0 deletions

View file

@ -322,6 +322,44 @@ class RetirementGoal(Base):
server_default=func.now())
class FireExample(Base):
"""One Reddit-sourced FIRE example.
`reddit_id` UNIQUE makes re-ingest idempotent. Fields are nullable
when the LLM couldn't extract them confidently — never inferred.
Currency normalisation (portfolio_gbp / annual_exp_gbp) happens at
extraction time using `fire_planner/fx.py`; `raw_currency` is kept
for traceability.
"""
__tablename__ = "fire_example"
__table_args__ = {"schema": SCHEMA_NAME} # noqa: RUF012
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
reddit_id: Mapped[str] = mapped_column(String(16), unique=True, nullable=False)
source_sub: Mapped[str] = mapped_column(String(64), nullable=False)
post_url: Mapped[str] = mapped_column(String, nullable=False)
post_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
post_title: Mapped[str] = mapped_column(String, nullable=False)
country: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
city: Mapped[str | None] = mapped_column(String(128), nullable=True)
portfolio_gbp: Mapped[Decimal | None] = mapped_column(Numeric(14, 2), nullable=True)
annual_exp_gbp: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
age: Mapped[int | None] = mapped_column(Integer, nullable=True)
family_size: Mapped[int | None] = mapped_column(Integer, nullable=True)
fi_status: Mapped[str | None] = mapped_column(String(24), nullable=True, index=True)
is_retired: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
raw_currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
raw_excerpt: Mapped[str | None] = mapped_column(String, nullable=True)
llm_model: Mapped[str] = mapped_column(String(64), nullable=False)
llm_confidence: Mapped[Decimal | None] = mapped_column(Numeric(3, 2), nullable=True)
extracted_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True),
nullable=False,
server_default=func.now())
ingested_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True),
nullable=False,
server_default=func.now())
def create_engine_from_env() -> AsyncEngine:
url = os.environ["DB_CONNECTION_STRING"]
return create_async_engine(url, pool_pre_ping=True)

View file

@ -0,0 +1,43 @@
"""Schema test — FireExample ORM round-trips through the in-memory engine."""
from __future__ import annotations
from datetime import date
from decimal import Decimal
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fire_planner.db import FireExample
@pytest.mark.asyncio
async def test_fire_example_round_trip(session: AsyncSession) -> None:
row = FireExample(
reddit_id="abc123",
source_sub="financialindependence",
post_url="https://reddit.com/r/financialindependence/abc123",
post_date=date(2026, 1, 1),
post_title="Hit £1m at 38, living in Manila",
country="Philippines",
city="Manila",
portfolio_gbp=Decimal("1000000.00"),
annual_exp_gbp=Decimal("14400.00"),
age=38,
family_size=2,
fi_status="FIRE",
is_retired=True,
raw_currency="GBP",
raw_excerpt="...£1m...Manila...",
llm_model="qwen3-8b",
llm_confidence=Decimal("0.82"),
)
session.add(row)
await session.commit()
result = await session.execute(select(FireExample).where(FireExample.reddit_id == "abc123"))
fetched = result.scalar_one()
assert fetched.country == "Philippines"
assert fetched.portfolio_gbp == Decimal("1000000.00")
assert fetched.fi_status == "FIRE"
assert fetched.is_retired is True