Compare commits

..

8 Commits

Author SHA1 Message Date
deadzilla
974c236489 docs: актуализировать README.md под pyproject.toml
Установка: pip install -r requirements.txt → pip install .
Архитектура: добавлен pyproject.toml в список файлов.
Зависимости: pyproject.toml указан как основной источник.
2026-07-22 22:42:44 +05:00
deadzilla
34f280e814 chore: закрыть задачу test_fetch_weather_http_error_no_fallback
Тест не найден в проекте и в git-истории — вероятно
не был реализован. Закрыто как неактуальное.
2026-07-22 22:39:11 +05:00
deadzilla
0c404c8837 chore: удалить артефакт nul и обновить ISSUES.md
Файл nul — результат Windows-команды dir > nul.
Уже был в .gitignore, не был в трекке — просто удалён.
ISSUES.md обновлён: все актуальные задачи закрыты.
2026-07-22 22:37:28 +05:00
deadzilla
78106e7b4c fix: добавить обработку ошибок в commands/morning.py
Если run_morning выбрасывает исключение, ошибка теперь
перехватывается: логируется с exc_info, пользователю
отправлено сообщение об ошибке. Тест обновлён — проверяется
что исключение не пробрасывается наружу.
2026-07-22 22:36:55 +05:00
deadzilla
0f944eafe7 refactor: убрать дублирование в TextHelpCommand.send_bot_help()
Ветки if/else по cog_or_none выполняли идентичную логику.
Объединены в один цикл — standalone-команды и команды из cog
обрабатываются одинаково.
2026-07-22 22:32:50 +05:00
deadzilla
4f2e3ec05b fix: заменить list comprehension на generator в stats.py
len([ch for ch in ...]) создавал временный список.
Заменено на sum(1 for ch in ...) — без выделения памяти
под промежуточный список.
2026-07-22 22:29:38 +05:00
deadzilla
8b59ddb0c9 fix: добавить add_done_callback в Scheduler._task
Если _scheduler_loop завершится с неожиданной ошибкой
(не CancelledError), это теперь залогировано через
_on_task_done callback. Штатное завершение через cancel()
пропускается без логов.
2026-07-22 22:28:13 +05:00
deadzilla
81a99aaeab feat: добавить pyproject.toml
Объединены зависимости из requirements.txt и requirements-dev.txt,
настройки pytest из pytest.ini, и базовая конфигурация ruff.
Dockerfile обновлён: установка зависимостей через pyproject.toml.
requirements.txt/pytest.ini сохранены для обратной совместимости.
2026-07-22 22:25:43 +05:00
9 changed files with 78 additions and 31 deletions

View File

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

View File

@ -5,7 +5,13 @@ Discord-бот для Магнитогорска. Команды погоды,
## Установка ## Установка
```bash ```bash
pip install -r requirements.txt pip install .
```
Или с dev-зависимостями:
```bash
pip install -e ".[dev]"
``` ```
## Запуск ## Запуск
@ -89,6 +95,7 @@ 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 команды
@ -243,7 +250,10 @@ DISCORD_TOKEN=ваш_токен docker-compose up
## Зависимости ## Зависимости
### Production (`requirements.txt`) Зависимости объявлены в `pyproject.toml` (`[project].dependencies` и `[project.optional-dependencies].dev`).
Файлы `requirements.txt` и `requirements-dev.txt` сохранены для обратной совместимости.
### Production
```txt ```txt
discord.py~=2.7.1 discord.py~=2.7.1
@ -252,7 +262,7 @@ requests~=2.34.2
defusedxml~=0.7.1 defusedxml~=0.7.1
``` ```
### Development (`requirements-dev.txt`) ### Development
```txt ```txt
pre-commit>=3.5.0 pre-commit>=3.5.0

9
bot.py
View File

@ -39,14 +39,7 @@ 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,5 +13,10 @@ 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)
await run_morning(ctx.bot, ctx.channel) try:
logger.info("%s: !morning завершен", ctx.author) 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,12 +14,10 @@ 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(
len( sum(
[ 1
ch for ch in guild.channels
for ch in guild.channels if not isinstance(ch, discord.CategoryChannel)
if not isinstance(ch, discord.CategoryChannel)
]
) )
for guild in guilds for guild in guilds
) )

25
pyproject.toml Normal file
View File

@ -0,0 +1,25 @@
[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,5 +80,8 @@ 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")
with pytest.raises(Exception, match="api error"): await cog.morning(cog, ctx)
await cog.morning(cog, ctx)
ctx.send.assert_awaited_once()
message = ctx.send.call_args[0][0]
assert "Ошибка" in message

View File

@ -157,8 +157,21 @@ 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()