- Зачем: - показать расхождения трекера и бэкенда на зерне заказа - сохранить отложенные покупки наблюдаемыми до следующего слепка - Что: - добавлены таблицы, представление и пересборка purchase_vs_orders - сумма покупки нормализована в Decimal на границе ODS - обновлены договоры и указатели на механику пересборки - Проверка: - `make config-test`, `make lint` - `cd generator && make docs lint typecheck test` - `make rebuild-storage`, явные `dds_transform` и `dm_transform` - `make smoke`, `make check-clickhouse`, `make check-services`
91 lines
5.1 KiB
Python
91 lines
5.1 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` дан как эталон для самопроверки.
|
||
|
||
Имена и типы колонок описывают сторону источника. Обычно ODS принимает их без
|
||
изменений. Исключение — `purchaseRevenue`: источник объявляет Float64, а ODS
|
||
хранит деньги в Decimal. Точный локальный тип назван в документации
|
||
хранилища. Имя в нашем стиле ждет в столбце «Нормализованное имя». Это имя
|
||
источника, приведенное к 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()
|