Initial commit - Church Music Database
This commit is contained in:
123
legacy-site/backend/rate_limiter.py
Normal file
123
legacy-site/backend/rate_limiter.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Rate limiting middleware for Flask API
|
||||
Implements token bucket algorithm for request throttling
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
from flask import request, jsonify
|
||||
import time
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
|
||||
class RateLimiter:
|
||||
"""
|
||||
Thread-safe rate limiter using token bucket algorithm
|
||||
"""
|
||||
def __init__(self):
|
||||
self.clients = defaultdict(lambda: {'tokens': 0, 'last_update': time.time(), 'initialized': False})
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def is_allowed(self, client_id, max_tokens=60, refill_rate=1.0):
|
||||
"""
|
||||
Check if request is allowed for client
|
||||
|
||||
Args:
|
||||
client_id: Unique identifier for client (IP address)
|
||||
max_tokens: Maximum tokens in bucket (requests per period)
|
||||
refill_rate: Tokens added per second
|
||||
|
||||
Returns:
|
||||
tuple: (is_allowed: bool, retry_after: int)
|
||||
"""
|
||||
with self.lock:
|
||||
now = time.time()
|
||||
client = self.clients[client_id]
|
||||
|
||||
# Initialize new clients with full bucket
|
||||
if not client.get('initialized', False):
|
||||
client['tokens'] = max_tokens
|
||||
client['initialized'] = True
|
||||
|
||||
# Calculate tokens to add based on time elapsed
|
||||
time_passed = now - client['last_update']
|
||||
client['tokens'] = min(
|
||||
max_tokens,
|
||||
client['tokens'] + time_passed * refill_rate
|
||||
)
|
||||
client['last_update'] = now
|
||||
|
||||
# Check if request is allowed
|
||||
if client['tokens'] >= 1:
|
||||
client['tokens'] -= 1
|
||||
return True, 0
|
||||
else:
|
||||
# Calculate retry-after time
|
||||
retry_after = int((1 - client['tokens']) / refill_rate) + 1
|
||||
return False, retry_after
|
||||
|
||||
def clear_client(self, client_id):
|
||||
"""Remove client from rate limiter (for testing/reset)"""
|
||||
with self.lock:
|
||||
if client_id in self.clients:
|
||||
del self.clients[client_id]
|
||||
|
||||
# Global rate limiter instance
|
||||
rate_limiter = RateLimiter()
|
||||
|
||||
def rate_limit(max_per_minute=60):
|
||||
"""
|
||||
Decorator to apply rate limiting to Flask routes
|
||||
|
||||
Usage:
|
||||
@app.route('/api/endpoint')
|
||||
@rate_limit(max_per_minute=30)
|
||||
def my_endpoint():
|
||||
...
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
# Get client identifier (IP address)
|
||||
client_id = request.remote_addr or 'unknown'
|
||||
|
||||
# Convert per-minute limit to per-second refill rate
|
||||
refill_rate = max_per_minute / 60.0
|
||||
|
||||
# Check if request is allowed
|
||||
is_allowed, retry_after = rate_limiter.is_allowed(
|
||||
client_id,
|
||||
max_tokens=max_per_minute,
|
||||
refill_rate=refill_rate
|
||||
)
|
||||
|
||||
if not is_allowed:
|
||||
response = jsonify({
|
||||
'error': 'rate_limit_exceeded',
|
||||
'message': f'Too many requests. Please try again in {retry_after} seconds.',
|
||||
'retry_after': retry_after
|
||||
})
|
||||
response.status_code = 429
|
||||
response.headers['Retry-After'] = str(retry_after)
|
||||
response.headers['X-RateLimit-Limit'] = str(max_per_minute)
|
||||
response.headers['X-RateLimit-Remaining'] = '0'
|
||||
return response
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
def get_rate_limit_headers(client_id, max_per_minute=60):
|
||||
"""
|
||||
Get rate limit headers for response
|
||||
|
||||
Returns dict of headers to add to response
|
||||
"""
|
||||
with rate_limiter.lock:
|
||||
client = rate_limiter.clients.get(client_id, {'tokens': max_per_minute})
|
||||
remaining = int(client.get('tokens', max_per_minute))
|
||||
|
||||
return {
|
||||
'X-RateLimit-Limit': str(max_per_minute),
|
||||
'X-RateLimit-Remaining': str(max(0, remaining)),
|
||||
'X-RateLimit-Reset': str(int(time.time() + 60))
|
||||
}
|
||||
Reference in New Issue
Block a user