Compare commits
3 Commits
1a6b7043ad
...
311cfa7513
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
311cfa7513 | ||
|
|
5e01cc6d8f | ||
|
|
ecff77ef66 |
@ -10,4 +10,5 @@ YANDEX_WEATHER_API_RATE=1
|
|||||||
YANDEX_WEATHER_API_BURST=3
|
YANDEX_WEATHER_API_BURST=3
|
||||||
HABR_RSS_RATE=1
|
HABR_RSS_RATE=1
|
||||||
HABR_RSS_BURST=2
|
HABR_RSS_BURST=2
|
||||||
|
WEATHER_CITY=Магнитогорск
|
||||||
WEATHER_CACHE_TTL=3600
|
WEATHER_CACHE_TTL=3600
|
||||||
|
|||||||
@ -16,7 +16,6 @@ RUN pip install --no-cache-dir -r requirements.txt && \
|
|||||||
|
|
||||||
# Копируем только нужные файлы (оптимизация размера образа)
|
# Копируем только нужные файлы (оптимизация размера образа)
|
||||||
COPY bot.py .
|
COPY bot.py .
|
||||||
COPY conftest.py .
|
|
||||||
COPY commands/ commands/
|
COPY commands/ commands/
|
||||||
COPY utils/ utils/
|
COPY utils/ utils/
|
||||||
# .env передаётся через docker-compose environment:
|
# .env передаётся через docker-compose environment:
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
- [x] **`pressure_to_mmhg(mb: Any)`** — заменён на `float | int | str | None` (`utils/pogoda.py`)
|
- [x] **`pressure_to_mmhg(mb: Any)`** — заменён на `float | int | str | None` (`utils/pogoda.py`)
|
||||||
- [x] **`conftest.py` и `bot.py` дублируют monkey-patch** — вынесен в `utils/compat.py`, оба файла импортируют оттуда
|
- [x] **`conftest.py` и `bot.py` дублируют monkey-patch** — вынесен в `utils/compat.py`, оба файла импортируют оттуда
|
||||||
- [x] **`RateLimiter` тесты зависят от реального времени** — добавлен `_time_func` параметр, все тесты используют контролируемую функцию времени
|
- [x] **`RateLimiter` тесты зависят от реального времени** — добавлен `_time_func` параметр, все тесты используют контролируемую функцию времени
|
||||||
- [x] **`Dockerfile` не копирует `conftest.py`** — добавлен `COPY conftest.py .`
|
- [x] ~~**`Dockerfile` не копирует `conftest.py`**~~ — `conftest.py` убран из Dockerfile (тестовый файл не нужен в production)
|
||||||
- [x] **`requirements.txt` без пиннинга версий** — `>=` заменён на `~=` (compatible release)
|
- [x] **`requirements.txt` без пиннинга версий** — `>=` заменён на `~=` (compatible release)
|
||||||
- [x] **Закомментированный тест `pressure_to_mmhg(0)`** — удалена устаревшая закомментированная строка
|
- [x] **Закомментированный тест `pressure_to_mmhg(0)`** — удалена устаревшая закомментированная строка
|
||||||
- [x] **`format_articles(None)` бросает `TypeError`** — добавлена валидация `None`, graceful fallback с сообщением
|
- [x] **`format_articles(None)` бросает `TypeError`** — добавлена валидация `None`, graceful fallback с сообщением
|
||||||
@ -81,9 +81,9 @@
|
|||||||
|
|
||||||
- [ ] **Отсутствует `pyproject.toml`** — проект использует `requirements.txt` + `requirements-dev.txt` без единого файла конфигурации. Рекомендуется `pyproject.toml` с `[project]`, настройками ruff и pytest
|
- [ ] **Отсутствует `pyproject.toml`** — проект использует `requirements.txt` + `requirements-dev.txt` без единого файла конфигурации. Рекомендуется `pyproject.toml` с `[project]`, настройками ruff и pytest
|
||||||
|
|
||||||
- [ ] **`commands/news.py` — дублирование кода для статей и постов** — блоки обработки `articles` и `posts` почти идентичны (проверка `None`, форматирование, fallback). Вынести в вспомогательную функцию
|
- [x] ~~**`commands/news.py` — дублирование кода для статей и постов**~~ — вынесено в `_format_feed_section()` (`commands/news.py`)
|
||||||
|
|
||||||
- [ ] **Город захардкожен в `API_URL_WEATHER`** — `Magnitogorsk` в URL `wttr.in`. При необходимости поддержки других городов потребуется рефакторинг. Минимум — задокументировать ограничение и вынести в env-переменную `WEATHER_CITY`
|
- [x] ~~**Город захардкожен в `API_URL_WEATHER`**~~ — вынесен в `WEATHER_CITY` env-переменную, шаблон `"Погода: {city}:"` (`utils/pogoda.py`, `.env.example`)
|
||||||
|
|
||||||
- [ ] **`Scheduler._task` без `add_done_callback`** — если `_scheduler_loop` завершится unexpectedly (не через `CancelledError`), это не будет залогировано. Добавить callback для логирования нештатного завершения task
|
- [ ] **`Scheduler._task` без `add_done_callback`** — если `_scheduler_loop` завершится unexpectedly (не через `CancelledError`), это не будет залогировано. Добавить callback для логирования нештатного завершения task
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from utils.news import (
|
from utils.news import (
|
||||||
fetch_rss,
|
fetch_rss,
|
||||||
@ -12,6 +14,29 @@ from utils.news import (
|
|||||||
logger = logging.getLogger(__name__)
|
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):
|
class News(commands.Cog):
|
||||||
"""Команда !news — свежие статьи и новости по AI с Habr"""
|
"""Команда !news — свежие статьи и новости по AI с Habr"""
|
||||||
|
|
||||||
@ -23,41 +48,28 @@ class News(commands.Cog):
|
|||||||
fetch_rss(RSS_URL_POSTS),
|
fetch_rss(RSS_URL_POSTS),
|
||||||
)
|
)
|
||||||
|
|
||||||
parts: list[str] = []
|
parts: list[str] = [
|
||||||
|
_format_feed_section(
|
||||||
# --- Статьи ---
|
|
||||||
if articles is None:
|
|
||||||
logger.warning(
|
|
||||||
"%s: !nw — не удалось получить статьи (API вернул None)", ctx.author
|
|
||||||
)
|
|
||||||
parts.append("Не удалось получить статьи.")
|
|
||||||
elif articles:
|
|
||||||
articles_text = format_articles(
|
|
||||||
articles,
|
articles,
|
||||||
|
str(ctx.author),
|
||||||
|
"статьи",
|
||||||
|
"статей",
|
||||||
|
"Не удалось получить статьи.",
|
||||||
|
"Статей пока нет.",
|
||||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
|
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
|
||||||
)
|
),
|
||||||
parts.append("\n".join(articles_text))
|
_format_feed_section(
|
||||||
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,
|
posts,
|
||||||
|
str(ctx.author),
|
||||||
|
"посты",
|
||||||
|
"постов",
|
||||||
|
"Не удалось получить новости.",
|
||||||
|
"Новостей пока нет.",
|
||||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/",
|
"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))
|
message = truncate_message("\n".join(parts))
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|||||||
@ -15,5 +15,7 @@ services:
|
|||||||
- YANDEX_WEATHER_API_BURST=${YANDEX_WEATHER_API_BURST:-3}
|
- YANDEX_WEATHER_API_BURST=${YANDEX_WEATHER_API_BURST:-3}
|
||||||
- HABR_RSS_RATE=${HABR_RSS_RATE:-1}
|
- HABR_RSS_RATE=${HABR_RSS_RATE:-1}
|
||||||
- HABR_RSS_BURST=${HABR_RSS_BURST:-2}
|
- HABR_RSS_BURST=${HABR_RSS_BURST:-2}
|
||||||
|
- WEATHER_CITY=${WEATHER_CITY:-Магнитогорск}
|
||||||
|
- WEATHER_CACHE_TTL=${WEATHER_CACHE_TTL:-3600}
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- TZ=Asia/Yekaterinburg
|
- TZ=Asia/Yekaterinburg
|
||||||
|
|||||||
@ -170,7 +170,7 @@ class TestRunMorning:
|
|||||||
second_call = channel.send.call_args_list[1]
|
second_call = channel.send.call_args_list[1]
|
||||||
message_text = second_call[0][0]
|
message_text = second_call[0][0]
|
||||||
assert "Утренний дайджест" in message_text
|
assert "Утренний дайджест" in message_text
|
||||||
assert "Погода в Магнитогорске" in message_text
|
assert "Погода: Магнитогорск" in message_text
|
||||||
|
|
||||||
|
|
||||||
class TestRunMorningWithFallback:
|
class TestRunMorningWithFallback:
|
||||||
@ -242,5 +242,5 @@ class TestRunMorningWithFallback:
|
|||||||
message_text = call_args[0][0]
|
message_text = call_args[0][0]
|
||||||
|
|
||||||
# Проверяем, что в тексте есть погода и нет fallback сообщения
|
# Проверяем, что в тексте есть погода и нет fallback сообщения
|
||||||
assert "Погода в Магнитогорске" in message_text
|
assert "Погода: Магнитогорск" in message_text
|
||||||
assert "Не удалось получить данные из внешних источников" not in message_text
|
assert "Не удалось получить данные из внешних источников" not in message_text
|
||||||
|
|||||||
@ -20,7 +20,10 @@ from utils.rate_limiter import yandex_weather_limiter
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Координаты Магнитогорска
|
# Город для погоды (переопределяется WEATHER_CITY)
|
||||||
|
_WEATHER_CITY: str = os.getenv("WEATHER_CITY", "Магнитогорск")
|
||||||
|
|
||||||
|
# Координаты города
|
||||||
_LATITUDE: float = 53.40716
|
_LATITUDE: float = 53.40716
|
||||||
_LONGITUDE: float = 58.980289
|
_LONGITUDE: float = 58.980289
|
||||||
|
|
||||||
@ -281,7 +284,7 @@ def format_weather_for_message(data: Optional[dict]) -> Optional[str]:
|
|||||||
lines = format_weather_data_for_console(data)
|
lines = format_weather_data_for_console(data)
|
||||||
if not lines:
|
if not lines:
|
||||||
return None
|
return None
|
||||||
return "Погода в Магнитогорске:\n" + "\n".join(lines)
|
return f"Погода: {_WEATHER_CITY}:\n" + "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def pressure_to_mmhg(mb: float | int | str | None) -> float | str:
|
def pressure_to_mmhg(mb: float | int | str | None) -> float | str:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user