- 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 теста пройдены
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""
|
||
Rate-limiter на основе токен-бакета для внешних API-вызовов.
|
||
|
||
Каждый API получает свой RateLimiter с настройками rate (токенов/сек)
|
||
и burst (максимальный размер бакета). Перед каждым запросом вызывается
|
||
async acquire(), который ждёт освобождения токена при необходимости.
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
import time
|
||
from typing import Final
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class RateLimiter:
|
||
"""Токен-бакет: заполняется со скоростью rate токенов/сек, максимум burst."""
|
||
|
||
def __init__(self, rate: float, burst: int) -> None:
|
||
"""
|
||
Args:
|
||
rate: Скорость пополнения токенов (токенов в секунду).
|
||
burst: Максимальный размер бакета.
|
||
"""
|
||
self.rate: float = rate
|
||
self.burst: int = burst
|
||
self.tokens: float = float(burst)
|
||
self.lock: asyncio.Lock = asyncio.Lock()
|
||
self._last_refill: float = time.monotonic()
|
||
|
||
def _refill(self) -> None:
|
||
"""Пополнить токены за прошедшее время."""
|
||
now: float = time.monotonic()
|
||
elapsed: float = now - self._last_refill
|
||
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
|
||
self._last_refill = now
|
||
|
||
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)
|
||
|
||
|
||
# --- Готовые лимитеры по API ---
|
||
|
||
# TheCatAPI: бесплатно, 1 req/sec, burst 3
|
||
_CAT_RATE: Final[float] = float(os.getenv("CAT_API_RATE", "1"))
|
||
_CAT_BURST: Final[int] = int(os.getenv("CAT_API_BURST", "3"))
|
||
|
||
# wttr.in: без ключа, 1 req/sec, burst 3
|
||
_WEATHER_RATE: Final[float] = float(os.getenv("WEATHER_API_RATE", "1"))
|
||
_WEATHER_BURST: Final[int] = int(os.getenv("WEATHER_API_BURST", "3"))
|
||
|
||
# Open-Meteo: fallback, 2 req/sec, burst 5
|
||
_OPEN_METEO_RATE: Final[float] = float(os.getenv("OPEN_METEO_API_RATE", "2"))
|
||
_OPEN_METEO_BURST: Final[int] = int(os.getenv("OPEN_METEO_API_BURST", "5"))
|
||
|
||
# Habr RSS: 1 req/sec, burst 2
|
||
_HABR_RSS_RATE: Final[float] = float(os.getenv("HABR_RSS_RATE", "1"))
|
||
_HABR_RSS_BURST: Final[int] = int(os.getenv("HABR_RSS_BURST", "2"))
|
||
|
||
# Экземпляры лимитеров
|
||
cat_limiter: RateLimiter = RateLimiter(_CAT_RATE, _CAT_BURST)
|
||
weather_limiter: RateLimiter = RateLimiter(_WEATHER_RATE, _WEATHER_BURST)
|
||
open_meteo_limiter: RateLimiter = RateLimiter(_OPEN_METEO_RATE, _OPEN_METEO_BURST)
|
||
habr_rss_limiter: RateLimiter = RateLimiter(_HABR_RSS_RATE, _HABR_RSS_BURST)
|