"""Durable rate limiting (sliding-window counter) over the rate_limit_events table. A durable counter table means limits survive worker restarts and multi-instance deploys. Keys are typically ``:``. No secrets are stored. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from sqlalchemy.orm import Session from ..models.rate_limit import RateLimitEvent def _utc_now() -> datetime: return datetime.now(timezone.utc) class RateLimiter: """Flush-only rate limiter; caller owns the transaction boundary.""" def __init__(self, session: Session, *, window: timedelta | None = None, limit: int | None = None): self.session = session self.window = window or timedelta(minutes=15) self.limit = limit or 60 def check_and_record(self, operation: str, *, key: str, organization_id: str | None = None) -> bool: """Record a hit if under the limit; return False when over the limit.""" cutoff = _utc_now() - self.window count = self.session.query(RateLimitEvent).filter( RateLimitEvent.operation == operation, RateLimitEvent.key == key, RateLimitEvent.created_at >= cutoff, ).count() if count >= self.limit: return False self.session.add( RateLimitEvent( operation=operation, key=key, organization_id=organization_id, ) ) # Rate-limit events are append-only side effects that must survive even # if the surrounding request transaction is later rolled back, so commit # them immediately. self.session.commit() return True