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:
@@ -14,6 +14,7 @@ from domain import (
|
||||
Transaction,
|
||||
)
|
||||
from infrastructure.cli.i18n import t
|
||||
from infrastructure.repositories import JsonFileRepository
|
||||
|
||||
app = typer.Typer(name="config", help=t("config.help"))
|
||||
console = Console()
|
||||
@@ -21,15 +22,17 @@ console = Console()
|
||||
DATA_DIR = Path("data")
|
||||
MODEL_PATH = DATA_DIR / "model.json"
|
||||
|
||||
_json_repo = JsonFileRepository()
|
||||
|
||||
|
||||
def _load_model() -> FinancialModel:
|
||||
if MODEL_PATH.exists():
|
||||
return FinancialModel.load(MODEL_PATH)
|
||||
return _json_repo.load(MODEL_PATH)
|
||||
return FinancialModel()
|
||||
|
||||
|
||||
def _save_model(model: FinancialModel) -> None:
|
||||
model.save(MODEL_PATH)
|
||||
_json_repo.save(model, MODEL_PATH)
|
||||
|
||||
|
||||
# ── base-currency ──────────────────────────────────────────────
|
||||
|
||||
@@ -11,7 +11,7 @@ from infrastructure.cli.config import app as config_app
|
||||
from infrastructure.cli.i18n import setup_i18n, t
|
||||
from application.forecast import ForecastService
|
||||
from application.scenarios import DEFAULT_SCENARIOS, ScenarioService
|
||||
from infrastructure.sync.excel_sync import ExcelSync
|
||||
from infrastructure.repositories import ExcelRepository, JsonFileRepository
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
@@ -28,15 +28,18 @@ MODEL_PATH = DATA_DIR / "model.json"
|
||||
|
||||
CURRENCY_OPTION = typer.Option(None, "--currency", "-c", help=t("cmd.forecast.opt.currency"))
|
||||
|
||||
_json_repo = JsonFileRepository()
|
||||
_excel_repo = ExcelRepository()
|
||||
|
||||
|
||||
def _load_model() -> FinancialModel:
|
||||
if MODEL_PATH.exists():
|
||||
return FinancialModel.load(MODEL_PATH)
|
||||
return _json_repo.load(MODEL_PATH)
|
||||
return FinancialModel()
|
||||
|
||||
|
||||
def _save_model(model: FinancialModel) -> None:
|
||||
model.save(MODEL_PATH)
|
||||
_json_repo.save(model, MODEL_PATH)
|
||||
|
||||
|
||||
def _get_converter(model: FinancialModel) -> CurrencyConverter:
|
||||
@@ -205,9 +208,8 @@ def import_xlsx(
|
||||
path: str = typer.Argument(..., help=t("cmd.import.arg.path")),
|
||||
) -> None:
|
||||
"""Импорт данных из Excel"""
|
||||
sync = ExcelSync()
|
||||
try:
|
||||
model = sync.import_model(path)
|
||||
model = _excel_repo.load(path)
|
||||
_save_model(model)
|
||||
console.print(t("cmd.import.ok",
|
||||
accounts=len(model.accounts),
|
||||
@@ -227,9 +229,8 @@ def export_xlsx(
|
||||
) -> None:
|
||||
"""Экспорт модели в Excel"""
|
||||
model = _load_model()
|
||||
sync = ExcelSync()
|
||||
try:
|
||||
sync.export_model(model, path)
|
||||
_excel_repo.save(model, path)
|
||||
console.print(t("cmd.export.ok", path=path))
|
||||
except Exception as e:
|
||||
console.print(f"[red]{t('cmd.export.err', error=str(e))}[/red]")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from infrastructure.repositories.excel_repository import ExcelRepository, ExcelRepositoryError
|
||||
from infrastructure.repositories.json_file_repository import JsonFileRepository
|
||||
|
||||
__all__ = ["JsonFileRepository", "ExcelRepository", "ExcelRepositoryError"]
|
||||
+27
-26
@@ -1,8 +1,10 @@
|
||||
"""ExcelRepository — хранит FinancialModel в .xlsx (5 листов + ModelInfo)."""
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from openpyxl import Workbook, load_workbook
|
||||
|
||||
from application.repositories.model_repository import ModelRepository
|
||||
from domain import (
|
||||
Account,
|
||||
Asset,
|
||||
@@ -14,7 +16,7 @@ from domain import (
|
||||
)
|
||||
|
||||
|
||||
class SyncError(Exception):
|
||||
class ExcelRepositoryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -46,13 +48,13 @@ _SHEET_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
class ExcelSync:
|
||||
def import_model(self, path: str | Path) -> FinancialModel:
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
raise SyncError(f"File not found: {path}")
|
||||
class ExcelRepository(ModelRepository):
|
||||
def load(self, path: str | Path) -> FinancialModel:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
raise ExcelRepositoryError(f"File not found: {p}")
|
||||
|
||||
wb = load_workbook(path, read_only=True, data_only=True)
|
||||
wb = load_workbook(p, read_only=True, data_only=True)
|
||||
model = FinancialModel()
|
||||
model.exchange_rates = []
|
||||
|
||||
@@ -73,7 +75,7 @@ class ExcelSync:
|
||||
val = row[i] if i < len(row) else None
|
||||
if val is not None:
|
||||
data[header] = str(val) if not isinstance(val, (int, float)) else val
|
||||
self._add_to_model(model, sheet_name, data)
|
||||
self._add_to_model(model, sheet_name, data, config["cls"])
|
||||
|
||||
if "ModelInfo" in wb.sheetnames:
|
||||
ws = wb["ModelInfo"]
|
||||
@@ -85,8 +87,8 @@ class ExcelSync:
|
||||
wb.close()
|
||||
return model
|
||||
|
||||
def export_model(self, model: FinancialModel, path: str | Path) -> None:
|
||||
path = Path(path)
|
||||
def save(self, model: FinancialModel, path: str | Path) -> None:
|
||||
p = Path(path)
|
||||
wb = Workbook()
|
||||
wb.remove(wb.active)
|
||||
|
||||
@@ -115,23 +117,22 @@ class ExcelSync:
|
||||
ws_info.append(["Property", "Value"])
|
||||
ws_info.append(["base_currency", model.base_currency])
|
||||
|
||||
wb.save(path)
|
||||
wb.save(p)
|
||||
|
||||
def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict) -> None:
|
||||
def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict, cls) -> None:
|
||||
try:
|
||||
cls = _SHEET_CONFIG[sheet_name]["cls"]
|
||||
instance = cls.model_validate(data)
|
||||
if sheet_name == "Accounts":
|
||||
model.accounts.append(instance)
|
||||
elif sheet_name == "Transactions":
|
||||
model.transactions.append(instance)
|
||||
elif sheet_name == "Recurring":
|
||||
model.recurring.append(instance)
|
||||
elif sheet_name == "Assets":
|
||||
model.assets.append(instance)
|
||||
elif sheet_name == "Liabilities":
|
||||
model.liabilities.append(instance)
|
||||
elif sheet_name == "ExchangeRates":
|
||||
model.exchange_rates.append(instance)
|
||||
except Exception as e:
|
||||
raise SyncError(f"Failed to parse row in {sheet_name}: {e}") from e
|
||||
raise ExcelRepositoryError(f"Failed to parse row in {sheet_name}: {e}") from e
|
||||
if sheet_name == "Accounts":
|
||||
model.accounts.append(instance)
|
||||
elif sheet_name == "Transactions":
|
||||
model.transactions.append(instance)
|
||||
elif sheet_name == "Recurring":
|
||||
model.recurring.append(instance)
|
||||
elif sheet_name == "Assets":
|
||||
model.assets.append(instance)
|
||||
elif sheet_name == "Liabilities":
|
||||
model.liabilities.append(instance)
|
||||
elif sheet_name == "ExchangeRates":
|
||||
model.exchange_rates.append(instance)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""JsonFileRepository — хранит FinancialModel в .json (включая version)."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from application.repositories.model_repository import ModelRepository
|
||||
from domain import FinancialModel
|
||||
|
||||
|
||||
class JsonFileRepository(ModelRepository):
|
||||
def load(self, path: str | Path) -> FinancialModel:
|
||||
p = Path(path)
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return FinancialModel.from_dict(data)
|
||||
|
||||
def save(self, model: FinancialModel, path: str | Path) -> None:
|
||||
p = Path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(p, "w", encoding="utf-8") as f:
|
||||
json.dump(model.to_dict(), f, indent=2, ensure_ascii=False)
|
||||
@@ -1,3 +0,0 @@
|
||||
from infrastructure.sync.excel_sync import ExcelSync, SyncError
|
||||
|
||||
__all__ = ["ExcelSync", "SyncError"]
|
||||
Reference in New Issue
Block a user