Files
clickstream-data-platform/generator/src/clickstream_generator/schema_doc.py
T
ddadminandClaude Opus 5 6a708cd5aa refactor(make): корень — про стенд, генератор — за своей дверью
- Зачем:
  - корневые цели смешивали два уровня: пять из шестнадцати начинались с cd generator.
  - цель, названная общерепозиторной, охватывала 31 файл Python из 34: даги и Superset не видел ни линт, ни типы.
- Что:
  - lint, typecheck, test, docs и inventory переехали в новый generator/Makefile.
  - корневой lint заведён по коду стенда — dags и infra/superset — с явными путями и закреплённой версией ruff.
  - заведён корневой ruff.toml: тот же список правил, target-version по младшему Python в образах стенда.
  - цели корня сгруппированы по использованию, осталось двенадцать.
  - два файла дагов переформатированы под новую проверку.
  - карта проверок, оба README, спека генератора и AGENTS.md приведены к двум дверям.
- Проверка:
  - make lint; make config-test — зелёные.
  - make -C generator lint; typecheck; test — зелёные, 407 тестов.
  - ruff check --show-files: из корня ровно три файла стенда, из generator/ — только его.
  - цена корневого lint замерена (0,4 с) и вписана в карту проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:31:07 +03:00

89 lines
4.9 KiB
Python

"""Сборка «описания выгрузки» — публичной документации формата события.
Аналог документации Метрики: по нему пишется сторона хранилища (DDL, матвью,
витрины), поэтому документ должен читаться сам по себе, без чтения кода. Всё
содержание берётся из контракта (`schema`), правится только там; свежесть
документа сторожит тест.
Запуск — из каталога генератора целью `make docs`.
"""
import argparse
from itertools import groupby
from pathlib import Path
from clickstream_generator.schema import COLUMNS, Column
PREAMBLE = """# Описание выгрузки: событие кликстрима
Документ собран из контракта схемы генератора
(`generator/src/clickstream_generator/schema.py`). Руками не править —
пересобрать: `make docs` из каталога `generator/`.
Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики.
Многозначное лежит в параллельных массивах, плюс одно сырое JSON-поле
`ecommerce`. Длина у массивов общая **внутри группы**, а не по всему
событию: `purchase*` — по элементу на заказ (у нас всегда один), `product*` —
по элементу на товар. Отдельной сущности «визит» в выгрузке нет — визиты
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
Имена и типы колонок — стороны источника. Хранилище принимает их как есть и
нормализует у себя: то же имя в нашем стиле ждёт в столбце «Нормализованное
имя». Это имя источника, приведённое к snake_case, а не имя атрибута в
модели данных: слой DDS складывает свою модель и называет атрибуты по ней.
Столбец «Тип numpy» показывает, чем колонка представлена внутри генератора;
у массивов это тип элемента. Номер — место колонки в выгрузке: порядок задан
контрактом.
Колонки группы «Ecommerce» заполнены только у торговых событий:
`add_to_cart` несёт один товар, `purchase` — состав заказа и блок
`purchase*`. У остальных событий они пусты.
Деньги: `productPrice` — целые рубли, округление формата. Точная сумма
заказа живёт в `purchaseRevenue` и в сыром `ecommerce`, поэтому пересчитать
выручку по разобранным массивам нельзя — цены каталога бывают с копейками.
Всего колонок: {count}."""
TABLE_HEADER = (
"| № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |",
"|---|---|---|---|---|---|",
)
def render() -> 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 += [table_row(next(numbers), column) for column in columns_of_group]
return "\n".join(lines) + "\n"
def table_row(number: int, column: Column) -> str:
"""Строка таблицы колонок; номер — место колонки в порядке выгрузки."""
cells = (
str(number),
f"`{column.name}`",
f"`{column.clickhouse_type}`",
f"`{column.numpy_dtype}`",
f"`{column.normalized_name}`",
column.comment,
)
return "| " + " | ".join(cells) + " |"
def main() -> None:
parser = argparse.ArgumentParser(
description="Собирает описание выгрузки из контракта схемы события."
)
parser.add_argument("output", type=Path, help="путь к файлу описания")
output = parser.parse_args().output
output.write_text(render(), encoding="utf-8")
print(f"Описание выгрузки собрано: {output}")
if __name__ == "__main__":
main()