#!/usr/bin/env python3 """Rate limiter for database queries to prevent spamming.""" import time from pathlib import Path from typing import Optional from alfred_utils import print_debug class RateLimiter: """ Rate limiter for database queries to prevent spamming. In theory the Alfred queue delay will would prevent that, but I added this to be safe. """ def __init__( self, min_interval: float = 1.0, cache_dir: Path = Path(".cache"), limiter_file: str = "rate_limit.txt", ): """Initialize rate limiter. Args: min_interval: Minimum seconds between operations cache_dir: Directory to store rate limit file limiter_file: Name of the rate limit file """ self.min_interval = min_interval self.cache_dir = Path(cache_dir) self.limiter_file = self.cache_dir / limiter_file # Ensure cache directory exists self.cache_dir.mkdir(parents=True, exist_ok=True) def _get_last_run(self) -> Optional[float]: """Get timestamp of last run from file.""" try: if self.limiter_file.exists(): with open(self.limiter_file, "r") as f: return float(f.read().strip()) except (ValueError, IOError): pass return None def _set_last_run(self, timestamp: float) -> None: """Save timestamp of last run to file.""" with open(self.limiter_file, "w") as f: f.write(str(timestamp)) def should_throttle(self) -> tuple[bool, float]: """Check if operation should be throttled. Returns: Tuple of (should_throttle, seconds_to_wait) """ current_time = time.time() last_run = self._get_last_run() if last_run is None: return False, 0 elapsed = current_time - last_run if elapsed < self.min_interval: wait_time = self.min_interval - elapsed return True, wait_time return False, 0 def wait_if_needed(self) -> bool: """Wait if rate limit requires it. Returns: True if we had to wait, False otherwise """ should_wait, wait_time = self.should_throttle() if should_wait: print_debug(f"Rate limiting: waiting {wait_time:.1f}s") time.sleep(wait_time) return True return False def record_operation(self) -> None: """Record that an operation just occurred.""" self._set_last_run(time.time()) def check_and_proceed(self) -> bool: """Combined check, wait, and record operation. Returns: True if operation can proceed (after any necessary wait) """ self.wait_if_needed() self.record_operation() return True