diff --git a/commands/pg.py b/commands/pg.py index d3adce2..ac6ebf2 100644 --- a/commands/pg.py +++ b/commands/pg.py @@ -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)) diff --git a/console_commands/cat.py b/console_commands/cat.py index 309c8c4..cfa0cbb 100644 --- a/console_commands/cat.py +++ b/console_commands/cat.py @@ -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}") diff --git a/console_commands/morning.py b/console_commands/morning.py index d29a7d8..305d3a7 100644 --- a/console_commands/morning.py +++ b/console_commands/morning.py @@ -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("Новостей пока нет.") diff --git a/console_commands/pogoda.py b/console_commands/pogoda.py index 52b88cb..8780af1 100644 --- a/console_commands/pogoda.py +++ b/console_commands/pogoda.py @@ -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("Не удалось получить данные о погоде.") diff --git a/tests/test_commands_pg.py b/tests/test_commands_pg.py index 0f35fcf..c2c8457 100644 --- a/tests/test_commands_pg.py +++ b/tests/test_commands_pg.py @@ -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): diff --git a/utils/__init__.py b/utils/__init__.py index 8b13789..5147d57 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -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", +] diff --git a/utils/morning_runner.py b/utils/morning_runner.py index 2cbe3c9..e873089 100644 --- a/utils/morning_runner.py +++ b/utils/morning_runner.py @@ -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)) diff --git a/utils/pogoda.py b/utils/pogoda.py index c1ee894..e7e5361 100644 --- a/utils/pogoda.py +++ b/utils/pogoda.py @@ -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 "—"