From c2c4401a166d49e1f05051319fb90a6c7810a672 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 28 Aug 2026 05:05:44 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(airflow):=20=D0=BF=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=20=D0=BC=D0=B8=D1=80=D0=B0=20=D0=BE=D0=B1=D1=8A=D1=8F?= =?UTF-8?q?=D1=81=D0=BD=D1=8F=D0=B5=D1=82=20=D0=BE=D1=81=D1=8C=20=D0=B4?= =?UTF-8?q?=D0=BD=D0=B5=D0=B9=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B5=20?= =?UTF-8?q?=D0=B8=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - оператор принимал служебную logical_date Airflow за дату мира (#111), а документацию читают реже, чем форму запуска. - Что: - у параметра days появилась подсказка description_md, а в докстринги работников добавлена фраза: день определяет world_position, служебные поля Airflow к оси мира отношения не имеют. - remember_played пишет в журнал сыгранные дни и новую позицию. - README дополнен абзацем про служебные поля за «Advanced Options»; из etl.md сняты устаревшие стыки на закрытые #7 и #2. - Проверка: - make lint, make config-test; ручной прогон world_next_day зелёный, позиция 9 -> 10, в журнале «сыграны дни 9–9, позиция теперь 10». Co-Authored-By: Claude Fable 5 --- README.md | 6 ++++++ dags/world_control.py | 28 +++++++++++++++++++++++++--- docs/architecture/etl.md | 6 +++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7a3e676..4d68f95 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,12 @@ uv run --project generator python -m clickstream_generator batch \ `0` — очистка закончена, а начальная генерация ещё не завершена. Работники начинают играть только с позиции `8` и больше. +Форма запуска работника спрашивает только своё — сколько дней играть. +Служебные поля Airflow в ней — «Logical date» и «Run ID» за створкой +«Advanced Options» — к оси мира отношения не имеют: какой день пойдёт первым, +смотрите в `world_position`, а какой сыгран — в журнале шага +`remember_played`. + Выключатель создаётся на паузе. Снимите — мир поедет сам; поставите обратно — встанет на границе модельных суток, доиграв начатый день. Форма пульта и доводы целиком — [ADR 0009](docs/adr/0009-world-control.md). diff --git a/dags/world_control.py b/dags/world_control.py index e991c08..92a44e7 100644 --- a/dags/world_control.py +++ b/dags/world_control.py @@ -28,6 +28,7 @@ from __future__ import annotations import datetime +import logging import os from airflow.exceptions import AirflowException @@ -141,7 +142,19 @@ def _ingest_orders() -> TriggerDagRunOperator: tags=TAGS, params={ "days": Param( - 1, type="integer", minimum=1, maximum=7, title="Сколько дней прожить" + 1, + type="integer", + minimum=1, + maximum=7, + title="Сколько дней прожить", + # description_md рендерится подсказкой прямо в форме запуска + # (params.rst, Airflow 3.3) — объяснение стоит там, где путают. + description_md=( + "Какие именно дни — решает не форма: первый несыгранный " + "день хранит переменная `world_position` (Admin → " + "Variables). Служебные поля Airflow — Logical date, Run " + "ID — к оси мира отношения не имеют." + ), ) }, ) @@ -149,14 +162,20 @@ def world_next_day(): """Прожить следующие дни пачкой, без пауз. Запускается руками. День по умолчанию один, но разгон вперёд идёт одним - нажимом, а не десятью: сколько дней играть — параметр запуска. + нажимом, а не десятью: сколько дней играть — параметр запуска. Какие + именно дни, знает переменная `world_position`, а не форма: логическая + дата прогона к оси мира отношения не имеет. """ @task def remember_played(first_day: int) -> None: """Позиция ставится по сыгранным дням и только по успеху.""" days = get_current_context()["params"]["days"] - Variable.set(WORLD_POSITION, str(first_day + days)) + position = first_day + days + Variable.set(WORLD_POSITION, str(position)) + logging.info( + "сыграны дни %s–%s, позиция теперь %s", first_day, position - 1, position + ) first_day = first_unplayed_day() played = _generator( @@ -196,12 +215,15 @@ def world_live_day(): Ускорение ×60: модельные сутки укладываются примерно в двадцать четыре реальные минуты, и суточная волна разворачивается на глазах. Запускается руками; чтобы мир жил так день за днём сам, есть выключатель `world_live`. + Какой день пойдёт, знает переменная `world_position`: логическая дата + прогона к оси мира отношения не имеет. """ @task def remember_played(first_day: int) -> None: """Позиция ставится по сыгранному дню и только по успеху.""" Variable.set(WORLD_POSITION, str(first_day + 1)) + logging.info("сыгран день %s, позиция теперь %s", first_day, first_day + 1) first_day = first_unplayed_day() played = _generator("play_day", ["live", "--day", PLAYED_DAY]) diff --git a/docs/architecture/etl.md b/docs/architecture/etl.md index 2d755e7..295bf09 100644 --- a/docs/architecture/etl.md +++ b/docs/architecture/etl.md @@ -137,9 +137,9 @@ ODS и ещё не собранный в DDS, затем при необходи ## Не входит - Партиционная переобработка — переиграть произвольный прошлый день или - диапазон, включая форму необязательных параметров того же дага, — этап 5 - (#7); туда же стык #111 про дату модельного мира и `logical_date`. -- Инкрементальный ETL вместо пересборки — техдолг #2. + диапазон, включая форму необязательных параметров того же дага. +- Инкрементальный ETL вместо пересборки — отклонён (#2, wontfix); учебная + сторона темы — идея лабы #149. - Состав витрин `dm_transform` и проверок `dq_check` — [дока слоя DM](dm.md). -- 2.54.0 From 17995d4c5e2f28c7b9cdab333c95aa88a5d38948 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 28 Aug 2026 05:14:51 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(airflow):=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=83=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BF=D1=83?= =?UTF-8?q?=D0=BB=D1=8C=D1=82=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D1=8B=20=D0=B6=D0=B8=D0=B2=D1=8B=D0=BC=20?= =?UTF-8?q?=D1=80=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - владелец на ревью PR #175 отметил кальку в подсказках; читать их будет менти, и язык здесь — часть продукта. - Что: - переписаны подсказка формы days, докстринги работников, строки журнала remember_played, абзац README и пункт в etl.md; смысл не менялся. - Проверка: - make lint, make config-test. Co-Authored-By: Claude Fable 5 --- README.md | 9 ++++----- dags/world_control.py | 24 ++++++++++++------------ docs/architecture/etl.md | 4 ++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4d68f95..1885271 100644 --- a/README.md +++ b/README.md @@ -262,11 +262,10 @@ uv run --project generator python -m clickstream_generator batch \ `0` — очистка закончена, а начальная генерация ещё не завершена. Работники начинают играть только с позиции `8` и больше. -Форма запуска работника спрашивает только своё — сколько дней играть. -Служебные поля Airflow в ней — «Logical date» и «Run ID» за створкой -«Advanced Options» — к оси мира отношения не имеют: какой день пойдёт первым, -смотрите в `world_position`, а какой сыгран — в журнале шага -`remember_played`. +В форме запуска можно выбрать только число дней. Первый день работник берет из +`world_position`. Поля «Logical date» и «Run ID» спрятаны в «Advanced Options». +Они нужны самому Airflow и на выбор дня не влияют. Сыгранные дни и новую +позицию работник пишет в журнал шага `remember_played`. Выключатель создаётся на паузе. Снимите — мир поедет сам; поставите обратно — встанет на границе модельных суток, доиграв начатый день. Форма пульта и доводы diff --git a/dags/world_control.py b/dags/world_control.py index 92a44e7..7574731 100644 --- a/dags/world_control.py +++ b/dags/world_control.py @@ -148,12 +148,12 @@ def _ingest_orders() -> TriggerDagRunOperator: maximum=7, title="Сколько дней прожить", # description_md рендерится подсказкой прямо в форме запуска - # (params.rst, Airflow 3.3) — объяснение стоит там, где путают. + # (params.rst, Airflow 3.3), чтобы правило было видно сразу. description_md=( - "Какие именно дни — решает не форма: первый несыгранный " - "день хранит переменная `world_position` (Admin → " - "Variables). Служебные поля Airflow — Logical date, Run " - "ID — к оси мира отношения не имеют." + "Форма задает только число дней. Номер первого несыгранного " + "дня работник берет из переменной `world_position` (Admin → " + "Variables). Поля Logical date и Run ID нужны самому Airflow " + "и на выбор дня не влияют." ), ) }, @@ -162,9 +162,9 @@ def world_next_day(): """Прожить следующие дни пачкой, без пауз. Запускается руками. День по умолчанию один, но разгон вперёд идёт одним - нажимом, а не десятью: сколько дней играть — параметр запуска. Какие - именно дни, знает переменная `world_position`, а не форма: логическая - дата прогона к оси мира отношения не имеет. + нажимом, а не десятью: сколько дней играть — параметр запуска. Первый + день работник берет из `world_position`. Логическая дата прогона на выбор + дня не влияет. """ @task @@ -174,7 +174,7 @@ def world_next_day(): position = first_day + days Variable.set(WORLD_POSITION, str(position)) logging.info( - "сыграны дни %s–%s, позиция теперь %s", first_day, position - 1, position + "дни %s–%s сыграны; новая позиция — %s", first_day, position - 1, position ) first_day = first_unplayed_day() @@ -215,15 +215,15 @@ def world_live_day(): Ускорение ×60: модельные сутки укладываются примерно в двадцать четыре реальные минуты, и суточная волна разворачивается на глазах. Запускается руками; чтобы мир жил так день за днём сам, есть выключатель `world_live`. - Какой день пойдёт, знает переменная `world_position`: логическая дата - прогона к оси мира отношения не имеет. + Следующий день работник берет из `world_position`. Логическая дата прогона + на выбор дня не влияет. """ @task def remember_played(first_day: int) -> None: """Позиция ставится по сыгранному дню и только по успеху.""" Variable.set(WORLD_POSITION, str(first_day + 1)) - logging.info("сыгран день %s, позиция теперь %s", first_day, first_day + 1) + logging.info("сыгран день %s; новая позиция — %s", first_day, first_day + 1) first_day = first_unplayed_day() played = _generator("play_day", ["live", "--day", PLAYED_DAY]) diff --git a/docs/architecture/etl.md b/docs/architecture/etl.md index 295bf09..ceff7ff 100644 --- a/docs/architecture/etl.md +++ b/docs/architecture/etl.md @@ -138,8 +138,8 @@ ODS и ещё не собранный в DDS, затем при необходи - Партиционная переобработка — переиграть произвольный прошлый день или диапазон, включая форму необязательных параметров того же дага. -- Инкрементальный ETL вместо пересборки — отклонён (#2, wontfix); учебная - сторона темы — идея лабы #149. +- Инкрементальный ETL вместо пересборки отклонен (#2, wontfix). Идея отдельной + лабы на эту тему — #149. - Состав витрин `dm_transform` и проверок `dq_check` — [дока слоя DM](dm.md). -- 2.54.0