74 lines
2.8 KiB
Python
74 lines
2.8 KiB
Python
|
|
"""add col_snapshot table for cached cost-of-living data
|
||
|
|
|
||
|
|
Revision ID: 0005
|
||
|
|
Revises: 0004
|
||
|
|
Create Date: 2026-05-21 12:00:00.000000
|
||
|
|
|
||
|
|
Phase 2 of the cost-of-living subsystem (`fire_planner.col`). Caches
|
||
|
|
Numbeo / Expatistan headline data with a 1-year TTL so the simulator
|
||
|
|
can scale `spending_gbp` to local prices without re-scraping per-call.
|
||
|
|
Refresh is async (Phase-3 CronJob); user-facing lookups never block on
|
||
|
|
the network in the steady state.
|
||
|
|
|
||
|
|
Unique on (city_slug, source_name) — multiple sources per city are
|
||
|
|
allowed; service.py reconciles them when computing the headline.
|
||
|
|
"""
|
||
|
|
from collections.abc import Sequence
|
||
|
|
|
||
|
|
import sqlalchemy as sa
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
|
||
|
|
revision: str = "0005"
|
||
|
|
down_revision: str | None = "0004"
|
||
|
|
branch_labels: str | Sequence[str] | None = None
|
||
|
|
depends_on: str | Sequence[str] | None = None
|
||
|
|
|
||
|
|
SCHEMA = "fire_planner"
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.create_table(
|
||
|
|
"col_snapshot",
|
||
|
|
sa.Column("id", sa.Integer(), nullable=False, autoincrement=True),
|
||
|
|
sa.Column("city_slug", sa.String(length=64), nullable=False),
|
||
|
|
sa.Column("city_display", sa.String(length=128), nullable=False),
|
||
|
|
sa.Column("country", sa.String(length=64), nullable=False),
|
||
|
|
sa.Column("source_name", sa.String(length=32), nullable=False),
|
||
|
|
sa.Column("source_url", sa.String(), nullable=True),
|
||
|
|
sa.Column("snapshot_date", sa.Date(), nullable=False),
|
||
|
|
sa.Column("fetched_at", sa.TIMESTAMP(timezone=True), nullable=False,
|
||
|
|
server_default=sa.func.now()),
|
||
|
|
sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False),
|
||
|
|
sa.Column("total_no_rent_gbp", sa.Numeric(12, 2), nullable=False),
|
||
|
|
sa.Column("total_with_rent_gbp", sa.Numeric(12, 2), nullable=False),
|
||
|
|
sa.Column("rent_1bed_center_gbp", sa.Numeric(12, 2), nullable=False),
|
||
|
|
sa.Column("rent_1bed_outside_gbp", sa.Numeric(12, 2), nullable=True),
|
||
|
|
sa.Column("raw_currency", sa.String(length=3), nullable=False,
|
||
|
|
server_default=sa.text("'GBP'")),
|
||
|
|
sa.Column("gbp_per_unit", sa.Numeric(12, 8), nullable=False,
|
||
|
|
server_default=sa.text("1")),
|
||
|
|
sa.Column("by_category_json", sa.JSON(), nullable=True),
|
||
|
|
sa.PrimaryKeyConstraint("id"),
|
||
|
|
sa.UniqueConstraint("city_slug", "source_name", name="uq_col_snapshot_city_source"),
|
||
|
|
schema=SCHEMA,
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
"ix_col_snapshot_city_slug",
|
||
|
|
"col_snapshot",
|
||
|
|
["city_slug"],
|
||
|
|
schema=SCHEMA,
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
"ix_col_snapshot_expires_at",
|
||
|
|
"col_snapshot",
|
||
|
|
["expires_at"],
|
||
|
|
schema=SCHEMA,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.drop_index("ix_col_snapshot_expires_at", table_name="col_snapshot", schema=SCHEMA)
|
||
|
|
op.drop_index("ix_col_snapshot_city_slug", table_name="col_snapshot", schema=SCHEMA)
|
||
|
|
op.drop_table("col_snapshot", schema=SCHEMA)
|