feat: admin.py для управления ботом через docker exec
- Добавлен console_commands/admin.py — CLI-скрипт с командами pogoda, news, cat, morning, help - Добавлен docstring к методу pg (отображается в !hp) - Обновлён README.md: архитектура, администрирование в Docker, тесты - Добавлены тесты test_admin.py (5 тестов) - Итого тестов: 223
This commit is contained in:
parent
a52bb94224
commit
12ed515cca
22
README.md
22
README.md
@ -67,6 +67,7 @@ commands/ # Discord команды (cogs)
|
||||
help.py # !hp — список команд (автогенерация из bot.commands)
|
||||
console_commands/ # Консольные команды
|
||||
__init__.py # ALL_CONSOLE_COMMANDS — явные импорты
|
||||
admin.py # admin — CLI для docker exec (pogoda, news, cat, morning, help)
|
||||
stop.py # stop — остановка бота
|
||||
news.py # news — новости с Habr
|
||||
pogoda.py # pogoda — погода в терминале
|
||||
@ -91,6 +92,8 @@ tests/ # pytest-тесты
|
||||
test_morning_runner.py# тесты morning runner-а
|
||||
test_help_discord.py # команда !hp — проверка формата вывода и контента
|
||||
test_help_console.py # консольная help — проверка списка команд
|
||||
test_logger.py # setup_logging — уровни, обработчики, формат
|
||||
test_admin.py # admin.py — CLI-скрипт для docker exec
|
||||
ISSUES.md # Задачи и баг-трекер проекта
|
||||
pytest.ini # Конфигурация pytest (asyncio_mode = auto)
|
||||
Dockerfile # Сборка образа бота (Python 3.14-slim, healthcheck)
|
||||
@ -130,9 +133,11 @@ python -m pytest tests/ -v
|
||||
| `test_morning_runner.py` | morning runner-а | 68 |
|
||||
| `test_help_discord.py` | команда `!hp` | 2 |
|
||||
| `test_help_console.py` | консольная `help` | 2 |
|
||||
| `test_logger.py` | `setup_logging` (уровни, обработчики, формат) | 9 |
|
||||
| `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 |
|
||||
| `test_admin.py` | `admin.py` — CLI для docker exec | 5 |
|
||||
|
||||
**Итого: 209 тестов.**
|
||||
**Итого: 223 теста.**
|
||||
|
||||
## Запуск в Docker
|
||||
|
||||
@ -155,6 +160,21 @@ DISCORD_TOKEN=ваш_токен docker-compose up
|
||||
- Консольный ввод отключён в Docker (stdin недоступен)
|
||||
- Версия Python настраивается через `ARG PYTHON_VERSION`
|
||||
|
||||
### Администрирование через docker exec
|
||||
|
||||
Для управления ботом из терминала (без Discord-чата) используйте `admin.py`:
|
||||
|
||||
```bash
|
||||
docker exec discord-bot python admin.py pogoda
|
||||
docker exec discord-bot python admin.py news
|
||||
docker exec discord-bot python admin.py cat
|
||||
docker exec discord-bot python admin.py morning
|
||||
docker exec discord-bot python admin.py help
|
||||
docker stop discord-bot # остановка бота
|
||||
```
|
||||
|
||||
Результат выводится в stdout терминала. Команды используют те же `utils`, что и Discord-команды.
|
||||
|
||||
## API и внешние сервисы
|
||||
|
||||
### Погода (!pg, !morning)
|
||||
|
||||
@ -10,6 +10,7 @@ class Pg(commands.Cog):
|
||||
|
||||
@commands.command(name="pg")
|
||||
async def pg(self, ctx):
|
||||
"""Прогноз погоды в Магнитогорске"""
|
||||
data = await fetch_weather(self.api_url)
|
||||
if data is None:
|
||||
await ctx.send("Не удалось получить данные о погоде.")
|
||||
|
||||
198
console_commands/admin.py
Normal file
198
console_commands/admin.py
Normal file
@ -0,0 +1,198 @@
|
||||
"""Админ-скрипт для управления ботом через docker exec.
|
||||
|
||||
Использование:
|
||||
docker exec discord-bot python admin.py pogoda
|
||||
docker exec discord-bot python admin.py news
|
||||
docker exec discord-bot python admin.py cat
|
||||
docker exec discord-bot python admin.py morning
|
||||
docker exec discord-bot python admin.py help
|
||||
"""
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Показать список команд."""
|
||||
print("\nАдмин-команды:")
|
||||
print("-" * 40)
|
||||
commands = [
|
||||
("pogoda", "Прогноз погоды в Магнитогорске"),
|
||||
("news", "Топ-5 статей и новостей AI с Habr"),
|
||||
("cat", "URL случайного котика"),
|
||||
("morning", "Утренний дайджест: погода + новости + котик"),
|
||||
("help", "Показать этот список"),
|
||||
]
|
||||
for name, desc in commands:
|
||||
print(f" {name:<12} — {desc}")
|
||||
print("-" * 40)
|
||||
print("\nОстановка бота: docker stop discord-bot\n")
|
||||
|
||||
|
||||
async def run_pogoda():
|
||||
"""Вывести прогноз погоды для Магнитогорска."""
|
||||
from utils.pogoda import (
|
||||
API_URL_WEATHER,
|
||||
fetch_weather,
|
||||
format_weather_data_for_console,
|
||||
)
|
||||
|
||||
data = await fetch_weather(API_URL_WEATHER)
|
||||
if data is None:
|
||||
print("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
formatted = format_weather_data_for_console(data)
|
||||
if not formatted:
|
||||
print("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
for line in formatted:
|
||||
print(line)
|
||||
|
||||
|
||||
async def run_news():
|
||||
"""Вывести топ-5 свежих статей по AI с Habr."""
|
||||
from utils.news import (
|
||||
RSS_URL_ARTICLES,
|
||||
RSS_URL_POSTS,
|
||||
fetch_rss,
|
||||
format_articles,
|
||||
)
|
||||
|
||||
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||
if articles is None:
|
||||
print("Не удалось получить новости.")
|
||||
return
|
||||
|
||||
if not articles:
|
||||
print("Новостей пока нет.")
|
||||
return
|
||||
|
||||
lines = format_articles(
|
||||
articles,
|
||||
"Статьи AI / Хабр",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
|
||||
)
|
||||
|
||||
posts = await fetch_rss(RSS_URL_POSTS)
|
||||
if posts is not None and posts:
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
format_articles(
|
||||
posts,
|
||||
"Новости AI / Хабр",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/",
|
||||
)
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
print(line)
|
||||
|
||||
|
||||
async def run_cat():
|
||||
"""Вывести URL случайного котика."""
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
url = await fetch_cat()
|
||||
if url is None:
|
||||
print("Не удалось получить котика.")
|
||||
return
|
||||
print(f"Котик: {url}")
|
||||
|
||||
|
||||
async def run_morning():
|
||||
"""Вывести утренний дайджест: погода + новости + котик."""
|
||||
from utils.cat import fetch_cat
|
||||
from utils.morning_runner import gather_morning
|
||||
from utils.news import format_articles
|
||||
from utils.pogoda import format_weather_data_for_console
|
||||
|
||||
data = await gather_morning()
|
||||
print("Доброе утро!\n")
|
||||
|
||||
# --- Котик ---
|
||||
if data.cat_url:
|
||||
print(f"Котик: {data.cat_url}\n")
|
||||
else:
|
||||
print("Котика получить не удалось.\n")
|
||||
|
||||
# --- Погода ---
|
||||
formatted = format_weather_data_for_console(data.weather)
|
||||
if formatted:
|
||||
print("**Погода в Магнитогорске:**")
|
||||
for line in formatted:
|
||||
print(line)
|
||||
else:
|
||||
print("Не удалось получить данные о погоде.")
|
||||
print()
|
||||
|
||||
# --- Новости: статьи ---
|
||||
if data.articles is not None:
|
||||
if data.articles:
|
||||
print(
|
||||
"\n".join(
|
||||
format_articles(
|
||||
data.articles,
|
||||
"Статьи AI / Хабр",
|
||||
"https://habr.com/ru/hubs/"
|
||||
"artificial_intelligence/articles/top/daily/",
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
print("Не удалось получить новости.")
|
||||
print()
|
||||
|
||||
# --- Новости: посты ---
|
||||
if data.posts is not None:
|
||||
if data.posts:
|
||||
print(
|
||||
"\n".join(
|
||||
format_articles(
|
||||
data.posts,
|
||||
"Новости AI / Хабр",
|
||||
"https://habr.com/ru/hubs/"
|
||||
"artificial_intelligence/news/top/daily/",
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
print("Не удалось получить новости.")
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"pogoda": run_pogoda,
|
||||
"news": run_news,
|
||||
"cat": run_cat,
|
||||
"morning": run_morning,
|
||||
"help": lambda: print_help(),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Точка входа: парсит аргумент и вызывает команду."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Укажите команду.")
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
handler = COMMANDS.get(command)
|
||||
|
||||
if handler is None:
|
||||
print(f"Неизвестная команда: {command}")
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if command == "help":
|
||||
handler()
|
||||
else:
|
||||
asyncio.run(handler())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
65
tests/test_admin.py
Normal file
65
tests/test_admin.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Тесты для admin.py — управление через docker exec."""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ADMIN_SCRIPT = "console_commands/admin.py"
|
||||
|
||||
|
||||
class TestAdminHelp:
|
||||
"""Тесты команды help."""
|
||||
|
||||
def test_help_shows_commands(self, capfd):
|
||||
"""Команда help выводит список команд."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, ADMIN_SCRIPT, "help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
output = result.stdout
|
||||
assert "pogoda" in output
|
||||
assert "news" in output
|
||||
assert "cat" in output
|
||||
assert "morning" in output
|
||||
assert "help" in output
|
||||
|
||||
def test_no_args_shows_help(self, capfd):
|
||||
"""Без аргументов выводит help и завершается с кодом 1."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, ADMIN_SCRIPT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert "pogoda" in result.stdout
|
||||
|
||||
def test_unknown_command(self, capfd):
|
||||
"""Неизвестная команда завершается с кодом 1."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, ADMIN_SCRIPT, "unknown"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert "unknown" in result.stdout.lower()
|
||||
|
||||
|
||||
class TestAdminImport:
|
||||
"""Тесты импорта модуля."""
|
||||
|
||||
def test_admin_module_imports(self):
|
||||
"""Модуль admin импортируется без ошибок."""
|
||||
from console_commands import admin
|
||||
|
||||
assert hasattr(admin, "main")
|
||||
assert hasattr(admin, "COMMANDS")
|
||||
assert len(admin.COMMANDS) == 5
|
||||
|
||||
def test_commands_mapping(self):
|
||||
"""Все команды зарегистрированы в маппинге."""
|
||||
from console_commands.admin import COMMANDS
|
||||
|
||||
expected = {"pogoda", "news", "cat", "morning", "help"}
|
||||
assert set(COMMANDS.keys()) == expected
|
||||
Loading…
x
Reference in New Issue
Block a user