Compare commits
No commits in common. "311cfa75132a2905b0345287bdcecad4b94f43b5" and "1a6b7043addf80f6ada88e17ad9ebf635b02d470" have entirely different histories.
311cfa7513
...
1a6b7043ad
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user