fix(generator): сторожа имён DDS и порядка строк, торговый словарь — по границе
- Зачем:
- слепая линия Кодекса (свежий тред, high) нашла три места, где обещание
контракта не подкреплено: имена для DDS не сверялись ни с чем, порядок
строк документа держался только на нумерации, а комментарий рекламировал
торговые события, которые мастер-спека прямо исключила.
- Что:
- имена для DDS записаны независимо и сверяются целиком: они не выводятся
правилом из имён Метрики, значит осмысленно неверное имя иначе молча
уезжает в опубликованное описание (проверено подменой referer).
- строки документа сверяются парами «номер, колонка»: рендер в другом
порядке больше не проходит зелёным (проверено перевёрнутым рендером).
- productEventType: detail и remove убраны из комментария — раздел 10
мастер-спеки отказался от полного словаря торговых событий Метрики;
стенд шлёт add и purchase.
- Проверка:
- make test (250 тестов), make lint;
- make docs, затем git diff --exit-code docs/ — пусто;
- обе новые проверки проверены мутациями: каждая краснеет своим тестом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,5 +90,5 @@ DDS». Столбец «Тип numpy» показывает, чем колонк
|
||||
| 43 | `productCategory` | `Array(String)` | `object` | `product_category` | категории тех же товаров |
|
||||
| 44 | `productPrice` | `Array(Int64)` | `int64` | `product_price` | цена за штуку целым числом: деньги генератор считает целыми |
|
||||
| 45 | `productQuantity` | `Array(UInt64)` | `uint64` | `product_quantity` | количество штук каждого товара |
|
||||
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: detail, add, remove, purchase |
|
||||
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём |
|
||||
| 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки |
|
||||
|
||||
@@ -429,7 +429,8 @@ COLUMNS: tuple[Column, ...] = (
|
||||
numpy_dtype="object",
|
||||
dds_name="product_event_type",
|
||||
group=ColumnGroup.ECOMMERCE,
|
||||
comment="действие с товаром: detail, add, remove, purchase",
|
||||
comment="действие с товаром: стенд шлёт add и purchase, полный"
|
||||
" словарь Метрики (detail, remove, impressions) не берём",
|
||||
),
|
||||
Column(
|
||||
name="ecommerce",
|
||||
|
||||
@@ -87,6 +87,60 @@ 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_]*$")
|
||||
ARRAY_TYPE = re.compile(r"^Array\((.+)\)$")
|
||||
@@ -124,6 +178,11 @@ def test_dds_names_are_unique():
|
||||
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()
|
||||
|
||||
@@ -16,7 +16,9 @@ from clickstream_generator.schema_doc import render
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DOC_PATH = REPO_ROOT / "docs" / "formats" / "clickstream-event.md"
|
||||
|
||||
TABLE_ROW = re.compile(r"^\| \d+ \|", re.MULTILINE)
|
||||
# Номер и имя колонки из строки таблицы: по ним сверяется не только состав
|
||||
# документа, но и его порядок — по нему сторона хранилища выпишет колонки.
|
||||
TABLE_ROW = re.compile(r"^\| (\d+) \| `([^`]+)` \|", re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -35,9 +37,11 @@ def test_every_column_has_a_row(rendered: str):
|
||||
assert len(TABLE_ROW.findall(rendered)) == len(COLUMNS)
|
||||
|
||||
|
||||
def test_rows_are_numbered_in_contract_order(rendered: str):
|
||||
numbers = [int(row.strip("| ")) for row in TABLE_ROW.findall(rendered)]
|
||||
assert numbers == list(range(1, len(COLUMNS) + 1))
|
||||
def test_rows_follow_contract_order(rendered: str):
|
||||
"""Строки идут в порядке контракта, а не просто нумеруются с 1 по 47."""
|
||||
rows = [(int(number), name) for number, name in TABLE_ROW.findall(rendered)]
|
||||
expected = [(number, column.name) for number, column in enumerate(COLUMNS, 1)]
|
||||
assert rows == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
|
||||
|
||||
Reference in New Issue
Block a user