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