feat(generator): каркас проекта и контракт схемы события
- Зачем:
- этап 2 начинается с формы: контракт схемы — источник истины и для
генерации событий, и для DDL хранилища, а имена пакета и модулей
задают границы всем следующим тикетам этапа.
- Что:
- заведён uv-проект generator/ (pyproject.toml и uv.lock в git; numpy,
pytest и ruff), пакет clickstream_generator.
- schema.py — контракт: чистые данные о 47 колонках выгрузки (имя
Метрики, тип ClickHouse, тип numpy, имя для DDS, группа); порядок
несёт сам кортеж COLUMNS, отдельного поля с номером нет намеренно.
- schema_doc.py собирает из контракта описание выгрузки
docs/formats/clickstream-event.md — по нему пишется сторона
хранилища; документ руками не правится.
- тесты: инварианты контракта (состав, уникальность, заполненность,
согласие типов и порядок групп) и свежесть описания выгрузки.
- цели make lint, make test и make docs; README, AGENTS.md и
CONTEXT.md дополнены генератором, форматами и словарной статьёй.
- Проверка:
- make test (248 тестов), make lint, make config-test;
- make docs, затем git diff --exit-code docs/ — пусто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""Сборка «описания выгрузки» — публичной документации формата события.
|
||||
|
||||
Аналог документации Метрики: по нему пишется сторона хранилища (DDL, матвью,
|
||||
витрины), поэтому документ должен читаться сам по себе, без чтения кода. Всё
|
||||
содержание берётся из контракта (`schema`), правится только там; свежесть
|
||||
документа сторожит тест.
|
||||
|
||||
Запуск — из корня репозитория целью `make docs`.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections.abc import Sequence
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
|
||||
from clickstream_generator.schema import COLUMNS, Column
|
||||
|
||||
PREAMBLE = """# Описание выгрузки: событие кликстрима
|
||||
|
||||
Документ собран из контракта схемы генератора
|
||||
(`generator/src/clickstream_generator/schema.py`). Руками не править —
|
||||
пересобрать: `make docs`.
|
||||
|
||||
Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики.
|
||||
Многозначное лежит в параллельных массивах одной длины, плюс одно сырое
|
||||
JSON-поле `ecommerce`. Отдельной сущности «визит» в выгрузке нет — визиты
|
||||
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
|
||||
|
||||
Имена и типы колонок — стороны источника. Хранилище принимает их как есть и
|
||||
нормализует у себя: своё snake_case-имя каждой колонки ждёт в столбце «Имя в
|
||||
DDS». Столбец «Тип numpy» показывает, чем колонка представлена внутри
|
||||
генератора; у массивов это тип элемента. Номер — место колонки в выгрузке:
|
||||
порядок задан контрактом.
|
||||
|
||||
Колонки группы «Ecommerce» заполнены только у торговых событий:
|
||||
`add_to_cart` несёт один товар, `purchase` — состав заказа и блок
|
||||
`purchase*`. У остальных событий они пусты.
|
||||
|
||||
Всего колонок: {count}."""
|
||||
|
||||
TABLE_HEADER = (
|
||||
"| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий |",
|
||||
"|---|---|---|---|---|---|",
|
||||
)
|
||||
|
||||
|
||||
def render(columns: Sequence[Column] = COLUMNS) -> str:
|
||||
"""Собирает документ целиком: преамбула и таблица колонок по группам."""
|
||||
lines = PREAMBLE.format(count=len(columns)).splitlines()
|
||||
numbers = iter(range(1, len(columns) + 1))
|
||||
for group, columns_of_group in groupby(columns, key=lambda column: column.group):
|
||||
lines += ["", f"## {group.value}", "", *TABLE_HEADER]
|
||||
lines += [row(next(numbers), column) for column in columns_of_group]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def row(number: int, column: Column) -> str:
|
||||
"""Строка таблицы; номер — место колонки в порядке выгрузки."""
|
||||
cells = (
|
||||
str(number),
|
||||
f"`{column.name}`",
|
||||
f"`{column.clickhouse_type}`",
|
||||
f"`{column.numpy_dtype}`",
|
||||
f"`{column.dds_name}`",
|
||||
column.comment,
|
||||
)
|
||||
return "| " + " | ".join(cells) + " |"
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Собирает описание выгрузки из контракта схемы события."
|
||||
)
|
||||
parser.add_argument("output", type=Path, help="путь к файлу описания")
|
||||
output = parser.parse_args(argv).output
|
||||
output.write_text(render(), encoding="utf-8")
|
||||
print(f"Описание выгрузки собрано: {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user