Compare commits

..

No commits in common. "311cfa75132a2905b0345287bdcecad4b94f43b5" and "1a6b7043addf80f6ada88e17ad9ebf635b02d470" have entirely different histories.

7 changed files with 37 additions and 54 deletions

View File

@ -10,5 +10,4 @@ YANDEX_WEATHER_API_RATE=1
YANDEX_WEATHER_API_BURST=3
HABR_RSS_RATE=1
HABR_RSS_BURST=2
WEATHER_CITY=Магнитогорск
WEATHER_CACHE_TTL=3600

View File

@ -16,6 +16,7 @@ RUN pip install --no-cache-dir -r requirements.txt && \
# Копируем только нужные файлы (оптимизация размера образа)
COPY bot.py .
COPY conftest.py .
COPY commands/ commands/
COPY utils/ utils/
# .env передаётся через docker-compose environment:

View File

@ -35,7 +35,7 @@
- [x] **`pressure_to_mmhg(mb: Any)`** — заменён на `float | int | str | None` (`utils/pogoda.py`)
- [x] **`conftest.py` и `bot.py` дублируют monkey-patch** — вынесен в `utils/compat.py`, оба файла импортируют оттуда
- [x] **`RateLimiter` тесты зависят от реального времени** — добавлен `_time_func` параметр, все тесты используют контролируемую функцию времени
- [x] ~~**`Dockerfile` не копирует `conftest.py`**~~`conftest.py` убран из Dockerfile (тестовый файл не нужен в production)
- [x] **`Dockerfile` не копирует `conftest.py`** — добавлен `COPY conftest.py .`
- [x] **`requirements.txt` без пиннинга версий** — `>=` заменён на `~=` (compatible release)
- [x] **Закомментированный тест `pressure_to_mmhg(0)`** — удалена устаревшая закомментированная строка
- [x] **`format_articles(None)` бросает `TypeError`** — добавлена валидация `None`, graceful fallback с сообщением
@ -81,9 +81,9 @@
- [ ] **Отсутствует `pyproject.toml`** — проект использует `requirements.txt` + `requirements-dev.txt` без единого файла конфигурации. Рекомендуется `pyproject.toml` с `[project]`, настройками ruff и pytest
- [x] ~~**`commands/news.py` — дублирование кода для статей и постов**~~ — вынесено в `_format_feed_section()` (`commands/news.py`)
- [ ] **`commands/news.py` — дублирование кода для статей и постов** — блоки обработки `articles` и `posts` почти идентичны (проверка `None`, форматирование, fallback). Вынести в вспомогательную функцию
- [x] ~~**Город захардкожен в `API_URL_WEATHER`**~~ — вынесен в `WEATHER_CITY` env-переменную, шаблон `"Погода: {city}:"` (`utils/pogoda.py`, `.env.example`)
- [ ] **Город захардкожен в `API_URL_WEATHER`**`Magnitogorsk` в URL `wttr.in`. При необходимости поддержки других городов потребуется рефакторинг. Минимум — задокументировать ограничение и вынести в env-переменную `WEATHER_CITY`
- [ ] **`Scheduler._task` без `add_done_callback`** — если `_scheduler_loop` завершится unexpectedly (не через `CancelledError`), это не будет залогировано. Добавить callback для логирования нештатного завершения task

View File

@ -1,7 +1,5 @@
import asyncio
import logging
from typing import Optional
from discord.ext import commands
from utils.news import (
fetch_rss,
@ -14,29 +12,6 @@ from utils.news import (
logger = logging.getLogger(__name__)
def _format_feed_section(
data: Optional[list[dict]],
author: str,
label_warning: str,
label_info: str,
msg_error: str,
msg_empty: str,
title: str,
link: str,
) -> str:
"""Форматировать один RSS-раздел (статьи или посты)."""
if data is None:
logger.warning(
"%s: !nw — не удалось получить %s (API вернул None)", author, label_warning
)
return msg_error
elif data:
return "\n".join(format_articles(data, title, link))
else:
logger.info("%s: !nw — %s нет в RSS", author, label_info)
return msg_empty
class News(commands.Cog):
"""Команда !news — свежие статьи и новости по AI с Habr"""
@ -48,28 +23,41 @@ class News(commands.Cog):
fetch_rss(RSS_URL_POSTS),
)
parts: list[str] = [
_format_feed_section(
parts: list[str] = []
# --- Статьи ---
if articles is None:
logger.warning(
"%s: !nw — не удалось получить статьи (API вернул None)", ctx.author
)
parts.append("Не удалось получить статьи.")
elif articles:
articles_text = format_articles(
articles,
str(ctx.author),
"статьи",
"статей",
"Не удалось получить статьи.",
"Статей пока нет.",
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
),
_format_feed_section(
)
parts.append("\n".join(articles_text))
else:
logger.info("%s: !nw — статей нет в RSS", ctx.author)
parts.append("Статей пока нет.")
# --- Посты ---
if posts is None:
logger.warning(
"%s: !nw — не удалось получить посты (API вернул None)", ctx.author
)
parts.append("\nНе удалось получить новости.")
elif posts:
posts_text = format_articles(
posts,
str(ctx.author),
"посты",
"постов",
"Не удалось получить новости.",
"Новостей пока нет.",
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/",
),
]
)
parts.append("\n" + "\n".join(posts_text))
else:
logger.info("%s: !nw — постов нет в RSS", ctx.author)
parts.append("\nНовостей пока нет.")
message = truncate_message("\n".join(parts))
await ctx.send(message)

View File

@ -15,7 +15,5 @@ services:
- YANDEX_WEATHER_API_BURST=${YANDEX_WEATHER_API_BURST:-3}
- HABR_RSS_RATE=${HABR_RSS_RATE:-1}
- HABR_RSS_BURST=${HABR_RSS_BURST:-2}
- WEATHER_CITY=${WEATHER_CITY:-Магнитогорск}
- WEATHER_CACHE_TTL=${WEATHER_CACHE_TTL:-3600}
- PYTHONUNBUFFERED=1
- TZ=Asia/Yekaterinburg

View File

@ -170,7 +170,7 @@ class TestRunMorning:
second_call = channel.send.call_args_list[1]
message_text = second_call[0][0]
assert "Утренний дайджест" in message_text
assert "Погода: Магнитогорск" in message_text
assert "Погода в Магнитогорске" in message_text
class TestRunMorningWithFallback:
@ -242,5 +242,5 @@ class TestRunMorningWithFallback:
message_text = call_args[0][0]
# Проверяем, что в тексте есть погода и нет fallback сообщения
assert "Погода: Магнитогорск" in message_text
assert "Погода в Магнитогорске" in message_text
assert "Не удалось получить данные из внешних источников" not in message_text

View File

@ -20,10 +20,7 @@ from utils.rate_limiter import yandex_weather_limiter
logger = logging.getLogger(__name__)
# Город для погоды (переопределяется WEATHER_CITY)
_WEATHER_CITY: str = os.getenv("WEATHER_CITY", "Магнитогорск")
# Координаты города
# Координаты Магнитогорска
_LATITUDE: float = 53.40716
_LONGITUDE: float = 58.980289
@ -284,7 +281,7 @@ def format_weather_for_message(data: Optional[dict]) -> Optional[str]:
lines = format_weather_data_for_console(data)
if not lines:
return None
return f"Погода: {_WEATHER_CITY}:\n" + "\n".join(lines)
return "Погода в Магнитогорске:\n" + "\n".join(lines)
def pressure_to_mmhg(mb: float | int | str | None) -> float | str: