From 6a708cd5aa4b2fc331461dadec218c45beceb9de Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 9 Aug 2026 21:31:07 +0300 Subject: [PATCH 1/3] =?UTF-8?q?refactor(make):=20=D0=BA=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D1=8C=20=E2=80=94=20=D0=BF=D1=80=D0=BE=20=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4,=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=E2=80=94=20=D0=B7=D0=B0=20=D1=81=D0=B2?= =?UTF-8?q?=D0=BE=D0=B5=D0=B9=20=D0=B4=D0=B2=D0=B5=D1=80=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - корневые цели смешивали два уровня: пять из шестнадцати начинались с 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 --- AGENTS.md | 4 +- Makefile | 59 +++++++++-------- README.md | 16 ++--- dags/test_clickhouse.py | 7 +- dags/test_kafka.py | 4 +- docs/architecture/testing.md | 64 +++++++++++++++---- docs/formats/clickstream-event.md | 2 +- docs/specs/2026-08-01-generator.md | 3 +- generator/Makefile | 27 ++++++++ generator/README.md | 9 ++- .../src/clickstream_generator/inventory.py | 2 +- .../src/clickstream_generator/schema_doc.py | 4 +- ruff.toml | 19 ++++++ 13 files changed, 154 insertions(+), 66 deletions(-) create mode 100644 generator/Makefile create mode 100644 ruff.toml diff --git a/AGENTS.md b/AGENTS.md index 7be781a..8604d48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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). diff --git a/Makefile b/Makefile index 03a5c18..1e66406 100644 --- a/Makefile +++ b/Makefile @@ -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)") diff --git a/README.md b/README.md index fc97785..0b61c29 100644 --- a/README.md +++ b/README.md @@ -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) — контракт работы в репозитории. diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py index 89e3f03..edf8391 100644 --- a/dags/test_clickhouse.py +++ b/dags/test_clickhouse.py @@ -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 diff --git a/dags/test_kafka.py b/dags/test_kafka.py index 7f081fd..17b418e 100644 --- a/dags/test_kafka.py +++ b/dags/test_kafka.py @@ -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() не пишет, а ставит сообщение в очередь: о судьбе записи diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index 67f6cb3..ffddba9 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -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. diff --git a/docs/formats/clickstream-event.md b/docs/formats/clickstream-event.md index ecf0686..6ec13dd 100644 --- a/docs/formats/clickstream-event.md +++ b/docs/formats/clickstream-event.md @@ -2,7 +2,7 @@ Документ собран из контракта схемы генератора (`generator/src/clickstream_generator/schema.py`). Руками не править — -пересобрать: `make docs`. +пересобрать: `make docs` из каталога `generator/`. Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики. Многозначное лежит в параллельных массивах, плюс одно сырое JSON-поле diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md index b7981c3..a66e7b5 100644 --- a/docs/specs/2026-08-01-generator.md +++ b/docs/specs/2026-08-01-generator.md @@ -903,7 +903,8 @@ `KubernetesPodOperator`. Побочно это единственный вариант, при котором «генератор — отдельная и заменяемая сущность» перестаёт быть словами: его зависимости не смешиваются с окружением Airflow. Хостовый `uv` остаётся для - `make test`, `make lint` и `make typecheck`. Цена названа и принимается: + целей генератора — `make test`, `make lint` и `make typecheck` из каталога + `generator/`. Цена названа и принимается: чтобы даг запускал контейнер, в Airflow пробрасывается сокет докера, а это доступ, равный root на хосте; на учебном стенде размен допустим, но записывается уроком — в бою так не делают. Отклонено: *генератор в образе diff --git a/generator/Makefile b/generator/Makefile new file mode 100644 index 0000000..f13ac4f --- /dev/null +++ b/generator/Makefile @@ -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 diff --git a/generator/README.md b/generator/README.md index 61250b4..6737fd5 100644 --- a/generator/README.md +++ b/generator/README.md @@ -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). diff --git a/generator/src/clickstream_generator/inventory.py b/generator/src/clickstream_generator/inventory.py index 57a58fa..0291acd 100644 --- a/generator/src/clickstream_generator/inventory.py +++ b/generator/src/clickstream_generator/inventory.py @@ -26,7 +26,7 @@ пересчитывается он и обычным `sha256sum` по сыгранному в файл дню (как именно — в README репозитория). -Собирается опись из корня репозитория целью `make inventory`, а свежесть её +Собирается опись из каталога генератора целью `make inventory`, а свежесть её сторожит тест — как и у «описания выгрузки». """ diff --git a/generator/src/clickstream_generator/schema_doc.py b/generator/src/clickstream_generator/schema_doc.py index 277dff1..befe95c 100644 --- a/generator/src/clickstream_generator/schema_doc.py +++ b/generator/src/clickstream_generator/schema_doc.py @@ -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-поле diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..94733d6 --- /dev/null +++ b/ruff.toml @@ -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"] From e4e5688775b97b5c70ea7be0829d7fa23cec915f Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 9 Aug 2026 21:49:54 +0300 Subject: [PATCH 2/3] =?UTF-8?q?refactor(make):=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=85=D0=BE=D0=BB=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - половина критерия приёмки стояла не там, где решено: предупреждение о ручном равенстве версий ruff адресовано тому, кто правит лок в generator/, а лежало в корневом Makefile. - довод «генератор — отдельная сущность» был выписан трижды почти дословно. - Что: - равенство версий и охват корневой цели названы в карте проверок; три примечания к таблице собраны списком. - названа цена занижения target-version: даги бегут на 3.13 и модернизаций не получают. - шапка generator/Makefile вырезана, корневая сжата до строки, объяснение в ruff.toml укорочено. - формулировки в AGENTS.md и README поправлены. - Проверка: - make lint; make config-test — зелёные. - make -C generator lint; typecheck — зелёные; исходники генератора не менялись. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 3 ++- Makefile | 7 ++----- README.md | 7 +++---- docs/architecture/testing.md | 17 ++++++++++++----- generator/Makefile | 4 ---- ruff.toml | 10 +++++----- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8604d48..b812a73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,8 @@ Airflow) и названия из кода. Если для понятия ес - Python — только через `uv`; проверка и формат — `ruff`. Цель `make lint` есть за двумя дверями и охватывает разное: в корне — код стенда, в - `generator/` — код генератора. Правишь одно — зови ту. + `generator/` — код генератора. Правишь даги или Superset — зови корневую, + правишь генератор — зови из `generator/`. - Изменения держать минимальными и в границах задания. - Куда класть новую проверку, что утверждает каждая цель `make` и почему смоук обязан оставаться быстрым — [`docs/architecture/testing.md`](docs/architecture/testing.md). diff --git a/Makefile b/Makefile index 1e66406..2f5b8a4 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,7 @@ GENERATOR_DAY ?= 0 GENERATOR_LIMIT ?= GENERATOR_SPEED ?= -# Цели корня — про стенд. Проверки генератора живут за своей дверью, в -# `generator/Makefile`: генератор — отдельная и заменяемая сущность со своим -# `pyproject.toml`, локом и образом, и цели у него свои. +# Цели корня — про стенд; проверки генератора — в `generator/Makefile`. .PHONY: up down clean ps logs smoke check-clickhouse check-services config-test lint generate-batch generate-live # --- Жизнь стенда --- @@ -47,8 +45,7 @@ config-test: # Пути названы вслух: без них ruff из корня прошёлся бы и по генератору, а у # того своя дверь и свой конфиг. Версия закреплена, потому что лока в корне # нет, а форматтер между версиями меняет вывод — иначе проверка однажды -# покраснела бы сама, без единой правки в репозитории. Держать её равной той, -# что в `generator/uv.lock`, приходится руками: сверять их некому. +# покраснела бы сама, без единой правки в репозитории. lint: uvx ruff@0.16.1 check dags infra/superset uvx ruff@0.16.1 format --check dags infra/superset diff --git a/README.md b/README.md index 0b61c29..a9e8475 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,9 @@ ClickHouse, пока мир не доедет. Повторный `make up` за Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что собирается из кода сегодня: правка генератора меняет мир, и опись надо -пересобрать — `make inventory` там же. `make check-clickhouse` в корне -сверяет с описью то, что доехало до -`ods.event`, подневно. Хеш дня можно пересчитать и руками — это обычный -`sha256sum` файла, который пишет файловый приёмник: +пересобрать — `make inventory` там же. `make check-clickhouse` в корне сверяет +с описью то, что доехало до `ods.event`, подневно. Хеш дня можно пересчитать и +руками — это обычный `sha256sum` файла, который пишет файловый приёмник: ```bash uv run --project generator python -m clickstream_generator batch \ diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index ffddba9..ff21045 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -54,11 +54,18 @@ генератора, генераторная — ни одного файла стенда. Человек в корне видит зелёное и может решить, что зелёный весь репозиторий. -Список правил у обеих один, а строгость нет: правила `UP` предлагают синтаксис -настолько новый, насколько позволяет `target-version`, — у генератора это 3.14 -из `requires-python`, у стенда 3.10, младшая из версий Python в его образах. -Один и тот же файл может пройти корневую проверку и не пройти генераторную. -Довод и замер — в комментарии `ruff.toml`. +Три тонкости, которых не видно из таблицы: + +- **Список правил у дверей один, а строгость нет.** Правила `UP` предлагают + синтаксис настолько новый, насколько позволяет `target-version`: у генератора + это 3.14 из `requires-python`, у стенда 3.10 — столько у Superset, младшего из + его образов. Один и тот же файл может пройти корневую проверку и не пройти + генераторную. Довод и замер — в комментарии `ruff.toml`. +- **Версия ruff у корневой цели закреплена в самом вызове** (`uvx ruff@0.16.1`) + и равна той, что в `generator/uv.lock`. Равенство держится руками: обновили + лок — поправьте и вызов, сверять их некому. +- **Охват держат аргументы вызова, а не устройство дерева.** Python, + положенный вне двух названных в таблице путей, не проверит ни одна из дверей. **Корневого `typecheck` нет, и это решение, а не пропуск.** Проверке типов мало прочитать файл: чтобы понять `from airflow.sdk import dag`, ей нужен diff --git a/generator/Makefile b/generator/Makefile index f13ac4f..4097a78 100644 --- a/generator/Makefile +++ b/generator/Makefile @@ -1,7 +1,3 @@ -# Цели генератора. Дверь у него своя, потому что своя и сущность: пакет с -# собственными `pyproject.toml`, `uv.lock` и образом, заменяемый целиком -# (спека генератора, раздел 1). Отсюда и короткие имена — каталог уже сказал, -# о ком речь, и суффикс `-generator` был бы вторым разом про то же. .PHONY: lint typecheck test docs inventory # --- Проверки --- diff --git a/ruff.toml b/ruff.toml index 94733d6..7e7455f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -2,17 +2,17 @@ # в `generator/pyproject.toml`: конфиг ruff ищется вверх по дереву от файла, # поэтому ближний к генератору выигрывает и этот его не подменяет. # -# Здесь отдельный файл, а не `pyproject.toml`, потому что пакета в корне нет: -# корневой `pyproject.toml` завёл бы в репозитории второй проект `uv` — с -# локом и окружением, — которого стенду не нужно. +# Отдельный файл, а не `pyproject.toml`: пакета в корне нет и заводить его +# ради настройки линтера незачем. # На каком 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 по умолчанию, и записано именно поэтому: -# сменится умолчание — проверка не поедет следом молча. +# Даги за это платят: они бегут на 3.13, и модернизаций, доступных только там, +# ruff им не предложит. Значение совпадает с умолчанием ruff, и записано +# именно поэтому: сменится умолчание — проверка не поедет следом молча. target-version = "py310" [lint] From c9b11224d25be350fa1d9bb6caa0874884bf8fe8 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 9 Aug 2026 21:58:48 +0300 Subject: [PATCH 3/3] =?UTF-8?q?refactor(ruff):=20=D0=BB=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=20=D1=81=D1=82=D0=B5=D0=BD=D0=B4=D0=B0=20=D0=B3?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D1=80=D0=B8=D1=82=20=D0=BD=D0=B0=20=D1=8F?= =?UTF-8?q?=D0=B7=D1=8B=D0=BA=D0=B5=20=D0=B4=D0=B0=D0=B3=D0=BE=D0=B2,=20?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D0=BC=D0=BB=D0=B0=D0=B4=D1=88=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - target-version по младшему образу (Superset, 3.10) занижал проверку для дагов: они бегут на 3.13, а ruff предлагал им идиомы старее их рантайма. На учебном стенде это вывернуто наизнанку — менти читает и правит именно даги. - защита от Superset была верна не по устройству, а по сегодняшнему содержимому одного файла настройки. - Что: - target-version в корневом ruff.toml поднят до py313, комментарий переписан. - две находки UP017 в дагах починены: datetime.timezone.utc заменён на datetime.UTC. - из карты проверок убран пункт про разную строгость дверей — с равными версиями он потерял предмет. - Проверка: - make lint; make config-test — зелёные. - make -C generator lint — зелёный; исходники генератора не менялись. - находок ruff в дагах теперь ровно четыре, как обещал тикет: два переформата и два UP017. Co-Authored-By: Claude Opus 5 --- dags/test_clickhouse.py | 2 +- dags/test_kafka.py | 2 +- docs/architecture/testing.md | 7 +------ ruff.toml | 14 +++++++------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py index edf8391..da31590 100644 --- a/dags/test_clickhouse.py +++ b/dags/test_clickhouse.py @@ -96,7 +96,7 @@ def _assert_tables_absent(client) -> None: @dag( dag_id="test_clickhouse", schedule=None, - start_date=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), + start_date=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC), catchup=False, tags=["проверка"], doc_md=__doc__, diff --git a/dags/test_kafka.py b/dags/test_kafka.py index 17b418e..0ea5f8c 100644 --- a/dags/test_kafka.py +++ b/dags/test_kafka.py @@ -77,7 +77,7 @@ class RecordAddress(NamedTuple): @dag( dag_id="test_kafka", schedule=None, - start_date=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), + start_date=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC), catchup=False, tags=["проверка"], doc_md=__doc__, diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index ff21045..97c28c3 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -54,13 +54,8 @@ генератора, генераторная — ни одного файла стенда. Человек в корне видит зелёное и может решить, что зелёный весь репозиторий. -Три тонкости, которых не видно из таблицы: +Две тонкости, которых не видно из таблицы: -- **Список правил у дверей один, а строгость нет.** Правила `UP` предлагают - синтаксис настолько новый, насколько позволяет `target-version`: у генератора - это 3.14 из `requires-python`, у стенда 3.10 — столько у Superset, младшего из - его образов. Один и тот же файл может пройти корневую проверку и не пройти - генераторную. Довод и замер — в комментарии `ruff.toml`. - **Версия ruff у корневой цели закреплена в самом вызове** (`uvx ruff@0.16.1`) и равна той, что в `generator/uv.lock`. Равенство держится руками: обновили лок — поправьте и вызов, сверять их некому. diff --git a/ruff.toml b/ruff.toml index 7e7455f..eac65e3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,13 +7,13 @@ # На каком 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 не знает. -# Даги за это платят: они бегут на 3.13, и модернизаций, доступных только там, -# ruff им не предложит. Значение совпадает с умолчанием ruff, и записано -# именно поэтому: сменится умолчание — проверка не поедет следом молча. -target-version = "py310" +# внутри образов. Стоит версия дагов: их читают и правят, и идиомы им нужны +# те, на которых они работают. Замер 9 августа 2026 года: +# `apache/airflow:3.3.0` несёт Python 3.13.14, `apache/superset:6.1.0` — 3.10.20. +# Настройке Superset от этого пока ни холодно ни жарко: модернизировать в ней +# нечего. Предложит ruff что-то не по её возрасту — заведём исключение тогда, +# с настоящим поводом. +target-version = "py313" [lint] select = ["E", "F", "I", "UP", "B"]