discordBot/utils/pogoda.py
deadzilla 29f2836cea feat: добавлено логирование во все модули
- utils/news.py: добавлен logger, логирование ошибок RSS
- utils/cat.py: добавлен logger, логирование ошибок TheCatAPI
- utils/pogoda.py: улучшены логи fallback/warning при ошибках API
- utils/rate_limiter.py: debug-логи при ожидании токенов
- commands/pg.py, news.py, cat.py, morning.py, status.py, stats.py: logger + логи ошибок и успешного выполнения команд
- console_commands/pogoda.py, news.py, cat.py, morning.py, status.py, stats.py: logger + логи выполнения
- bot.py: логи запуска/остановки, проверки конфигурации, маршрутизации консольных команд, f-строки -> %s формат
- ISSUES.md: снят флаг задачи по логированию
- все 243 теста пройдены
2026-06-12 18:58:33 +05:00

200 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging
import requests
from requests.exceptions import ConnectionError, Timeout, SSLError
from utils.rate_limiter import weather_limiter, open_meteo_limiter
logger = logging.getLogger(__name__)
API_URL_WEATHER = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
_session = requests.Session()
async def fetch_weather(api_url, timeout=10, max_retries=3):
"""Получить данные о погоде с retry."""
await weather_limiter.acquire()
for attempt in range(max_retries):
try:
response = await asyncio.to_thread(_session.get, api_url, timeout=timeout)
response.raise_for_status()
return response.json()
except (SSLError, ConnectionError, Timeout):
if attempt < max_retries - 1:
delay = 2 ** attempt
logger.warning("Попытка %d не удалась. Повтор через %d сек...", attempt + 1, delay)
await asyncio.sleep(delay)
continue
break
except requests.RequestException as e:
logger.error("Ошибка при получении данных: %s", e)
break
logger.warning("Все попытки wttr.in не удались, переход на Open-Meteo")
return await fetch_open_meteo()
async def fetch_open_meteo(lat=53.4069, lon=58.9797, timeout=10, max_retries=3):
"""Fallback на Open-Meteo API."""
await open_meteo_limiter.acquire()
url = (
f"https://api.open-meteo.com/v1/forecast?"
f"latitude={lat}&longitude={lon}&current=temperature,"
f"apparent_temperature,weather_code,wind_speed_10m,"
f"relative_humidity_2m,pressure_msl&timezone=Asia/Chelyabinsk"
)
for attempt in range(max_retries):
try:
response = await asyncio.to_thread(_session.get, url, timeout=timeout)
response.raise_for_status()
data = response.json()
current = data.get("current", {})
# weather_code WMO код -> перевод (https://open-meteo.com/en/docs)
weather_code = current.get("weather_code", None)
desc = wmo_to_russian(weather_code)
return {
"current_condition": [{
"temp_C": current.get("temperature", ""),
"FeelsLikeC": current.get("apparent_temperature", ""),
"weatherDesc": [{"value": desc}],
"humidity": current.get("relative_humidity_2m", ""),
"windspeedKmph": current.get("wind_speed_10m", ""),
"pressure": current.get("pressure_msl", ""),
}]
}
except (SSLError, ConnectionError, Timeout):
if attempt < max_retries - 1:
delay = 2 ** attempt
logger.warning("Попытка %d не удалась. Повтор через %d сек...", attempt + 1, delay)
await asyncio.sleep(delay)
continue
break
except requests.RequestException as e:
logger.error("Ошибка при получении данных: %s", e)
return None
logger.warning("Все попытки Open-Meteo не удались")
return None
def wmo_to_russian(code):
"""Перевод WMO weather code в русский."""
mapping = {
0: "Ясно",
1: "Ясно", 2: "Переменная облачность",
3: "Пасмурно",
45: "Туман", 48: "Туман",
51: "Лёгкая морось", 53: "Морось", 55: "Сильная морось",
56: "Ледяная морось", 57: "Сильная ледяная морось",
61: "Небольшой дождь", 63: "Дождь", 65: "Сильный дождь",
66: "Ледяной дождь", 67: "Сильный ледяной дождь",
71: "Небольшой снег", 73: "Снег", 75: "Сильный снег",
77: "Снежная крупа",
80: "Небольшой ливень", 81: "Ливень", 82: "Сильный ливень",
85: "Снежный ливень", 86: "Сильный снежный ливень",
95: "Гроза", 96: "Гроза с градом", 99: "Сильная гроза с градом",
}
return mapping.get(code, "Неизвестно")
def translate_weather(en):
if not en:
return ""
mapping = {
"Moderate or heavy freezing rain in area": "Ледяной дождь",
"Moderate or heavy sleet in area": "Слякоть",
"Moderate or heavy snow in area": "Снег",
"Moderate or heavy rain in area": "Дождь",
"Thundery outbreaks in nearby": "Гроза вблизи",
"Patchy rain nearby": "Местами дождь",
"Patchy snow nearby": "Местами снег",
"Patchy sleet nearby": "Местами слякоть",
"Heavy freezing rain": "Сильный ледяной дождь",
"Heavy snow": "Сильный снег",
"Heavy rain": "Сильный дождь",
"Moderate or heavy rain at times": "Дождь",
"Moderate or heavy snow at times": "Снег",
"Blowing snow": "Метель",
"Patchy light drizzle": "Местами лёгкая морось",
"Moderate or heavy freezing rain at a distance": "Ледяной дождь",
"Moderate or heavy sleet at a distance": "Слякоть",
"Light rain shower": "Небольшой дождь",
"Heavy rain shower": "Сильный дождь",
"Moderate rain": "Умеренный дождь",
"Light rain": "Небольшой дождь",
"Moderate rain at times": "Умеренный дождь",
"Heavy rain at times": "Сильный дождь",
"Light snow": "Небольшой снег",
"Moderate snow": "Умеренный снег",
"Patchy light snow": "Местами лёгкий снег",
"Partly cloudy": "Переменная облачность",
"Moderate or light sleet": "Слякоть",
"Light freezing rain": "Лёгкий ледяной дождь",
"Foggy": "Туманно",
"Fog": "Туман",
"Mist": "Туман",
"Haze": "Дымка",
"Overcast": "Пасмурно",
"Cloudy": "Облачно",
"Clear": "Ясно",
"Sunny": "Ясно",
}
for key, value in mapping.items():
if key.lower() in en.lower():
return value
return en
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
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 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 ""
try:
return round(float(mb) * 0.750062, 1)
except (ValueError, TypeError):
return ""