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] **Настроить логирование** — уровни, формат, вывод в файл/консоль
|
||||
- [ ] **Добавить тесты для `console_commands/`** — есть только `test_help_console.py`, нужны `test_pogoda_console.py`, `test_news_console.py`, `test_morning_console.py`
|
||||
- [ ] **Проверка наличия `.env`** — добавить явную проверку с информативным сообщением
|
||||
|
||||
|
||||
29
bot.py
29
bot.py
@ -45,18 +45,18 @@ class BotRunner:
|
||||
|
||||
@self.bot.event
|
||||
async def on_ready() -> None:
|
||||
logger.info(f"Бот вошёл как {self.bot.user}")
|
||||
logger.info("Бот вошёл как %s", self.bot.user)
|
||||
for cog_class in ALL_COMMANDS:
|
||||
cog = cog_class()
|
||||
await self.bot.add_cog(cog)
|
||||
for cog in self.bot.cogs:
|
||||
logger.info(f" Загружен: {cog}")
|
||||
logger.info(" Загружен: %s", cog)
|
||||
|
||||
# Запуск планировщика
|
||||
morning_time = os.getenv("MORNING_TIME", "07:00")
|
||||
self.scheduler = Scheduler(self.bot, morning_time)
|
||||
self.bot._scheduler = self.scheduler
|
||||
logger.info(f" Планировщик запущен (время: {morning_time})")
|
||||
logger.info(" Планировщик запущен (время: %s)", morning_time)
|
||||
|
||||
self.bot_ready.set()
|
||||
|
||||
@ -103,6 +103,7 @@ class BotRunner:
|
||||
|
||||
def console_input(self) -> None:
|
||||
"""Обработка ввода команд из консоли."""
|
||||
logger.info("Консольный режим ввода запущен")
|
||||
self.bot_ready.wait()
|
||||
self._print_commands()
|
||||
|
||||
@ -110,6 +111,7 @@ class BotRunner:
|
||||
try:
|
||||
choice = input("\nВыберите команду (номер): ").strip()
|
||||
if choice == "0":
|
||||
logger.info("Пользователь выбрал команду stop через консоль")
|
||||
print("\nОстановка бота...")
|
||||
self.stop_event.set()
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
@ -126,6 +128,7 @@ class BotRunner:
|
||||
if 0 < idx <= len(available):
|
||||
cmd_name = list(available.keys())[idx - 1]
|
||||
cmd_func = ALL_CONSOLE_COMMANDS[cmd_name]
|
||||
logger.info("Выполняется консольная команда: %s", cmd_name)
|
||||
if inspect.iscoroutinefunction(cmd_func):
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
cmd_func(self.stop_event, self.bot), self.bot.loop
|
||||
@ -133,31 +136,35 @@ class BotRunner:
|
||||
else:
|
||||
cmd_func(self.stop_event, self.bot)
|
||||
else:
|
||||
logger.warning("Неизвестная консольная команда: %s", choice)
|
||||
print(f"Неизвестная команда: {choice}")
|
||||
except (ValueError, IndexError):
|
||||
logger.warning("Неверный формат ввода консоли: %s", choice)
|
||||
print(f"Неверный формат: {choice}")
|
||||
self._print_commands()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
logger.info("Консольный ввод завершен (EOF/KeyboardInterrupt)")
|
||||
self.stop_event.set()
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.bot.close(), self.bot.loop
|
||||
).result(timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при остановке бота: {e}")
|
||||
logger.error("Ошибка при остановке бота: %s", e)
|
||||
break
|
||||
|
||||
def run(self, token: str) -> None:
|
||||
"""Запустить бота."""
|
||||
logger.info("Запуск бота...")
|
||||
try:
|
||||
self.bot.run(token)
|
||||
except discord.LoginFailure as e:
|
||||
logger.critical(f"Ошибка авторизации бота: {e}", exc_info=True)
|
||||
logger.critical("Ошибка авторизации бота: %s", e, exc_info=True)
|
||||
logger.error("Токен неверный или бот отключён. Код ошибки: %s", e)
|
||||
sys.exit(1)
|
||||
except discord.HTTPException as e:
|
||||
logger.critical(
|
||||
f"HTTP ошибка при подключении к Discord: {e}", exc_info=True
|
||||
"HTTP ошибка при подключении к Discord: %s", e, exc_info=True
|
||||
)
|
||||
logger.error(
|
||||
"Сбой соединения с Discord API. Проверьте доступность сервиса."
|
||||
@ -165,12 +172,12 @@ class BotRunner:
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.critical(
|
||||
f"Непредвиденная ошибка при запуске бота: {e}", exc_info=True
|
||||
"Непредвиденная ошибка при запуске бота: %s", e, exc_info=True
|
||||
)
|
||||
logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Остановка бота...")
|
||||
logger.info("Получен сигнал KeyboardInterrupt")
|
||||
self.stop_event.set()
|
||||
if self.scheduler:
|
||||
self.scheduler.stop()
|
||||
@ -182,6 +189,7 @@ class BotRunner:
|
||||
|
||||
def _validate_config() -> None:
|
||||
"""Проверить конфигурацию при запуске."""
|
||||
logger.info("Проверка конфигурации...")
|
||||
token = os.getenv("DISCORD_TOKEN")
|
||||
if not token:
|
||||
logger.error("Токен Discord не найден в .env")
|
||||
@ -209,10 +217,13 @@ def _validate_config() -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Конфигурация проверена успешно")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from utils.logger import setup_logging
|
||||
|
||||
logger.info("=== Запуск Discord бота ===")
|
||||
setup_logging()
|
||||
|
||||
_validate_config()
|
||||
@ -225,6 +236,8 @@ if __name__ == "__main__":
|
||||
logger.info("Введите 'stop' для остановки бота")
|
||||
thread = threading.Thread(target=runner.console_input, daemon=True)
|
||||
thread.start()
|
||||
else:
|
||||
logger.info("Консольный режим отключен (stdin не интерактивный)")
|
||||
|
||||
token = os.getenv("DISCORD_TOKEN")
|
||||
runner.run(token)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cat(commands.Cog):
|
||||
"""Команда !cat — случайный котик"""
|
||||
@ -12,6 +15,7 @@ class Cat(commands.Cog):
|
||||
"""Получить случайного котика"""
|
||||
url = await fetch_cat()
|
||||
if url is None:
|
||||
logger.warning("%s: !cat — не удалось получить котика (API вернул None)", ctx.author)
|
||||
await ctx.send("Не удалось получить котика. Попробуйте позже.")
|
||||
return
|
||||
|
||||
@ -21,3 +25,4 @@ class Cat(commands.Cog):
|
||||
)
|
||||
embed.set_image(url=url)
|
||||
await ctx.send(embed=embed)
|
||||
logger.info("%s: !cat выполнена", ctx.author)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from utils.morning_runner import run_morning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Morning(commands.Cog):
|
||||
"""Команда !morning — погода и новости утром"""
|
||||
@ -13,4 +16,6 @@ class Morning(commands.Cog):
|
||||
@commands.command(name="morning")
|
||||
async def morning(self, ctx):
|
||||
"""Погода, лучшие статьи за сутки и котик"""
|
||||
logger.info("%s: !morning запущен", ctx.author)
|
||||
await run_morning(ctx.bot, ctx.channel)
|
||||
logger.info("%s: !morning завершен", ctx.author)
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class News(commands.Cog):
|
||||
"""Команда !news — свежие статьи и новости по AI с Habr"""
|
||||
@ -11,10 +14,12 @@ class News(commands.Cog):
|
||||
"""Топ-5 свежих статей и новостей по AI с Habr"""
|
||||
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||
if articles is None:
|
||||
logger.warning("%s: !nw — не удалось получить статьи (API вернул None)", ctx.author)
|
||||
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
||||
return
|
||||
|
||||
if not articles:
|
||||
logger.info("%s: !nw — статей нет в RSS", ctx.author)
|
||||
await ctx.send("Новостей пока нет.")
|
||||
return
|
||||
|
||||
@ -36,6 +41,7 @@ class News(commands.Cog):
|
||||
)
|
||||
|
||||
if posts is None:
|
||||
logger.warning("%s: !nw — не удалось получить посты (API вернул None)", ctx.author)
|
||||
embed.add_field(
|
||||
name="Новости",
|
||||
value="Не удалось получить новости.",
|
||||
@ -51,6 +57,7 @@ class News(commands.Cog):
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
logger.info("%s: !nw — постов нет в RSS", ctx.author)
|
||||
embed.add_field(
|
||||
name="Новости",
|
||||
value="Новостей пока нет.",
|
||||
@ -58,3 +65,4 @@ class News(commands.Cog):
|
||||
)
|
||||
|
||||
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 utils.pogoda import API_URL_WEATHER, fetch_weather, format_weather_data_for_console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pg(commands.Cog):
|
||||
"""Команда !pg — прогноз погоды для Магнитогорска"""
|
||||
@ -13,12 +16,15 @@ class Pg(commands.Cog):
|
||||
"""Прогноз погоды в Магнитогорске"""
|
||||
data = await fetch_weather(self.api_url)
|
||||
if data is None:
|
||||
logger.warning("%s: !pg — не удалось получить погоду (API вернул None)", ctx.author)
|
||||
await ctx.send("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
formatted = format_weather_data_for_console(data)
|
||||
if not formatted:
|
||||
logger.warning("%s: !pg — данные погоды пустые", ctx.author)
|
||||
await ctx.send("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
await ctx.send("\n".join(formatted))
|
||||
logger.info("%s: !pg выполнена", ctx.author)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Stats(commands.Cog):
|
||||
"""Команда !stats — статистика серверов"""
|
||||
@ -27,3 +30,4 @@ class Stats(commands.Cog):
|
||||
embed.add_field(name="Пинг", value=f"{latency_ms} мс", inline=True)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
logger.info("%s: !stats выполнена", ctx.author)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Status(commands.Cog):
|
||||
"""Команда !status — статус бота, пинг, uptime"""
|
||||
@ -24,6 +27,7 @@ class Status(commands.Cog):
|
||||
embed.add_field(name="Статус", value="Online", inline=True)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
logger.info("%s: !status выполнена", ctx.author)
|
||||
|
||||
@staticmethod
|
||||
def _format_uptime(total_seconds: float) -> str:
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import logging
|
||||
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def cat(stop_event, bot):
|
||||
"""Вывести URL случайного котика"""
|
||||
logger.info("Консольная команда: cat")
|
||||
url = await fetch_cat()
|
||||
if url is None:
|
||||
logger.warning("Консольная команда cat: не удалось получить котика")
|
||||
print("Не удалось получить котика.")
|
||||
return
|
||||
print(f"Котик: {url}")
|
||||
logger.info("Консольная команда cat: выполнена")
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def morning(stop_event, bot):
|
||||
"""Вывести погоду, лучшие статьи за сутки и котик"""
|
||||
logger.info("Консольная команда: morning")
|
||||
data = await gather_morning()
|
||||
|
||||
print("Доброе утро!\n")
|
||||
@ -14,6 +19,7 @@ async def morning(stop_event, bot):
|
||||
if data.cat_url:
|
||||
print(f"Котик: {data.cat_url}\n")
|
||||
else:
|
||||
logger.warning("Консольная команда morning: не удалось получить котика")
|
||||
print("Котика получить не удалось.\n")
|
||||
|
||||
# --- Погода ---
|
||||
@ -23,6 +29,7 @@ async def morning(stop_event, bot):
|
||||
for line in formatted:
|
||||
print(line)
|
||||
else:
|
||||
logger.warning("Консольная команда morning: не удалось получить погоду")
|
||||
print("Не удалось получить данные о погоде.")
|
||||
|
||||
print()
|
||||
@ -37,8 +44,10 @@ async def morning(stop_event, bot):
|
||||
)
|
||||
print("\n".join(lines))
|
||||
else:
|
||||
logger.info("Консольная команда morning: статей нет в RSS")
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
logger.warning("Консольная команда morning: не удалось получить статьи")
|
||||
print("Не удалось получить новости.")
|
||||
|
||||
print()
|
||||
@ -53,6 +62,10 @@ async def morning(stop_event, bot):
|
||||
)
|
||||
print("\n".join(lines))
|
||||
else:
|
||||
logger.info("Консольная команда morning: постов нет в RSS")
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
logger.warning("Консольная команда morning: не удалось получить посты")
|
||||
print("Не удалось получить новости.")
|
||||
|
||||
logger.info("Консольная команда morning: завершен")
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
import logging
|
||||
|
||||
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def news(stop_event, bot):
|
||||
"""Вывести топ-5 свежих статей по AI с Habr"""
|
||||
logger.info("Консольная команда: news")
|
||||
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||
if articles is None:
|
||||
logger.warning("Консольная команда news: не удалось получить статьи")
|
||||
print("Не удалось получить новости.")
|
||||
return
|
||||
|
||||
if not articles:
|
||||
logger.info("Консольная команда news: статей нет в RSS")
|
||||
print("Новостей пока нет.")
|
||||
return
|
||||
|
||||
@ -16,10 +23,15 @@ async def news(stop_event, bot):
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||
|
||||
posts = await fetch_rss(RSS_URL_POSTS)
|
||||
if posts:
|
||||
if posts is None:
|
||||
logger.warning("Консольная команда news: не удалось получить посты")
|
||||
elif posts:
|
||||
lines.append("")
|
||||
lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
|
||||
else:
|
||||
logger.info("Консольная команда news: постов нет в RSS")
|
||||
|
||||
for line in lines:
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def pogoda(stop_event, bot):
|
||||
"""Вывести прогноз погоды для Магнитогорска"""
|
||||
logger.info("Консольная команда: pogoda")
|
||||
data = await fetch_weather(API_URL_WEATHER)
|
||||
|
||||
if data is None:
|
||||
logger.warning("Консольная команда pogoda: не удалось получить данные о погоде")
|
||||
print("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
formatted = format_weather_data_for_console(data)
|
||||
if not formatted:
|
||||
logger.warning("Консольная команда pogoda: данные погоды пустые")
|
||||
print("Не удалось получить данные о погоде.")
|
||||
return
|
||||
|
||||
for line in formatted:
|
||||
print(line)
|
||||
logger.info("Консольная команда pogoda: выполнена")
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import logging
|
||||
|
||||
import discord
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def stats(stop_event, bot):
|
||||
"""Показать статистику серверов: серверы, каналы, пользователи"""
|
||||
if stop_event.is_set():
|
||||
return None
|
||||
|
||||
logger.info("Консольная команда: stats")
|
||||
guilds = bot.guilds
|
||||
total_guilds = len(guilds)
|
||||
total_channels = sum(
|
||||
@ -23,3 +28,4 @@ def stats(stop_event, bot):
|
||||
print(f" Пользователей: {total_members}")
|
||||
print(f" Пинг: {latency_ms} мс")
|
||||
print("=" * 40)
|
||||
logger.info("Консольная команда stats: выполнена")
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def status(stop_event, bot):
|
||||
"""Показать статус бота: пинг и время работы"""
|
||||
if stop_event.is_set():
|
||||
return None
|
||||
|
||||
logger.info("Консольная команда: status")
|
||||
latency_ms = round(bot.latency * 1000, 1)
|
||||
start_time = getattr(bot, "_start_time", time.time())
|
||||
uptime_seconds = time.time() - start_time
|
||||
@ -18,6 +22,7 @@ def status(stop_event, bot):
|
||||
print(f" Uptime: {uptime_str}")
|
||||
print(f" Статус: Online")
|
||||
print("=" * 40)
|
||||
logger.info("Консольная команда status: выполнена")
|
||||
|
||||
|
||||
def _format_uptime(total_seconds: float) -> str:
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from utils.rate_limiter import cat_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CAT_API_URL = "https://api.thecatapi.com/v1/images/search"
|
||||
|
||||
_session = requests.Session()
|
||||
@ -17,5 +20,6 @@ async def fetch_cat() -> str | None:
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data[0]["url"]
|
||||
except (requests.exceptions.RequestException, IndexError, KeyError):
|
||||
except (requests.exceptions.RequestException, IndexError, KeyError) as e:
|
||||
logger.error("Ошибка при получении котика: %s", e)
|
||||
return None
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
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_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,
|
||||
})
|
||||
return articles[:10]
|
||||
except requests.exceptions.RequestException:
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error("Ошибка при получении RSS (%s): %s", url, e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ async def fetch_weather(api_url, timeout=10, max_retries=3):
|
||||
logger.error("Ошибка при получении данных: %s", e)
|
||||
break
|
||||
|
||||
# Fallback: Open-Meteo API (без ключа, HTTPS)
|
||||
logger.warning("Все попытки wttr.in не удались, переход на 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)
|
||||
return None
|
||||
|
||||
logger.warning("Все попытки Open-Meteo не удались")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@ -39,12 +39,16 @@ class RateLimiter:
|
||||
|
||||
async def acquire(self, token: int = 1) -> None:
|
||||
"""Забрать token токенов, ждать если их нет."""
|
||||
wait_count = 0
|
||||
while True:
|
||||
async with self.lock:
|
||||
self._refill()
|
||||
if self.tokens >= token:
|
||||
self.tokens -= token
|
||||
if wait_count:
|
||||
logger.debug("RateLimiter: ждал %d раз(а)", wait_count)
|
||||
return
|
||||
wait_count += 1
|
||||
# Ждём достаточно времени для восстановления 1 токена
|
||||
await asyncio.sleep(token / self.rate)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user