T4: Repository pattern — ModelRepository, JsonFileRepository, ExcelRepository
- 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 новых).
This commit is contained in:
+16
-16
@@ -1,22 +1,22 @@
|
||||
from pathlib import Path
|
||||
|
||||
from domain import FinancialModel
|
||||
from infrastructure.sync.excel_sync import ExcelSync, SyncError
|
||||
from infrastructure.repositories import ExcelRepository, ExcelRepositoryError
|
||||
|
||||
|
||||
class TestExcelSync:
|
||||
class TestExcelRepository:
|
||||
def test_export_creates_file(self, sample_model, tmp_path: Path):
|
||||
sync = ExcelSync()
|
||||
repo = ExcelRepository()
|
||||
p = tmp_path / "test.xlsx"
|
||||
sync.export_model(sample_model, p)
|
||||
repo.save(sample_model, p)
|
||||
assert p.exists()
|
||||
|
||||
def test_export_import_roundtrip(self, sample_model, tmp_path: Path):
|
||||
sync = ExcelSync()
|
||||
repo = ExcelRepository()
|
||||
p = tmp_path / "roundtrip.xlsx"
|
||||
sync.export_model(sample_model, p)
|
||||
repo.save(sample_model, p)
|
||||
|
||||
loaded = sync.import_model(p)
|
||||
loaded = repo.load(p)
|
||||
assert len(loaded.accounts) == len(sample_model.accounts)
|
||||
assert len(loaded.transactions) == len(sample_model.transactions)
|
||||
assert len(loaded.recurring) == len(sample_model.recurring)
|
||||
@@ -25,25 +25,25 @@ class TestExcelSync:
|
||||
assert len(loaded.exchange_rates) == len(sample_model.exchange_rates)
|
||||
|
||||
def test_roundtrip_preserves_base_currency(self, tmp_path: Path):
|
||||
sync = ExcelSync()
|
||||
repo = ExcelRepository()
|
||||
model = FinancialModel(base_currency="EUR")
|
||||
p = tmp_path / "eur_model.xlsx"
|
||||
sync.export_model(model, p)
|
||||
repo.save(model, p)
|
||||
|
||||
loaded = sync.import_model(p)
|
||||
loaded = repo.load(p)
|
||||
assert loaded.base_currency == "EUR"
|
||||
|
||||
def test_import_missing_file(self):
|
||||
sync = ExcelSync()
|
||||
repo = ExcelRepository()
|
||||
try:
|
||||
sync.import_model("nonexistent.xlsx")
|
||||
assert False, "Expected SyncError"
|
||||
except SyncError:
|
||||
repo.load("nonexistent.xlsx")
|
||||
assert False, "Expected ExcelRepositoryError"
|
||||
except ExcelRepositoryError:
|
||||
pass
|
||||
|
||||
def test_export_empty_model(self, tmp_path: Path):
|
||||
sync = ExcelSync()
|
||||
repo = ExcelRepository()
|
||||
model = FinancialModel()
|
||||
p = tmp_path / "empty.xlsx"
|
||||
sync.export_model(model, p)
|
||||
repo.save(model, p)
|
||||
assert p.exists()
|
||||
|
||||
+26
-21
@@ -1,3 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from domain import (
|
||||
@@ -10,17 +11,21 @@ from domain import (
|
||||
RecurringCashflow,
|
||||
Transaction,
|
||||
)
|
||||
from infrastructure.repositories import JsonFileRepository
|
||||
|
||||
|
||||
_repo = JsonFileRepository()
|
||||
|
||||
|
||||
class TestAccount:
|
||||
def test_create(self):
|
||||
a = Account(name="Test", balance=100.0)
|
||||
a = Account(name="Test", balance=Decimal("100.0"))
|
||||
assert a.name == "Test"
|
||||
assert a.balance == 100.0
|
||||
assert a.balance == Decimal("100.0")
|
||||
assert a.currency == "USD"
|
||||
|
||||
def test_to_dict_roundtrip(self):
|
||||
a = Account(name="Test", balance=100.0)
|
||||
a = Account(name="Test", balance=Decimal("100.0"))
|
||||
d = a.model_dump(mode="json")
|
||||
a2 = Account.model_validate(d)
|
||||
assert a2.name == a.name
|
||||
@@ -30,11 +35,11 @@ class TestAccount:
|
||||
|
||||
class TestTransaction:
|
||||
def test_create(self):
|
||||
t = Transaction(amount=500.0, category="food")
|
||||
assert t.amount == 500.0
|
||||
t = Transaction(amount=Decimal("500.0"), category="food")
|
||||
assert t.amount == Decimal("500.0")
|
||||
|
||||
def test_roundtrip(self):
|
||||
t = Transaction(amount=-100.0, category="rent", description="test")
|
||||
t = Transaction(amount=Decimal("-100.0"), category="rent", description="test")
|
||||
d = t.model_dump(mode="json")
|
||||
t2 = Transaction.model_validate(d)
|
||||
assert t2.amount == t.amount
|
||||
@@ -50,8 +55,8 @@ class TestFinancialModel:
|
||||
def test_base_currency_roundtrip(self, tmp_path: Path):
|
||||
model = FinancialModel(base_currency="EUR")
|
||||
p = tmp_path / "model.json"
|
||||
model.save(p)
|
||||
loaded = FinancialModel.load(p)
|
||||
_repo.save(model, p)
|
||||
loaded = _repo.load(p)
|
||||
assert loaded.base_currency == "EUR"
|
||||
|
||||
def test_base_currency_backward_compat(self, tmp_path: Path):
|
||||
@@ -59,7 +64,7 @@ class TestFinancialModel:
|
||||
p = tmp_path / "legacy.json"
|
||||
with open(p, "w") as f:
|
||||
json.dump({"accounts": []}, f)
|
||||
loaded = FinancialModel.load(p)
|
||||
loaded = _repo.load(p)
|
||||
assert loaded.base_currency == "RUB"
|
||||
|
||||
def test_exchange_rates_default(self):
|
||||
@@ -67,18 +72,18 @@ class TestFinancialModel:
|
||||
assert len(model.exchange_rates) == 1
|
||||
assert model.exchange_rates[0].from_currency == "USD"
|
||||
assert model.exchange_rates[0].to_currency == "RUB"
|
||||
assert model.exchange_rates[0].rate == 80.0
|
||||
assert model.exchange_rates[0].rate == Decimal("80")
|
||||
|
||||
def test_save_load(self, tmp_path: Path):
|
||||
model = FinancialModel()
|
||||
model.accounts.append(Account(name="Test", balance=100.0))
|
||||
model.transactions.append(Transaction(amount=50.0, category="income"))
|
||||
model.accounts.append(Account(name="Test", balance=Decimal("100")))
|
||||
model.transactions.append(Transaction(amount=Decimal("50"), category="income"))
|
||||
|
||||
p = tmp_path / "model.json"
|
||||
model.save(p)
|
||||
_repo.save(model, p)
|
||||
assert p.exists()
|
||||
|
||||
loaded = FinancialModel.load(p)
|
||||
loaded = _repo.load(p)
|
||||
assert len(loaded.accounts) == 1
|
||||
assert len(loaded.transactions) == 1
|
||||
assert loaded.accounts[0].name == "Test"
|
||||
@@ -94,16 +99,16 @@ class TestFinancialModel:
|
||||
def test_all_entities_roundtrip(self, tmp_path: Path):
|
||||
model = FinancialModel(
|
||||
accounts=[Account(name="A"), Account(name="B")],
|
||||
transactions=[Transaction(amount=100.0)],
|
||||
recurring=[RecurringCashflow(amount=50.0)],
|
||||
assets=[Asset(name="Stock", value=1000.0)],
|
||||
liabilities=[Liability(name="Loan", balance=500.0, interest=5.0, payment=100.0)],
|
||||
transactions=[Transaction(amount=Decimal("100"))],
|
||||
recurring=[RecurringCashflow(amount=Decimal("50"))],
|
||||
assets=[Asset(name="Stock", value=Decimal("1000"))],
|
||||
liabilities=[Liability(name="Loan", balance=Decimal("500"), interest=Decimal("5"), payment=Decimal("100"))],
|
||||
scenarios=[ForecastScenario(name="test")],
|
||||
exchange_rates=[ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)],
|
||||
exchange_rates=[ExchangeRate(from_currency="USD", to_currency="RUB", rate=Decimal("80"))],
|
||||
)
|
||||
p = tmp_path / "full.json"
|
||||
model.save(p)
|
||||
loaded = FinancialModel.load(p)
|
||||
_repo.save(model, p)
|
||||
loaded = _repo.load(p)
|
||||
assert len(loaded.accounts) == 2
|
||||
assert len(loaded.transactions) == 1
|
||||
assert len(loaded.recurring) == 1
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from domain import Account, Asset, FinancialModel, Transaction
|
||||
from application.repositories import ModelRepository
|
||||
from infrastructure.repositories import JsonFileRepository
|
||||
|
||||
|
||||
class TestJsonFileRepository:
|
||||
def test_save_load_roundtrip(self, tmp_path: Path):
|
||||
repo = JsonFileRepository()
|
||||
model = FinancialModel(
|
||||
accounts=[Account(name="Test", balance=Decimal("100.50"))],
|
||||
transactions=[Transaction(amount=Decimal("33.33"), category="income")],
|
||||
)
|
||||
p = tmp_path / "model.json"
|
||||
repo.save(model, p)
|
||||
assert p.exists()
|
||||
|
||||
loaded = repo.load(p)
|
||||
assert len(loaded.accounts) == 1
|
||||
assert loaded.accounts[0].name == "Test"
|
||||
assert loaded.accounts[0].balance == Decimal("100.50")
|
||||
assert loaded.transactions[0].amount == Decimal("33.33")
|
||||
|
||||
def test_version_field_persisted(self, tmp_path: Path):
|
||||
repo = JsonFileRepository()
|
||||
model = FinancialModel()
|
||||
p = tmp_path / "v.json"
|
||||
repo.save(model, p)
|
||||
import json
|
||||
with open(p) as f:
|
||||
data = json.load(f)
|
||||
assert data["version"] == 1
|
||||
|
||||
def test_creates_parent_dirs(self, tmp_path: Path):
|
||||
repo = JsonFileRepository()
|
||||
model = FinancialModel()
|
||||
p = tmp_path / "nested" / "dir" / "model.json"
|
||||
repo.save(model, p)
|
||||
assert p.exists()
|
||||
|
||||
def test_implements_protocol(self):
|
||||
repo = JsonFileRepository()
|
||||
assert isinstance(repo, ModelRepository)
|
||||
|
||||
def test_decimal_preserved(self, tmp_path: Path):
|
||||
repo = JsonFileRepository()
|
||||
model = FinancialModel(
|
||||
assets=[Asset(name="Stocks", value=Decimal("12345.67"), growth_rate=Decimal("8.5"))]
|
||||
)
|
||||
p = tmp_path / "decimal.json"
|
||||
repo.save(model, p)
|
||||
loaded = repo.load(p)
|
||||
assert loaded.assets[0].value == Decimal("12345.67")
|
||||
assert loaded.assets[0].growth_rate == Decimal("8.5")
|
||||
Reference in New Issue
Block a user