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 теста пройдены
This commit is contained in:
deadzilla 2026-06-12 18:58:33 +05:00
parent c37baafacc
commit 29f2836cea
18 changed files with 122 additions and 13 deletions

View File

@ -3,7 +3,7 @@
## Средний приоритет ## Средний приоритет
- [x] **Добавить rate-limiting** для API-вызовов (TheCatAPI, wttr.in, Habr RSS) - [x] **Добавить rate-limiting** для API-вызовов (TheCatAPI, wttr.in, Habr RSS)
- [ ] **Настроить логирование** — уровни, формат, вывод в файл/консоль - [x] **Настроить логирование** — уровни, формат, вывод в файл/консоль
- [ ] **Добавить тесты для `console_commands/`** — есть только `test_help_console.py`, нужны `test_pogoda_console.py`, `test_news_console.py`, `test_morning_console.py` - [ ] **Добавить тесты для `console_commands/`** — есть только `test_help_console.py`, нужны `test_pogoda_console.py`, `test_news_console.py`, `test_morning_console.py`
- [ ] **Проверка наличия `.env`** — добавить явную проверку с информативным сообщением - [ ] **Проверка наличия `.env`** — добавить явную проверку с информативным сообщением

29
bot.py
View File

@ -45,18 +45,18 @@ class BotRunner:
@self.bot.event @self.bot.event
async def on_ready() -> None: async def on_ready() -> None:
logger.info(f"Бот вошёл как {self.bot.user}") logger.info("Бот вошёл как %s", self.bot.user)
for cog_class in ALL_COMMANDS: for cog_class in ALL_COMMANDS:
cog = cog_class() cog = cog_class()
await self.bot.add_cog(cog) await self.bot.add_cog(cog)
for cog in self.bot.cogs: for cog in self.bot.cogs:
logger.info(f" Загружен: {cog}") logger.info(" Загружен: %s", cog)
# Запуск планировщика # Запуск планировщика
morning_time = os.getenv("MORNING_TIME", "07:00") morning_time = os.getenv("MORNING_TIME", "07:00")
self.scheduler = Scheduler(self.bot, morning_time) self.scheduler = Scheduler(self.bot, morning_time)
self.bot._scheduler = self.scheduler self.bot._scheduler = self.scheduler
logger.info(f" Планировщик запущен (время: {morning_time})") logger.info(" Планировщик запущен (время: %s)", morning_time)
self.bot_ready.set() self.bot_ready.set()
@ -103,6 +103,7 @@ class BotRunner:
def console_input(self) -> None: def console_input(self) -> None:
"""Обработка ввода команд из консоли.""" """Обработка ввода команд из консоли."""
logger.info("Консольный режим ввода запущен")
self.bot_ready.wait() self.bot_ready.wait()
self._print_commands() self._print_commands()
@ -110,6 +111,7 @@ class BotRunner:
try: try:
choice = input("\nВыберите команду (номер): ").strip() choice = input("\nВыберите команду (номер): ").strip()
if choice == "0": if choice == "0":
logger.info("Пользователь выбрал команду stop через консоль")
print("\nОстановка бота...") print("\nОстановка бота...")
self.stop_event.set() self.stop_event.set()
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
@ -126,6 +128,7 @@ class BotRunner:
if 0 < idx <= len(available): if 0 < idx <= len(available):
cmd_name = list(available.keys())[idx - 1] cmd_name = list(available.keys())[idx - 1]
cmd_func = ALL_CONSOLE_COMMANDS[cmd_name] cmd_func = ALL_CONSOLE_COMMANDS[cmd_name]
logger.info("Выполняется консольная команда: %s", cmd_name)
if inspect.iscoroutinefunction(cmd_func): if inspect.iscoroutinefunction(cmd_func):
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
cmd_func(self.stop_event, self.bot), self.bot.loop cmd_func(self.stop_event, self.bot), self.bot.loop
@ -133,31 +136,35 @@ class BotRunner:
else: else:
cmd_func(self.stop_event, self.bot) cmd_func(self.stop_event, self.bot)
else: else:
logger.warning("Неизвестная консольная команда: %s", choice)
print(f"Неизвестная команда: {choice}") print(f"Неизвестная команда: {choice}")
except (ValueError, IndexError): except (ValueError, IndexError):
logger.warning("Неверный формат ввода консоли: %s", choice)
print(f"Неверный формат: {choice}") print(f"Неверный формат: {choice}")
self._print_commands() self._print_commands()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
logger.info("Консольный ввод завершен (EOF/KeyboardInterrupt)")
self.stop_event.set() self.stop_event.set()
try: try:
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self.bot.close(), self.bot.loop self.bot.close(), self.bot.loop
).result(timeout=5) ).result(timeout=5)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при остановке бота: {e}") logger.error("Ошибка при остановке бота: %s", e)
break break
def run(self, token: str) -> None: def run(self, token: str) -> None:
"""Запустить бота.""" """Запустить бота."""
logger.info("Запуск бота...")
try: try:
self.bot.run(token) self.bot.run(token)
except discord.LoginFailure as e: except discord.LoginFailure as e:
logger.critical(f"Ошибка авторизации бота: {e}", exc_info=True) logger.critical("Ошибка авторизации бота: %s", e, exc_info=True)
logger.error("Токен неверный или бот отключён. Код ошибки: %s", e) logger.error("Токен неверный или бот отключён. Код ошибки: %s", e)
sys.exit(1) sys.exit(1)
except discord.HTTPException as e: except discord.HTTPException as e:
logger.critical( logger.critical(
f"HTTP ошибка при подключении к Discord: {e}", exc_info=True "HTTP ошибка при подключении к Discord: %s", e, exc_info=True
) )
logger.error( logger.error(
"Сбой соединения с Discord API. Проверьте доступность сервиса." "Сбой соединения с Discord API. Проверьте доступность сервиса."
@ -165,12 +172,12 @@ class BotRunner:
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:
logger.critical( logger.critical(
f"Непредвиденная ошибка при запуске бота: {e}", exc_info=True "Непредвиденная ошибка при запуске бота: %s", e, exc_info=True
) )
logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__) logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__)
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Остановка бота...") logger.info("Получен сигнал KeyboardInterrupt")
self.stop_event.set() self.stop_event.set()
if self.scheduler: if self.scheduler:
self.scheduler.stop() self.scheduler.stop()
@ -182,6 +189,7 @@ class BotRunner:
def _validate_config() -> None: def _validate_config() -> None:
"""Проверить конфигурацию при запуске.""" """Проверить конфигурацию при запуске."""
logger.info("Проверка конфигурации...")
token = os.getenv("DISCORD_TOKEN") token = os.getenv("DISCORD_TOKEN")
if not token: if not token:
logger.error("Токен Discord не найден в .env") logger.error("Токен Discord не найден в .env")
@ -209,10 +217,13 @@ def _validate_config() -> None:
) )
sys.exit(1) sys.exit(1)
logger.info("Конфигурация проверена успешно")
if __name__ == "__main__": if __name__ == "__main__":
from utils.logger import setup_logging from utils.logger import setup_logging
logger.info("=== Запуск Discord бота ===")
setup_logging() setup_logging()
_validate_config() _validate_config()
@ -225,6 +236,8 @@ if __name__ == "__main__":
logger.info("Введите 'stop' для остановки бота") logger.info("Введите 'stop' для остановки бота")
thread = threading.Thread(target=runner.console_input, daemon=True) thread = threading.Thread(target=runner.console_input, daemon=True)
thread.start() thread.start()
else:
logger.info("Консольный режим отключен (stdin не интерактивный)")
token = os.getenv("DISCORD_TOKEN") token = os.getenv("DISCORD_TOKEN")
runner.run(token) runner.run(token)

View File

@ -1,8 +1,11 @@
import logging
import discord import discord
from discord.ext import commands from discord.ext import commands
from utils.cat import fetch_cat from utils.cat import fetch_cat
logger = logging.getLogger(__name__)
class Cat(commands.Cog): class Cat(commands.Cog):
"""Команда !cat — случайный котик""" """Команда !cat — случайный котик"""
@ -12,6 +15,7 @@ class Cat(commands.Cog):
"""Получить случайного котика""" """Получить случайного котика"""
url = await fetch_cat() url = await fetch_cat()
if url is None: if url is None:
logger.warning("%s: !cat — не удалось получить котика (API вернул None)", ctx.author)
await ctx.send("Не удалось получить котика. Попробуйте позже.") await ctx.send("Не удалось получить котика. Попробуйте позже.")
return return
@ -21,3 +25,4 @@ class Cat(commands.Cog):
) )
embed.set_image(url=url) embed.set_image(url=url)
await ctx.send(embed=embed) await ctx.send(embed=embed)
logger.info("%s: !cat выполнена", ctx.author)

View File

@ -1,8 +1,11 @@
import logging
import discord import discord
from discord.ext import commands from discord.ext import commands
from utils.morning_runner import run_morning from utils.morning_runner import run_morning
logger = logging.getLogger(__name__)
class Morning(commands.Cog): class Morning(commands.Cog):
"""Команда !morning — погода и новости утром""" """Команда !morning — погода и новости утром"""
@ -13,4 +16,6 @@ class Morning(commands.Cog):
@commands.command(name="morning") @commands.command(name="morning")
async def morning(self, ctx): async def morning(self, ctx):
"""Погода, лучшие статьи за сутки и котик""" """Погода, лучшие статьи за сутки и котик"""
logger.info("%s: !morning запущен", ctx.author)
await run_morning(ctx.bot, ctx.channel) await run_morning(ctx.bot, ctx.channel)
logger.info("%s: !morning завершен", ctx.author)

View File

@ -1,7 +1,10 @@
import logging
import discord import discord
from discord.ext import commands from discord.ext import commands
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
logger = logging.getLogger(__name__)
class News(commands.Cog): class News(commands.Cog):
"""Команда !news — свежие статьи и новости по AI с Habr""" """Команда !news — свежие статьи и новости по AI с Habr"""
@ -11,10 +14,12 @@ class News(commands.Cog):
"""Топ-5 свежих статей и новостей по AI с Habr""" """Топ-5 свежих статей и новостей по AI с Habr"""
articles = await fetch_rss(RSS_URL_ARTICLES) articles = await fetch_rss(RSS_URL_ARTICLES)
if articles is None: if articles is None:
logger.warning("%s: !nw — не удалось получить статьи (API вернул None)", ctx.author)
await ctx.send("Не удалось получить новости. Попробуйте позже.") await ctx.send("Не удалось получить новости. Попробуйте позже.")
return return
if not articles: if not articles:
logger.info("%s: !nw — статей нет в RSS", ctx.author)
await ctx.send("Новостей пока нет.") await ctx.send("Новостей пока нет.")
return return
@ -36,6 +41,7 @@ class News(commands.Cog):
) )
if posts is None: if posts is None:
logger.warning("%s: !nw — не удалось получить посты (API вернул None)", ctx.author)
embed.add_field( embed.add_field(
name="Новости", name="Новости",
value="Не удалось получить новости.", value="Не удалось получить новости.",
@ -51,6 +57,7 @@ class News(commands.Cog):
inline=False, inline=False,
) )
else: else:
logger.info("%s: !nw — постов нет в RSS", ctx.author)
embed.add_field( embed.add_field(
name="Новости", name="Новости",
value="Новостей пока нет.", value="Новостей пока нет.",
@ -58,3 +65,4 @@ class News(commands.Cog):
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
logger.info("%s: !nw выполнена (статей: %d, постов: %d)", ctx.author, len(articles), len(posts) if posts else 0)

View File

@ -1,6 +1,9 @@
import logging
from discord.ext import commands from discord.ext import commands
from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console
logger = logging.getLogger(__name__)
class Pg(commands.Cog): class Pg(commands.Cog):
"""Команда !pg — прогноз погоды для Магнитогорска""" """Команда !pg — прогноз погоды для Магнитогорска"""
@ -13,12 +16,15 @@ class Pg(commands.Cog):
"""Прогноз погоды в Магнитогорске""" """Прогноз погоды в Магнитогорске"""
data = await fetch_weather(self.api_url) data = await fetch_weather(self.api_url)
if data is None: if data is None:
logger.warning("%s: !pg — не удалось получить погоду (API вернул None)", ctx.author)
await ctx.send("Не удалось получить данные о погоде.") await ctx.send("Не удалось получить данные о погоде.")
return return
formatted = format_weather_data_for_console(data) formatted = format_weather_data_for_console(data)
if not formatted: if not formatted:
logger.warning("%s: !pg — данные погоды пустые", ctx.author)
await ctx.send("Не удалось получить данные о погоде.") await ctx.send("Не удалось получить данные о погоде.")
return return
await ctx.send("\n".join(formatted)) await ctx.send("\n".join(formatted))
logger.info("%s: !pg выполнена", ctx.author)

View File

@ -1,6 +1,9 @@
import logging
import discord import discord
from discord.ext import commands from discord.ext import commands
logger = logging.getLogger(__name__)
class Stats(commands.Cog): class Stats(commands.Cog):
"""Команда !stats — статистика серверов""" """Команда !stats — статистика серверов"""
@ -27,3 +30,4 @@ class Stats(commands.Cog):
embed.add_field(name="Пинг", value=f"{latency_ms} мс", inline=True) embed.add_field(name="Пинг", value=f"{latency_ms} мс", inline=True)
await ctx.send(embed=embed) await ctx.send(embed=embed)
logger.info("%s: !stats выполнена", ctx.author)

View File

@ -1,8 +1,11 @@
import logging
import time import time
import discord import discord
from discord.ext import commands from discord.ext import commands
logger = logging.getLogger(__name__)
class Status(commands.Cog): class Status(commands.Cog):
"""Команда !status — статус бота, пинг, uptime""" """Команда !status — статус бота, пинг, uptime"""
@ -24,6 +27,7 @@ class Status(commands.Cog):
embed.add_field(name="Статус", value="Online", inline=True) embed.add_field(name="Статус", value="Online", inline=True)
await ctx.send(embed=embed) await ctx.send(embed=embed)
logger.info("%s: !status выполнена", ctx.author)
@staticmethod @staticmethod
def _format_uptime(total_seconds: float) -> str: def _format_uptime(total_seconds: float) -> str:

View File

@ -1,10 +1,17 @@
import logging
from utils.cat import fetch_cat from utils.cat import fetch_cat
logger = logging.getLogger(__name__)
async def cat(stop_event, bot): async def cat(stop_event, bot):
"""Вывести URL случайного котика""" """Вывести URL случайного котика"""
logger.info("Консольная команда: cat")
url = await fetch_cat() url = await fetch_cat()
if url is None: if url is None:
logger.warning("Консольная команда cat: не удалось получить котика")
print("Не удалось получить котика.") print("Не удалось получить котика.")
return return
print(f"Котик: {url}") print(f"Котик: {url}")
logger.info("Консольная команда cat: выполнена")

View File

@ -1,11 +1,16 @@
import logging
from utils.cat import fetch_cat from utils.cat import fetch_cat
from utils.morning_runner import gather_morning from utils.morning_runner import gather_morning
from utils.news import RSS_URL_ARTICLES, RSS_URL_POSTS, format_articles from utils.news import RSS_URL_ARTICLES, RSS_URL_POSTS, format_articles
from utils.pogoda import format_weather_data_for_console from utils.pogoda import format_weather_data_for_console
logger = logging.getLogger(__name__)
async def morning(stop_event, bot): async def morning(stop_event, bot):
"""Вывести погоду, лучшие статьи за сутки и котик""" """Вывести погоду, лучшие статьи за сутки и котик"""
logger.info("Консольная команда: morning")
data = await gather_morning() data = await gather_morning()
print("Доброе утро!\n") print("Доброе утро!\n")
@ -14,6 +19,7 @@ async def morning(stop_event, bot):
if data.cat_url: if data.cat_url:
print(f"Котик: {data.cat_url}\n") print(f"Котик: {data.cat_url}\n")
else: else:
logger.warning("Консольная команда morning: не удалось получить котика")
print("Котика получить не удалось.\n") print("Котика получить не удалось.\n")
# --- Погода --- # --- Погода ---
@ -23,6 +29,7 @@ async def morning(stop_event, bot):
for line in formatted: for line in formatted:
print(line) print(line)
else: else:
logger.warning("Консольная команда morning: не удалось получить погоду")
print("Не удалось получить данные о погоде.") print("Не удалось получить данные о погоде.")
print() print()
@ -37,8 +44,10 @@ async def morning(stop_event, bot):
) )
print("\n".join(lines)) print("\n".join(lines))
else: else:
logger.info("Консольная команда morning: статей нет в RSS")
print("Новостей пока нет.") print("Новостей пока нет.")
else: else:
logger.warning("Консольная команда morning: не удалось получить статьи")
print("Не удалось получить новости.") print("Не удалось получить новости.")
print() print()
@ -53,6 +62,10 @@ async def morning(stop_event, bot):
) )
print("\n".join(lines)) print("\n".join(lines))
else: else:
logger.info("Консольная команда morning: постов нет в RSS")
print("Новостей пока нет.") print("Новостей пока нет.")
else: else:
logger.warning("Консольная команда morning: не удалось получить посты")
print("Не удалось получить новости.") print("Не удалось получить новости.")
logger.info("Консольная команда morning: завершен")

View File

@ -1,14 +1,21 @@
import logging
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
logger = logging.getLogger(__name__)
async def news(stop_event, bot): async def news(stop_event, bot):
"""Вывести топ-5 свежих статей по AI с Habr""" """Вывести топ-5 свежих статей по AI с Habr"""
logger.info("Консольная команда: news")
articles = await fetch_rss(RSS_URL_ARTICLES) articles = await fetch_rss(RSS_URL_ARTICLES)
if articles is None: if articles is None:
logger.warning("Консольная команда news: не удалось получить статьи")
print("Не удалось получить новости.") print("Не удалось получить новости.")
return return
if not articles: if not articles:
logger.info("Консольная команда news: статей нет в RSS")
print("Новостей пока нет.") print("Новостей пока нет.")
return return
@ -16,10 +23,15 @@ async def news(stop_event, bot):
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/") "https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
posts = await fetch_rss(RSS_URL_POSTS) posts = await fetch_rss(RSS_URL_POSTS)
if posts: if posts is None:
logger.warning("Консольная команда news: не удалось получить посты")
elif posts:
lines.append("") lines.append("")
lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr", lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")) "https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
else:
logger.info("Консольная команда news: постов нет в RSS")
for line in lines: for line in lines:
print(line) print(line)
logger.info("Консольная команда news: выполнена (статей: %d, постов: %d)", len(articles), len(posts) if posts else 0)

View File

@ -1,18 +1,26 @@
import logging
from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console from utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console
logger = logging.getLogger(__name__)
async def pogoda(stop_event, bot): async def pogoda(stop_event, bot):
"""Вывести прогноз погоды для Магнитогорска""" """Вывести прогноз погоды для Магнитогорска"""
logger.info("Консольная команда: pogoda")
data = await fetch_weather(API_URL_WEATHER) data = await fetch_weather(API_URL_WEATHER)
if data is None: if data is None:
logger.warning("Консольная команда pogoda: не удалось получить данные о погоде")
print("Не удалось получить данные о погоде.") print("Не удалось получить данные о погоде.")
return return
formatted = format_weather_data_for_console(data) formatted = format_weather_data_for_console(data)
if not formatted: if not formatted:
logger.warning("Консольная команда pogoda: данные погоды пустые")
print("Не удалось получить данные о погоде.") print("Не удалось получить данные о погоде.")
return return
for line in formatted: for line in formatted:
print(line) print(line)
logger.info("Консольная команда pogoda: выполнена")

View File

@ -1,11 +1,16 @@
import logging
import discord import discord
logger = logging.getLogger(__name__)
def stats(stop_event, bot): def stats(stop_event, bot):
"""Показать статистику серверов: серверы, каналы, пользователи""" """Показать статистику серверов: серверы, каналы, пользователи"""
if stop_event.is_set(): if stop_event.is_set():
return None return None
logger.info("Консольная команда: stats")
guilds = bot.guilds guilds = bot.guilds
total_guilds = len(guilds) total_guilds = len(guilds)
total_channels = sum( total_channels = sum(
@ -23,3 +28,4 @@ def stats(stop_event, bot):
print(f" Пользователей: {total_members}") print(f" Пользователей: {total_members}")
print(f" Пинг: {latency_ms} мс") print(f" Пинг: {latency_ms} мс")
print("=" * 40) print("=" * 40)
logger.info("Консольная команда stats: выполнена")

View File

@ -1,11 +1,15 @@
import logging
import time import time
logger = logging.getLogger(__name__)
def status(stop_event, bot): def status(stop_event, bot):
"""Показать статус бота: пинг и время работы""" """Показать статус бота: пинг и время работы"""
if stop_event.is_set(): if stop_event.is_set():
return None return None
logger.info("Консольная команда: status")
latency_ms = round(bot.latency * 1000, 1) latency_ms = round(bot.latency * 1000, 1)
start_time = getattr(bot, "_start_time", time.time()) start_time = getattr(bot, "_start_time", time.time())
uptime_seconds = time.time() - start_time uptime_seconds = time.time() - start_time
@ -18,6 +22,7 @@ def status(stop_event, bot):
print(f" Uptime: {uptime_str}") print(f" Uptime: {uptime_str}")
print(f" Статус: Online") print(f" Статус: Online")
print("=" * 40) print("=" * 40)
logger.info("Консольная команда status: выполнена")
def _format_uptime(total_seconds: float) -> str: def _format_uptime(total_seconds: float) -> str:

View File

@ -1,9 +1,12 @@
import asyncio import asyncio
import logging
import requests import requests
from utils.rate_limiter import cat_limiter from utils.rate_limiter import cat_limiter
logger = logging.getLogger(__name__)
CAT_API_URL = "https://api.thecatapi.com/v1/images/search" CAT_API_URL = "https://api.thecatapi.com/v1/images/search"
_session = requests.Session() _session = requests.Session()
@ -17,5 +20,6 @@ async def fetch_cat() -> str | None:
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
return data[0]["url"] return data[0]["url"]
except (requests.exceptions.RequestException, IndexError, KeyError): except (requests.exceptions.RequestException, IndexError, KeyError) as e:
logger.error("Ошибка при получении котика: %s", e)
return None return None

View File

@ -1,10 +1,13 @@
import asyncio import asyncio
import logging
from datetime import datetime from datetime import datetime
import requests import requests
from utils.rate_limiter import habr_rss_limiter from utils.rate_limiter import habr_rss_limiter
logger = logging.getLogger(__name__)
RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru" RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru"
RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru" RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru"
@ -66,7 +69,8 @@ async def fetch_rss(url):
"tags": tags, "tags": tags,
}) })
return articles[:10] return articles[:10]
except requests.exceptions.RequestException: except requests.exceptions.RequestException as e:
logger.error("Ошибка при получении RSS (%s): %s", url, e)
return None return None

View File

@ -31,7 +31,7 @@ async def fetch_weather(api_url, timeout=10, max_retries=3):
logger.error("Ошибка при получении данных: %s", e) logger.error("Ошибка при получении данных: %s", e)
break break
# Fallback: Open-Meteo API (без ключа, HTTPS) logger.warning("Все попытки wttr.in не удались, переход на Open-Meteo")
return await fetch_open_meteo() return await fetch_open_meteo()
@ -74,6 +74,7 @@ async def fetch_open_meteo(lat=53.4069, lon=58.9797, timeout=10, max_retries=3):
logger.error("Ошибка при получении данных: %s", e) logger.error("Ошибка при получении данных: %s", e)
return None return None
logger.warning("Все попытки Open-Meteo не удались")
return None return None

View File

@ -39,12 +39,16 @@ class RateLimiter:
async def acquire(self, token: int = 1) -> None: async def acquire(self, token: int = 1) -> None:
"""Забрать token токенов, ждать если их нет.""" """Забрать token токенов, ждать если их нет."""
wait_count = 0
while True: while True:
async with self.lock: async with self.lock:
self._refill() self._refill()
if self.tokens >= token: if self.tokens >= token:
self.tokens -= token self.tokens -= token
if wait_count:
logger.debug("RateLimiter: ждал %d раз(а)", wait_count)
return return
wait_count += 1
# Ждём достаточно времени для восстановления 1 токена # Ждём достаточно времени для восстановления 1 токена
await asyncio.sleep(token / self.rate) await asyncio.sleep(token / self.rate)