Compare commits
No commits in common. "974c23648922cb2fa72b1f9c58c23894f1bd6861" and "089a77ce1705b4fc2d4c99cb5f7cda09d553ab99" have entirely different histories.
974c236489
...
089a77ce17
@ -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/*
|
||||||
|
|
||||||
|
|||||||
14
ISSUES.md
14
ISSUES.md
@ -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 байт). Удалить из рабочей директории
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
16
README.md
16
README.md
@ -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
9
bot.py
@ -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 ""
|
||||||
|
|||||||
@ -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)
|
||||||
except Exception:
|
|
||||||
logger.error("%s: !morning ошибка", ctx.author, exc_info=True)
|
|
||||||
await ctx.send("Ошибка при выполнении команды. Попробуйте позже.")
|
|
||||||
else:
|
|
||||||
logger.info("%s: !morning завершен", ctx.author)
|
logger.info("%s: !morning завершен", ctx.author)
|
||||||
|
|||||||
@ -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
|
[
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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"
|
|
||||||
@ -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")
|
||||||
|
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
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user