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

9
bot.py
View File

@ -39,7 +39,14 @@ class TextHelpCommand(commands.HelpCommand):
) -> None:
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:
if not command.hidden:
desc = command.short_doc or ""

View File

@ -13,10 +13,5 @@ class Morning(commands.Cog):
async def morning(self, ctx: commands.Context) -> None:
"""Погода, лучшие статьи за сутки и котик"""
logger.info("%s: !morning запущен", ctx.author)
try:
await run_morning(ctx.bot, ctx.channel)
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
total_guilds = len(guilds)
total_channels = sum(
sum(
1
len(
[
ch
for ch in guild.channels
if not isinstance(ch, discord.CategoryChannel)
]
)
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
async def test_morning_run_morning_raises(self) -> None:
"""Ошибка в run_morning ловится, пользователю отправлено сообщение."""
"""Ошибка в run_morning должна пробрасываться."""
from commands.morning import Morning
cog = Morning()
@ -80,8 +80,5 @@ class TestMorningCommand:
with patch("commands.morning.run_morning", new_callable=AsyncMock) as mock_run:
mock_run.side_effect = Exception("api error")
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
self._running = True
self._task = asyncio.create_task(self._scheduler_loop())
self._task.add_done_callback(self._on_task_done)
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):
if self._task and not self._task.done():
self._task.cancel()