From 12ed515cca6189637a7064c13e85ee2522347c79 Mon Sep 17 00:00:00 2001 From: deadzilla Date: Fri, 12 Jun 2026 16:06:35 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20admin.py=20=D0=B4=D0=BB=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BC=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20doc?= =?UTF-8?q?ker=20exec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен console_commands/admin.py — CLI-скрипт с командами pogoda, news, cat, morning, help - Добавлен docstring к методу pg (отображается в !hp) - Обновлён README.md: архитектура, администрирование в Docker, тесты - Добавлены тесты test_admin.py (5 тестов) - Итого тестов: 223 --- README.md | 22 ++++- commands/pg.py | 1 + console_commands/admin.py | 198 ++++++++++++++++++++++++++++++++++++++ tests/test_admin.py | 65 +++++++++++++ 4 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 console_commands/admin.py create mode 100644 tests/test_admin.py diff --git a/README.md b/README.md index 157bcc4..664fdc1 100644 --- a/README.md +++ b/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) diff --git a/commands/pg.py b/commands/pg.py index ac6ebf2..25ca7ec 100644 --- a/commands/pg.py +++ b/commands/pg.py @@ -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("Не удалось получить данные о погоде.") diff --git a/console_commands/admin.py b/console_commands/admin.py new file mode 100644 index 0000000..d2ecf59 --- /dev/null +++ b/console_commands/admin.py @@ -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() diff --git a/tests/test_admin.py b/tests/test_admin.py new file mode 100644 index 0000000..5033d25 --- /dev/null +++ b/tests/test_admin.py @@ -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