diff --git a/.dockerignore b/.dockerignore index 849fd7c..ab647e3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,15 @@ .git .idea .venv -wheels extra docs tests **/__pycache__ *.pyc .env +# артефакты Nuitka при локальных прогонах (вне Docker) +*.build +*.dist +*.onefile-build +.nuitka-cache +# wheels/ НЕ в игноре: нужен в контексте сборки для OFFLINE=1 (make wheels) diff --git a/.gitignore b/.gitignore index c12fddf..740c0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ extra/ .venv/ .idea/ __pycache__/ -wheels/ +wheels/* +!wheels/.gitkeep docs/*.bkp docs/*.dtmp build/ diff --git a/Dockerfile b/Dockerfile index 5f1c049..d0db03f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,98 @@ # Сервис worker (#78): выполняет задачи из очереди в Google Chrome под Xvfb. -FROM python:3.12-slim-bookworm +# +# builder: компилирует src/ (через entry_worker.py) в самодостаточный standalone-бинарник Nuitka — +# в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements.txt. +FROM python:3.12-slim-bookworm AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 +# build-essential (gcc + libc6-dev — заголовки Python.h тянут stdlib.h/limits.h) — компилятор для +# сгенерированного Nuitka C-кода; patchelf — правит RPATH бинарника в standalone-режиме; +# ccache — ускоряет повторные сборки при пересборке образа +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential patchelf ccache \ + && rm -rf /var/lib/apt/lists/* + +# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md +ARG OFFLINE=0 +WORKDIR /build +COPY requirements.txt requirements-build.txt ./ +COPY wheels/ ./wheels/ +RUN set -e; \ + if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=./wheels"; else PIP_OPTS=""; fi; \ + pip install $PIP_OPTS -r requirements.txt -r requirements-build.txt + +COPY src/ ./src/ + +# standalone: entry_worker.dist/ — изолированная папка со своим Python-рантаймом и всеми пакетами, которые +# реально нужны воркеру (patchright, playwright, browserforge, dramatiq, db, tasks, engine, antibot, captcha, +# полный api/ — см. worker/windows.py). worker.actors попадает в сборку только из-за явного import в +# entry_worker.py: dramatiq грузит его по строке "worker.actors" динамически, статика Nuitka это не видит +# --include-package-data: Nuitka копирует только код, а patchright/browserforge везут с собой +# небольшой код + большие данные не-.py (драйвер patchright — Node.js бинарник и JS, browserforge и +# apify_fingerprint_datapoints — zip/json датасеты отпечатков), без них падают с FileNotFoundError в runtime +RUN python -m nuitka --mode=standalone \ + --include-package=worker --include-package=engine --include-package=antibot --include-package=captcha \ + --include-package=db --include-package=tasks --include-package=api \ + --include-package-data=patchright --include-package-data=browserforge \ + --include-package-data=apify_fingerprint_datapoints \ + --include-data-files=src/api/js/canvas_noise.js=api/js/canvas_noise.js \ + --output-dir=/build/dist \ + src/entry_worker.py + +# runtime: Python тут остаётся — он нужен только на один шаг (установка Chrome через `patchright install`, +# тот же механизм, что и раньше), а дальше запускается готовый standalone-бинарник, а не исходники из +# requirements.txt. Полный интерпретатор здесь — осознанный компромисс: переносить apt-зависимости Chrome +# между стадиями вручную хрупко (patchright сам решает, что ставить под конкретную версию Chrome). +FROM python:3.12-slim-bookworm AS runtime + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + # Xvfb — браузер запускается в headful-режиме (headless=False), нужен виртуальный дисплей RUN apt-get update \ && apt-get install -y --no-install-recommends tini xvfb xauth fonts-liberation fonts-noto-color-emoji ca-certificates \ && rm -rf /var/lib/apt/lists/* -WORKDIR /app - -COPY requirements.txt . -RUN pip install -r requirements.txt - -# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями -RUN patchright install --with-deps chrome \ - && rm -rf /var/lib/apt/lists/* +# real_chrome=True → channel "chrome": ставим Google Chrome stable вместе с системными зависимостями. +# Версия patchright — как в requirements.txt (#109): её CLI и сам пакет worker'у в runtime не нужны, +# только Chrome и его системные библиотеки, которые он поставит через apt +ARG OFFLINE=0 +COPY wheels/ /tmp/wheels/ +RUN set -e; \ + if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=/tmp/wheels"; else PIP_OPTS=""; fi; \ + pip install $PIP_OPTS patchright==1.63.0 \ + && patchright install --with-deps chrome \ + && pip uninstall -y patchright \ + && rm -rf /tmp/wheels /var/lib/apt/lists/* /root/.cache RUN useradd --create-home --uid 1000 app \ && mkdir -p /app/extra \ && chown -R app:app /app -COPY --chown=app:app src/ ./src/ +WORKDIR /app +COPY --from=builder --chown=app:app /build/dist/entry_worker.dist/ ./ USER app -WORKDIR /app/src # профили браузера (extra/user_data_dir) и geoip-кэш — в volume VOLUME ["/app/extra"] # tini как PID 1: пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb (иначе — зомби) ENTRYPOINT ["/usr/bin/tini", "-g", "--"] -# один процесс: открытые окна браузера живут в его памяти; потоков — с запасом над слотами, чтобы -# задачи в уже открытые окна не ждали, пока потоки заняты задачами без свободного слота -CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' dramatiq worker.actors --processes 1 --threads ${WORKER_THREADS:-$((${BROWSER_SLOTS:-4} * 2))}"] +# BROWSER_SLOTS/WORKER_THREADS читает сам entry_worker.bin (--processes 1, потоков — вдвое больше слотов) +CMD ["sh", "-c", "exec xvfb-run -a --server-args='-screen 0 1920x1080x24' ./entry_worker.bin"] + +# test (make test-browser; не публикуется, не используется по умолчанию — docker build должен указывать +# --target=runtime): entry_worker.bin — скомпилированный бинарник, pytest/tests/harness.py не может +# подключиться к его internals напрямую. Этой стадии нужны тот же Chrome/Xvfb из runtime и исходники +# как обычный интерпретируемый Python рядом с бинарником — они не конфликтуют (разные пути) +FROM runtime AS test +USER root +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY --chown=app:app src/ ./src/ +USER app +WORKDIR /app diff --git a/Dockerfile.api b/Dockerfile.api index ca7658b..ef6a8ec 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -1,24 +1,58 @@ # Сервис api (#78): принимает задачи и отдаёт результаты. Без Chrome — лёгкий образ. -FROM python:3.12-slim-bookworm +# +# builder: компилирует src/ (через entry_api.py) в самодостаточный standalone-бинарник Nuitka — +# в финальный образ не попадают ни исходники .py, ни gcc/pip, ни сам requirements-api.txt. +FROM python:3.12-slim-bookworm AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 -WORKDIR /app +# build-essential (gcc + libc6-dev — заголовки Python.h тянут stdlib.h/limits.h) — компилятор для +# сгенерированного Nuitka C-кода; patchelf — правит RPATH бинарника в standalone-режиме; +# ccache — ускоряет повторные сборки при пересборке образа +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential patchelf ccache \ + && rm -rf /var/lib/apt/lists/* -COPY requirements-api.txt . -RUN pip install -r requirements-api.txt \ +# OFFLINE=1: ставить зависимости только из wheels/ (make wheels), без обращения к PyPI — см. docs/Deployment.md +ARG OFFLINE=0 +WORKDIR /build +COPY requirements-api.txt requirements-build.txt ./ +COPY wheels/ ./wheels/ +RUN set -e; \ + if [ "$OFFLINE" = "1" ]; then PIP_OPTS="--no-index --find-links=./wheels"; else PIP_OPTS=""; fi; \ + pip install $PIP_OPTS -r requirements-api.txt -r requirements-build.txt + +COPY src/main.py src/entry_api.py ./src/ +COPY src/tasks/ ./src/tasks/ +COPY src/db/ ./src/db/ +COPY src/api/__init__.py src/api/schemas.py ./src/api/ + +# standalone: entry_api.dist/ — изолированная папка со своим Python-рантаймом и всеми пакетами, которые +# реально нужны entry_api.py (uvicorn, fastapi, db, tasks, api.schemas); world.actors/api.actions сюда не +# попадают — entry_api.py их не импортирует. --include-package на первом месте — на случай отложенного +# импорта, который статический анализ Nuitka мог не увидеть +RUN python -m nuitka --mode=standalone \ + --include-package=api --include-package=db --include-package=tasks \ + --output-dir=/build/dist \ + src/entry_api.py + +# runtime: без Python, pip и исходников — только то, что нужно скомпилированному бинарнику +FROM debian:bookworm-slim AS runtime + +# libpq5 — asyncpg/SQLAlchemy ходят в Postgres через системную libpq, Nuitka её не компилирует, а +# подключает динамически +RUN apt-get update \ + && apt-get install -y --no-install-recommends libpq5 ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ && useradd --create-home --uid 1000 app -COPY --chown=app:app src/main.py ./src/ -COPY --chown=app:app src/tasks/ ./src/tasks/ -COPY --chown=app:app src/db/ ./src/db/ -COPY --chown=app:app src/api/__init__.py src/api/schemas.py ./src/api/ +WORKDIR /app +COPY --from=builder --chown=app:app /build/dist/entry_api.dist/ ./ USER app -WORKDIR /app/src EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +# dbcli — админ-CLI клиентов и ключей (ранее `python -m db.cli`): docker compose exec api ./entry_api.bin dbcli ... +CMD ["./entry_api.bin"] diff --git a/Makefile b/Makefile index 18fafcb..f7cfe90 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ PIP := $(PYTHON) -m pip WHEELS_DIR := wheels REQUIREMENTS := requirements.txt ver ?= latest +offline ?= 0 MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev run dev-deps dev-key PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS)) @@ -41,10 +42,11 @@ help: @echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)" @echo "make sync-offline - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу" @echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения" - @echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)" + @echo "make wheels - собрать wheel-пакеты для requirements*.txt (api, worker, nuitka) в $(WHEELS_DIR)/" @echo "make clean - удалить $(WHEELS_DIR)/" - @echo "make build [ver=latest] - собрать образы $(API_IMAGE): и $(WORKER_IMAGE):" - @echo "make push ver= - собрать образы и отправить в $(REGISTRY)" + @echo "make build [ver=latest] [offline=1] - собрать образы $(API_IMAGE): и $(WORKER_IMAGE):;" + @echo " offline=1 — ставить python-зависимости только из $(WHEELS_DIR)/ (make wheels), без PyPI" + @echo "make push ver= [offline=1] - собрать образы и отправить в $(REGISTRY)" @echo "make release ver= [out=registry|file] - собрать образы и пакет для деплоя в build// (compose, .env);" @echo " образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz" @echo "make up - собрать и запустить локально: api, worker, rabbitmq, redis" @@ -87,14 +89,14 @@ freeze: venv wheels: venv @mkdir -p $(WHEELS_DIR) - $(PIP) wheel -r $(REQUIREMENTS) -w $(WHEELS_DIR) + $(PIP) wheel -r $(REQUIREMENTS) -r requirements-api.txt -r requirements-build.txt -w $(WHEELS_DIR) clean: rm -rf $(WHEELS_DIR) build: - docker build -f Dockerfile.api -t $(API_IMAGE):$(ver) . - docker build -t $(WORKER_IMAGE):$(ver) . + docker build --build-arg OFFLINE=$(offline) -f Dockerfile.api -t $(API_IMAGE):$(ver) . + docker build --build-arg OFFLINE=$(offline) --target=runtime -t $(WORKER_IMAGE):$(ver) . push: build for img in $(API_IMAGE) $(WORKER_IMAGE); do \ @@ -134,17 +136,17 @@ smoke: $(PYTHON) tests/smoke.py $(API_URL) $(API_KEY) key-create: - docker compose exec api python -m db.cli create "$(name)" + docker compose exec api ./entry_api.bin dbcli create "$(name)" key-list: - docker compose exec api python -m db.cli list + docker compose exec api ./entry_api.bin dbcli list key-revoke: - docker compose exec api python -m db.cli revoke $(id) + docker compose exec api ./entry_api.bin dbcli revoke $(id) days ?= 7 stats: - docker compose exec api python -m db.cli stats --days $(days) + docker compose exec api ./entry_api.bin dbcli stats --days $(days) sync-dev: venv $(PIP) install -r requirements-dev.txt @@ -157,11 +159,15 @@ client-build: venv $(PIP) wheel ./client -w client/dist --no-deps @echo "Готово: client/dist/ — pip install client/dist/robotex-*.whl" -test-browser: build +# entry_worker.bin — скомпилированный Nuitka-бинарник, pytest не может подключиться к его internals +# напрямую (tests/harness.py импортирует api/worker как обычные модули), поэтому тесты идут не против +# $(WORKER_IMAGE), а против стадии test — тот же Chrome/Xvfb, но исходники как интерпретируемый Python +test-browser: + docker build --build-arg OFFLINE=$(offline) --target=test -t $(WORKER_IMAGE)-test:$(ver) . docker run --rm --shm-size=2g \ -v $(CURDIR)/tests:/app/tests:ro -v $(CURDIR)/pytest.ini:/app/pytest.ini:ro \ -v $(CURDIR)/requirements-dev.txt:/app/requirements-dev.txt:ro -v $(CURDIR)/client/src:/app/client/src:ro \ - -w /app $(WORKER_IMAGE):$(ver) sh -c 'pip install -q --user -r requirements-dev.txt && \ + -w /app $(WORKER_IMAGE)-test:$(ver) sh -c 'pip install -q --user -r requirements-dev.txt && \ xvfb-run -a --server-args="-screen 0 1920x1080x24" python -m pytest -m browser -p no:cacheprovider -s' RELEASE_DIR := build/$(ver) @@ -206,7 +212,7 @@ endif '```' \ '' \ 'Настройки (порт, слоты, CPU и память воркера, пароли RabbitMQ и Postgres) — в .env.' \ - 'Ключ клиенту: docker compose exec api python -m db.cli create <имя>' > $(RELEASE_DIR)/README.md + 'Ключ клиенту: docker compose exec api ./entry_api.bin dbcli create <имя>' > $(RELEASE_DIR)/README.md @echo "Готово: $(RELEASE_DIR)/" # позволяет передавать имя пакета как аргумент: make install redis diff --git a/docker-compose.yml b/docker-compose.yml index 1e572f8..fcd2d65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: build: context: . dockerfile: Dockerfile.api + args: + OFFLINE: ${OFFLINE:-0} image: robotex-api:latest restart: unless-stopped ports: @@ -24,7 +26,11 @@ services: postgres: {condition: service_healthy} worker: - build: . + build: + context: . + target: runtime + args: + OFFLINE: ${OFFLINE:-0} image: robotex-worker:latest restart: unless-stopped env_file: diff --git a/docs/Deployment.md b/docs/Deployment.md index b3dda41..3755101 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -47,7 +47,7 @@ make key-list # клиенты: начало ключа, акти make key-revoke id=3 # отозвать ``` -На сервере без make: `docker compose exec api python -m db.cli create acme` (и `list`, `revoke `, `stats`). +На сервере без make: `docker compose exec api ./entry_api.bin dbcli create acme` (и `list`, `revoke `, `stats`). ## Статистика использования @@ -70,17 +70,28 @@ make stats days=7 ## Что внутри образа worker +Сборка двухстадийная (`Dockerfile`): `builder` компилирует `src/` (через `entry_worker.py`) в самодостаточный +standalone-бинарник Nuitka, `runtime` берёт из него только готовую папку `entry_worker.dist/` — исходники `.py`, +`requirements.txt`, gcc и pip в финальный образ не попадают. + | Компонент | Зачем | |---|---| -| `python:3.12-slim-bookworm` | базовый образ, Python той же версии, что и в `.venv` | -| `requirements.txt` | ставится из PyPI (папка `wheels/` в образ не попадает — она под Ubuntu Noble и неполная) | -| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome` | +| `entry_worker.bin` (standalone, builder) | скомпилированный Nuitka код `src/` + его Python-зависимости; оборачивает `dramatiq worker.actors --processes 1 --threads N` | +| `python:3.12-slim-bookworm` (runtime) | базовый образ — Python тут нужен только на шаге установки Chrome (`patchright install`), сам бинарник от него не зависит | +| Google Chrome stable (`patchright install --with-deps chrome`) | сессия запускается с `real_chrome=True` → channel `chrome`; после установки пакет `patchright` из runtime удаляется | | Xvfb (`xvfb-run`) | браузер работает в headful-режиме (`headless=False`), нужен виртуальный дисплей 1920x1080x24 | | `tini` (ENTRYPOINT, PID 1) | пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb — без него копятся зомби-процессы | | пользователь `app` (uid 1000) | Chrome не запускается от root без `--no-sandbox` | -Рабочая директория — `/app/src`. Воркер — один процесс dramatiq (`--processes 1`: открытые окна живут в его памяти), -потоков `WORKER_THREADS` (по умолчанию `2 × BROWSER_SLOTS`). +`make build offline=1` ставит python-зависимости builder-стадии только из `wheels/` (`make wheels`), без обращения +к PyPI — установку самого Chrome (apt + скачивание) это не затрагивает, сеть для неё нужна всегда. Wheels, собранные +`pip wheel`, переносимы между дистрибутивами, только если для пакета есть готовый `manylinux`-wheel на PyPI; если +для какого-то пакета его нет и pip собирает колесо из исходников на хосте — используйте для `make wheels` машину +или контейнер на той же базе, что и образ (`python:3.12-slim-bookworm`), иначе колесо может не завестись в сборке. + +Рабочая директория — `/app`. Воркер — один процесс dramatiq (`--processes 1`: открытые окна живут в его памяти), +потоков `WORKER_THREADS` (по умолчанию `2 × BROWSER_SLOTS`) — читает `entry_worker.py` сам, в `CMD` эти флаги больше +не передаются. ## Данные и volume diff --git a/requirements-build.txt b/requirements-build.txt new file mode 100644 index 0000000..7a35d21 --- /dev/null +++ b/requirements-build.txt @@ -0,0 +1,3 @@ +# зависимости builder-стадии (Dockerfile, Dockerfile.api): компиляция src/ в standalone-бинарник. +# В финальный образ не попадает — ставится только в стадии builder. +Nuitka==4.3.4 diff --git a/requirements.txt b/requirements.txt index 3353eca..10e849a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,85 +1,33 @@ 2captcha-python==2.1.1 aiofiles==25.1.0 -aiohappyeyeballs==2.7.1 -aiohttp==3.14.3 -aiosignal==1.4.0 annotated-doc==0.0.5 annotated-types==0.8.0 anyio==4.15.1 apify_fingerprint_datapoints==0.15.0 asyncpg==0.32.0 -attrs==26.1.0 -blessed==1.49.0 browserforge==1.2.4 -camoufox==0.5.6 certifi==2026.7.22 charset-normalizer==3.5.1 click==8.5.0 -distro==1.9.0 dramatiq==2.2.1 -editor==1.8.0 fastapi==0.141.1 -frozenlist==1.8.0 -geoip2==5.3.0 greenlet==3.5.5 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.19 -inquirer==3.4.1 -jinxed==2.1.0 -language-tags==1.3.1 -lxml==6.1.3 -marionette_driver==3.7.1 -markdown-it-py==4.2.0 -maxminddb==3.1.1 -mdurl==0.1.2 -mozdevice==4.2.0 -mozfile==3.0.0 -mozinfo==1.2.3 -mozlog==8.1.0 -mozprocess==1.4.0 -mozprofile==3.0.0 -mozrunner==8.4.0 -mozsystemmonitor==1.0.1 -mozterm==1.0.0 -mozversion==2.4.0 msgspec==0.21.1 -multidict==6.7.1 -numpy==2.5.3 -orjson==3.12.0 patchright==1.63.0 pika==1.4.4 -platformdirs==4.11.7 playwright==1.62.0 -propcache==0.5.2 -psutil==7.2.2 pydantic==2.13.5 pydantic_core==2.46.5 pyee==13.0.1 -Pygments==2.21.0 -PySide6==6.11.2 -PySide6_Addons==6.11.2 -PySide6_Essentials==6.11.2 -PySocks==1.7.1 -PyYAML==6.0.3 -readchar==4.2.2 redis==8.1.0 requests==2.34.2 -rich==15.0.0 -rich-click==1.9.9 -runs==1.3.0 -screeninfo==0.8.1 -shiboken6==6.11.2 -six==1.17.0 SQLAlchemy==2.1.4 starlette==1.6.0 typing_extensions==4.16.0 typing-inspection==0.4.4 -ua-parser==1.0.2 -ua-parser-builtins==202606 urllib3==2.7.0 uvicorn==0.52.4 -wcwidth==0.8.3 -xmod==1.10.0 -yarl==1.24.5 diff --git a/src/entry_api.py b/src/entry_api.py new file mode 100644 index 0000000..d926870 --- /dev/null +++ b/src/entry_api.py @@ -0,0 +1,23 @@ +""" +Единая точка входа для Nuitka-сборки образа api: без аргументов — FastAPI-сервер (uvicorn), с первым +аргументом `dbcli` — админ-CLI клиентов и ключей (ранее `python -m db.cli`, см. db/cli.py). Один бинарник +вместо двух: standalone-сборка Nuitka не делит общий рантайм между несколькими точками входа. +""" +import sys + +import uvicorn + +import db.cli +from main import app + + +def main() -> None: + if len(sys.argv) > 1 and sys.argv[1] == "dbcli": + sys.argv = sys.argv[1:] + db.cli.main() + return + uvicorn.run(app, host="0.0.0.0", port=8000) + + +if __name__ == "__main__": + main() diff --git a/src/entry_worker.py b/src/entry_worker.py new file mode 100644 index 0000000..1e644af --- /dev/null +++ b/src/entry_worker.py @@ -0,0 +1,22 @@ +""" +Точка входа для Nuitka-сборки образа worker: оборачивает `dramatiq worker.actors --processes 1 +--threads N` (dramatiq.cli.main() сам разбирает sys.argv, как и обычный CLI). Прямой `import worker.actors` +обязателен: dramatiq импортирует брокер по строке "worker.actors" динамически через importlib, а статический +анализ Nuitka такое не видит и без явного импорта не включит модуль в standalone-сборку. +""" +import os +import sys + +import worker.actors # noqa: F401 +from dramatiq.cli import main as dramatiq_main + + +def main() -> None: + slots = int(os.getenv("BROWSER_SLOTS", "4")) + threads = os.getenv("WORKER_THREADS") or str(slots * 2) + sys.argv = ["dramatiq", "worker.actors", "--processes", "1", "--threads", threads] + sys.exit(dramatiq_main()) + + +if __name__ == "__main__": + main() diff --git a/wheels/.gitkeep b/wheels/.gitkeep new file mode 100644 index 0000000..e69de29