65 lines
1.8 KiB
Python
65 lines
1.8 KiB
Python
|
|
"""Pydantic models for per-city cost-of-living data.
|
||
|
|
|
||
|
|
Every category figure is monthly GBP for a single person — the
|
||
|
|
denomination the simulator expects when scaling `spending_gbp`. The
|
||
|
|
source object retains the original currency, FX rate, and snapshot
|
||
|
|
date so we can re-validate or update a stale baseline.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import date
|
||
|
|
from decimal import Decimal
|
||
|
|
from typing import Literal
|
||
|
|
|
||
|
|
from pydantic import BaseModel, ConfigDict, Field
|
||
|
|
|
||
|
|
SourceName = Literal["numbeo", "expatistan", "baseline", "manual"]
|
||
|
|
|
||
|
|
|
||
|
|
class ColSource(BaseModel):
|
||
|
|
"""Provenance for a CityCostIndex entry — where did the numbers come
|
||
|
|
from and when. The simulator surfaces this in the SimulateResult so
|
||
|
|
the user can audit which baseline was applied."""
|
||
|
|
|
||
|
|
model_config = ConfigDict(frozen=True)
|
||
|
|
|
||
|
|
name: SourceName
|
||
|
|
url: str | None = None
|
||
|
|
snapshot_date: date
|
||
|
|
raw_currency: str = "GBP"
|
||
|
|
gbp_per_unit: Decimal = Decimal("1")
|
||
|
|
|
||
|
|
|
||
|
|
class CategoryBreakdown(BaseModel):
|
||
|
|
"""Per-category monthly costs in GBP for a single person."""
|
||
|
|
|
||
|
|
model_config = ConfigDict(frozen=True)
|
||
|
|
|
||
|
|
rent_1bed_center: Decimal
|
||
|
|
rent_1bed_outside: Decimal | None = None
|
||
|
|
groceries: Decimal
|
||
|
|
restaurants: Decimal
|
||
|
|
transport: Decimal
|
||
|
|
utilities: Decimal
|
||
|
|
leisure: Decimal
|
||
|
|
|
||
|
|
|
||
|
|
class CityCostIndex(BaseModel):
|
||
|
|
"""One city's headline cost-of-living snapshot."""
|
||
|
|
|
||
|
|
model_config = ConfigDict(frozen=True)
|
||
|
|
|
||
|
|
city: str
|
||
|
|
city_slug: str = Field(min_length=1)
|
||
|
|
country: str
|
||
|
|
total_single_no_rent_gbp: Decimal
|
||
|
|
total_single_with_rent_gbp: Decimal
|
||
|
|
breakdown: CategoryBreakdown
|
||
|
|
source: ColSource
|
||
|
|
|
||
|
|
@property
|
||
|
|
def total_monthly_gbp(self) -> Decimal:
|
||
|
|
"""The number the simulator uses for ratios — `with rent` is the
|
||
|
|
right anchor because moving location changes rent too."""
|
||
|
|
return self.total_single_with_rent_gbp
|