Command Runner
Django-приложение для запуска management-команд из админки с мониторингом выполнения в реальном времени и возможностью принудительной остановки.
Возможности
- Запуск любой management-команды Django из белого списка через веб-интерфейс.
- Передача позиционных и именованных аргументов в формате shell
(
--steps 5 --name "Hello World"). - Мониторинг выполнения в реальном времени: статус, прогресс-бар, лог.
- Перехват stdout/stderr команды в лог запуска.
- Остановка выполнения: задачи в очереди снимаются мгновенно
(
job.cancelдля RQ,revokeдля Celery); выполняющиеся задачи убиваются (send_stop_job_command/kill_horseдля RQ,revoke(terminate=True)для Celery). - Поддержка двух бэкендов очередей: Celery и RQ (выбор в настройках).
- Хранение состояния и логов в общем кэше (Redis).
- Доступ только для staff-пользователей.
- Ограничение: одновременно выполняется только одна команда (глобальный лок в Redis).
Требования
- Python 3.10+
- Django 3.2+
- Redis (для кэша и очереди)
- Один из бэкендов очереди:
celery+ настроенный брокер (Redis/RabbitMQ) илиrq(использует Redis)
Установка
1. Установите зависимости
pip install django django-redis redis
# для RQ:
pip install rq
# для Celery:
pip install celery
2. Скопируйте приложение
Скопируйте пакет admin_cmd_runner/ в ваш проект (например, в
your_project_name/apps/admin_cmd_runner/) и добавьте его в INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"admin_cmd_runner",
]
3. Настройте settings.py
# Выбор бэкенда: "celery" или "rq"
COMMAND_RUNNER_QUEUE = "rq"
# Redis для RQ-очереди
COMMAND_RUNNER_RQ_REDIS_URL = "redis://redis:6379/2"
COMMAND_RUNNER_RQ_QUEUE = "default"
# Для django-rq worker: без этого блока воркер подключается к localhost:6379/0
RQ_QUEUES = {
"default": {"URL": COMMAND_RUNNER_RQ_REDIS_URL},
"high": {"URL": COMMAND_RUNNER_RQ_REDIS_URL},
}
# Общий кэш для состояния (обязательно Redis или Memcached)
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://redis:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
},
}
# Белый список команд
COMMAND_RUNNER_COMMANDS = [
{
"name": "demo_long_command",
"label": "Демонстрационная долгая команда",
"args_help": "--steps N",
},
# ...
]
4. Подключите URL-маршруты
В urls.py проекта:
from django.urls import include, path
urlpatterns += [
path("admin-cmd-runner/", include("admin_cmd_runner.urls")),
]
5. Запустите воркер
Для RQ:
# голый RQ:
rq worker default
# или через django-rq:
python manage.py rqworker default
Для Celery:
celery -A proj worker -l info
6. Запустите веб-сервер
python manage.py runserver
Использование
- Откройте
/admin-cmd-runner/под staff-пользователем. - Выберите команду из выпадающего списка.
- Введите аргументы (необязательно): позиционные
arg1 arg2; именованные--steps 5 --name "Hello World"; флаги--verbose(без значения →True); всё после--считается позиционным. - Нажмите «Запустить». Откроется страница запуска с прогресс-баром и логом, обновляющимся раз в секунду.
- Нажмите «Остановить» для принудительной остановки. Для задач в очереди остановка происходит мгновенно; для выполняющихся — через 1–3 секунды (зависит от бэкенда).
Тестирование
Тесты не требуют Redis, БД или установленных rq/celery: кэш
подменяется на LocMemCache через override_settings, а тесты бэкендов
пропускаются (skipUnless), если соответствующая библиотека не установлена.
Запуск в проекте
Если admin_cmd_runner подключён в INSTALLED_APPS вашего Django-проекта:
# все тесты приложения (по app label)
python manage.py test admin_cmd_runner
# только пакет тестов
python manage.py test admin_cmd_runner.tests
# конкретный модуль
python manage.py test admin_cmd_runner.tests.test_store
# конкретный тест-класс
python manage.py test admin_cmd_runner.tests.test_store.LockTests
# конкретный метод
python manage.py test admin_cmd_runner.tests.test_store.LockTests.test_acquire_and_get
Если приложение подключено по полному пути (your_project_name.apps.admin_cmd_runner),
используйте label или полный путь к пакету тестов:
python manage.py test apps_admin_cmd_runner
# или
python manage.py test your_project_name.apps.admin_cmd_runner.tests
Запуск при разработке пакета (uv)
Из корня репозитория (там, где pyproject.toml):
# установить uv (один раз)
curl -LsSf https://astral.sh/uv/install.sh | sh
# создать виртуальное окружение и установить пакет
# в editable-режиме со всеми dev-зависимостями
uv sync --extra dev
# тесты с покрытием (конфиг pytest — в pyproject.toml)
uv run pytest admin_cmd_runner/tests
# только линтер
uv run ruff check admin_cmd_runner tests
# только проверка типов
uv run mypy admin_cmd_runner
# всё сразу (lint + typecheck + test)
uv run ruff check admin_cmd_runner tests \
&& uv run mypy admin_cmd_runner \
&& uv run pytest admin_cmd_runner/tests
Управление зависимостями:
# добавить dev-зависимость
uv add --dev pytest-xdist
# обновить lock-файл без установки
uv lock
# синхронизировать окружение с lock-файлом
uv sync --extra dev
# конкретная версия Python
uv python install 3.12
uv venv --python 3.12
Покрытие
Порог покрытия задан в pyproject.toml (--cov-fail-under=70). HTML-отчёт:
uv run pytest admin_cmd_runner/tests --cov-report=html
open htmlcov/index.html
Запуск в Docker
docker compose exec django python manage.py test admin_cmd_runner
Архитектура
admin_cmd_runner/
├── __init__.py — описание приложения
├── apps.py — AppConfig
├── admin.py — без моделей, только подсказка по меню
├── urls.py — URL-маршруты
├── views.py — монитор, запуск, остановка, статус
├── forms.py — форма и парсер аргументов
├── registry.py — реестр доступных команд из settings
├── store.py — состояние и логи в общем кэше
├── reporter.py — Reporter для прогресса и остановки
├── jobs.py — транспортно-независимое исполнение команды
├── queue/
│ ├── __init__.py — фабрика бэкендов
│ ├── base.py — абстрактный интерфейс
│ ├── celery_backend.py — реализация на Celery
│ └── rq_backend.py — реализация на RQ
├── management/
│ └── commands/
│ ├── _base.py — ReporterCommand с доступом к reporter
│ └── demo_long_command.py — демо-команда с прогрессом и stop
└── templates/
└── admin_cmd_runner/
└── monitor.html — UI с polling
Поток выполнения
- Запуск.
start_runвалидирует форму, захватывает глобальный лок (cache.add), создаёт состояние (status=queued), ставит задачу в бэкенд очереди и сохраняетtask_id. - Исполнение. Воркер вызывает
execute_command, которая приводит типыkwargsпоadd_argument(type=...), ставитstatus=running, устанавливаетReporterв качестве stdout, выполняет команду черезcall_commandи вfinallyфиксирует терминальный статус и снимает лок. - Мониторинг. Фронт раз в секунду запрашивает
status_api, который возвращает состояние и новые строки лога (since=N). - Остановка.
stop_runвыставляет флагstop, вызываетbackend.revoke(task_id): дляqueued— задача снимается, статус сразуstopped; дляstarted— отправляется команда остановки, статусstopping, а финализация вstoppedпроисходит вstatus_apiпо факту смерти процесса в бэкенде (или по таймаутуSTOP_GRACE_SECONDS).
Хранилище (Redis)
| Ключ | Назначение | TTL |
|---|---|---|
cmdrun:lock |
id активного запуска (один на проект) | 24 ч |
cmdrun:state:<run_id> |
состояние запуска (status, progress, error) | 24 ч |
cmdrun:log:<run_id> |
лог (список строк, ограничен 2000 записей) | 24 ч |
cmdrun:stop:<run_id> |
флаг запроса остановки | 6 ч |
cmdrun:task:<run_id> |
task_id/job_id в бэкенде | 24 ч |
Важно: кэш должен быть общим для всех процессов (web и worker),
поэтому LocMemCache не подходит — нужен Redis или Memcached.
Написание собственных команд
Наследуйтесь от ReporterCommand, чтобы получить доступ к
self.reporter (прогресс, лог, проверка остановки):
from admin_cmd_runner.management.commands._base import ReporterCommand
class Command(ReporterCommand):
help = "Что делает команда"
def add_arguments(self, parser):
parser.add_argument("--count", type=int, default=10)
def handle(self, *args, **options):
count = options["count"]
for i in range(count):
self.reporter.check_stop() # поднимет CommandStopRequested при stop
self.stdout.write(f"Шаг {i + 1}/{count}") # попадёт в лог
self.reporter.progress(
int((i + 1) / count * 100),
f"Шаг {i + 1}/{count}",
)
# ... полезная работа ...
Если команда не наследует ReporterCommand, она всё равно попадёт
в лог (stdout перехватывается), но не сможет обновлять прогресс и
кооперативно проверять флаг остановки (останется только жёсткий kill).
Docker Compose
Пример для запуска вместе с Redis и воркером RQ:
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
django:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
depends_on:
- redis
rq:
build: .
command: python manage.py rqworker default
environment:
- DJANGO_SETTINGS_MODULE=your_project_name.settings.dev
- PYTHONUNBUFFERED=1
volumes:
- .:/code
depends_on:
- redis
- django
restart: "always"
# ВАЖНО: не добавляйте stdin_open: true — иначе rqworker
# получит EOF на stdin и будет завершаться каждые ~30 секунд.
Известные ограничения
- Одновременно выполняется только одна команда (глобальный лок).
- Стандартный
int/float-парсинг kwargs работает черезadd_argument(type=...)команды; для кастомных типов (напримерdatetime) используйте callable-type, он будет вызван. - Лог ограничен 2000 строками на запуск (старые обрезаются).
kill_horseв RQ не даёт выполнитьсяfinally— финализация статуса происходит на стороне web-процесса черезstatus_api.- 1–2 лишних запроса
/status/после нажатия «Остановить» — неизбежны: они попадают в окно между кликом и фактической смертью процесса воркера.
Metadata
Release files for django-admin-cmd-runner 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| django_admin_cmd_runner-0.2.0.tar.gz | 40.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_admin_cmd_runner-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.2 kB
Release files / django_admin_cmd_runner-0.2.0.tar.gz
| Download URL | django_admin_cmd_runner-0.2.0.tar.gz |
|---|---|
| Size | 40.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e9d0f374ff0b7fb54e3aa669621474360dea03798a9e7f338a4b9ab9da997c8
|
|
BLAKE2b-256 checksum How to use checksums |
fc167ba4d9b75e04fc383bcd4a427210bce5a7209f5fcd9acf64cbee4cd44f63
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / django_admin_cmd_runner-0.2.0-py3-none-any.whl
| Download URL | django_admin_cmd_runner-0.2.0-py3-none-any.whl |
|---|---|
| Size | 45.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
65e18e4a4cd5a14aac51000111dd9e646eba791a47d85f0fa7bfcd7c94db569f
|
|
BLAKE2b-256 checksum How to use checksums |
99c8c778980838f2fd95712b115ea2f8a64d1d75018e0d9ade18f3bad4d029f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for django-admin-cmd-runner 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.