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:
parent
c37baafacc
commit
29f2836cea
@ -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
29
bot.py
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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: выполнена")
|
||||||
|
|||||||
@ -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: завершен")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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: выполнена")
|
||||||
|
|||||||
@ -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: выполнена")
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user