feat: models/enums.py — 27 enum'ов DBML v5.13, фабрика pg_enum, DBML_VERSION

Тест сверяет имена и упорядоченные значения с блоками Enum {} DBML через pydbml.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-03 18:47:23 +04:00
parent 1c56d39b09
commit 8e6223a273
5 changed files with 426 additions and 1 deletions

View File

@ -19,6 +19,9 @@ env =
APP_ENV=test
ENV_FILE=.env.test
norecursedirs = .git __pycache__ .venv env build dist
# pydbml (dev-зависимость) вызывает устаревшие API pyparsing — шум библиотеки, не нашего кода
filterwarnings =
ignore::DeprecationWarning:pydbml.*
markers =
integration: тесты, которым нужны PostgreSQL/NATS (docker compose); запускаются отдельно от юнит-тестов
fixtures: тесты на реальных данных из ../fixtures (внутренний доступ)

View File

@ -2,4 +2,7 @@
from .base import Base, UUIDPrimaryMixin
__all__ = ["Base", "UUIDPrimaryMixin"]
#: Версия DBML, которой соответствуют модели (сверяется с шапкой DBML в check_dbml_vs_orm).
DBML_VERSION = "5.13"
__all__ = ["DBML_VERSION", "Base", "UUIDPrimaryMixin"]

338
api/src/models/enums.py Normal file
View File

@ -0,0 +1,338 @@
"""Enum'ы модели данных DBML v5.13 (27 штук) и фабрика `pg_enum` для PostgreSQL-колонок.
Источник истины — docs/model/data-model-v5.13.dbml (блоки `Enum {}`). Значения — строки DBML буквально, в порядке
DBML (включая кириллические `"ЮЛ"`/`"ИП"` и значения в разном регистре); `notes` DBML значениями не являются.
Имя члена совпадает со значением (все значения DBML — валидные python-идентификаторы, кириллица допустима).
Классы — `enum.StrEnum` (подкласс `str`) — удобно для pydantic/JSON; `values_callable` в `pg_enum` в БД пишет `.value`.
"""
import enum
from sqlalchemy import Enum as SAEnum
SCHEMA = "dkb"
class EntityType(enum.StrEnum):
"""DBML `entity_type`."""
ЮЛ = "ЮЛ" # noqa: PLC2401 — значение DBML буквально
ИП = "ИП" # noqa: PLC2401 — значение DBML буквально
class RelatedGroupKind(enum.StrEnum):
"""DBML `related_group_kind`."""
gsk = "gsk"
gsz = "gsz"
class AccountKind(enum.StrEnum):
"""DBML `account_kind`."""
A = "A"
P = "P"
AP = "AP"
class AccountRowType(enum.StrEnum):
"""DBML `account_row_type`."""
account = "account"
subaccount = "subaccount"
analytic = "analytic"
class BusinessSegment(enum.StrEnum):
"""DBML `business_segment`."""
DKB = "DKB"
DMSB = "DMSB"
DM = "DM"
class DocumentType(enum.StrEnum):
"""DBML `document_type`."""
BALANCE = "BALANCE"
INCOME = "INCOME"
OSV = "OSV"
DISCLOSURE = "DISCLOSURE"
OTHER = "OTHER"
class FileType(enum.StrEnum):
"""DBML `file_type`."""
XLS = "XLS"
XLSX = "XLSX"
XLSB = "XLSB"
PDF = "PDF"
PNG = "PNG"
JPG = "JPG"
class RecognitionPipeline(enum.StrEnum):
"""DBML `recognition_pipeline`."""
tabular = "tabular"
image = "image"
class PeriodQuarter(enum.StrEnum):
"""DBML `period_quarter`."""
Q1 = "Q1"
Q2 = "Q2"
Q3 = "Q3"
Q4 = "Q4"
class FileStatus(enum.StrEnum):
"""DBML `file_status`."""
UPLOADED = "UPLOADED"
PROCESSING = "PROCESSING"
PROCESSED = "PROCESSED"
ERROR = "ERROR"
SUPERSEDED = "SUPERSEDED"
class DocumentStatus(enum.StrEnum):
"""DBML `document_status`."""
RECOGNIZED = "RECOGNIZED"
NEEDS_CHECK = "NEEDS_CHECK"
CONFIRMED = "CONFIRMED"
ERROR = "ERROR"
class S3BucketRole(enum.StrEnum):
"""DBML `s3_bucket_role`."""
uploads = "uploads"
results = "results"
exports = "exports"
archive = "archive"
class RecognitionStatus(enum.StrEnum):
"""DBML `recognition_status`."""
queued = "queued"
published = "published"
processing = "processing"
done = "done"
needs_check = "needs_check"
error = "error"
timeout = "timeout"
dlq = "dlq"
class StatementType(enum.StrEnum):
"""DBML `statement_type`."""
balance = "balance"
income = "income"
class OsvGroupType(enum.StrEnum):
"""DBML `osv_group_type`."""
ACCOUNT = "ACCOUNT"
DEPARTMENT = "DEPARTMENT"
COUNTERPARTY = "COUNTERPARTY"
CONTRACT = "CONTRACT"
OTHER = "OTHER"
class OsvDataType(enum.StrEnum):
"""DBML `osv_data_type`."""
BU = "BU"
CURRENCY = "CURRENCY"
QUANTITY = "QUANTITY"
SUM = "SUM"
class ConsolidationScenario(enum.StrEnum):
"""DBML `consolidation_scenario`."""
CREATE = "CREATE"
CONFIRM = "CONFIRM"
class ConsolidationApproach(enum.StrEnum):
"""DBML `consolidation_approach`."""
BASIC = "BASIC"
EXTENDED = "EXTENDED"
class ConsolidationStatus(enum.StrEnum):
"""DBML `consolidation_status`."""
INITIATED = "INITIATED"
DOCUMENTS_PROCESSING = "DOCUMENTS_PROCESSING"
DATA_CORRECTION = "DATA_CORRECTION"
CONSOLIDATION_RF = "CONSOLIDATION_RF"
ADJUSTMENT = "ADJUSTMENT"
VERIFICATION_RF = "VERIFICATION_RF"
CONSOLIDATION_GO = "CONSOLIDATION_GO"
VERIFICATION_GO = "VERIFICATION_GO"
VERIFIED = "VERIFIED"
CANCELLED = "CANCELLED"
REJECTED = "REJECTED"
ARCHIVED = "ARCHIVED"
class CorrectionStatus(enum.StrEnum):
"""DBML `correction_status`."""
DRAFT = "DRAFT"
APPLIED = "APPLIED"
REVOKED = "REVOKED"
class DocumentCheckResult(enum.StrEnum):
"""DBML `document_check_result`."""
MATCH = "MATCH"
MISMATCH = "MISMATCH"
UNKNOWN = "UNKNOWN"
class LeHistoryEvent(enum.StrEnum):
"""DBML `le_history_event`."""
CREATE = "CREATE"
UPDATE = "UPDATE"
MOVE_TO_GROUP = "MOVE_TO_GROUP"
CLOSE = "CLOSE"
REMOVE_FROM_GROUP = "REMOVE_FROM_GROUP"
class ExSnapshotStatus(enum.StrEnum):
"""DBML `ex_snapshot_status`."""
ACTUAL = "ACTUAL"
ARCHIVE = "ARCHIVE"
class PermissionObject(enum.StrEnum):
"""DBML `permission_object`."""
DIRECTORY = "DIRECTORY"
DOCUMENT = "DOCUMENT"
CONSOLIDATION = "CONSOLIDATION"
KFO = "KFO"
EX = "EX"
EXPORT = "EXPORT"
USERS = "USERS"
AUDIT = "AUDIT"
class PermissionAction(enum.StrEnum):
"""DBML `permission_action`."""
VIEW = "VIEW"
EDIT = "EDIT"
APPROVE = "APPROVE"
VERIFY = "VERIFY"
DELETE = "DELETE"
EXPORT = "EXPORT"
ADMIN = "ADMIN"
class AuditSection(enum.StrEnum):
"""DBML `audit_section`."""
DIRECTORIES = "DIRECTORIES"
CONSOLIDATION = "CONSOLIDATION"
PRIMARY_DOCUMENTS = "PRIMARY_DOCUMENTS"
RECOGNITION = "RECOGNITION"
KFO = "KFO"
EXPORT = "EXPORT"
ACCESS = "ACCESS"
SESSION = "SESSION"
USERS = "USERS"
class AuditAction(enum.StrEnum):
"""DBML `audit_action`."""
LOGIN = "LOGIN"
LOGIN_FAILED = "LOGIN_FAILED"
LOGOUT = "LOGOUT"
CONSOLIDATION_CREATE = "CONSOLIDATION_CREATE"
CONSOLIDATION_STATUS_CHANGE = "CONSOLIDATION_STATUS_CHANGE"
CONSOLIDATION_APPROVE = "CONSOLIDATION_APPROVE"
CONSOLIDATION_VERIFY = "CONSOLIDATION_VERIFY"
GROUP_CHANGE = "GROUP_CHANGE"
MEMBERSHIP_CHANGE = "MEMBERSHIP_CHANGE"
ASSIGNMENT_CHANGE = "ASSIGNMENT_CHANGE"
FILE_UPLOAD = "FILE_UPLOAD"
FILE_DELETE = "FILE_DELETE"
FILE_REPLACE = "FILE_REPLACE"
DOCUMENT_CONFIRM = "DOCUMENT_CONFIRM"
CONSOLIDATION_LOCK = "CONSOLIDATION_LOCK"
CONSOLIDATION_LOCK_TAKEOVER = "CONSOLIDATION_LOCK_TAKEOVER"
DISCLOSURE_EDIT = "DISCLOSURE_EDIT"
RECOGNIZED_DATA_EDIT = "RECOGNIZED_DATA_EDIT"
OSV_COUNTERPARTY_MAP = "OSV_COUNTERPARTY_MAP"
OSV_TYPE_OVERRIDE = "OSV_TYPE_OVERRIDE"
CORRECTION_CREATE = "CORRECTION_CREATE"
CORRECTION_EDIT = "CORRECTION_EDIT"
CORRECTION_REVOKE = "CORRECTION_REVOKE"
GOODWILL_EDIT = "GOODWILL_EDIT"
EXPORT = "EXPORT"
DIRECTORY_IMPORT = "DIRECTORY_IMPORT"
USER_ROLE_CHANGE = "USER_ROLE_CHANGE"
ROLE_PERMISSION_CHANGE = "ROLE_PERMISSION_CHANGE"
#: Реестр: имя enum в DBML (= имя типа в PostgreSQL) → класс.
ENUMS: dict[str, type[enum.Enum]] = {
"entity_type": EntityType,
"related_group_kind": RelatedGroupKind,
"account_kind": AccountKind,
"account_row_type": AccountRowType,
"business_segment": BusinessSegment,
"document_type": DocumentType,
"file_type": FileType,
"recognition_pipeline": RecognitionPipeline,
"period_quarter": PeriodQuarter,
"file_status": FileStatus,
"document_status": DocumentStatus,
"s3_bucket_role": S3BucketRole,
"recognition_status": RecognitionStatus,
"statement_type": StatementType,
"osv_group_type": OsvGroupType,
"osv_data_type": OsvDataType,
"consolidation_scenario": ConsolidationScenario,
"consolidation_approach": ConsolidationApproach,
"consolidation_status": ConsolidationStatus,
"correction_status": CorrectionStatus,
"document_check_result": DocumentCheckResult,
"le_history_event": LeHistoryEvent,
"ex_snapshot_status": ExSnapshotStatus,
"permission_object": PermissionObject,
"permission_action": PermissionAction,
"audit_section": AuditSection,
"audit_action": AuditAction,
}
def pg_enum(name: str) -> SAEnum:
"""Тип колонки для enum DBML `name`: нативный PG-enum `dkb.<name>`, значения — `.value` членов.
Создание типа — поведение SQLAlchemy по умолчанию (`create_type=True`, `checkfirst`): `Base.metadata.create_all`
создаёт типы сам; миграция 0001 создаёт их явно.
"""
return SAEnum(
ENUMS[name],
name=name,
schema=SCHEMA,
native_enum=True,
values_callable=lambda e: [m.value for m in e],
)

24
api/tests/dbml_helpers.py Normal file
View File

@ -0,0 +1,24 @@
"""Поиск и разбор DBML-модели для тестов (переиспользуется тестами чётности ORM и DBML)."""
import os
from pathlib import Path
import pytest
from pydbml import PyDBML
DBML_FILE_NAME = "data-model-v5.13.dbml"
def dbml_path() -> Path:
"""Путь к DBML: каталог docs из `ARM_KA_DOCS`, иначе соседний `../../docs` относительно `arm-core/api`."""
docs = os.environ.get("ARM_KA_DOCS")
docs_dir = Path(docs) if docs else Path(__file__).resolve().parents[3] / "docs"
return docs_dir / "model" / DBML_FILE_NAME
def load_dbml() -> PyDBML:
"""Разобрать DBML через pydbml; без файла — пропуск теста с явной причиной."""
path = dbml_path()
if not path.is_file():
pytest.skip(f"docs недоступен: нет {path} (задайте ARM_KA_DOCS или положите docs рядом с arm-core)")
return PyDBML.parse_file(path)

57
api/tests/test_enums.py Normal file
View File

@ -0,0 +1,57 @@
"""Enum'ы ORM против блоков `Enum {}` DBML v5.13: имена, значения и их порядок (без БД)."""
import enum
import re
import pytest
from sqlalchemy import Enum as SAEnum
from src.models import DBML_VERSION
from src.models.enums import ENUMS, pg_enum
from tests.dbml_helpers import dbml_path, load_dbml
EXPECTED_ENUM_COUNT = 27
def test_dbml_version_format() -> None:
assert re.fullmatch(r"\d+\.\d+", DBML_VERSION)
assert DBML_VERSION == "5.13"
assert f"v{DBML_VERSION}" in dbml_path().name
def test_enums_count_and_names_match_dbml() -> None:
dbml_enums = {e.name for e in load_dbml().enums}
assert len(dbml_enums) == EXPECTED_ENUM_COUNT
assert set(ENUMS) == dbml_enums
def test_enum_values_match_dbml_in_order() -> None:
for dbml_enum in load_dbml().enums:
expected = [item.name for item in dbml_enum.items]
actual = [m.value for m in ENUMS[dbml_enum.name]]
assert actual == expected, f"enum {dbml_enum.name}: ожидалось {expected}, фактически {actual}"
def test_enum_classes_are_str_enums_with_member_name_equal_value() -> None:
for name, cls in ENUMS.items():
assert issubclass(cls, enum.Enum) and issubclass(cls, str), name
assert [m.name for m in cls] == [m.value for m in cls], name
def test_cyrillic_values_kept_verbatim() -> None:
assert [m.value for m in ENUMS["entity_type"]] == ["ЮЛ", "ИП"]
def test_pg_enum_factory_gives_dbml_named_type_in_dkb_schema() -> None:
for name, cls in ENUMS.items():
sa_enum = pg_enum(name)
assert isinstance(sa_enum, SAEnum)
assert sa_enum.name == name
assert sa_enum.schema == "dkb"
assert sa_enum.native_enum is True
assert sa_enum.enums == [m.value for m in cls], name
def test_pg_enum_unknown_name_fails() -> None:
with pytest.raises(KeyError):
pg_enum("no_such_enum")