refactor(generator): нормализованное имя вместо имени в DDS, тесты на имена сняты

- Зачем:
  - контракт вёл себя как хозяин чужого слоя: поле называлось dds_name, в
    описании стоял столбец «Имя в DDS», а два теста прибивали имена
    гвоздями. Спека же задала вид имени (snake_case), а не список: имена
    атрибутов складывает модель данных DDS, и решать это не трекеру.
- Что:
  - поле контракта и столбец описания стали нормализованным именем: имя
    источника в нашем стиле. В описании и в докстринге сказано прямо, что
    слой DDS называет атрибуты по своей модели.
  - сняты оба теста на имена — копия имён DDS и конспект состава по
    мастер-спеке. Они не проверяли верность имени, только неизменность, а
    неизменность и так сторожит пересборка описания: молчаливой правки
    контракта не бывает, она всплывает диффом документа.
  - остались проверки формы: 47 колонок, уникальность, стили имён,
    заполненность, согласие типов numpy и ClickHouse, порядок групп.
  - спека генератора (раздел 3) и 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:
2026-08-01 23:01:04 +03:00
co-authored by Claude Opus 5
parent aabd339a26
commit 24c8dd9b98
7 changed files with 95 additions and 199 deletions
+6
View File
@@ -43,6 +43,12 @@ Python-модуль с описателями колонок события —
Из него выводятся генератор, валидация и документация формата; хранилище Из него выводятся генератор, валидация и документация формата; хранилище
строится по документации, не по модулю. строится по документации, не по модулю.
**Нормализованное имя**:
Имя колонки источника, приведённое к нашему стилю (snake_case). Живёт в
контракте схемы и в описании выгрузки. Не то же, что имя атрибута в модели
данных: слой DDS складывает модель и называет атрибуты по ней.
_Избегать_: имя в DDS
**Описание выгрузки**: **Описание выгрузки**:
Публичная документация формата события: таблица колонок, собранная из Публичная документация формата события: таблица колонок, собранная из
контракта схемы. По ней пишется сторона хранилища — как в бою по документации контракта схемы. По ней пишется сторона хранилища — как в бою по документации
+11 -9
View File
@@ -10,10 +10,12 @@ JSON-поле `ecommerce`. Отдельной сущности «визит» в
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки. собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
Имена и типы колонок — стороны источника. Хранилище принимает их как есть и Имена и типы колонок — стороны источника. Хранилище принимает их как есть и
нормализует у себя: своё snake_case-имя каждой колонки ждёт в столбце «Имя в нормализует у себя: то же имя в нашем стиле ждёт в столбце «Нормализованное
DDS». Столбец «Тип numpy» показывает, чем колонка представлена внутри имя». Это имя источника, приведённое к snake_case, а не имя атрибута в
генератора; у массивов это тип элемента. Номер — место колонки в выгрузке: модели данных: слой DDS складывает свою модель и называет атрибуты по ней.
порядок задан контрактом. Столбец «Тип numpy» показывает, чем колонка представлена внутри генератора;
у массивов это тип элемента. Номер — место колонки в выгрузке: порядок задан
контрактом.
Колонки группы «Ecommerce» заполнены только у торговых событий: Колонки группы «Ecommerce» заполнены только у торговых событий:
`add_to_cart` несёт один товар, `purchase` — состав заказа и блок `add_to_cart` несёт один товар, `purchase` — состав заказа и блок
@@ -23,7 +25,7 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
## Идентификаторы и время ## Идентификаторы и время
| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий | | № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 1 | `WatchID` | `UInt64` | `uint64` | `watch_id` | id события — хита; держится ниже 2^53, выше числа в JSON округляются | | 1 | `WatchID` | `UInt64` | `uint64` | `watch_id` | id события — хита; держится ниже 2^53, выше числа в JSON округляются |
| 2 | `VisitID` | `UInt64` | `uint64` | `visit_id` | id визита от генератора — эталон лабы: собери сессии сам и сравни | | 2 | `VisitID` | `UInt64` | `uint64` | `visit_id` | id визита от генератора — эталон лабы: собери сессии сам и сравни |
@@ -37,7 +39,7 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
## Страница и атрибуция ## Страница и атрибуция
| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий | | № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 10 | `URL` | `String` | `object` | `url` | адрес страницы события | | 10 | `URL` | `String` | `object` | `url` | адрес страницы события |
| 11 | `Referer` | `String` | `object` | `referer` | адрес, с которого посетитель пришёл на страницу | | 11 | `Referer` | `String` | `object` | `referer` | адрес, с которого посетитель пришёл на страницу |
@@ -53,7 +55,7 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
## Браузер, устройство, гео ## Браузер, устройство, гео
| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий | | № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 21 | `Browser` | `String` | `object` | `browser` | браузер посетителя | | 21 | `Browser` | `String` | `object` | `browser` | браузер посетителя |
| 22 | `BrowserMajorVersion` | `UInt16` | `uint16` | `browser_major_version` | старшая версия браузера | | 22 | `BrowserMajorVersion` | `UInt16` | `uint16` | `browser_major_version` | старшая версия браузера |
@@ -72,14 +74,14 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
## Массивы и параметры ## Массивы и параметры
| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий | | № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 35 | `GoalsReached` | `Array(UInt32)` | `uint32` | `goals_reached` | id достигнутых целей; на стенде их две — корзина и покупка | | 35 | `GoalsReached` | `Array(UInt32)` | `uint32` | `goals_reached` | id достигнутых целей; на стенде их две — корзина и покупка |
| 36 | `ParsedParamsKey1` | `Array(String)` | `object` | `parsed_params_key1` | свои параметры сайта, один уровень — например вариант A/B-теста | | 36 | `ParsedParamsKey1` | `Array(String)` | `object` | `parsed_params_key1` | свои параметры сайта, один уровень — например вариант A/B-теста |
## Ecommerce ## Ecommerce
| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий | | № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 37 | `purchaseID` | `Array(String)` | `object` | `purchase_id` | номер заказа; у события purchase — один элемент | | 37 | `purchaseID` | `Array(String)` | `object` | `purchase_id` | номер заказа; у события purchase — один элемент |
| 38 | `purchaseRevenue` | `Array(Float64)` | `float64` | `purchase_revenue` | выручка заказа глазами клиента; Float64, как у Метрики — на этом держится урок о расхождениях с бэкендом | | 38 | `purchaseRevenue` | `Array(Float64)` | `float64` | `purchase_revenue` | выручка заказа глазами клиента; Float64, как у Метрики — на этом держится урок о расхождениях с бэкендом |
+7 -3
View File
@@ -156,9 +156,13 @@
валидация и публичное «описание выгрузки» в доках — рендеренная таблица валидация и публичное «описание выгрузки» в доках — рендеренная таблица
колонок, аналог документации Метрики. колонок, аналог документации Метрики.
- **Форма контракта — импортируемый python-модуль с чистыми данными**: - **Форма контракта — импортируемый python-модуль с чистыми данными**:
описатели колонок (имя Метрики, тип ClickHouse, тип numpy, snake_case-имя описатели колонок (имя Метрики, тип ClickHouse, тип numpy, нормализованное
для DDS, группа полей, порядок), никакой логики. Читаемость для менти snake_case-имя, группа полей, порядок), никакой логики. Читаемость для
несёт рендеренная таблица в доках, не модуль. менти несёт рендеренная таблица в доках, не модуль. Уточнение при
исполнении (#36): нормализованное имя — имя источника, приведённое к
нашему стилю, а не имя атрибута в модели данных. Слой DDS складывает свою
модель и называет атрибуты по ней; `dds.v_event` эти имена берёт (раздел 7
мастер-спеки), но контракт их не диктует и тестами не сторожит.
- **Сторона хранилища пишется по документации, не генерируется.** DDL - **Сторона хранилища пишется по документации, не генерируется.** DDL
`ods.event`, SELECT матвью, `dds.v_event`, трансформации — работа `ods.event`, SELECT матвью, `dds.v_event`, трансформации — работа
следующих этапов по «описанию выгрузки», как в бою хранилище адаптируется следующих этапов по «описанию выгрузки», как в бою хранилище адаптируется
+53 -51
View File
@@ -20,10 +20,12 @@
- `numpy_dtype` чем колонка представлена внутри генератора; у массивов это - `numpy_dtype` чем колонка представлена внутри генератора; у массивов это
тип элемента. Строки живут в `object`-массивах: numpy-строки фиксированной тип элемента. Строки живут в `object`-массивах: numpy-строки фиксированной
длины стенду ничего не дают. длины стенду ничего не дают.
- `dds_name` наше snake_case-имя, под которым колонка появится в DDS. - `normalized_name` то же имя в нашем стиле: snake_case, механический
Перевод механический, акроним идёт одним куском (`utm_source`, `has_gclid`, перевод имени источника, акроним одним куском (`utm_source`, `has_gclid`,
`ip_address`); единственное исключение `client_timezone`: «timezone» `ip_address`); единственное исключение `client_timezone`: «timezone»
пишем одним словом. пишем одним словом. Это имя источника, приведённое к нашему стилю, а не
имя атрибута в модели данных: слой DDS складывает свою модель и называет
атрибуты по ней нормализованное имя ему отправная точка, не обязанность.
- `group` раздел описания выгрузки; колонки одной группы идут подряд. - `group` раздел описания выгрузки; колонки одной группы идут подряд.
- `comment` строка описания для менти, попадает в документ как есть. - `comment` строка описания для менти, попадает в документ как есть.
@@ -54,7 +56,7 @@ class Column:
name: str name: str
clickhouse_type: str clickhouse_type: str
numpy_dtype: str numpy_dtype: str
dds_name: str normalized_name: str
group: ColumnGroup group: ColumnGroup
comment: str comment: str
@@ -64,7 +66,7 @@ COLUMNS: tuple[Column, ...] = (
name="WatchID", name="WatchID",
clickhouse_type="UInt64", clickhouse_type="UInt64",
numpy_dtype="uint64", numpy_dtype="uint64",
dds_name="watch_id", normalized_name="watch_id",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="id события — хита; держится ниже 2^53, выше числа в JSON округляются", comment="id события — хита; держится ниже 2^53, выше числа в JSON округляются",
), ),
@@ -72,7 +74,7 @@ COLUMNS: tuple[Column, ...] = (
name="VisitID", name="VisitID",
clickhouse_type="UInt64", clickhouse_type="UInt64",
numpy_dtype="uint64", numpy_dtype="uint64",
dds_name="visit_id", normalized_name="visit_id",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="id визита от генератора — эталон лабы: собери сессии сам и сравни", comment="id визита от генератора — эталон лабы: собери сессии сам и сравни",
), ),
@@ -80,7 +82,7 @@ COLUMNS: tuple[Column, ...] = (
name="ClientID", name="ClientID",
clickhouse_type="UInt64", clickhouse_type="UInt64",
numpy_dtype="uint64", numpy_dtype="uint64",
dds_name="client_id", normalized_name="client_id",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="анонимный id браузера — кука; по хешу от неё таблица шардируется", comment="анонимный id браузера — кука; по хешу от неё таблица шардируется",
), ),
@@ -88,7 +90,7 @@ COLUMNS: tuple[Column, ...] = (
name="CounterID", name="CounterID",
clickhouse_type="UInt32", clickhouse_type="UInt32",
numpy_dtype="uint32", numpy_dtype="uint32",
dds_name="counter_id", normalized_name="counter_id",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="id счётчика: на стенде константа, сайт один", comment="id счётчика: на стенде константа, сайт один",
), ),
@@ -96,7 +98,7 @@ COLUMNS: tuple[Column, ...] = (
name="EventDate", name="EventDate",
clickhouse_type="Date", clickhouse_type="Date",
numpy_dtype="datetime64[D]", numpy_dtype="datetime64[D]",
dds_name="event_date", normalized_name="event_date",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="дата события; по ней режется партиция", comment="дата события; по ней режется партиция",
), ),
@@ -104,7 +106,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTCEventTime", name="UTCEventTime",
clickhouse_type="DateTime", clickhouse_type="DateTime",
numpy_dtype="datetime64[s]", numpy_dtype="datetime64[s]",
dds_name="utc_event_time", normalized_name="utc_event_time",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="время события в UTC — единственная метка времени, как у Метрики", comment="время события в UTC — единственная метка времени, как у Метрики",
), ),
@@ -112,7 +114,7 @@ COLUMNS: tuple[Column, ...] = (
name="ClientTimeZone", name="ClientTimeZone",
clickhouse_type="Int16", clickhouse_type="Int16",
numpy_dtype="int16", numpy_dtype="int16",
dds_name="client_timezone", normalized_name="client_timezone",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="смещение часового пояса клиента от UTC, в минутах", comment="смещение часового пояса клиента от UTC, в минутах",
), ),
@@ -120,7 +122,7 @@ COLUMNS: tuple[Column, ...] = (
name="EventType", name="EventType",
clickhouse_type="LowCardinality(String)", clickhouse_type="LowCardinality(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="event_type", normalized_name="event_type",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="тип события: pageview, add_to_cart, purchase — добавка стенда," comment="тип события: pageview, add_to_cart, purchase — добавка стенда,"
" у Метрики такого поля нет", " у Метрики такого поля нет",
@@ -129,7 +131,7 @@ COLUMNS: tuple[Column, ...] = (
name="Sign", name="Sign",
clickhouse_type="Int8", clickhouse_type="Int8",
numpy_dtype="int8", numpy_dtype="int8",
dds_name="sign", normalized_name="sign",
group=ColumnGroup.IDENTIFIERS, group=ColumnGroup.IDENTIFIERS,
comment="всегда 1: колонка формата, исправлений записей генератор не шлёт", comment="всегда 1: колонка формата, исправлений записей генератор не шлёт",
), ),
@@ -137,7 +139,7 @@ COLUMNS: tuple[Column, ...] = (
name="URL", name="URL",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="url", normalized_name="url",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="адрес страницы события", comment="адрес страницы события",
), ),
@@ -145,7 +147,7 @@ COLUMNS: tuple[Column, ...] = (
name="Referer", name="Referer",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="referer", normalized_name="referer",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="адрес, с которого посетитель пришёл на страницу", comment="адрес, с которого посетитель пришёл на страницу",
), ),
@@ -153,7 +155,7 @@ COLUMNS: tuple[Column, ...] = (
name="Title", name="Title",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="title", normalized_name="title",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="заголовок страницы", comment="заголовок страницы",
), ),
@@ -161,7 +163,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTMSource", name="UTMSource",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="utm_source", normalized_name="utm_source",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="метка utm_source: площадка перехода", comment="метка utm_source: площадка перехода",
), ),
@@ -169,7 +171,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTMMedium", name="UTMMedium",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="utm_medium", normalized_name="utm_medium",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="метка utm_medium: тип трафика", comment="метка utm_medium: тип трафика",
), ),
@@ -177,7 +179,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTMCampaign", name="UTMCampaign",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="utm_campaign", normalized_name="utm_campaign",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="метка utm_campaign: рекламная кампания", comment="метка utm_campaign: рекламная кампания",
), ),
@@ -185,7 +187,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTMContent", name="UTMContent",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="utm_content", normalized_name="utm_content",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="метка utm_content: что различает объявления одной кампании", comment="метка utm_content: что различает объявления одной кампании",
), ),
@@ -193,7 +195,7 @@ COLUMNS: tuple[Column, ...] = (
name="UTMTerm", name="UTMTerm",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="utm_term", normalized_name="utm_term",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="метка utm_term: ключевое слово перехода", comment="метка utm_term: ключевое слово перехода",
), ),
@@ -201,7 +203,7 @@ COLUMNS: tuple[Column, ...] = (
name="LastTrafficSource", name="LastTrafficSource",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="last_traffic_source", normalized_name="last_traffic_source",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="последний источник трафика: organic, direct, ad и подобные", comment="последний источник трафика: organic, direct, ad и подобные",
), ),
@@ -209,7 +211,7 @@ COLUMNS: tuple[Column, ...] = (
name="HasGCLID", name="HasGCLID",
clickhouse_type="UInt8", clickhouse_type="UInt8",
numpy_dtype="uint8", numpy_dtype="uint8",
dds_name="has_gclid", normalized_name="has_gclid",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="1, если в адресе была метка Google Ads", comment="1, если в адресе была метка Google Ads",
), ),
@@ -217,7 +219,7 @@ COLUMNS: tuple[Column, ...] = (
name="YCLID", name="YCLID",
clickhouse_type="UInt64", clickhouse_type="UInt64",
numpy_dtype="uint64", numpy_dtype="uint64",
dds_name="yclid", normalized_name="yclid",
group=ColumnGroup.PAGE, group=ColumnGroup.PAGE,
comment="id клика Яндекс Директа; без метки — 0", comment="id клика Яндекс Директа; без метки — 0",
), ),
@@ -225,7 +227,7 @@ COLUMNS: tuple[Column, ...] = (
name="Browser", name="Browser",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="browser", normalized_name="browser",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="браузер посетителя", comment="браузер посетителя",
), ),
@@ -233,7 +235,7 @@ COLUMNS: tuple[Column, ...] = (
name="BrowserMajorVersion", name="BrowserMajorVersion",
clickhouse_type="UInt16", clickhouse_type="UInt16",
numpy_dtype="uint16", numpy_dtype="uint16",
dds_name="browser_major_version", normalized_name="browser_major_version",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="старшая версия браузера", comment="старшая версия браузера",
), ),
@@ -241,7 +243,7 @@ COLUMNS: tuple[Column, ...] = (
name="BrowserLanguage", name="BrowserLanguage",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="browser_language", normalized_name="browser_language",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="язык браузера", comment="язык браузера",
), ),
@@ -249,7 +251,7 @@ COLUMNS: tuple[Column, ...] = (
name="OperatingSystem", name="OperatingSystem",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="operating_system", normalized_name="operating_system",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="операционная система с версией", comment="операционная система с версией",
), ),
@@ -257,7 +259,7 @@ COLUMNS: tuple[Column, ...] = (
name="OperatingSystemRoot", name="OperatingSystemRoot",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="operating_system_root", normalized_name="operating_system_root",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="семейство операционной системы, без версии", comment="семейство операционной системы, без версии",
), ),
@@ -265,7 +267,7 @@ COLUMNS: tuple[Column, ...] = (
name="DeviceCategory", name="DeviceCategory",
clickhouse_type="UInt8", clickhouse_type="UInt8",
numpy_dtype="uint8", numpy_dtype="uint8",
dds_name="device_category", normalized_name="device_category",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="тип устройства кодами Метрики: 1 — десктоп, 2 — телефон," comment="тип устройства кодами Метрики: 1 — десктоп, 2 — телефон,"
" 3 — планшет, 4 — телевизор; у Метрики это строка, у нас число", " 3 — планшет, 4 — телевизор; у Метрики это строка, у нас число",
@@ -274,7 +276,7 @@ COLUMNS: tuple[Column, ...] = (
name="MobilePhoneModel", name="MobilePhoneModel",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="mobile_phone_model", normalized_name="mobile_phone_model",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="модель телефона; на десктопе пусто", comment="модель телефона; на десктопе пусто",
), ),
@@ -282,7 +284,7 @@ COLUMNS: tuple[Column, ...] = (
name="ScreenWidth", name="ScreenWidth",
clickhouse_type="UInt16", clickhouse_type="UInt16",
numpy_dtype="uint16", numpy_dtype="uint16",
dds_name="screen_width", normalized_name="screen_width",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="ширина экрана в пикселях", comment="ширина экрана в пикселях",
), ),
@@ -290,7 +292,7 @@ COLUMNS: tuple[Column, ...] = (
name="ScreenHeight", name="ScreenHeight",
clickhouse_type="UInt16", clickhouse_type="UInt16",
numpy_dtype="uint16", numpy_dtype="uint16",
dds_name="screen_height", normalized_name="screen_height",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="высота экрана в пикселях", comment="высота экрана в пикселях",
), ),
@@ -298,7 +300,7 @@ COLUMNS: tuple[Column, ...] = (
name="IPAddress", name="IPAddress",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="ip_address", normalized_name="ip_address",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="IP-адрес посетителя", comment="IP-адрес посетителя",
), ),
@@ -306,7 +308,7 @@ COLUMNS: tuple[Column, ...] = (
name="RegionCountry", name="RegionCountry",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="region_country", normalized_name="region_country",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="страна кодом ISO", comment="страна кодом ISO",
), ),
@@ -314,7 +316,7 @@ COLUMNS: tuple[Column, ...] = (
name="RegionCity", name="RegionCity",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="region_city", normalized_name="region_city",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="город, название по-английски", comment="город, название по-английски",
), ),
@@ -322,7 +324,7 @@ COLUMNS: tuple[Column, ...] = (
name="RegionCountryID", name="RegionCountryID",
clickhouse_type="UInt32", clickhouse_type="UInt32",
numpy_dtype="uint32", numpy_dtype="uint32",
dds_name="region_country_id", normalized_name="region_country_id",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="числовой id страны в справочнике регионов Яндекса", comment="числовой id страны в справочнике регионов Яндекса",
), ),
@@ -330,7 +332,7 @@ COLUMNS: tuple[Column, ...] = (
name="RegionCityID", name="RegionCityID",
clickhouse_type="UInt32", clickhouse_type="UInt32",
numpy_dtype="uint32", numpy_dtype="uint32",
dds_name="region_city_id", normalized_name="region_city_id",
group=ColumnGroup.CLIENT, group=ColumnGroup.CLIENT,
comment="числовой id города в том же справочнике", comment="числовой id города в том же справочнике",
), ),
@@ -338,7 +340,7 @@ COLUMNS: tuple[Column, ...] = (
name="GoalsReached", name="GoalsReached",
clickhouse_type="Array(UInt32)", clickhouse_type="Array(UInt32)",
numpy_dtype="uint32", numpy_dtype="uint32",
dds_name="goals_reached", normalized_name="goals_reached",
group=ColumnGroup.PARAMS, group=ColumnGroup.PARAMS,
comment="id достигнутых целей; на стенде их две — корзина и покупка", comment="id достигнутых целей; на стенде их две — корзина и покупка",
), ),
@@ -346,7 +348,7 @@ COLUMNS: tuple[Column, ...] = (
name="ParsedParamsKey1", name="ParsedParamsKey1",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="parsed_params_key1", normalized_name="parsed_params_key1",
group=ColumnGroup.PARAMS, group=ColumnGroup.PARAMS,
comment="свои параметры сайта, один уровень — например вариант A/B-теста", comment="свои параметры сайта, один уровень — например вариант A/B-теста",
), ),
@@ -354,7 +356,7 @@ COLUMNS: tuple[Column, ...] = (
name="purchaseID", name="purchaseID",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="purchase_id", normalized_name="purchase_id",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="номер заказа; у события purchase — один элемент", comment="номер заказа; у события purchase — один элемент",
), ),
@@ -362,7 +364,7 @@ COLUMNS: tuple[Column, ...] = (
name="purchaseRevenue", name="purchaseRevenue",
clickhouse_type="Array(Float64)", clickhouse_type="Array(Float64)",
numpy_dtype="float64", numpy_dtype="float64",
dds_name="purchase_revenue", normalized_name="purchase_revenue",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="выручка заказа глазами клиента; Float64, как у Метрики —" comment="выручка заказа глазами клиента; Float64, как у Метрики —"
" на этом держится урок о расхождениях с бэкендом", " на этом держится урок о расхождениях с бэкендом",
@@ -371,7 +373,7 @@ COLUMNS: tuple[Column, ...] = (
name="purchaseCurrency", name="purchaseCurrency",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="purchase_currency", normalized_name="purchase_currency",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="валюта заказа", comment="валюта заказа",
), ),
@@ -379,7 +381,7 @@ COLUMNS: tuple[Column, ...] = (
name="purchaseCoupon", name="purchaseCoupon",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="purchase_coupon", normalized_name="purchase_coupon",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="купон заказа, если был применён", comment="купон заказа, если был применён",
), ),
@@ -387,7 +389,7 @@ COLUMNS: tuple[Column, ...] = (
name="productID", name="productID",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="product_id", normalized_name="product_id",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="id товаров события", comment="id товаров события",
), ),
@@ -395,7 +397,7 @@ COLUMNS: tuple[Column, ...] = (
name="productName", name="productName",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="product_name", normalized_name="product_name",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="названия тех же товаров", comment="названия тех же товаров",
), ),
@@ -403,7 +405,7 @@ COLUMNS: tuple[Column, ...] = (
name="productCategory", name="productCategory",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="product_category", normalized_name="product_category",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="категории тех же товаров", comment="категории тех же товаров",
), ),
@@ -411,7 +413,7 @@ COLUMNS: tuple[Column, ...] = (
name="productPrice", name="productPrice",
clickhouse_type="Array(Int64)", clickhouse_type="Array(Int64)",
numpy_dtype="int64", numpy_dtype="int64",
dds_name="product_price", normalized_name="product_price",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="цена за штуку целым числом: деньги генератор считает целыми", comment="цена за штуку целым числом: деньги генератор считает целыми",
), ),
@@ -419,7 +421,7 @@ COLUMNS: tuple[Column, ...] = (
name="productQuantity", name="productQuantity",
clickhouse_type="Array(UInt64)", clickhouse_type="Array(UInt64)",
numpy_dtype="uint64", numpy_dtype="uint64",
dds_name="product_quantity", normalized_name="product_quantity",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="количество штук каждого товара", comment="количество штук каждого товара",
), ),
@@ -427,7 +429,7 @@ COLUMNS: tuple[Column, ...] = (
name="productEventType", name="productEventType",
clickhouse_type="Array(String)", clickhouse_type="Array(String)",
numpy_dtype="object", numpy_dtype="object",
dds_name="product_event_type", normalized_name="product_event_type",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="действие с товаром: стенд шлёт add и purchase, полный" comment="действие с товаром: стенд шлёт add и purchase, полный"
" словарь Метрики (detail, remove, impressions) не берём", " словарь Метрики (detail, remove, impressions) не берём",
@@ -436,7 +438,7 @@ COLUMNS: tuple[Column, ...] = (
name="ecommerce", name="ecommerce",
clickhouse_type="String", clickhouse_type="String",
numpy_dtype="object", numpy_dtype="object",
dds_name="ecommerce", normalized_name="ecommerce",
group=ColumnGroup.ECOMMERCE, group=ColumnGroup.ECOMMERCE,
comment="сырой JSON события, как отдаёт Метрика — материал лабы" comment="сырой JSON события, как отдаёт Метрика — материал лабы"
" про разбор JSON внутри колонки", " про разбор JSON внутри колонки",
@@ -26,10 +26,12 @@ JSON-поле `ecommerce`. Отдельной сущности «визит» в
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки. собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
Имена и типы колонок стороны источника. Хранилище принимает их как есть и Имена и типы колонок стороны источника. Хранилище принимает их как есть и
нормализует у себя: своё snake_case-имя каждой колонки ждёт в столбце «Имя в нормализует у себя: то же имя в нашем стиле ждёт в столбце «Нормализованное
DDS». Столбец «Тип numpy» показывает, чем колонка представлена внутри имя». Это имя источника, приведённое к snake_case, а не имя атрибута в
генератора; у массивов это тип элемента. Номер место колонки в выгрузке: модели данных: слой DDS складывает свою модель и называет атрибуты по ней.
порядок задан контрактом. Столбец «Тип numpy» показывает, чем колонка представлена внутри генератора;
у массивов это тип элемента. Номер место колонки в выгрузке: порядок задан
контрактом.
Колонки группы «Ecommerce» заполнены только у торговых событий: Колонки группы «Ecommerce» заполнены только у торговых событий:
`add_to_cart` несёт один товар, `purchase` состав заказа и блок `add_to_cart` несёт один товар, `purchase` состав заказа и блок
@@ -38,7 +40,7 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
Всего колонок: {count}.""" Всего колонок: {count}."""
TABLE_HEADER = ( TABLE_HEADER = (
"| № | Колонка | Тип ClickHouse | Тип numpy | Имя в DDS | Комментарий |", "| № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |",
"|---|---|---|---|---|---|", "|---|---|---|---|---|---|",
) )
@@ -60,7 +62,7 @@ def table_row(number: int, column: Column) -> str:
f"`{column.name}`", f"`{column.name}`",
f"`{column.clickhouse_type}`", f"`{column.clickhouse_type}`",
f"`{column.numpy_dtype}`", f"`{column.numpy_dtype}`",
f"`{column.dds_name}`", f"`{column.normalized_name}`",
column.comment, column.comment,
) )
return "| " + " | ".join(cells) + " |" return "| " + " | ".join(cells) + " |"
+9 -129
View File
@@ -1,9 +1,10 @@
"""Инварианты контракта схемы события. """Инварианты контракта схемы события.
Контракт чистые данные, поэтому проверять в нём нечего кроме связности: Контракт чистые данные, поэтому проверять в нём нечего кроме связности:
состав, уникальность имён, заполненность полей, согласие типов и порядок. состав на месте, имена уникальны и в своих стилях, атрибуты заполнены, типы
Это и есть сторож границы «трекер | хранилище»: молчаливый дрейф колонок согласованы, группы идут подряд. Имена колонок тесты не сторожат намеренно:
ловится здесь, а не в DDL через неделю. любая правка контракта проходит через пересборку описания выгрузки, а её
дифф виден в ревью лучше, чем правка внутри питона.
""" """
import re import re
@@ -16,60 +17,6 @@ from clickstream_generator.schema import COLUMNS, Column, ColumnGroup
# Состав решён мастер-спекой (раздел 1.2) и в этом тикете не переоткрывается. # Состав решён мастер-спекой (раздел 1.2) и в этом тикете не переоткрывается.
EXPECTED_COLUMN_COUNT = 47 EXPECTED_COLUMN_COUNT = 47
# Тот же состав, переписанный с мастер-спеки отдельно от контракта: группа,
# имя, тип. Дубль намеренный — только независимая запись ловит молчаливое
# переименование колонки, подмену типа или перестановку. Правка контракта без
# правки спеки краснеет здесь, и это единственный способ узнать о ней вовремя.
MASTER_SPEC_COMPOSITION = (
(ColumnGroup.IDENTIFIERS, "WatchID", "UInt64"),
(ColumnGroup.IDENTIFIERS, "VisitID", "UInt64"),
(ColumnGroup.IDENTIFIERS, "ClientID", "UInt64"),
(ColumnGroup.IDENTIFIERS, "CounterID", "UInt32"),
(ColumnGroup.IDENTIFIERS, "EventDate", "Date"),
(ColumnGroup.IDENTIFIERS, "UTCEventTime", "DateTime"),
(ColumnGroup.IDENTIFIERS, "ClientTimeZone", "Int16"),
(ColumnGroup.IDENTIFIERS, "EventType", "LowCardinality(String)"),
(ColumnGroup.IDENTIFIERS, "Sign", "Int8"),
(ColumnGroup.PAGE, "URL", "String"),
(ColumnGroup.PAGE, "Referer", "String"),
(ColumnGroup.PAGE, "Title", "String"),
(ColumnGroup.PAGE, "UTMSource", "String"),
(ColumnGroup.PAGE, "UTMMedium", "String"),
(ColumnGroup.PAGE, "UTMCampaign", "String"),
(ColumnGroup.PAGE, "UTMContent", "String"),
(ColumnGroup.PAGE, "UTMTerm", "String"),
(ColumnGroup.PAGE, "LastTrafficSource", "String"),
(ColumnGroup.PAGE, "HasGCLID", "UInt8"),
(ColumnGroup.PAGE, "YCLID", "UInt64"),
(ColumnGroup.CLIENT, "Browser", "String"),
(ColumnGroup.CLIENT, "BrowserMajorVersion", "UInt16"),
(ColumnGroup.CLIENT, "BrowserLanguage", "String"),
(ColumnGroup.CLIENT, "OperatingSystem", "String"),
(ColumnGroup.CLIENT, "OperatingSystemRoot", "String"),
(ColumnGroup.CLIENT, "DeviceCategory", "UInt8"),
(ColumnGroup.CLIENT, "MobilePhoneModel", "String"),
(ColumnGroup.CLIENT, "ScreenWidth", "UInt16"),
(ColumnGroup.CLIENT, "ScreenHeight", "UInt16"),
(ColumnGroup.CLIENT, "IPAddress", "String"),
(ColumnGroup.CLIENT, "RegionCountry", "String"),
(ColumnGroup.CLIENT, "RegionCity", "String"),
(ColumnGroup.CLIENT, "RegionCountryID", "UInt32"),
(ColumnGroup.CLIENT, "RegionCityID", "UInt32"),
(ColumnGroup.PARAMS, "GoalsReached", "Array(UInt32)"),
(ColumnGroup.PARAMS, "ParsedParamsKey1", "Array(String)"),
(ColumnGroup.ECOMMERCE, "purchaseID", "Array(String)"),
(ColumnGroup.ECOMMERCE, "purchaseRevenue", "Array(Float64)"),
(ColumnGroup.ECOMMERCE, "purchaseCurrency", "Array(String)"),
(ColumnGroup.ECOMMERCE, "purchaseCoupon", "Array(String)"),
(ColumnGroup.ECOMMERCE, "productID", "Array(String)"),
(ColumnGroup.ECOMMERCE, "productName", "Array(String)"),
(ColumnGroup.ECOMMERCE, "productCategory", "Array(String)"),
(ColumnGroup.ECOMMERCE, "productPrice", "Array(Int64)"),
(ColumnGroup.ECOMMERCE, "productQuantity", "Array(UInt64)"),
(ColumnGroup.ECOMMERCE, "productEventType", "Array(String)"),
(ColumnGroup.ECOMMERCE, "ecommerce", "String"),
)
# Соответствие «тип ClickHouse — тип numpy», записанное независимо от # Соответствие «тип ClickHouse — тип numpy», записанное независимо от
# контракта: если пара в контракте разъедется, сойтись они уже не смогут. # контракта: если пара в контракте разъедется, сойтись они уже не смогут.
NUMPY_BY_CLICKHOUSE_TYPE = { NUMPY_BY_CLICKHOUSE_TYPE = {
@@ -87,62 +34,8 @@ NUMPY_BY_CLICKHOUSE_TYPE = {
"DateTime": "datetime64[s]", "DateTime": "datetime64[s]",
} }
# Имена для DDS — не производная от имён Метрики, а решение тикета #36:
# вывести их правилом нельзя (акронимы, «timezone» одним словом), поэтому
# сверять их не с чем, кроме такой же независимой записи. Без неё осмысленно
# неверное имя молча уезжает в опубликованное описание выгрузки.
EXPECTED_DDS_NAMES = {
"WatchID": "watch_id",
"VisitID": "visit_id",
"ClientID": "client_id",
"CounterID": "counter_id",
"EventDate": "event_date",
"UTCEventTime": "utc_event_time",
"ClientTimeZone": "client_timezone",
"EventType": "event_type",
"Sign": "sign",
"URL": "url",
"Referer": "referer",
"Title": "title",
"UTMSource": "utm_source",
"UTMMedium": "utm_medium",
"UTMCampaign": "utm_campaign",
"UTMContent": "utm_content",
"UTMTerm": "utm_term",
"LastTrafficSource": "last_traffic_source",
"HasGCLID": "has_gclid",
"YCLID": "yclid",
"Browser": "browser",
"BrowserMajorVersion": "browser_major_version",
"BrowserLanguage": "browser_language",
"OperatingSystem": "operating_system",
"OperatingSystemRoot": "operating_system_root",
"DeviceCategory": "device_category",
"MobilePhoneModel": "mobile_phone_model",
"ScreenWidth": "screen_width",
"ScreenHeight": "screen_height",
"IPAddress": "ip_address",
"RegionCountry": "region_country",
"RegionCity": "region_city",
"RegionCountryID": "region_country_id",
"RegionCityID": "region_city_id",
"GoalsReached": "goals_reached",
"ParsedParamsKey1": "parsed_params_key1",
"purchaseID": "purchase_id",
"purchaseRevenue": "purchase_revenue",
"purchaseCurrency": "purchase_currency",
"purchaseCoupon": "purchase_coupon",
"productID": "product_id",
"productName": "product_name",
"productCategory": "product_category",
"productPrice": "product_price",
"productQuantity": "product_quantity",
"productEventType": "product_event_type",
"ecommerce": "ecommerce",
}
METRICA_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9]*$") METRICA_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9]*$")
DDS_NAME = re.compile(r"^[a-z][a-z0-9_]*$") NORMALIZED_NAME = re.compile(r"^[a-z][a-z0-9_]*$")
ARRAY_TYPE = re.compile(r"^Array\((.+)\)$") ARRAY_TYPE = re.compile(r"^Array\((.+)\)$")
@@ -160,35 +53,22 @@ def test_column_count():
assert len(COLUMNS) == EXPECTED_COLUMN_COUNT assert len(COLUMNS) == EXPECTED_COLUMN_COUNT
def test_composition_matches_master_spec():
"""Состав, имена, типы и порядок — те же, что в разделе 1.2 мастер-спеки."""
composition = tuple(
(column.group, column.name, column.clickhouse_type) for column in COLUMNS
)
assert composition == MASTER_SPEC_COMPOSITION
def test_metrica_names_are_unique(): def test_metrica_names_are_unique():
names = [column.name for column in COLUMNS] names = [column.name for column in COLUMNS]
assert len(set(names)) == len(names) assert len(set(names)) == len(names)
def test_dds_names_are_unique(): def test_normalized_names_are_unique():
names = [column.dds_name for column in COLUMNS] names = [column.normalized_name for column in COLUMNS]
assert len(set(names)) == len(names) assert len(set(names)) == len(names)
def test_dds_names_are_the_ones_we_chose():
"""Переименование колонки в DDS — решение, а не правка мимоходом."""
assert {column.name: column.dds_name for column in COLUMNS} == EXPECTED_DDS_NAMES
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name) @pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
def test_attributes_are_filled(column: Column): def test_attributes_are_filled(column: Column):
assert column.name.strip() assert column.name.strip()
assert column.clickhouse_type.strip() assert column.clickhouse_type.strip()
assert column.numpy_dtype.strip() assert column.numpy_dtype.strip()
assert column.dds_name.strip() assert column.normalized_name.strip()
assert column.comment.strip() assert column.comment.strip()
assert isinstance(column.group, ColumnGroup) assert isinstance(column.group, ColumnGroup)
@@ -196,7 +76,7 @@ def test_attributes_are_filled(column: Column):
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name) @pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
def test_names_keep_their_styles(column: Column): def test_names_keep_their_styles(column: Column):
assert METRICA_NAME.match(column.name), "имя источника — как в выгрузке Метрики" assert METRICA_NAME.match(column.name), "имя источника — как в выгрузке Метрики"
assert DDS_NAME.match(column.dds_name), "имя для DDS — snake_case" assert NORMALIZED_NAME.match(column.normalized_name), "наше имя — snake_case"
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name) @pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
+1 -1
View File
@@ -51,7 +51,7 @@ def test_column_is_described_in_full(column: Column, rendered: str):
column.name, column.name,
column.clickhouse_type, column.clickhouse_type,
column.numpy_dtype, column.numpy_dtype,
column.dds_name, column.normalized_name,
column.comment, column.comment,
) )
assert any( assert any(