feat: закрываю Sprint 1 — все 3 задачи выполнены
Задачи Sprint 1 (Critical Fixes): - [1.1] Fallback в пустом embed (!morning) — добавлена проверка has_real_data и fallback сообщение при отключении внешних API - [1.2] Обработка ошибок токена/сети в bot.run() — raise_exception=True + логирование LoginFailure, HTTPException, Exception - [1.3] Рефакторинг парсинга погоды — вынесено в format_weather_data_for_console(), убран дублирующий код из 2 файлов Изменения: • utils/morning_runner.py — добавлена проверка has_real_data после формирования description_lines • bot.py — обработчики исключений для запуска бота с детализированным логированием и пользовательскими сообщениями • utils/pogoda.py — новая функция format_weather_data_for_console() для центрального форматирования погодных данных • console_commands/pogoda.py — замена 15 строк дублирующейся логики на вызов единой функции (24→8 строк) • console_commands/morning.py — аналогичные изменения для команды morning (19→13 строк с погодой) • tests/test_morning_runner.py — +2 теста для fallback сценариев empty embed и only weather data • tests/test_bot.py — новый файл с 2 тестами на проверку кода обработки ошибок • tests/test_pogoda.py — +6 тестов для format_weather_data_for_console() Статистика тестирования: • Общее количество тестов: 200 (было 190) • Новые тесты: 10 • Все тесты проходят успешно Примечания: - Сообщения без восклицательных знаков согласно preferencem
This commit is contained in:
parent
1a7f2d75b8
commit
71bcd66794
14
bot.py
14
bot.py
@ -131,7 +131,19 @@ if __name__ == "__main__":
|
|||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot.run(token)
|
bot.run(token, raise_exception=True)
|
||||||
|
except discord.LoginFailure as e:
|
||||||
|
logger.critical(f"Ошибка авторизации бота: {e}", exc_info=True)
|
||||||
|
print(f"❌ Токен неверный или бот отключён. Код ошибки: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.critical(f"HTTP ошибка при подключении к Discord: {e}", exc_info=True)
|
||||||
|
print(f"❌ Сбой соединения с Discord API. Проверьте доступность сервиса.")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(f"Непредвиденная ошибка при запуске бота: {e}", exc_info=True)
|
||||||
|
print(f"❌ Критическая ошибка при запуске. Код ошибки: {type(e).__name__}")
|
||||||
|
sys.exit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nОстановка бота...")
|
print("\nОстановка бота...")
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
|
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.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||||
from utils.cat import fetch_cat
|
from utils.cat import fetch_cat
|
||||||
|
|
||||||
@ -27,26 +27,11 @@ async def morning(stop_event, bot):
|
|||||||
|
|
||||||
# --- Погода ---
|
# --- Погода ---
|
||||||
if weather_data is not None:
|
if weather_data is not None:
|
||||||
current = weather_data.get("current_condition", [{}])[0]
|
formatted = format_weather_data_for_console(weather_data)
|
||||||
if current:
|
if formatted:
|
||||||
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)
|
|
||||||
|
|
||||||
print(f"**Погода в Магнитогорске:**")
|
print(f"**Погода в Магнитогорске:**")
|
||||||
print(f"Температура: {temp}°C (ощущается как {feels_like}°C)")
|
for line in formatted:
|
||||||
print(f"Описание: {description}")
|
print(line)
|
||||||
print(f"Влажность: {humidity}%")
|
|
||||||
print(f"Ветер: {wind} м/с")
|
|
||||||
print(f"Давление: {pressure_mm} мм рт. ст.")
|
|
||||||
else:
|
else:
|
||||||
print("Не удалось получить данные о погоде.")
|
print("Не удалось получить данные о погоде.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from utils.pogoda import fetch_weather, fetch_open_meteo, wmo_to_russian, translate_weather, pressure_to_mmhg
|
from utils.pogoda import fetch_weather, format_weather_data_for_console
|
||||||
|
|
||||||
|
|
||||||
async def pogoda(stop_event, bot):
|
async def pogoda(stop_event, bot):
|
||||||
@ -10,25 +10,10 @@ async def pogoda(stop_event, bot):
|
|||||||
print("Не удалось получить данные о погоде.")
|
print("Не удалось получить данные о погоде.")
|
||||||
return
|
return
|
||||||
|
|
||||||
current = data.get("current_condition", [{}])[0]
|
formatted = format_weather_data_for_console(data)
|
||||||
if not current:
|
if not formatted:
|
||||||
print("Не удалось получить данные о погоде.")
|
print("Не удалось получить данные о погоде.")
|
||||||
return
|
return
|
||||||
|
|
||||||
temp = current.get("temp_C", "—")
|
for line in formatted:
|
||||||
feels_like = current.get("FeelsLikeC", "—")
|
print(line)
|
||||||
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)
|
|
||||||
|
|
||||||
print(f"Температура: {temp}°C (ощущается как {feels_like}°C)")
|
|
||||||
print(f"Описание: {description}")
|
|
||||||
print(f"Влажность: {humidity}%")
|
|
||||||
print(f"Ветер: {wind} м/с")
|
|
||||||
print(f"Давление: {pressure_mm} мм рт. ст.")
|
|
||||||
|
|||||||
54
tests/test_bot.py
Normal file
54
tests/test_bot.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Тесты для bot.py — проверка обработки ошибок запуска бота.
|
||||||
|
|
||||||
|
Покрывают пункт 1.2 из PLAN_OF_WORKS.md:
|
||||||
|
- raise_exception=True в bot.run()
|
||||||
|
- Логирование и обработка исключений (LoginFailure, HTTPException)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# Добавляем корень проекта в путь импорта
|
||||||
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
class TestBotInit:
|
||||||
|
"""Тесты для инициализации бота."""
|
||||||
|
|
||||||
|
def test_bot_created_with_default_prefix(self):
|
||||||
|
"""Проверка, что бот создан с правильным префиксом команд."""
|
||||||
|
import bot
|
||||||
|
|
||||||
|
assert bot.bot.command_prefix == "!", (
|
||||||
|
f"Команда должна быть с префиксом '!', а не '{bot.bot.command_prefix}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBotErrorHandlingCodeExists:
|
||||||
|
"""Тесты для проверки наличия кода обработки ошибок в bot.py."""
|
||||||
|
|
||||||
|
def test_error_handling_code_exists(self):
|
||||||
|
"""Проверка, что код обработки ошибок существует в файле bot.py."""
|
||||||
|
with open(ROOT_DIR / "bot.py", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Проверяем наличие raise_exception=True
|
||||||
|
assert "raise_exception=True" in content, (
|
||||||
|
"В bot.py должен присутствовать параметр raise_exception=True"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем наличие обработки LoginFailure
|
||||||
|
assert "LoginFailure" in content, "В bot.py должна быть обработка LoginFailure"
|
||||||
|
|
||||||
|
# Проверяем наличие обработки HTTPException
|
||||||
|
assert "HTTPException" in content, "В bot.py должна быть обработка HTTPException"
|
||||||
|
|
||||||
|
# Проверяем наличие логирования ошибок
|
||||||
|
assert "logger.critical" in content, "В bot.py должно быть критическое логирование"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
@ -57,7 +57,7 @@ class TestSchedulerCalculateNextRun:
|
|||||||
next_run = scheduler._calculate_next_run()
|
next_run = scheduler._calculate_next_run()
|
||||||
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
|
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
|
||||||
|
|
||||||
def test_next_run_tomorrow_after_time(self):
|
def test_next_run_tomorning_after_time(self):
|
||||||
"""Если сейчас позже времени — вернуть завтра."""
|
"""Если сейчас позже времени — вернуть завтра."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
scheduler = Scheduler(bot, "14:00")
|
scheduler = Scheduler(bot, "14:00")
|
||||||
@ -156,3 +156,65 @@ class TestRunMorning:
|
|||||||
call_args = channel.send.call_args[1]
|
call_args = channel.send.call_args[1]
|
||||||
assert "embed" in call_args
|
assert "embed" in call_args
|
||||||
assert call_args["embed"] is not None
|
assert call_args["embed"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunMorningWithFallback:
|
||||||
|
"""Тесты fallback в пустом embed."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_morning_empty_embed_fallback(self):
|
||||||
|
"""run_morning должен добавлять fallback сообщение при пустых данных."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
channel = AsyncMock()
|
||||||
|
channel.name = "test-channel"
|
||||||
|
channel.guild.me = MagicMock()
|
||||||
|
channel.permissions_for.return_value.send_messages = True
|
||||||
|
|
||||||
|
# Все API возвращают None/пусто
|
||||||
|
with patch("utils.morning_runner.fetch_weather", new=AsyncMock(return_value=None)), \
|
||||||
|
patch("utils.morning_runner.fetch_rss", new=AsyncMock(return_value=None)), \
|
||||||
|
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)), \
|
||||||
|
patch("utils.morning_runner.discord.Embed") as mock_embed_class:
|
||||||
|
|
||||||
|
embed_mock = AsyncMock()
|
||||||
|
mock_embed_class.return_value = embed_mock
|
||||||
|
|
||||||
|
await run_morning(bot, channel)
|
||||||
|
|
||||||
|
# Убедимся, что send был вызван
|
||||||
|
channel.send.assert_called_once()
|
||||||
|
call_args = channel.send.call_args[1]
|
||||||
|
assert "embed" in call_args
|
||||||
|
|
||||||
|
# Проверяем, что description содержит fallback сообщение
|
||||||
|
embed_description = call_args["embed"].description
|
||||||
|
assert "Не удалось получить данные из внешних источников" in embed_description
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_morning_only_weather_data(self):
|
||||||
|
"""run_morning должен корректно обрабатывать только погоду без новостей."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
channel = AsyncMock()
|
||||||
|
channel.name = "test-channel"
|
||||||
|
channel.guild.me = MagicMock()
|
||||||
|
channel.permissions_for.return_value.send_messages = True
|
||||||
|
|
||||||
|
weather_data = {
|
||||||
|
"current_condition": [
|
||||||
|
{"temp_C": "20", "FeelsLikeC": "22", "weatherDesc": [{"value": "Clear"}], "humidity": "50", "windspeedKmph": "10", "pressure": "1013"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.fetch_weather", new=AsyncMock(return_value=weather_data)), \
|
||||||
|
patch("utils.morning_runner.fetch_rss", new=AsyncMock(side_effect=[None, None])), \
|
||||||
|
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)):
|
||||||
|
await run_morning(bot, channel)
|
||||||
|
|
||||||
|
channel.send.assert_called_once()
|
||||||
|
call_args = channel.send.call_args[1]
|
||||||
|
assert "embed" in call_args
|
||||||
|
|
||||||
|
# Проверяем, что в embed есть только погода и нет fallback сообщения
|
||||||
|
embed_description = call_args["embed"].description
|
||||||
|
assert "Погода в Магнитогорске" in embed_description
|
||||||
|
assert "Не удалось получить данные из внешних источников" not in embed_description
|
||||||
|
|||||||
@ -1,9 +1,110 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from utils.pogoda import translate_weather, pressure_to_mmhg, wmo_to_russian
|
from utils.pogoda import translate_weather, pressure_to_mmhg, wmo_to_russian, format_weather_data_for_console
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatWeatherDataForConsole:
|
||||||
|
"""Тесты функции format_weather_data_for_console()."""
|
||||||
|
|
||||||
|
def test_format_valid_data(self):
|
||||||
|
"""Полные данные должны быть отформатированы корректно."""
|
||||||
|
data = {
|
||||||
|
"current_condition": [{
|
||||||
|
"temp_C": "25",
|
||||||
|
"FeelsLikeC": "26",
|
||||||
|
"weatherDesc": [{"value": "Clear"}],
|
||||||
|
"humidity": "45",
|
||||||
|
"windspeedKmph": "10",
|
||||||
|
"pressure": "1013",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
|
||||||
|
assert isinstance(result, list), "Результат должен быть списком строк"
|
||||||
|
assert len(result) == 5, "Должно быть 5 полей погоды"
|
||||||
|
assert "Температура: 25°C (ощущается как 26°C)" in result[0]
|
||||||
|
assert "Описание: Ясно" in result[1]
|
||||||
|
assert "Влажность: 45%" in result[2]
|
||||||
|
assert "Ветер: 2.8 м/с" in result[3] # 10 / 3.6 = 2.777... ≈ 2.8
|
||||||
|
assert "Давление: 759.8 мм рт. ст." in result[4]
|
||||||
|
|
||||||
|
def test_format_empty_data(self):
|
||||||
|
"""Пустые данные должны возвращать None."""
|
||||||
|
data = {
|
||||||
|
"current_condition": [{}]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
|
||||||
|
assert result is None, "Пустые данные должны возвращать None"
|
||||||
|
|
||||||
|
def test_format_missing_current_condition(self):
|
||||||
|
"""Отсутствие current_condition должно вернуть None."""
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
|
||||||
|
assert result is None, "Отсутствие current_condition должно вернуть None"
|
||||||
|
|
||||||
|
def test_format_with_dashes(self):
|
||||||
|
"""Неизвестные значения должны отображаться как '—'."""
|
||||||
|
data = {
|
||||||
|
"current_condition": [{
|
||||||
|
"temp_C": "—",
|
||||||
|
"FeelsLikeC": "—",
|
||||||
|
"weatherDesc": [{"value": "—"}],
|
||||||
|
"humidity": "—",
|
||||||
|
"windspeedKmph": "—",
|
||||||
|
"pressure": "—",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
|
||||||
|
assert isinstance(result, list), "Результат должен быть списком строк"
|
||||||
|
assert "Температура: —°C (ощущается как —°C)" in result[0]
|
||||||
|
assert "Описание: —" in result[1]
|
||||||
|
assert "Влажность: —%" in result[2]
|
||||||
|
assert "Ветер: — м/с" in result[3]
|
||||||
|
assert "Давление: — мм рт. ст." in result[4]
|
||||||
|
|
||||||
|
def test_format_wind_conversion(self):
|
||||||
|
"""Проверка конвертации ветра из км/ч в м/с."""
|
||||||
|
data = {
|
||||||
|
"current_condition": [{
|
||||||
|
"temp_C": "20",
|
||||||
|
"FeelsLikeC": "19",
|
||||||
|
"weatherDesc": [{"value": "Cloudy"}],
|
||||||
|
"humidity": "60",
|
||||||
|
"windspeedKmph": "36",
|
||||||
|
"pressure": "1000",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
# 36 / 3.6 = 10.0
|
||||||
|
assert "Ветер: 10.0 м/с" in result[3]
|
||||||
|
|
||||||
|
def test_format_negative_temperature(self):
|
||||||
|
"""Отрицательная температура должна отображаться корректно."""
|
||||||
|
data = {
|
||||||
|
"current_condition": [{
|
||||||
|
"temp_C": "-5",
|
||||||
|
"FeelsLikeC": "-10",
|
||||||
|
"weatherDesc": [{"value": "Snow"}],
|
||||||
|
"humidity": "80",
|
||||||
|
"windspeedKmph": "20",
|
||||||
|
"pressure": "980",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = format_weather_data_for_console(data)
|
||||||
|
|
||||||
|
assert isinstance(result, list), "Результат должен быть списком строк"
|
||||||
|
assert "Температура: -5°C (ощущается как -10°C)" in result[0]
|
||||||
|
|
||||||
|
|
||||||
class TestTranslateWeather:
|
class TestTranslateWeather:
|
||||||
"""Тесты функции translate_weather() — перевод описания погоды из английского в русский."""
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"english, expected",
|
"english, expected",
|
||||||
|
|||||||
@ -37,11 +37,13 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
|||||||
embed.set_thumbnail(url=cat_url)
|
embed.set_thumbnail(url=cat_url)
|
||||||
|
|
||||||
description_lines = []
|
description_lines = []
|
||||||
|
has_real_data = False
|
||||||
|
|
||||||
# --- Погода ---
|
# --- Погода ---
|
||||||
if weather_data is not None:
|
if weather_data is not None:
|
||||||
current = weather_data.get("current_condition", [{}])[0]
|
current = weather_data.get("current_condition", [{}])[0]
|
||||||
if current:
|
if current:
|
||||||
|
has_real_data = True
|
||||||
temp = current.get("temp_C", "—")
|
temp = current.get("temp_C", "—")
|
||||||
feels_like = current.get("FeelsLikeC", "—")
|
feels_like = current.get("FeelsLikeC", "—")
|
||||||
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "—"))
|
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "—"))
|
||||||
@ -72,6 +74,7 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
|||||||
# --- Новости: статьи ---
|
# --- Новости: статьи ---
|
||||||
if articles is not None:
|
if articles is not None:
|
||||||
if articles:
|
if articles:
|
||||||
|
has_real_data = True
|
||||||
lines = format_articles(articles,
|
lines = format_articles(articles,
|
||||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||||
@ -86,6 +89,7 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
|||||||
# --- Новости: посты ---
|
# --- Новости: посты ---
|
||||||
if posts is not None:
|
if posts is not None:
|
||||||
if posts:
|
if posts:
|
||||||
|
has_real_data = True
|
||||||
lines = format_articles(posts,
|
lines = format_articles(posts,
|
||||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
||||||
@ -95,6 +99,13 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
|||||||
else:
|
else:
|
||||||
description_lines.append("Не удалось получить новости.")
|
description_lines.append("Не удалось получить новости.")
|
||||||
|
|
||||||
|
# Fallback для пустых данных
|
||||||
|
if not has_real_data:
|
||||||
|
description_lines = [
|
||||||
|
"Не удалось получить данные из внешних источников.",
|
||||||
|
"Проверьте доступность API и повторите попытку позже."
|
||||||
|
]
|
||||||
|
|
||||||
embed.description = "\n".join(description_lines)
|
embed.description = "\n".join(description_lines)
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
logger.info("✅ Утренний дайджест отправлен в #%s", channel.name)
|
logger.info("✅ Утренний дайджест отправлен в #%s", channel.name)
|
||||||
|
|||||||
@ -136,6 +136,38 @@ def translate_weather(en):
|
|||||||
return en
|
return en
|
||||||
|
|
||||||
|
|
||||||
|
def format_weather_data_for_console(data):
|
||||||
|
"""
|
||||||
|
Форматировать погодные данные для консольного вывода.
|
||||||
|
|
||||||
|
:param data: Ответ от API (dict)
|
||||||
|
:return: Строки с отформатированной погодой
|
||||||
|
"""
|
||||||
|
current = data.get("current_condition", [{}])[0]
|
||||||
|
if not current:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return [
|
||||||
|
f"Температура: {temp}°C (ощущается как {feels_like}°C)",
|
||||||
|
f"Описание: {description}",
|
||||||
|
f"Влажность: {humidity}%",
|
||||||
|
f"Ветер: {wind} м/с",
|
||||||
|
f"Давление: {pressure_mm} мм рт. ст.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
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