- application/repositories/model_repository.py: ModelRepository Protocol with load(path) and save(model, path) - infrastructure/repositories/json_file_repository.py: JsonFileRepository (вынесен из FinancialModel.save/load) - infrastructure/repositories/excel_repository.py: ExcelRepository (бывший ExcelSync, переименован) - FinancialModel.save/load удалены — теперь только данные - infrastructure/sync/ удалён (ExcelSync перенесён в repositories/) - infrastructure/cli/main.py и config.py используют JsonFileRepository + ExcelRepository через локальные singletons - test_excel_sync.py переименован в test для ExcelRepository - Новый test_repositories.py: 5 тестов для JsonFileRepository + протокол - test_model.py: использован JsonFileRepository Tests: 68/68 pass (было 63 + 5 новых).
72 lines
3.3 KiB
Python
72 lines
3.3 KiB
Python
from pydantic import BaseModel, Field
|
||
|
||
from domain.account import Account
|
||
from domain.asset import Asset
|
||
from domain.currency import DEFAULT_RATES, ExchangeRate
|
||
from domain.liability import Liability
|
||
from domain.recurring import RecurringCashflow
|
||
from domain.scenario import ForecastScenario
|
||
from domain.transaction import Transaction
|
||
|
||
|
||
class FinancialModel(BaseModel):
|
||
"""Корневая модель финансового плана.
|
||
|
||
Сериализация (save/load) вынесена в `application.repositories.ModelRepository` —
|
||
JsonFileRepository и ExcelRepository. Сам класс хранит только данные.
|
||
"""
|
||
|
||
SCHEMA_VERSION: int = 1 # NB: не Field — это class-level metadata, не pydantic field
|
||
|
||
base_currency: str = "RUB"
|
||
accounts: list[Account] = Field(default_factory=list)
|
||
transactions: list[Transaction] = Field(default_factory=list)
|
||
recurring: list[RecurringCashflow] = Field(default_factory=list)
|
||
assets: list[Asset] = Field(default_factory=list)
|
||
liabilities: list[Liability] = Field(default_factory=list)
|
||
scenarios: list[ForecastScenario] = Field(default_factory=list)
|
||
exchange_rates: list[ExchangeRate] = Field(
|
||
default_factory=lambda: [r.model_copy() for r in DEFAULT_RATES]
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
"""Сериализация в dict с version и UUID-as-str (для JSON)."""
|
||
return {
|
||
"version": self.SCHEMA_VERSION,
|
||
"base_currency": self.base_currency,
|
||
"accounts": [a.model_dump(mode="json") for a in self.accounts],
|
||
"transactions": [t.model_dump(mode="json") for t in self.transactions],
|
||
"recurring": [r.model_dump(mode="json") for r in self.recurring],
|
||
"assets": [a.model_dump(mode="json") for a in self.assets],
|
||
"liabilities": [li.model_dump(mode="json") for li in self.liabilities],
|
||
"scenarios": [s.model_dump(mode="json") for s in self.scenarios],
|
||
"exchange_rates": [e.model_dump(mode="json") for e in self.exchange_rates],
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: dict) -> "FinancialModel":
|
||
version = data.get("version", 0)
|
||
if version == 0:
|
||
return cls._from_legacy(data)
|
||
if version == 1:
|
||
return cls._from_v1(data)
|
||
raise ValueError(f"Unsupported FinancialModel version: {version}")
|
||
|
||
@classmethod
|
||
def _from_v1(cls, data: dict) -> "FinancialModel":
|
||
return cls(
|
||
base_currency=data.get("base_currency", "RUB"),
|
||
accounts=[Account.model_validate(a) for a in data.get("accounts", [])],
|
||
transactions=[Transaction.model_validate(t) for t in data.get("transactions", [])],
|
||
recurring=[RecurringCashflow.model_validate(r) for r in data.get("recurring", [])],
|
||
assets=[Asset.model_validate(a) for a in data.get("assets", [])],
|
||
liabilities=[Liability.model_validate(li) for li in data.get("liabilities", [])],
|
||
scenarios=[ForecastScenario.model_validate(s) for s in data.get("scenarios", [])],
|
||
exchange_rates=[ExchangeRate.model_validate(r) for r in data.get("exchange_rates", [])],
|
||
)
|
||
|
||
@classmethod
|
||
def _from_legacy(cls, data: dict) -> "FinancialModel":
|
||
# Legacy: файлы без version. Структура совпадает с v1.
|
||
return cls._from_v1(data)
|