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>
This commit is contained in:
@@ -77,7 +77,9 @@ Airflow) и названия из кода. Если для понятия ес
|
||||
|
||||
## Код и данные
|
||||
|
||||
- Python — только через `uv`; проверка и формат — `ruff` (`make lint`).
|
||||
- Python — только через `uv`; проверка и формат — `ruff`. Цель `make lint`
|
||||
есть за двумя дверями и охватывает разное: в корне — код стенда, в
|
||||
`generator/` — код генератора. Правишь одно — зови ту.
|
||||
- Изменения держать минимальными и в границах задания.
|
||||
- Куда класть новую проверку, что утверждает каждая цель `make` и почему смоук
|
||||
обязан оставаться быстрым — [`docs/architecture/testing.md`](docs/architecture/testing.md).
|
||||
|
||||
@@ -3,7 +3,12 @@ GENERATOR_DAY ?= 0
|
||||
GENERATOR_LIMIT ?=
|
||||
GENERATOR_SPEED ?=
|
||||
|
||||
.PHONY: up down clean ps logs generate-batch generate-live config-test lint typecheck test docs inventory smoke check-clickhouse check-services
|
||||
# Цели корня — про стенд. Проверки генератора живут за своей дверью, в
|
||||
# `generator/Makefile`: генератор — отдельная и заменяемая сущность со своим
|
||||
# `pyproject.toml`, локом и образом, и цели у него свои.
|
||||
.PHONY: up down clean ps logs smoke check-clickhouse check-services config-test lint generate-batch generate-live
|
||||
|
||||
# --- Жизнь стенда ---
|
||||
|
||||
# Второй шаг: `--wait` дожидается служб, а приём событий асинхронный — довод
|
||||
# целиком в шапке скрипта.
|
||||
@@ -23,33 +28,7 @@ ps:
|
||||
logs:
|
||||
$(COMPOSE) logs --follow
|
||||
|
||||
generate-batch:
|
||||
$(COMPOSE) --profile generator run --rm generator batch --day "$(GENERATOR_DAY)" \
|
||||
$(if $(GENERATOR_LIMIT),--limit "$(GENERATOR_LIMIT)")
|
||||
|
||||
generate-live:
|
||||
$(COMPOSE) --profile generator run --rm generator live --day "$(GENERATOR_DAY)" \
|
||||
$(if $(GENERATOR_SPEED),--speed "$(GENERATOR_SPEED)")
|
||||
|
||||
config-test:
|
||||
COMPOSE_BIN="$(COMPOSE)" ./scripts/config-test.sh
|
||||
|
||||
lint:
|
||||
cd generator && uv run ruff check && uv run ruff format --check
|
||||
|
||||
typecheck:
|
||||
cd generator && uv run ty check
|
||||
|
||||
test:
|
||||
cd generator && uv run pytest
|
||||
|
||||
docs:
|
||||
cd generator && uv run python -m clickstream_generator.schema_doc \
|
||||
../docs/formats/clickstream-event.md
|
||||
|
||||
inventory:
|
||||
cd generator && uv run python -m clickstream_generator.inventory \
|
||||
../data/world-inventory.json
|
||||
# --- Проверки, которым нужен поднятый стенд ---
|
||||
|
||||
smoke:
|
||||
COMPOSE_BIN="$(COMPOSE)" ./scripts/stand-smoke.sh
|
||||
@@ -59,3 +38,27 @@ check-clickhouse:
|
||||
|
||||
check-services:
|
||||
COMPOSE_BIN="$(COMPOSE)" ./scripts/stand-services.sh
|
||||
|
||||
# --- Проверки, которым стенд не нужен ---
|
||||
|
||||
config-test:
|
||||
COMPOSE_BIN="$(COMPOSE)" ./scripts/config-test.sh
|
||||
|
||||
# Пути названы вслух: без них ruff из корня прошёлся бы и по генератору, а у
|
||||
# того своя дверь и свой конфиг. Версия закреплена, потому что лока в корне
|
||||
# нет, а форматтер между версиями меняет вывод — иначе проверка однажды
|
||||
# покраснела бы сама, без единой правки в репозитории. Держать её равной той,
|
||||
# что в `generator/uv.lock`, приходится руками: сверять их некому.
|
||||
lint:
|
||||
uvx ruff@0.16.1 check dags infra/superset
|
||||
uvx ruff@0.16.1 format --check dags infra/superset
|
||||
|
||||
# --- Наполнение миром ---
|
||||
|
||||
generate-batch:
|
||||
$(COMPOSE) --profile generator run --rm generator batch --day "$(GENERATOR_DAY)" \
|
||||
$(if $(GENERATOR_LIMIT),--limit "$(GENERATOR_LIMIT)")
|
||||
|
||||
generate-live:
|
||||
$(COMPOSE) --profile generator run --rm generator live --day "$(GENERATOR_DAY)" \
|
||||
$(if $(GENERATOR_SPEED),--speed "$(GENERATOR_SPEED)")
|
||||
|
||||
@@ -31,9 +31,8 @@ Prometheus, Grafana и общая база Postgres для метаданных.
|
||||
`grep`, `sed`, `tail`, `sleep` и `timeout`. `jq` нужен и самому `make up`: им
|
||||
читается опись мира, по которой он ждёт заливки. По умолчанию должны быть свободны
|
||||
порты `23000`, `28080`, `28088`, `28123`, `28124`, `29000`, `29001`, `29090`
|
||||
и `29092`. Проверкам без стенда — `make config-test`, `make lint`,
|
||||
`make typecheck`, `make test` — и сборкам `make docs` и `make inventory` нужен
|
||||
`uv`.
|
||||
и `29092`. Проверкам без стенда — `make config-test` и `make lint` в корне,
|
||||
всем целям генератора в `generator/` — нужен `uv`.
|
||||
|
||||
Сначала скопируйте настройки стенда:
|
||||
|
||||
@@ -140,9 +139,10 @@ ClickHouse, пока мир не доедет. Повторный `make up` за
|
||||
событий и хеш его байтов. Опись отвечает на единственный вопрос: тот ли это
|
||||
мир, что был вчера.
|
||||
|
||||
Спрашивают её двое. `make test` сверяет опись с тем, что собирается из кода
|
||||
сегодня: правка генератора меняет мир, и опись надо пересобрать —
|
||||
`make inventory`. `make check-clickhouse` сверяет с описью то, что доехало до
|
||||
Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что
|
||||
собирается из кода сегодня: правка генератора меняет мир, и опись надо
|
||||
пересобрать — `make inventory` там же. `make check-clickhouse` в корне
|
||||
сверяет с описью то, что доехало до
|
||||
`ods.event`, подневно. Хеш дня можно пересчитать и руками — это обычный
|
||||
`sha256sum` файла, который пишет файловый приёмник:
|
||||
|
||||
@@ -326,6 +326,6 @@ Superset закреплён на 6.1.0; драйвер `clickhouse-connect`, ф
|
||||
- [docs/research/](docs/research/) — исследования; сейчас это формат
|
||||
кликстрима Яндекса, по которому строится модель события.
|
||||
- [docs/formats/](docs/formats/) — описания форматов источников: по ним
|
||||
пишется сторона хранилища. Собираются из кода командой `make docs`, руками
|
||||
не правятся.
|
||||
пишется сторона хранилища. Собираются из кода командой `make docs` в
|
||||
`generator/`, руками не правятся.
|
||||
- [AGENTS.md](AGENTS.md) — контракт работы в репозитории.
|
||||
|
||||
@@ -75,8 +75,7 @@ def _table_engines(client, source: str) -> list[tuple[str, str]]:
|
||||
|
||||
def _drop_tables(client) -> None:
|
||||
client.command(
|
||||
f"DROP TABLE IF EXISTS default.{DISTRIBUTED_TABLE} "
|
||||
f"ON CLUSTER {CLUSTER} SYNC"
|
||||
f"DROP TABLE IF EXISTS default.{DISTRIBUTED_TABLE} ON CLUSTER {CLUSTER} SYNC"
|
||||
)
|
||||
client.command(
|
||||
f"DROP TABLE IF EXISTS default.{LOCAL_TABLE} ON CLUSTER {CLUSTER} SYNC"
|
||||
@@ -178,9 +177,7 @@ def test_clickhouse():
|
||||
"""
|
||||
).result_rows
|
||||
if len(node_2_rows) != 1:
|
||||
raise RuntimeError(
|
||||
f"не удалось определить имя ноды 2: {node_2_rows}"
|
||||
)
|
||||
raise RuntimeError(f"не удалось определить имя ноды 2: {node_2_rows}")
|
||||
distributed_rows = client.query(
|
||||
f"""
|
||||
SELECT _shard_num, hostName(), marker
|
||||
|
||||
+1
-3
@@ -100,9 +100,7 @@ def test_kafka():
|
||||
if error is not None:
|
||||
delivery_errors.append(str(error))
|
||||
else:
|
||||
addresses.append(
|
||||
RecordAddress(message.partition(), message.offset())
|
||||
)
|
||||
addresses.append(RecordAddress(message.partition(), message.offset()))
|
||||
|
||||
producer = Producer(PRODUCER_CONFIG)
|
||||
# produce() не пишет, а ставит сообщение в очередь: о судьбе записи
|
||||
|
||||
@@ -29,19 +29,45 @@
|
||||
|
||||
## Карта целей
|
||||
|
||||
Стенд нужен трём целям из семи. Цена — замер, см. «Что проверено»: `make test`,
|
||||
`make smoke`, `make check-clickhouse` и `make check-services` перемерены
|
||||
7 августа 2026 года на приёмке этапа 2; остальные три стоят с замера 6 августа.
|
||||
Цели живут за двумя дверями. В корне — цели стенда; в `generator/` — цели
|
||||
генератора: он отдельный пакет со своим `pyproject.toml`, локом и образом, и
|
||||
спрашивают его отдельно.
|
||||
|
||||
| Цель | Что утверждает | Стенд | Цена |
|
||||
|---|---|---|---|
|
||||
| `make lint` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||
| `make typecheck` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
|
||||
| `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с |
|
||||
| `make config-test` | Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
|
||||
| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 9 с |
|
||||
| `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, счёт событий стартового мира против описи | нужен | 8 с |
|
||||
| `make check-services` | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 59 с |
|
||||
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено»:
|
||||
`make test`, `make smoke`, `make check-clickhouse` и `make check-services`
|
||||
перемерены 7 августа 2026 года на приёмке этапа 2, корневой `make lint` —
|
||||
9 августа при его появлении; остальные стоят с замера 6 августа.
|
||||
|
||||
| Цель | Откуда | Что утверждает | Стенд | Цена |
|
||||
|---|---|---|---|---|
|
||||
| `make config-test` | корень | Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
|
||||
| `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||
| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 9 с |
|
||||
| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, счёт событий стартового мира против описи | нужен | 8 с |
|
||||
| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 59 с |
|
||||
| `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||
| `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
|
||||
| `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с |
|
||||
|
||||
**Строк с именем `make lint` две, и это не опечатка.** Одноимённые цели за
|
||||
разными дверями охватывают разное: корневая не видит ни одного файла
|
||||
генератора, генераторная — ни одного файла стенда. Человек в корне видит
|
||||
зелёное и может решить, что зелёный весь репозиторий.
|
||||
|
||||
Список правил у обеих один, а строгость нет: правила `UP` предлагают синтаксис
|
||||
настолько новый, насколько позволяет `target-version`, — у генератора это 3.14
|
||||
из `requires-python`, у стенда 3.10, младшая из версий Python в его образах.
|
||||
Один и тот же файл может пройти корневую проверку и не пройти генераторную.
|
||||
Довод и замер — в комментарии `ruff.toml`.
|
||||
|
||||
**Корневого `typecheck` нет, и это решение, а не пропуск.** Проверке типов мало
|
||||
прочитать файл: чтобы понять `from airflow.sdk import dag`, ей нужен
|
||||
установленный Airflow, а он живёт в образе, не на машине — 132 пакета ради двух
|
||||
файлов пробников (замер 9 августа 2026 года, `uv pip compile`). Цена решения
|
||||
называется честно: две настоящие находки `ty` в `dags/test_kafka.py` постоянного
|
||||
сторожа не получают. Вопрос вернётся на этапе 5, когда придут настоящие даги:
|
||||
тогда окружение окупится, и условие возврата — именно это, а не «стало
|
||||
неудобно».
|
||||
|
||||
Цена самого `make up` — 2 м 50 с с нуля (`make clean` перед ним — ещё 23 с) и
|
||||
1 м 5 с на живом стенде. В цену целей она не входит, но записана здесь по той
|
||||
@@ -74,8 +100,9 @@ ClickHouse отвечает сразу.
|
||||
| PR или задача | плюс цели, которых правка касалась: DAG-и, Superset или Kafka — `check-services`; DDL, кластер или данные — `check-clickhouse` |
|
||||
| Приёмка этапа | `make up` с нуля и все три цели на стенде |
|
||||
|
||||
Правки генератора добавляют к этому `make lint`, `make typecheck` и
|
||||
`make test`: стенд им не нужен, а `make test` из них самая дорогая.
|
||||
Правки генератора добавляют к этому `make lint`, `make typecheck` и `make test`
|
||||
из каталога `generator/`: стенд им не нужен, а `make test` из них самая
|
||||
дорогая. Правки дагов и Superset — корневой `make lint`.
|
||||
|
||||
Тринадцать секунд из её семидесяти одной — пересборка восьми модельных дней
|
||||
для сверки с описью мира. Дешевле хеши не сравнить: чтобы узнать, тот ли
|
||||
@@ -192,6 +219,15 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
|
||||
|
||||
## Что проверено
|
||||
|
||||
**Замер 9 августа 2026 года при появлении корневого `lint` (#65).** Три прогона
|
||||
подряд — 0,39, 0,36 и 0,39 секунды на трёх файлах стенда; в таблице 0,4 с. На
|
||||
машине с непрогретым кешем первый прогон дороже: `uvx` сначала скачивает ruff.
|
||||
|
||||
Тогда же снято, что двери не перекрываются: `ruff check --show-files` из корня
|
||||
перечисляет ровно три файла стенда, из `generator/` — только файлы генератора.
|
||||
Корневой конфиг правила генератору не подменяет: конфиг ищется вверх по дереву
|
||||
от файла, и ближний выигрывает.
|
||||
|
||||
**Перезамер 7 августа 2026 года при исполнении #42.** Подъём стенда с нуля
|
||||
(`make clean && make up`) — 2 м 50 с, из них 22,5 с занимает сама заливка:
|
||||
18,7 с генерация восьми дней и 3,8 с доставка 401 185 сообщений в Kafka.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Документ собран из контракта схемы генератора
|
||||
(`generator/src/clickstream_generator/schema.py`). Руками не править —
|
||||
пересобрать: `make docs`.
|
||||
пересобрать: `make docs` из каталога `generator/`.
|
||||
|
||||
Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики.
|
||||
Многозначное лежит в параллельных массивах, плюс одно сырое JSON-поле
|
||||
|
||||
@@ -903,7 +903,8 @@
|
||||
`KubernetesPodOperator`. Побочно это единственный вариант, при котором
|
||||
«генератор — отдельная и заменяемая сущность» перестаёт быть словами: его
|
||||
зависимости не смешиваются с окружением Airflow. Хостовый `uv` остаётся для
|
||||
`make test`, `make lint` и `make typecheck`. Цена названа и принимается:
|
||||
целей генератора — `make test`, `make lint` и `make typecheck` из каталога
|
||||
`generator/`. Цена названа и принимается:
|
||||
чтобы даг запускал контейнер, в Airflow пробрасывается сокет докера, а это
|
||||
доступ, равный root на хосте; на учебном стенде размен допустим, но
|
||||
записывается уроком — в бою так не делают. Отклонено: *генератор в образе
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Цели генератора. Дверь у него своя, потому что своя и сущность: пакет с
|
||||
# собственными `pyproject.toml`, `uv.lock` и образом, заменяемый целиком
|
||||
# (спека генератора, раздел 1). Отсюда и короткие имена — каталог уже сказал,
|
||||
# о ком речь, и суффикс `-generator` был бы вторым разом про то же.
|
||||
.PHONY: lint typecheck test docs inventory
|
||||
|
||||
# --- Проверки ---
|
||||
|
||||
lint:
|
||||
uv run ruff check
|
||||
uv run ruff format --check
|
||||
|
||||
typecheck:
|
||||
uv run ty check
|
||||
|
||||
test:
|
||||
uv run pytest
|
||||
|
||||
# --- Сборка из кода ---
|
||||
|
||||
docs:
|
||||
uv run python -m clickstream_generator.schema_doc \
|
||||
../docs/formats/clickstream-event.md
|
||||
|
||||
inventory:
|
||||
uv run python -m clickstream_generator.inventory \
|
||||
../data/world-inventory.json
|
||||
+7
-2
@@ -92,12 +92,17 @@ D0 живёт предыстория, поэтому любой день соб
|
||||
|
||||
Проиграть день — [быстрый старт](../README.md#как-позвать-генератор) и `--help`.
|
||||
|
||||
Из корня репозитория:
|
||||
Из этого каталога — цели генератора живут здесь, а не в корне:
|
||||
|
||||
- `make test` — тесты генератора;
|
||||
- `make lint` — ruff: проверка и формат;
|
||||
- `make typecheck` — ty: проверка типов;
|
||||
- `make docs` — пересобрать описание выгрузки.
|
||||
- `make docs` — пересобрать описание выгрузки;
|
||||
- `make inventory` — пересобрать опись мира.
|
||||
|
||||
В корне репозитория `make lint` тоже есть, но он про код стенда — даги и
|
||||
Superset. Одноимённые цели за разными дверями охватывают разное:
|
||||
[карта проверок](../docs/architecture/testing.md).
|
||||
|
||||
Python и зависимости — через `uv`, версии закреплены в `uv.lock`: на этом
|
||||
держится обещание побайтовой воспроизводимости (спека, раздел 2).
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
пересчитывается он и обычным `sha256sum` по сыгранному в файл дню (как
|
||||
именно — в README репозитория).
|
||||
|
||||
Собирается опись из корня репозитория целью `make inventory`, а свежесть её
|
||||
Собирается опись из каталога генератора целью `make inventory`, а свежесть её
|
||||
сторожит тест — как и у «описания выгрузки».
|
||||
"""
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
содержание берётся из контракта (`schema`), правится только там; свежесть
|
||||
документа сторожит тест.
|
||||
|
||||
Запуск — из корня репозитория целью `make docs`.
|
||||
Запуск — из каталога генератора целью `make docs`.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -18,7 +18,7 @@ PREAMBLE = """# Описание выгрузки: событие кликстр
|
||||
|
||||
Документ собран из контракта схемы генератора
|
||||
(`generator/src/clickstream_generator/schema.py`). Руками не править —
|
||||
пересобрать: `make docs`.
|
||||
пересобрать: `make docs` из каталога `generator/`.
|
||||
|
||||
Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики.
|
||||
Многозначное лежит в параллельных массивах, плюс одно сырое JSON-поле
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Настройка ruff для кода стенда — дагов и Superset. У генератора своя,
|
||||
# в `generator/pyproject.toml`: конфиг ruff ищется вверх по дереву от файла,
|
||||
# поэтому ближний к генератору выигрывает и этот его не подменяет.
|
||||
#
|
||||
# Здесь отдельный файл, а не `pyproject.toml`, потому что пакета в корне нет:
|
||||
# корневой `pyproject.toml` завёл бы в репозитории второй проект `uv` — с
|
||||
# локом и окружением, — которого стенду не нужно.
|
||||
|
||||
# На каком Python побежит код, ruff сам не знает: у генератора он берёт ответ
|
||||
# из `requires-python`, а у стенда спрашивать некого — даги и Superset живут
|
||||
# внутри образов. Замер 9 августа 2026 года: `apache/airflow:3.3.0` несёт
|
||||
# Python 3.13.14, `apache/superset:6.1.0` — 3.10.20. Стоит младшая из двух,
|
||||
# иначе ruff предложил бы Superset синтаксис, которого его Python не знает.
|
||||
# Совпадает со значением ruff по умолчанию, и записано именно поэтому:
|
||||
# сменится умолчание — проверка не поедет следом молча.
|
||||
target-version = "py310"
|
||||
|
||||
[lint]
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
Reference in New Issue
Block a user