"""Загрузчик справочников продукта: `python -m scripts.seed [--dev]` (запуск из `api/`). Spec reference-data-seed; design D-4 и решение #18 (R-5): `run_seed(session, *, dev)` не делает commit — коммитит CLI. Источник данных — `api/seed/core/*.yaml|csv` (+ `api/seed/dev/` при `--dev`). Загрузка двухфазная: 1. `load_seed` (`seed_lib/loaders.py`) читает ВСЕ файлы в память и проверяет их (обязательные поля, enum'ы из `src.models.enums`, ссылки внутри наборов); любая ошибка — `SeedError`, база при этом не затрагивается. 2. `apply_seed` (`seed_lib/apply.py`) применяет набор к БД шагами в порядке зависимостей: upsert по натуральному ключу, без удалений. План счетов (`core/chart_of_accounts.csv`, design D-9) — ДОПУЩЕНИЕ OQ-47: полного плана 1С клиента нет, в файле Приказ Минфина № 94н: 62 активных синтетических счёта (01–05, 07–11, 14–16, 19–21, 23, 25, 26, 28, 29, 40–46, 50–52, 55, 57–60, 62, 63, 66–71, 73, 75–77, 79–84, 86, 90, 91, 94, 96–99; номера 06, 12, 13, 17, 18, 22, 24, 27, 30–39, 47–49, 53, 54, 56, 61, 64, 65, 72, 74, 78, 85, 87–89, 92, 93, 95 в плане отсутствуют), забалансовые 001–011 (`kind = A`, `is_off_balance = true`) и только те субсчета и подсубсчета типового 1С БП 3.0, на которые ссылаются §4/§6 правил и чек-лист; `kind` субсчетов наследуется от счёта. Остальное добавляет администратор (ARCH-004). Реестр кодов для проверки ссылок правил и чек-листа — `scripts.seed_lib.model.account_codes(data)`. Правила элиминации ББ (`core/elimination_rules_v1.yaml`, §4, версия 1) и чек-лист (`core/required_document_rules.yaml`, ASSUMPTION(OQ-46)): `seed_lib/loaders_rules.py` проверяет, что каждый счёт есть в плане счетов набора, ДО записи в БД; `seed_lib/apply_rules.py` — upsert (составы счетов и чек-лист приводятся к файлу без удалений; ключи без уникального индекса — в Python, NULL в `account_id` не плодит дубли). Общие помощники чтения — `seed_lib/readers.py`. Dev-данные (`--dev`, `seed/dev/`, задача 15): 2 региональных филиала (`regional_offices.yaml`, upsert по `name`) и по одному пользователю на каждую системную роль (`users.yaml`, `<роль>@armka.local`, upsert `app_user` по `email`, `user_role` по (user_id, role_id)). Ссылки пользователей на роли и филиалы проверяются до записи. Без `--dev` ни пользователи, ни dev-филиалы не загружаются. ASSUMPTION: dev-данные — только для локальной разработки и тестовых стендов; в прод `--dev` не запускать (защиты флага нет, спека её не требует). Удалений нет: роль, убранная из `users.yaml`, остаётся назначенной. Расширение (задачи 13–15): поле в `SeedData` (`seed_lib/model.py`), читатель в `load_seed`, шаг в `apply_seed`. """ import argparse import asyncio import sys from pathlib import Path from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from scripts.seed_lib.apply import apply_seed from scripts.seed_lib.loaders import DEFAULT_SEED_DIR, load_seed from scripts.seed_lib.model import SeedData, SeedError, SeedReport __all__ = ["DEFAULT_SEED_DIR", "SeedError", "SeedReport", "apply_seed", "load_seed", "main", "run_seed"] async def run_seed(session: AsyncSession, *, dev: bool = False, seed_dir: Path | None = None) -> SeedReport: """Загрузить и применить seed (решение #18 / R-5): валидация до записи, без commit. `seed_dir` — каталог с `core/` (и `dev/`), по умолчанию `api/seed`; подменяется в тестах. """ return await apply_seed(session, load_seed(seed_dir, dev=dev)) async def _run_cli(data: SeedData) -> SeedReport: # импорт здесь: настройки читаются из ENV_FILE только при реальном запуске, # поэтому `--help` и ошибки валидации работают без env from src.core.config import settings # noqa: PLC0415 engine = create_async_engine( settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}} ) try: async with async_sessionmaker(engine, expire_on_commit=False)() as session, session.begin(): return await apply_seed(session, data) finally: await engine.dispose() def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="python -m scripts.seed", description="Загрузка справочников продукта.") parser.add_argument( "--dev", action="store_true", help="дополнительно загрузить dev-данные из seed/dev/ (только для разработки, не для прода)", ) parser.add_argument("--seed-dir", type=Path, default=DEFAULT_SEED_DIR, help="каталог seed (по умолчанию api/seed)") args = parser.parse_args(argv) try: data = load_seed(args.seed_dir, dev=args.dev) report = asyncio.run(_run_cli(data)) except SeedError as error: print(f"Ошибка seed: {error}", file=sys.stderr) return 1 print(report.format()) return 0 if __name__ == "__main__": sys.exit(main())