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)
|
help.py # !hp — список команд (автогенерация из bot.commands)
|
||||||
console_commands/ # Консольные команды
|
console_commands/ # Консольные команды
|
||||||
__init__.py # ALL_CONSOLE_COMMANDS — явные импорты
|
__init__.py # ALL_CONSOLE_COMMANDS — явные импорты
|
||||||
|
admin.py # admin — CLI для docker exec (pogoda, news, cat, morning, help)
|
||||||
stop.py # stop — остановка бота
|
stop.py # stop — остановка бота
|
||||||
news.py # news — новости с Habr
|
news.py # news — новости с Habr
|
||||||
pogoda.py # pogoda — погода в терминале
|
pogoda.py # pogoda — погода в терминале
|
||||||
@ -91,6 +92,8 @@ tests/ # pytest-тесты
|
|||||||
test_morning_runner.py# тесты morning runner-а
|
test_morning_runner.py# тесты morning runner-а
|
||||||
test_help_discord.py # команда !hp — проверка формата вывода и контента
|
test_help_discord.py # команда !hp — проверка формата вывода и контента
|
||||||
test_help_console.py # консольная help — проверка списка команд
|
test_help_console.py # консольная help — проверка списка команд
|
||||||
|
test_logger.py # setup_logging — уровни, обработчики, формат
|
||||||
|
test_admin.py # admin.py — CLI-скрипт для docker exec
|
||||||
ISSUES.md # Задачи и баг-трекер проекта
|
ISSUES.md # Задачи и баг-трекер проекта
|
||||||
pytest.ini # Конфигурация pytest (asyncio_mode = auto)
|
pytest.ini # Конфигурация pytest (asyncio_mode = auto)
|
||||||
Dockerfile # Сборка образа бота (Python 3.14-slim, healthcheck)
|
Dockerfile # Сборка образа бота (Python 3.14-slim, healthcheck)
|
||||||
@ -130,9 +133,11 @@ python -m pytest tests/ -v
|
|||||||
| `test_morning_runner.py` | morning runner-а | 68 |
|
| `test_morning_runner.py` | morning runner-а | 68 |
|
||||||
| `test_help_discord.py` | команда `!hp` | 2 |
|
| `test_help_discord.py` | команда `!hp` | 2 |
|
||||||
| `test_help_console.py` | консольная `help` | 2 |
|
| `test_help_console.py` | консольная `help` | 2 |
|
||||||
|
| `test_logger.py` | `setup_logging` (уровни, обработчики, формат) | 9 |
|
||||||
| `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 |
|
| `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 |
|
||||||
|
| `test_admin.py` | `admin.py` — CLI для docker exec | 5 |
|
||||||
|
|
||||||
**Итого: 209 тестов.**
|
**Итого: 223 теста.**
|
||||||
|
|
||||||
## Запуск в Docker
|
## Запуск в Docker
|
||||||
|
|
||||||
@ -155,6 +160,21 @@ DISCORD_TOKEN=ваш_токен docker-compose up
|
|||||||
- Консольный ввод отключён в Docker (stdin недоступен)
|
- Консольный ввод отключён в Docker (stdin недоступен)
|
||||||
- Версия Python настраивается через `ARG PYTHON_VERSION`
|
- Версия 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 и внешние сервисы
|
## API и внешние сервисы
|
||||||
|
|
||||||
### Погода (!pg, !morning)
|
### Погода (!pg, !morning)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class Pg(commands.Cog):
|
|||||||
|
|
||||||
@commands.command(name="pg")
|
@commands.command(name="pg")
|
||||||
async def pg(self, ctx):
|
async def pg(self, ctx):
|
||||||
|
"""Прогноз погоды в Магнитогорске"""
|
||||||
data = await fetch_weather(self.api_url)
|
data = await fetch_weather(self.api_url)
|
||||||
if data is None:
|
if data is None:
|
||||||
await ctx.send("Не удалось получить данные о погоде.")
|
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