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
|
APP_ENV=test
|
||||||
ENV_FILE=.env.test
|
ENV_FILE=.env.test
|
||||||
norecursedirs = .git __pycache__ .venv env build dist
|
norecursedirs = .git __pycache__ .venv env build dist
|
||||||
|
# pydbml (dev-зависимость) вызывает устаревшие API pyparsing — шум библиотеки, не нашего кода
|
||||||
|
filterwarnings =
|
||||||
|
ignore::DeprecationWarning:pydbml.*
|
||||||
markers =
|
markers =
|
||||||
integration: тесты, которым нужны PostgreSQL/NATS (docker compose); запускаются отдельно от юнит-тестов
|
integration: тесты, которым нужны PostgreSQL/NATS (docker compose); запускаются отдельно от юнит-тестов
|
||||||
fixtures: тесты на реальных данных из ../fixtures (внутренний доступ)
|
fixtures: тесты на реальных данных из ../fixtures (внутренний доступ)
|
||||||
|
|||||||
@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
from .base import Base, UUIDPrimaryMixin
|
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