94 lines
6.1 KiB
Python
94 lines
6.1 KiB
Python
"""Загрузчик справочников продукта: `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())
|