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:
parent
1c56d39b09
commit
8e6223a273
@ -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 (внутренний доступ)
|
||||
|
||||
@ -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
338
api/src/models/enums.py
Normal 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
24
api/tests/dbml_helpers.py
Normal 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
57
api/tests/test_enums.py
Normal 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")
|
||||
Loading…
x
Reference in New Issue
Block a user