- Зачем:
- до сих пор генератор умел собирать день, но не умел его отдать: топик
hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
события до стенда и закрывает форму на проводе, на которую обопрётся
типизированный ODS (#43).
- сериализатор один по решению спеки: второе место, печатающее событие в
JSON, разошлось бы с первым молча.
- Что:
- serialize.py — канонический сериализатор на orjson: единственное место,
где событие целиком становится JSON; 47 ключей всегда, «пусто» это
пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
ecommerce в commerce.py вторым сериализатором не считается — правило
про событие, а не про блок внутри него.
- sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
ключом он был бы ключом лишь на вид.
- player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
live (темп ×60), несколько дней одним запуском, ограниченная пачка,
раздельные тайминги генерации и доставки, лаг в логе.
- день на оси и имя топика умолчаний не имеют: параметр, описывающий
среду или позицию, приходит от зовущего, иначе отказ до генерации.
Умолчания зерна, числа дней и темпа остаются — они описывают мир.
- generator/Dockerfile — свой образ: зависимости из uv.lock, база
закреплена до патча, раскладка репозитория сохранена ради каталога
товаров. Образ Airflow не тронут.
- разовая служба compose под профилем, цели generate-batch и
generate-live, .dockerignore, tmp/ в .gitignore.
- решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
- make test 406 passed, make lint, make typecheck, make config-test.
- побайтовый детерминизм: два прогона дня в независимых процессах дают
один sha256; день в контейнере совпадает с днём на машине.
- на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
Distributed сошёлся — отправлено 50626, в таблице 50626.
- топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
clickhouse-02 раздел 1 (24258).
- живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
темп ×60, лаг печатается.
- форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
строкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
3.5 KiB
Docker
51 lines
3.5 KiB
Docker
# Образ генератора: тот самый «канонический контейнер», внутри которого спека
|
|
# обещает побайтовую воспроизводимость (раздел 2). Отсюда правило: пакеты
|
|
# ставятся только из `uv.lock` — второй список зависимостей писал бы на стенде
|
|
# не те байты, что сторожит `make test`.
|
|
#
|
|
# Контекст сборки — корень репозитория, а не `generator/`: в образ едет ещё и
|
|
# каталог товаров.
|
|
# База записана здесь и прибита к patch-версии: это канонический контейнер из
|
|
# раздела 2 спеки, и его база входит в тот же фиксированный набор, что `uv.lock`.
|
|
# Из `.env` её не берём — иначе описание воспроизводимой сборки распалось бы
|
|
# между Dockerfile и обвязкой запуска.
|
|
FROM python:3.14.7-slim
|
|
|
|
COPY --from=ghcr.io/astral-sh/uv:0.11.21 /uv /bin/uv
|
|
|
|
# Раскладка внутри образа повторяет раскладку репозитория: `/app` — его корень.
|
|
# Так надо не для красоты. Каталог товаров генератор ищет от своего модуля
|
|
# вверх по дереву (`catalog.py`, `parents[3]`): каталог — часть мира, а не
|
|
# запуска, и снаружи не настраивается (решение #39). Значит, взаимное
|
|
# расположение пакета и `data/` обязано сохраниться, иначе первый же импорт
|
|
# каталога упрётся в отсутствующий файл.
|
|
WORKDIR /app/generator
|
|
|
|
ENV UV_LINK_MODE=copy \
|
|
UV_PYTHON_DOWNLOADS=never
|
|
|
|
# Зависимости — своим слоем: он пересобирается, только когда меняется лок.
|
|
# `--extra kafka` ставит клиента, которым живёт приёмник Kafka; `--no-dev`
|
|
# оставляет за бортом pytest, ruff и ty — их дом на машине разработчика.
|
|
COPY generator/pyproject.toml generator/uv.lock ./
|
|
RUN uv sync --frozen --no-dev --no-install-project --extra kafka
|
|
|
|
# Пакет в `.venv` не ставится: он лежит исходником и виден через `PYTHONPATH`.
|
|
# Поставленный не editable, он переехал бы в `site-packages`, и те же
|
|
# `parents[3]` указали бы внутрь `.venv` — каталог в образе есть, а модуль его
|
|
# не находит. Заодно правка кода не трогает слой зависимостей.
|
|
COPY generator/src ./src
|
|
COPY data /app/data
|
|
|
|
ENV PATH="/app/generator/.venv/bin:$PATH" \
|
|
PYTHONPATH="/app/generator/src" \
|
|
PYTHONUNBUFFERED=1
|
|
|
|
# Не root: генератору хватает права читать своё и писать в сеть.
|
|
RUN useradd --create-home --uid 1000 generator
|
|
USER generator
|
|
|
|
# Параметры прогона передаёт зовущий — аргументами или окружением; итог он
|
|
# читает кодом возврата, а логи идут в стандартный вывод.
|
|
ENTRYPOINT ["python", "-m", "clickstream_generator"]
|