T6: Decompose CLI — main.py → 9 command files, config.py → config_cmd.py

Split infrastructure/cli/:
- main.py (330 LOC) → 9 files in infrastructure/cli/commands/:
  * init.py, forecast.py, scenario.py, whatif.py, compare.py
  * import_xlsx.py, export_xlsx.py, analyze.py, info.py
- config.py (428 LOC) → commands/config_cmd.py (15 sub-commands)
- main.py → app.py (entry point: registers all commands)
- New: paths.py (MODEL_PATH, DATA_DIR)
- New: services.py (composition root + helpers: load_model, save_model,
  build_services, resolve_currency, convert_value, fmt_amount)
- cli/__init__.py (empty) удалён

Each command module: <100 LOC.
pyproject.toml: cf = infrastructure.cli.app:app
tests/test_cli.py: import обновлён на infrastructure.cli.app

CLI behavior идентично, все команды работают.
Tests: 67/67 pass.
This commit is contained in:
2026-10-08 16:56:49 +03:00
parent 541a76835f
commit b6b64a9878
17 changed files with 543 additions and 400 deletions
+60
View File
@@ -0,0 +1,60 @@
"""Composition root + общие хелперы для CLI-команд."""
from __future__ import annotations
from decimal import Decimal
from rich.console import Console
from application.forecast import ForecastService
from application.scenarios import ScenarioService
from domain import CurrencyConverter, FinancialModel
from infrastructure.ai.assistant import AssistantService
from infrastructure.cli.paths import MODEL_PATH
from infrastructure.repositories import JsonFileRepository
console = Console()
_json_repo = JsonFileRepository()
def load_model() -> FinancialModel:
if MODEL_PATH.exists():
return _json_repo.load(MODEL_PATH)
return FinancialModel()
def save_model(model: FinancialModel) -> None:
_json_repo.save(model, MODEL_PATH)
def build_services(model: FinancialModel) -> tuple[ForecastService, ScenarioService, AssistantService, CurrencyConverter]:
"""Composition root: собрать граф сервисов с зависимостями."""
converter = CurrencyConverter(model.exchange_rates)
forecast_service = ForecastService(model)
scenario_service = ScenarioService(model)
assistant_service = AssistantService(
model, forecast_service=forecast_service, converter=converter
)
return forecast_service, scenario_service, assistant_service, converter
def resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
target = currency or model.base_currency
converter = CurrencyConverter(model.exchange_rates)
symbol = converter.get_symbol(target)
return target, symbol
def convert_value(
converter: CurrencyConverter, amount: Decimal, from_curr: str, to_curr: str
) -> Decimal:
if to_curr == from_curr:
return amount
return converter.convert(amount, from_curr, to_curr)
def fmt_amount(amount: Decimal | float, currency: str, symbol: str) -> str:
if isinstance(amount, Decimal):
return f"{symbol}{amount:,.2f}"
return f"{symbol}{amount:,.2f}"