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:
parent
2188a7d3fd
commit
560dc558a9
@ -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} мм рт. ст."
|
|
||||||
)
|
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|||||||
@ -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("Новостей пока нет.")
|
||||||
|
|||||||
@ -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("Не удалось получить данные о погоде.")
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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 "—"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user