Andrey Epifancev 186ae802e6 feat: seed ролей, прав, матрицы §10 и Головного офиса, загрузчик scripts.seed
Двухфазный загрузчик: валидация yaml целиком до записи (SeedError), затем upsert по
натуральному ключу без удалений; run_seed(session, *, dev, seed_dir) без commit, CLI коммитит.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 21:07:07 +04:00

118 lines
4.3 KiB
Python

"""Фаза 2: применение проверенного набора к БД — upsert по натуральному ключу, без удалений и без commit."""
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from scripts.seed_lib.model import SeedData, SeedReport, TableStat
from src.models.access import Permission, Role, RolePermission
from src.models.directories import RegionalOffice
async def _upsert(
session: AsyncSession,
model: Any,
key: str,
rows: list[dict[str, Any]],
stat: TableStat,
) -> None:
"""Вставить новые и обновить изменившиеся строки по натуральному ключу `key`; удалений нет."""
if not rows:
return
key_column = getattr(model, key)
value_names = [name for name in rows[0] if name != key]
existing: dict[Any, tuple[Any, ...]] = {
row[0]: tuple(row[1:])
for row in (await session.execute(select(key_column, *(getattr(model, name) for name in value_names)))).all()
}
changed = []
for row in rows:
current = existing.get(row[key])
if current is None:
stat.inserted += 1
changed.append(row)
elif current != tuple(row[name] for name in value_names):
stat.updated += 1
changed.append(row)
else:
stat.unchanged += 1
if changed:
statement = pg_insert(model)
statement = statement.on_conflict_do_update(
index_elements=[key_column], set_={name: statement.excluded[name] for name in value_names}
)
await session.execute(statement, changed)
async def _apply_role_permissions(session: AsyncSession, data: SeedData, stat: TableStat) -> None:
role_ids = {code: id_ for id_, code in (await session.execute(select(Role.id, Role.code))).all()}
permission_ids = {code: id_ for id_, code in (await session.execute(select(Permission.id, Permission.code))).all()}
existing = set((await session.execute(select(RolePermission.role_id, RolePermission.permission_id))).all())
new_rows = []
for role_code, codes in data.role_permissions.items():
for code in codes:
pair = (role_ids[role_code], permission_ids[code])
if pair in existing:
stat.unchanged += 1
else:
stat.inserted += 1
new_rows.append(
{"role_id": pair[0], "permission_id": pair[1], "granted_by": None, "granted_at": func.now()}
)
if new_rows:
# .values(): func.now() допустим только во встроенном VALUES, не в executemany
statement = (
pg_insert(RolePermission)
.values(new_rows)
.on_conflict_do_nothing(index_elements=["role_id", "permission_id"])
)
await session.execute(statement)
async def apply_seed(session: AsyncSession, data: SeedData) -> SeedReport:
"""Фаза 2: upsert набора в порядке зависимостей. Без commit — транзакцией управляет вызывающий."""
report = SeedReport()
await _upsert(
session,
RegionalOffice,
"name",
[{"name": o.name, "is_head_office": o.is_head_office, "is_active": o.is_active} for o in data.regional_offices],
report.stat("regional_office"),
)
await _upsert(
session,
Role,
"code",
[
{
"code": r.code,
"name": r.name,
"description": r.description,
"is_system": r.is_system,
"is_active": r.is_active,
}
for r in data.roles
],
report.stat("role"),
)
await _upsert(
session,
Permission,
"code",
[
{
"code": p.code,
"object_type": p.object_type,
"action": p.action,
"name": p.name,
"description": p.description,
}
for p in data.permissions
],
report.stat("permission"),
)
await _apply_role_permissions(session, data, report.stat("role_permission"))
return report