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 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): class Pg(commands.Cog):
"""Команда !pg — прогноз погоды для Магнитогорска""" """Команда !pg — прогноз погоды для Магнитогорска"""
def __init__(self): def __init__(self):
self.api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru" self.api_url = API_URL_WEATHER
@commands.command(name="pg") @commands.command(name="pg")
async def pg(self, ctx): async def pg(self, ctx):
data = await fetch_weather(self.api_url) data = await fetch_weather(self.api_url)
if data is None: if data is None:
return
current = data.get("current_condition", [{}])[0]
if not current:
await ctx.send("Не удалось получить данные о погоде.") await ctx.send("Не удалось получить данные о погоде.")
return return
temp = current.get("temp_C", "") formatted = format_weather_data_for_console(data)
feels_like = current.get("FeelsLikeC", "") if not formatted:
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "")) await ctx.send("Не удалось получить данные о погоде.")
humidity = current.get("humidity", "") return
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) await ctx.send("\n".join(formatted))
await ctx.send(
f"Температура: {temp}°C (ощущается как {feels_like}°C)\n"
f"Описание: {description}\n"
f"Влажность: {humidity}%\n"
f"Ветер: {wind} м/с\n"
f"Давление: {pressure_mm} мм рт. ст."
)

View File

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

View File

@ -62,14 +62,14 @@ class TestPgCommand:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_pg_fetch_returns_none(self): async def test_pg_fetch_returns_none(self):
"""fetch_weather вернул None — бот должен ничего не отправить.""" """fetch_weather вернул None — бот должен сообщить об ошибке."""
cog = self._make_cog() cog = self._make_cog()
ctx = self._make_ctx() ctx = self._make_ctx()
with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=None)): with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=None)):
await cog.pg.callback(cog, ctx) await cog.pg.callback(cog, ctx)
ctx.send.assert_not_called() ctx.send.assert_called_once_with("Не удалось получить данные о погоде.")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_pg_empty_current_condition(self): 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 asyncio
import logging import logging
import os import os
from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord.ext import tasks 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.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
from utils.cat import fetch_cat from utils.cat import fetch_cat
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def run_morning(bot: "commands.Bot", channel: discord.TextChannel): @dataclass
"""Выполнить утренний дайджест и отправить в канал.""" class MorningData:
try: """Собранные данные для утреннего дайджеста."""
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru" 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( weather_data, articles, posts, cat_url = await asyncio.gather(
fetch_weather(api_url), fetch_weather(API_URL_WEATHER),
fetch_rss(RSS_URL_ARTICLES), fetch_rss(RSS_URL_ARTICLES),
fetch_rss(RSS_URL_POSTS), fetch_rss(RSS_URL_POSTS),
fetch_cat(), 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:
data = await gather_morning()
# --- Формируем embed --- # --- Формируем embed ---
embed = discord.Embed(title="🌅 Утренний дайджест!", color=0xF4A460) embed = discord.Embed(title="🌅 Утренний дайджест!", color=0xF4A460)
# Котик как thumbnail # Котик как thumbnail
if cat_url: if data.cat_url:
embed.set_thumbnail(url=cat_url) embed.set_thumbnail(url=data.cat_url)
description_lines = [] description_lines = []
has_real_data = False has_real_data = False
# --- Погода --- # --- Погода ---
if weather_data is not None: weather_text = format_weather_for_embed(data.weather)
current = weather_data.get("current_condition", [{}])[0] if weather_text:
if current:
has_real_data = True has_real_data = True
temp = current.get("temp_C", "") description_lines.append(weather_text)
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("Не удалось получить данные о погоде.")
else: else:
description_lines.append("Не удалось получить данные о погоде.") description_lines.append("Не удалось получить данные о погоде.")
description_lines.append("") description_lines.append("")
# --- Новости: статьи --- # --- Новости: статьи ---
if articles is not None: if data.articles is not None:
if articles: if data.articles:
has_real_data = True has_real_data = True
lines = format_articles(articles, lines = format_articles(data.articles,
"Лучшие статьи за сутки / Искусственный интеллект / Хабr", "Лучшие статьи за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/") "https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
description_lines.append("\n".join(lines)) description_lines.append("\n".join(lines))
@ -87,10 +88,10 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
description_lines.append("") description_lines.append("")
# --- Новости: посты --- # --- Новости: посты ---
if posts is not None: if data.posts is not None:
if posts: if data.posts:
has_real_data = True has_real_data = True
lines = format_articles(posts, lines = format_articles(data.posts,
"Лучшие новости за сутки / Искусственный интеллект / Хабr", "Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/") "https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
description_lines.append("\n".join(lines)) description_lines.append("\n".join(lines))

View File

@ -2,6 +2,8 @@ import asyncio
import requests import requests
from requests.exceptions import ConnectionError, Timeout, SSLError from requests.exceptions import ConnectionError, Timeout, SSLError
API_URL_WEATHER = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
_session = requests.Session() _session = requests.Session()
@ -143,6 +145,8 @@ def format_weather_data_for_console(data):
:param data: Ответ от API (dict) :param data: Ответ от API (dict)
:return: Строки с отформатированной погодой :return: Строки с отформатированной погодой
""" """
if data is None:
return None
current = data.get("current_condition", [{}])[0] current = data.get("current_condition", [{}])[0]
if not current: if not current:
return None 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): def pressure_to_mmhg(mb):
if mb == "" or not mb: if mb == "" or not mb:
return "" return ""