Compare commits

..

No commits in common. "974c23648922cb2fa72b1f9c58c23894f1bd6861" and "089a77ce1705b4fc2d4c99cb5f7cda09d553ab99" have entirely different histories.

9 changed files with 31 additions and 78 deletions

View File

@ -9,8 +9,8 @@ WORKDIR /app
RUN mkdir -p logs RUN mkdir -p logs
# Устанавливаем зависимости и утилиту ps для healthcheck # Устанавливаем зависимости и утилиту ps для healthcheck
COPY pyproject.toml . COPY requirements.txt .
RUN pip install --no-cache-dir . && \ RUN pip install --no-cache-dir -r requirements.txt && \
apt-get update && apt-get install -y --no-install-recommends tzdata procps && \ apt-get update && apt-get install -y --no-install-recommends tzdata procps && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@ -6,21 +6,21 @@
### Средний приоритет ### Средний приоритет
- [x] ~~Отсутствует `pyproject.toml`~~ — создан `pyproject.toml`, `Dockerfile` обновлён (`81a99aa`) - [ ] **Отсутствует `pyproject.toml`** — проект использует `requirements.txt` + `requirements-dev.txt` без единого файла конфигурации. Рекомендуется `pyproject.toml` с `[project]`, настройками ruff и pytest
- [x] ~~`Scheduler._task` без `add_done_callback`~~ — добавлен `_on_task_done` callback (`8b59ddb`) - [ ] **`Scheduler._task` без `add_done_callback`** — если `_scheduler_loop` завершится unexpectedly (не через `CancelledError`), это не будет залогировано. Добавить callback для логирования нештатного завершения task
- [x] ~~Тест `test_fetch_weather_http_error_no_fallback`~~ — тест не найден в проекте, вероятно не был реализован. Закрыто как неактуальное. - [ ] **Тест `test_fetch_weather_http_error_no_fallback` — raises bare `Exception` вместо `requests.HTTPError`** — в реальности `raise_for_status()` бросает `requests.HTTPError`, который ловится `except requests.RequestException` и корректно переходит на fallback. Тест проверяет несуществующий сценарий
### Низкий приоритет ### Низкий приоритет
- [x] ~~`commands/stats.py` — list comprehension вместо generator~~ — заменено на `sum(1 for ...)` (`4f2e3ec`) - [ ] **`commands/stats.py` — list comprehension вместо generator** — `len([ch for ch in guild.channels if ...])` создаёт временный список. Заменить на `sum(1 for ch in guild.channels if ...)`
- [x] ~~`TextHelpCommand.send_bot_help()` — дублирование логики~~ — объединено в один цикл (`0f944ea`) - [ ] **`TextHelpCommand.send_bot_help()` — дублирование логики** — ветки `if cog_or_none is None` и `else` делают одинаковую работу по итерации команд. Объединить в общий цикл
- [x] ~~`commands/morning.py` не ловит исключения `run_morning`~~ — добавлен try/except + сообщение пользователю (`78106e7`) - [ ] **`commands/morning.py` не ловит исключения `run_morning`** — если `run_morning` выбросит, лог `!morning завершен` не запишется, а ошибка уйдёт в `on_command_error`. Добавить `try/except` для локальной обработки и логирования
- [x] ~~Файл `nul` в корне проекта~~ — удалён (уже был в .gitignore, коммит не нужен) - [ ] **Файл `nul` в корне проекта** — артефакт Windows (29 байт). Удалить из рабочей директории
--- ---

View File

@ -5,13 +5,7 @@ Discord-бот для Магнитогорска. Команды погоды,
## Установка ## Установка
```bash ```bash
pip install . pip install -r requirements.txt
```
Или с dev-зависимостями:
```bash
pip install -e ".[dev]"
``` ```
## Запуск ## Запуск
@ -95,7 +89,6 @@ docker-compose.yml # Запуск бота в Docker
.dockerignore # Исключения для Docker-контекста .dockerignore # Исключения для Docker-контекста
.gitignore # Исключения для Git (venv, .env, логи, IDE) .gitignore # Исключения для Git (venv, .env, логи, IDE)
.pre-commit-config.yaml # pre-commit хуки (ruff lint + ruff-format) .pre-commit-config.yaml # pre-commit хуки (ruff lint + ruff-format)
pyproject.toml # Единый файл конфигурации: зависимости, pytest, ruff
``` ```
### Добавление Discord команды ### Добавление Discord команды
@ -250,10 +243,7 @@ DISCORD_TOKEN=ваш_токен docker-compose up
## Зависимости ## Зависимости
Зависимости объявлены в `pyproject.toml` (`[project].dependencies` и `[project.optional-dependencies].dev`). ### Production (`requirements.txt`)
Файлы `requirements.txt` и `requirements-dev.txt` сохранены для обратной совместимости.
### Production
```txt ```txt
discord.py~=2.7.1 discord.py~=2.7.1
@ -262,7 +252,7 @@ requests~=2.34.2
defusedxml~=0.7.1 defusedxml~=0.7.1
``` ```
### Development ### Development (`requirements-dev.txt`)
```txt ```txt
pre-commit>=3.5.0 pre-commit>=3.5.0

9
bot.py
View File

@ -39,7 +39,14 @@ class TextHelpCommand(commands.HelpCommand):
) -> None: ) -> None:
lines: list[str] = ["Доступные команды:"] lines: list[str] = ["Доступные команды:"]
for _cog_or_none, cog_commands in mapping.items(): for cog_or_none, cog_commands in mapping.items():
if cog_or_none is None:
# Standalone-команды (без cog) — показываем если не hidden
for command in cog_commands:
if not command.hidden:
desc = command.short_doc or ""
lines.append(f" !{command.name} - {desc}")
continue
for command in cog_commands: for command in cog_commands:
if not command.hidden: if not command.hidden:
desc = command.short_doc or "" desc = command.short_doc or ""

View File

@ -13,10 +13,5 @@ class Morning(commands.Cog):
async def morning(self, ctx: commands.Context) -> None: async def morning(self, ctx: commands.Context) -> None:
"""Погода, лучшие статьи за сутки и котик""" """Погода, лучшие статьи за сутки и котик"""
logger.info("%s: !morning запущен", ctx.author) logger.info("%s: !morning запущен", ctx.author)
try: await run_morning(ctx.bot, ctx.channel)
await run_morning(ctx.bot, ctx.channel) logger.info("%s: !morning завершен", ctx.author)
except Exception:
logger.error("%s: !morning ошибка", ctx.author, exc_info=True)
await ctx.send("Ошибка при выполнении команды. Попробуйте позже.")
else:
logger.info("%s: !morning завершен", ctx.author)

View File

@ -14,10 +14,12 @@ class Stats(commands.Cog):
guilds = ctx.bot.guilds guilds = ctx.bot.guilds
total_guilds = len(guilds) total_guilds = len(guilds)
total_channels = sum( total_channels = sum(
sum( len(
1 [
for ch in guild.channels ch
if not isinstance(ch, discord.CategoryChannel) for ch in guild.channels
if not isinstance(ch, discord.CategoryChannel)
]
) )
for guild in guilds for guild in guilds
) )

View File

@ -1,25 +0,0 @@
[project]
name = "discord-bot"
version = "0.1.0"
description = "Discord бот с утренним дайджестом"
requires-python = ">=3.11"
dependencies = [
"discord.py~=2.7.1",
"python-dotenv~=1.2.2",
"requests~=2.34.2",
"defusedxml~=0.7.1",
]
[project.optional-dependencies]
dev = [
"pre-commit>=3.5.0",
"pytest>=7.4.0",
"pytest-asyncio>=0.21.0",
"ruff>=0.8.0",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
[tool.ruff]
target-version = "py311"

View File

@ -72,7 +72,7 @@ class TestMorningCommand:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_morning_run_morning_raises(self) -> None: async def test_morning_run_morning_raises(self) -> None:
"""Ошибка в run_morning ловится, пользователю отправлено сообщение.""" """Ошибка в run_morning должна пробрасываться."""
from commands.morning import Morning from commands.morning import Morning
cog = Morning() cog = Morning()
@ -80,8 +80,5 @@ class TestMorningCommand:
with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run: with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run:
mock_run.side_effect = Exception("api error") mock_run.side_effect = Exception("api error")
await cog.morning(cog, ctx) with pytest.raises(Exception, match="api error"):
await cog.morning(cog, ctx)
ctx.send.assert_awaited_once()
message = ctx.send.call_args[0][0]
assert "Ошибка" in message

View File

@ -157,21 +157,8 @@ class Scheduler:
return return
self._running = True self._running = True
self._task = asyncio.create_task(self._scheduler_loop()) self._task = asyncio.create_task(self._scheduler_loop())
self._task.add_done_callback(self._on_task_done)
logger.info("Планировщик запущен (время: %s)", self.morning_time) logger.info("Планировщик запущен (время: %s)", self.morning_time)
def _on_task_done(self, task: asyncio.Task) -> None:
"""Логировать нештатное завершение task.
CancelledError штатное завершение при stop(), поэтому пропускается.
Любое другое исключение признак ошибки в _scheduler_loop.
"""
if task.cancelled():
return
exc = task.exception()
if exc and not isinstance(exc, asyncio.CancelledError):
logger.error("Scheduler завершился с ошибкой: %s", exc)
def _stop_scheduler(self): def _stop_scheduler(self):
if self._task and not self._task.done(): if self._task and not self._task.done():
self._task.cancel() self._task.cancel()