aker-property-ai / app /models.py
Aker Deploy
feat(obs+evals): mirror main commits f8f81de + 1132c64
af01fe3
Raw
History Blame Contribute Delete
7.56 kB
"""ORM models for the rent-roll domain."""
from __future__ import annotations
from datetime import date, datetime
from sqlalchemy import (
String, Integer, Float, Date, Boolean, ForeignKey, JSON, Index, DateTime, Text
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
# Use JSONB on Postgres for indexed `->`/`->>` access on raw_row; fall back
# to generic JSON on other dialects (SQLite tests, MySQL, etc.).
JSON_VARIANT = JSON().with_variant(JSONB(), "postgresql")
class Base(DeclarativeBase):
pass
class Property(Base):
__tablename__ = "properties"
property_code: Mapped[str] = mapped_column(String(32), primary_key=True)
property_name: Mapped[str] = mapped_column(String(255))
property_type: Mapped[str] = mapped_column(String(32)) # r / a / c / land / other
address: Mapped[str | None] = mapped_column(String(255), nullable=True)
units: Mapped[list["Unit"]] = relationship(back_populates="property", cascade="all, delete-orphan")
leases: Mapped[list["Lease"]] = relationship(back_populates="property", cascade="all, delete-orphan")
snapshots: Mapped[list["RentSnapshot"]] = relationship(back_populates="property", cascade="all, delete-orphan")
class Unit(Base):
__tablename__ = "units"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
property_code: Mapped[str] = mapped_column(String(32), ForeignKey("properties.property_code"), index=True)
unit_number: Mapped[str] = mapped_column(String(64))
unit_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
bedrooms: Mapped[float | None] = mapped_column(Float, nullable=True)
bathrooms: Mapped[float | None] = mapped_column(Float, nullable=True)
sqft: Mapped[float | None] = mapped_column(Float, nullable=True)
market_rent: Mapped[float | None] = mapped_column(Float, nullable=True)
property: Mapped[Property] = relationship(back_populates="units")
__table_args__ = (
Index("ix_units_code_unit", "property_code", "unit_number", unique=True),
)
class Lease(Base):
__tablename__ = "leases"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
property_code: Mapped[str] = mapped_column(String(32), ForeignKey("properties.property_code"), index=True)
unit_number: Mapped[str] = mapped_column(String(64))
tenant_id: Mapped[str | None] = mapped_column(String(64), nullable=True) # already redacted upstream
lease_start: Mapped[date | None] = mapped_column(Date, nullable=True)
lease_end: Mapped[date | None] = mapped_column(Date, nullable=True)
monthly_rent: Mapped[float | None] = mapped_column(Float, nullable=True)
balance: Mapped[float | None] = mapped_column(Float, nullable=True)
status: Mapped[str | None] = mapped_column(String(32), nullable=True)
# v4 additions — were silently dropped before:
resident_deposit: Mapped[float | None] = mapped_column(Float, nullable=True)
other_deposit: Mapped[float | None] = mapped_column(Float, nullable=True)
move_out_date: Mapped[date | None] = mapped_column(Date, nullable=True)
property: Mapped[Property] = relationship(back_populates="leases")
class RentSnapshot(Base):
"""One row per (property, unit, month). Powers time-series queries."""
__tablename__ = "rent_snapshots"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
property_code: Mapped[str] = mapped_column(String(32), ForeignKey("properties.property_code"), index=True)
snapshot_month: Mapped[date] = mapped_column(Date, index=True)
unit_number: Mapped[str] = mapped_column(String(64))
monthly_rent: Mapped[float | None] = mapped_column(Float, nullable=True)
occupied: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
raw_row: Mapped[dict | None] = mapped_column(JSON_VARIANT, nullable=True)
property: Mapped[Property] = relationship(back_populates="snapshots")
charge_lines: Mapped[list["RentChargeLine"]] = relationship(back_populates="snapshot", cascade="all, delete-orphan")
__table_args__ = (
Index("ix_snap_code_month_unit", "property_code", "snapshot_month", "unit_number"),
)
class RentChargeLine(Base):
"""One row per charge line within a unit block.
Preserves the granularity that `RentSnapshot.raw_row.charges` lost — that
dict sums charges per code, so two PARKING lines of $75 and $100 collapsed
into a single $175. This table keeps every line item with its position
(`line_index`) so the agent can answer "list the individual parking fees".
"""
__tablename__ = "rent_charge_lines"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
snapshot_id: Mapped[int] = mapped_column(Integer, ForeignKey("rent_snapshots.id", ondelete="CASCADE"), index=True)
property_code: Mapped[str] = mapped_column(String(32), ForeignKey("properties.property_code"), index=True)
snapshot_month: Mapped[date] = mapped_column(Date, index=True)
unit_number: Mapped[str] = mapped_column(String(64))
line_index: Mapped[int] = mapped_column(Integer)
charge_code: Mapped[str] = mapped_column(String(32), index=True)
amount: Mapped[float | None] = mapped_column(Float, nullable=True)
snapshot: Mapped["RentSnapshot"] = relationship(back_populates="charge_lines")
__table_args__ = (
Index("ix_charge_code_month_unit", "property_code", "snapshot_month", "unit_number"),
)
# ---------------------------------------------------------------------------
# Evaluation harness — persisted alongside the rent-roll schema in Supabase
# so the Monitoring UI can read run history without a separate datastore.
# ---------------------------------------------------------------------------
class EvalRun(Base):
__tablename__ = "eval_runs"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
trigger: Mapped[str] = mapped_column(String(32)) # manual | scheduled | cli
status: Mapped[str] = mapped_column(String(16)) # running | completed | failed
summary: Mapped[dict | None] = mapped_column(JSON_VARIANT, nullable=True)
cases: Mapped[list["EvalCase"]] = relationship(
back_populates="run", cascade="all, delete-orphan",
)
class EvalCase(Base):
__tablename__ = "eval_cases"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
run_id: Mapped[str] = mapped_column(String(64), ForeignKey("eval_runs.id", ondelete="CASCADE"), index=True)
golden_id: Mapped[str] = mapped_column(String(128))
property_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
question: Mapped[str] = mapped_column(Text)
answer: Mapped[str | None] = mapped_column(Text, nullable=True)
scores: Mapped[dict | None] = mapped_column(JSON_VARIANT, nullable=True)
ok: Mapped[bool] = mapped_column(Boolean, default=False)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
trace_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
run: Mapped["EvalRun"] = relationship(back_populates="cases")