Compare commits
8 Commits
089a77ce17
...
974c236489
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
974c236489 | ||
|
|
34f280e814 | ||
|
|
0c404c8837 | ||
|
|
78106e7b4c | ||
|
|
0f944eafe7 | ||
|
|
4f2e3ec05b | ||
|
|
8b59ddb0c9 | ||
|
|
81a99aaeab |
@ -9,8 +9,8 @@ WORKDIR /app
|
||||
RUN mkdir -p logs
|
||||
|
||||
# Устанавливаем зависимости и утилиту ps для healthcheck
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
COPY pyproject.toml .
|
||||
RUN pip install --no-cache-dir . && \
|
||||
apt-get update && apt-get install -y --no-install-recommends tzdata procps && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
14
ISSUES.md
14
ISSUES.md
@ -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, коммит не нужен)
|
||||
|
||||
---
|
||||
|
||||
|
||||
16
README.md
16
README.md
@ -5,7 +5,13 @@ Discord-бот для Магнитогорска. Команды погоды,
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
```
|
||||
|
||||
Или с dev-зависимостями:
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Запуск
|
||||
@ -89,6 +95,7 @@ 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 команды
|
||||
@ -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
|
||||
discord.py~=2.7.1
|
||||
@ -252,7 +262,7 @@ requests~=2.34.2
|
||||
defusedxml~=0.7.1
|
||||
```
|
||||
|
||||
### Development (`requirements-dev.txt`)
|
||||
### Development
|
||||
|
||||
```txt
|
||||
pre-commit>=3.5.0
|
||||
|
||||
9
bot.py
9
bot.py
@ -39,14 +39,7 @@ class TextHelpCommand(commands.HelpCommand):
|
||||
) -> None:
|
||||
lines: list[str] = ["Доступные команды:"]
|
||||
|
||||
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 _cog_or_none, cog_commands in mapping.items():
|
||||
for command in cog_commands:
|
||||
if not command.hidden:
|
||||
desc = command.short_doc or ""
|
||||
|
||||
@ -13,5 +13,10 @@ 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)
|
||||
|
||||
@ -14,12 +14,10 @@ class Stats(commands.Cog):
|
||||
guilds = ctx.bot.guilds
|
||||
total_guilds = len(guilds)
|
||||
total_channels = sum(
|
||||
len(
|
||||
[
|
||||
ch
|
||||
sum(
|
||||
1
|
||||
for ch in guild.channels
|
||||
if not isinstance(ch, discord.CategoryChannel)
|
||||
]
|
||||
)
|
||||
for guild in guilds
|
||||
)
|
||||
|
||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal 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"
|
||||
@ -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,5 +80,8 @@ 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
|
||||
|
||||
@ -157,8 +157,21 @@ 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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user