refactor: убрал дублирование логики получения данных

- utils/pogoda.py: добавлена API_URL_WEATHER, format_weather_for_embed(),
  проверка None в format_weather_data_for_console()
- utils/morning_runner.py: вынесен MorningData (dataclass) и gather_morning();
  run_morning() использует их вместо ручного asyncio.gather
- utils/__init__.py: экспортирован публичный API (__all__)
- commands/pg.py: убран ручной парсинг погоды, используется
  format_weather_data_for_console()
- console_commands/morning.py: дубликат asyncio.gather заменён на gather_morning()
- console_commands/pogoda.py: хардкод URL заменён на API_URL_WEATHER
- console_commands/cat.py: заглушка заменена на рабочий вызов fetch_cat()
- tests/test_commands_pg.py: обновлён тест fetch_returns_none (бот теперь
  отправляет сообщение об ошибке вместо молчаливого возврата)
This commit is contained in:
deadzilla 2026-06-09 17:37:18 +05:00
parent 2188a7d3fd
commit 560dc558a9
8 changed files with 143 additions and 112 deletions

View File

@ -1,42 +1,23 @@
import discord
from discord.ext import commands
from utils.pogoda import fetch_weather, fetch_open_meteo, wmo_to_russian, translate_weather, pressure_to_mmhg
from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console
class Pg(commands.Cog):
"""Команда !pg — прогноз погоды для Магнитогорска"""
def __init__(self):
self.api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
self.api_url = API_URL_WEATHER
@commands.command(name="pg")
async def pg(self, ctx):
data = await fetch_weather(self.api_url)
if data is None:
return
current = data.get("current_condition", [{}])[0]
if not current:
await ctx.send("Не удалось получить данные о погоде.")
return
temp = current.get("temp_C", "")
feels_like = current.get("FeelsLikeC", "")
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", ""))
humidity = current.get("humidity", "")
wind_kmh = current.get("windspeedKmph", "")
try:
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "" else ""
except (ValueError, TypeError):
wind = ""
pressure_mb = current.get("pressure", "")
formatted = format_weather_data_for_console(data)
if not formatted:
await ctx.send("Не удалось получить данные о погоде.")
return
pressure_mm = pressure_to_mmhg(pressure_mb)
await ctx.send(
f"Температура: {temp}°C (ощущается как {feels_like}°C)\n"
f"Описание: {description}\n"
f"Влажность: {humidity}%\n"
f"Ветер: {wind} м/с\n"
f"Давление: {pressure_mm} мм рт. ст."
)
await ctx.send("\n".join(formatted))

View File

@ -1,3 +1,10 @@
def cat(stop_event, bot):
"""Заглушка: тут должен быть котик"""
print("Заглушка: тут должен быть котик")
from utils.cat import fetch_cat
async def cat(stop_event, bot):
"""Вывести URL случайного котика"""
url = await fetch_cat()
if url is None:
print("Не удалось получить котика.")
return
print(f"Котик: {url}")

View File

@ -1,50 +1,40 @@
import asyncio
from utils.pogoda import fetch_weather, format_weather_data_for_console
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
from utils.cat import fetch_cat
from utils.morning_runner import gather_morning
from utils.news import RSS_URL_ARTICLES, RSS_URL_POSTS, format_articles
from utils.pogoda import format_weather_data_for_console
async def morning(stop_event, bot):
"""Вывести погоду, лучшие статьи за сутки и котика"""
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
# Параллельный запрос погоды, новостей и котика
weather_data, articles, posts, cat_url = await asyncio.gather(
fetch_weather(api_url),
fetch_rss(RSS_URL_ARTICLES),
fetch_rss(RSS_URL_POSTS),
fetch_cat(),
)
"""Вывести погоду, лучшие статьи за сутки и котик"""
data = await gather_morning()
print("Доброе утро!\n")
# --- Котик ---
if cat_url:
print(f"Котик: {cat_url}\n")
if data.cat_url:
print(f"Котик: {data.cat_url}\n")
else:
print("Котика получить не удалось.\n")
# --- Погода ---
if weather_data is not None:
formatted = format_weather_data_for_console(weather_data)
if formatted:
print(f"**Погода в Магнитогорске:**")
for line in formatted:
print(line)
else:
print("Не удалось получить данные о погоде.")
formatted = format_weather_data_for_console(data.weather)
if formatted:
print("**Погода в Магнитогорске:**")
for line in formatted:
print(line)
else:
print("Не удалось получить данные о погоде.")
print()
# --- Новости: статьи ---
if articles is not None:
if articles:
lines = format_articles(articles,
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
if data.articles is not None:
if data.articles:
lines = format_articles(
data.articles,
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
)
print("\n".join(lines))
else:
print("Новостей пока нет.")
@ -54,11 +44,13 @@ async def morning(stop_event, bot):
print()
# --- Новости: посты ---
if posts is not None:
if posts:
lines = format_articles(posts,
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
if data.posts is not None:
if data.posts:
lines = format_articles(
data.posts,
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/",
)
print("\n".join(lines))
else:
print("Новостей пока нет.")

View File

@ -1,10 +1,9 @@
from utils.pogoda import fetch_weather, format_weather_data_for_console
from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console
async def pogoda(stop_event, bot):
"""Вывести прогноз погоды для Магнитогорска"""
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
data = await fetch_weather(api_url)
data = await fetch_weather(API_URL_WEATHER)
if data is None:
print("Не удалось получить данные о погоде.")

View File

@ -62,14 +62,14 @@ class TestPgCommand:
@pytest.mark.asyncio
async def test_pg_fetch_returns_none(self):
"""fetch_weather вернул None — бот должен ничего не отправить."""
"""fetch_weather вернул None — бот должен сообщить об ошибке."""
cog = self._make_cog()
ctx = self._make_ctx()
with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=None)):
await cog.pg.callback(cog, ctx)
ctx.send.assert_not_called()
ctx.send.assert_called_once_with("Не удалось получить данные о погоде.")
@pytest.mark.asyncio
async def test_pg_empty_current_condition(self):

View File

@ -1 +1,38 @@
from .pogoda import (
API_URL_WEATHER,
fetch_weather,
fetch_open_meteo,
format_weather_data_for_console,
format_weather_for_embed,
pressure_to_mmhg,
translate_weather,
wmo_to_russian,
)
from .news import (
RSS_URL_ARTICLES,
RSS_URL_POSTS,
fetch_rss,
format_articles,
truncate_title,
)
from .cat import fetch_cat
__all__ = [
# Погода
"API_URL_WEATHER",
"fetch_weather",
"fetch_open_meteo",
"format_weather_data_for_console",
"format_weather_for_embed",
"pressure_to_mmhg",
"translate_weather",
"wmo_to_russian",
# Новости
"RSS_URL_ARTICLES",
"RSS_URL_POSTS",
"fetch_rss",
"format_articles",
"truncate_title",
# Котики
"fetch_cat",
]

View File

@ -3,79 +3,80 @@
import asyncio
import logging
import os
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional
import discord
from discord.ext import commands
from discord.ext import tasks
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
from utils.pogoda import (
API_URL_WEATHER,
fetch_weather,
format_weather_for_embed,
)
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
from utils.cat import fetch_cat
logger = logging.getLogger(__name__)
@dataclass
class MorningData:
"""Собранные данные для утреннего дайджеста."""
weather: Optional[dict]
articles: Optional[list]
posts: Optional[list]
cat_url: Optional[str]
async def gather_morning() -> MorningData:
"""Собрать все данные для утреннего дайджеста параллельно."""
weather_data, articles, posts, cat_url = await asyncio.gather(
fetch_weather(API_URL_WEATHER),
fetch_rss(RSS_URL_ARTICLES),
fetch_rss(RSS_URL_POSTS),
fetch_cat(),
)
return MorningData(
weather=weather_data,
articles=articles,
posts=posts,
cat_url=cat_url,
)
async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
"""Выполнить утренний дайджест и отправить в канал."""
try:
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
# Параллельный запрос погоды, новостей и котика
weather_data, articles, posts, cat_url = await asyncio.gather(
fetch_weather(api_url),
fetch_rss(RSS_URL_ARTICLES),
fetch_rss(RSS_URL_POSTS),
fetch_cat(),
)
data = await gather_morning()
# --- Формируем embed ---
embed = discord.Embed(title="🌅 Утренний дайджест!", color=0xF4A460)
# Котик как thumbnail
if cat_url:
embed.set_thumbnail(url=cat_url)
if data.cat_url:
embed.set_thumbnail(url=data.cat_url)
description_lines = []
has_real_data = False
# --- Погода ---
if weather_data is not None:
current = weather_data.get("current_condition", [{}])[0]
if current:
has_real_data = True
temp = current.get("temp_C", "")
feels_like = current.get("FeelsLikeC", "")
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", ""))
humidity = current.get("humidity", "")
wind_kmh = current.get("windspeedKmph", "")
try:
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "" else ""
except (ValueError, TypeError):
wind = ""
pressure_mb = current.get("pressure", "")
pressure_mm = pressure_to_mmhg(pressure_mb)
description_lines.append(
f"**Погода в Магнитогорске:**\n"
f"Температура: {temp}°C (ощущается как {feels_like}°C)\n"
f"Описание: {description}\n"
f"Влажность: {humidity}%\n"
f"Ветер: {wind} м/с\n"
f"Давление: {pressure_mm} мм рт. ст."
)
else:
description_lines.append("Не удалось получить данные о погоде.")
weather_text = format_weather_for_embed(data.weather)
if weather_text:
has_real_data = True
description_lines.append(weather_text)
else:
description_lines.append("Не удалось получить данные о погоде.")
description_lines.append("")
# --- Новости: статьи ---
if articles is not None:
if articles:
if data.articles is not None:
if data.articles:
has_real_data = True
lines = format_articles(articles,
lines = format_articles(data.articles,
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
description_lines.append("\n".join(lines))
@ -87,10 +88,10 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
description_lines.append("")
# --- Новости: посты ---
if posts is not None:
if posts:
if data.posts is not None:
if data.posts:
has_real_data = True
lines = format_articles(posts,
lines = format_articles(data.posts,
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
description_lines.append("\n".join(lines))

View File

@ -2,6 +2,8 @@ import asyncio
import requests
from requests.exceptions import ConnectionError, Timeout, SSLError
API_URL_WEATHER = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
_session = requests.Session()
@ -143,6 +145,8 @@ def format_weather_data_for_console(data):
:param data: Ответ от API (dict)
:return: Строки с отформатированной погодой
"""
if data is None:
return None
current = data.get("current_condition", [{}])[0]
if not current:
return None
@ -168,6 +172,16 @@ def format_weather_data_for_console(data):
]
def format_weather_for_embed(data):
"""Форматировать погоду для Discord embed (с заголовком)."""
if data is None:
return None
lines = format_weather_data_for_console(data)
if not lines:
return None
return "**Погода в Магнитогорске:**\n" + "\n".join(lines)
def pressure_to_mmhg(mb):
if mb == "" or not mb:
return ""