""" Simple in-memory rate limiter for Quart endpoints. Uses a sliding-window counter keyed by IP address or user ID. """ import time import asyncio from collections import defaultdict from functools import wraps from quart import request, jsonify class RateLimiter: """Thread-safe in-memory rate limiter using sliding window.""" def __init__(self): # {key: [(timestamp, count), ...]} self._buckets: dict[str, list] = defaultdict(list) self._lock = asyncio.Lock() async def is_allowed(self, key: str, max_requests: int, window_seconds: int) -> bool: """Return True if the request is within the rate limit.""" async with self._lock: now = time.monotonic() cutoff = now - window_seconds bucket = self._buckets[key] # Remove expired entries self._buckets[key] = [ts for ts in bucket if ts > cutoff] if len(self._buckets[key]) >= max_requests: return False self._buckets[key].append(now) return True _limiter = RateLimiter() def rate_limit(max_requests: int, window_seconds: int, key_func=None): """ Decorator that rate-limits a Quart route. Args: max_requests: Maximum number of requests allowed. window_seconds: Time window in seconds. key_func: Callable that returns the rate-limit key from the request. Defaults to client IP address. """ def decorator(f): @wraps(f) async def wrapper(*args, **kwargs): if key_func: key = key_func() else: # Default: rate limit by IP key = f"{f.__name__}:{request.remote_addr}" allowed = await _limiter.is_allowed(key, max_requests, window_seconds) if not allowed: return jsonify({"message": "Too many requests. Please try again later."}), 429 return await f(*args, **kwargs) return wrapper return decorator def ip_key(): """Rate limit key: function name + client IP.""" return f"{request.endpoint}:{request.remote_addr}"