release: bump version to 0.3.0
- Refactor Redis backend connection handling and pool management - Update algorithm implementations with improved type annotations - Enhance config loader validation with stricter Pydantic schemas - Improve decorator and middleware error handling - Expand example scripts with better docstrings and usage patterns - Add new 00_basic_usage.py example for quick start - Reorganize examples directory structure - Fix type annotation inconsistencies across core modules - Update dependencies in pyproject.toml
This commit is contained in:
211
docs/api/algorithms.rst
Normal file
211
docs/api/algorithms.rst
Normal file
@@ -0,0 +1,211 @@
|
||||
Algorithms API
|
||||
==============
|
||||
|
||||
Rate limiting algorithms and the factory function to create them.
|
||||
|
||||
Algorithm Enum
|
||||
--------------
|
||||
|
||||
.. py:class:: Algorithm
|
||||
|
||||
Enumeration of available rate limiting algorithms.
|
||||
|
||||
.. py:attribute:: TOKEN_BUCKET
|
||||
:value: "token_bucket"
|
||||
|
||||
Token bucket algorithm. Allows bursts up to bucket capacity, then refills
|
||||
at a steady rate.
|
||||
|
||||
.. py:attribute:: SLIDING_WINDOW
|
||||
:value: "sliding_window"
|
||||
|
||||
Sliding window log algorithm. Tracks exact timestamps for precise limiting.
|
||||
Higher memory usage.
|
||||
|
||||
.. py:attribute:: FIXED_WINDOW
|
||||
:value: "fixed_window"
|
||||
|
||||
Fixed window algorithm. Simple time-based windows. Efficient but has
|
||||
boundary issues.
|
||||
|
||||
.. py:attribute:: LEAKY_BUCKET
|
||||
:value: "leaky_bucket"
|
||||
|
||||
Leaky bucket algorithm. Smooths out request rate for consistent throughput.
|
||||
|
||||
.. py:attribute:: SLIDING_WINDOW_COUNTER
|
||||
:value: "sliding_window_counter"
|
||||
|
||||
Sliding window counter algorithm. Balances precision and efficiency.
|
||||
This is the default.
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import Algorithm, rate_limit
|
||||
|
||||
@rate_limit(100, 60, algorithm=Algorithm.TOKEN_BUCKET)
|
||||
async def endpoint(request: Request):
|
||||
return {"status": "ok"}
|
||||
|
||||
BaseAlgorithm
|
||||
-------------
|
||||
|
||||
.. py:class:: BaseAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Abstract base class for rate limiting algorithms.
|
||||
|
||||
:param limit: Maximum requests allowed in the window.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds.
|
||||
:type window_size: float
|
||||
:param backend: Storage backend for rate limit state.
|
||||
:type backend: Backend
|
||||
:param burst_size: Maximum burst size. Defaults to limit.
|
||||
:type burst_size: int | None
|
||||
|
||||
.. py:method:: check(key)
|
||||
:async:
|
||||
|
||||
Check if a request is allowed and update state.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:returns: Tuple of (allowed, RateLimitInfo).
|
||||
:rtype: tuple[bool, RateLimitInfo]
|
||||
|
||||
.. py:method:: reset(key)
|
||||
:async:
|
||||
|
||||
Reset the rate limit state for a key.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
|
||||
.. py:method:: get_state(key)
|
||||
:async:
|
||||
|
||||
Get current state without consuming a token.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:returns: Current rate limit info or None.
|
||||
:rtype: RateLimitInfo | None
|
||||
|
||||
TokenBucketAlgorithm
|
||||
--------------------
|
||||
|
||||
.. py:class:: TokenBucketAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Token bucket algorithm implementation.
|
||||
|
||||
Tokens are added to the bucket at a rate of ``limit / window_size`` per second.
|
||||
Each request consumes one token. If no tokens are available, the request is
|
||||
rejected.
|
||||
|
||||
The ``burst_size`` parameter controls the maximum bucket capacity, allowing
|
||||
short bursts of traffic.
|
||||
|
||||
**State stored:**
|
||||
|
||||
- ``tokens``: Current number of tokens in the bucket
|
||||
- ``last_update``: Timestamp of last update
|
||||
|
||||
SlidingWindowAlgorithm
|
||||
----------------------
|
||||
|
||||
.. py:class:: SlidingWindowAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Sliding window log algorithm implementation.
|
||||
|
||||
Stores the timestamp of every request within the window. Provides the most
|
||||
accurate rate limiting but uses more memory.
|
||||
|
||||
**State stored:**
|
||||
|
||||
- ``timestamps``: List of request timestamps within the window
|
||||
|
||||
FixedWindowAlgorithm
|
||||
--------------------
|
||||
|
||||
.. py:class:: FixedWindowAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Fixed window algorithm implementation.
|
||||
|
||||
Divides time into fixed windows and counts requests in each window. Simple
|
||||
and efficient, but allows up to 2x the limit at window boundaries.
|
||||
|
||||
**State stored:**
|
||||
|
||||
- ``count``: Number of requests in current window
|
||||
- ``window_start``: Start timestamp of current window
|
||||
|
||||
LeakyBucketAlgorithm
|
||||
--------------------
|
||||
|
||||
.. py:class:: LeakyBucketAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Leaky bucket algorithm implementation.
|
||||
|
||||
Requests fill a bucket that "leaks" at a constant rate. Smooths out traffic
|
||||
for consistent throughput.
|
||||
|
||||
**State stored:**
|
||||
|
||||
- ``water_level``: Current water level in the bucket
|
||||
- ``last_update``: Timestamp of last update
|
||||
|
||||
SlidingWindowCounterAlgorithm
|
||||
-----------------------------
|
||||
|
||||
.. py:class:: SlidingWindowCounterAlgorithm(limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Sliding window counter algorithm implementation.
|
||||
|
||||
Maintains counters for current and previous windows, calculating a weighted
|
||||
average based on window progress. Balances precision and memory efficiency.
|
||||
|
||||
**State stored:**
|
||||
|
||||
- ``prev_count``: Count from previous window
|
||||
- ``curr_count``: Count in current window
|
||||
- ``current_window``: Start timestamp of current window
|
||||
|
||||
get_algorithm
|
||||
-------------
|
||||
|
||||
.. py:function:: get_algorithm(algorithm, limit, window_size, backend, *, burst_size=None)
|
||||
|
||||
Factory function to create algorithm instances.
|
||||
|
||||
:param algorithm: The algorithm type to create.
|
||||
:type algorithm: Algorithm
|
||||
:param limit: Maximum requests allowed.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds.
|
||||
:type window_size: float
|
||||
:param backend: Storage backend.
|
||||
:type backend: Backend
|
||||
:param burst_size: Maximum burst size.
|
||||
:type burst_size: int | None
|
||||
:returns: An algorithm instance.
|
||||
:rtype: BaseAlgorithm
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.core.algorithms import get_algorithm, Algorithm
|
||||
from fastapi_traffic import MemoryBackend
|
||||
|
||||
backend = MemoryBackend()
|
||||
algorithm = get_algorithm(
|
||||
Algorithm.TOKEN_BUCKET,
|
||||
limit=100,
|
||||
window_size=60,
|
||||
backend=backend,
|
||||
burst_size=20,
|
||||
)
|
||||
|
||||
allowed, info = await algorithm.check("user:123")
|
||||
266
docs/api/backends.rst
Normal file
266
docs/api/backends.rst
Normal file
@@ -0,0 +1,266 @@
|
||||
Backends API
|
||||
============
|
||||
|
||||
Storage backends for rate limit state.
|
||||
|
||||
Backend (Base Class)
|
||||
--------------------
|
||||
|
||||
.. py:class:: Backend
|
||||
|
||||
Abstract base class for rate limit storage backends.
|
||||
|
||||
All backends must implement these methods:
|
||||
|
||||
.. py:method:: get(key)
|
||||
:async:
|
||||
|
||||
Get the current state for a key.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:returns: The stored state dictionary or None if not found.
|
||||
:rtype: dict[str, Any] | None
|
||||
|
||||
.. py:method:: set(key, value, *, ttl)
|
||||
:async:
|
||||
|
||||
Set the state for a key with TTL.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:param value: The state dictionary to store.
|
||||
:type value: dict[str, Any]
|
||||
:param ttl: Time-to-live in seconds.
|
||||
:type ttl: float
|
||||
|
||||
.. py:method:: delete(key)
|
||||
:async:
|
||||
|
||||
Delete the state for a key.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
|
||||
.. py:method:: exists(key)
|
||||
:async:
|
||||
|
||||
Check if a key exists.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:returns: True if the key exists.
|
||||
:rtype: bool
|
||||
|
||||
.. py:method:: increment(key, amount=1)
|
||||
:async:
|
||||
|
||||
Atomically increment a counter.
|
||||
|
||||
:param key: The rate limit key.
|
||||
:type key: str
|
||||
:param amount: The amount to increment by.
|
||||
:type amount: int
|
||||
:returns: The new value after incrementing.
|
||||
:rtype: int
|
||||
|
||||
.. py:method:: clear()
|
||||
:async:
|
||||
|
||||
Clear all rate limit data.
|
||||
|
||||
.. py:method:: close()
|
||||
:async:
|
||||
|
||||
Close the backend connection.
|
||||
|
||||
Backends support async context manager protocol:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with MemoryBackend() as backend:
|
||||
await backend.set("key", {"count": 1}, ttl=60)
|
||||
|
||||
MemoryBackend
|
||||
-------------
|
||||
|
||||
.. py:class:: MemoryBackend(max_size=10000, cleanup_interval=60)
|
||||
|
||||
In-memory storage backend with LRU eviction and TTL cleanup.
|
||||
|
||||
:param max_size: Maximum number of keys to store.
|
||||
:type max_size: int
|
||||
:param cleanup_interval: How often to clean expired entries (seconds).
|
||||
:type cleanup_interval: float
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import MemoryBackend, RateLimiter
|
||||
|
||||
backend = MemoryBackend(max_size=10000)
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
.. py:method:: get_stats()
|
||||
|
||||
Get statistics about the backend.
|
||||
|
||||
:returns: Dictionary with stats like key count, memory usage.
|
||||
:rtype: dict[str, Any]
|
||||
|
||||
.. py:method:: start_cleanup()
|
||||
:async:
|
||||
|
||||
Start the background cleanup task.
|
||||
|
||||
.. py:method:: stop_cleanup()
|
||||
:async:
|
||||
|
||||
Stop the background cleanup task.
|
||||
|
||||
SQLiteBackend
|
||||
-------------
|
||||
|
||||
.. py:class:: SQLiteBackend(db_path, cleanup_interval=300)
|
||||
|
||||
SQLite storage backend for persistent rate limiting.
|
||||
|
||||
:param db_path: Path to the SQLite database file.
|
||||
:type db_path: str | Path
|
||||
:param cleanup_interval: How often to clean expired entries (seconds).
|
||||
:type cleanup_interval: float
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import SQLiteBackend, RateLimiter
|
||||
|
||||
backend = SQLiteBackend("rate_limits.db")
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await limiter.initialize()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await limiter.close()
|
||||
|
||||
.. py:method:: initialize()
|
||||
:async:
|
||||
|
||||
Initialize the database schema.
|
||||
|
||||
Features:
|
||||
|
||||
- WAL mode for better concurrent performance
|
||||
- Automatic schema creation
|
||||
- Connection pooling
|
||||
- Background cleanup of expired entries
|
||||
|
||||
RedisBackend
|
||||
------------
|
||||
|
||||
.. py:class:: RedisBackend
|
||||
|
||||
Redis storage backend for distributed rate limiting.
|
||||
|
||||
.. py:method:: from_url(url, *, key_prefix="", **kwargs)
|
||||
:classmethod:
|
||||
|
||||
Create a RedisBackend from a Redis URL. This is an async classmethod.
|
||||
|
||||
:param url: Redis connection URL.
|
||||
:type url: str
|
||||
:param key_prefix: Prefix for all keys.
|
||||
:type key_prefix: str
|
||||
:returns: Configured RedisBackend instance.
|
||||
:rtype: RedisBackend
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.backends.redis import RedisBackend
|
||||
from fastapi_traffic import RateLimiter
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
backend = await RedisBackend.from_url("redis://localhost:6379/0")
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
**Connection examples:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Simple connection
|
||||
backend = await RedisBackend.from_url("redis://localhost:6379/0")
|
||||
|
||||
# With password
|
||||
backend = await RedisBackend.from_url("redis://:password@localhost:6379/0")
|
||||
|
||||
# With key prefix
|
||||
backend = await RedisBackend.from_url(
|
||||
"redis://localhost:6379/0",
|
||||
key_prefix="myapp:ratelimit:",
|
||||
)
|
||||
|
||||
.. py:method:: get_stats()
|
||||
:async:
|
||||
|
||||
Get statistics about the Redis backend.
|
||||
|
||||
:returns: Dictionary with stats like key count, memory usage.
|
||||
:rtype: dict[str, Any]
|
||||
|
||||
Features:
|
||||
|
||||
- Atomic operations via Lua scripts
|
||||
- Automatic key expiration
|
||||
- Connection pooling
|
||||
- Support for Redis Sentinel and Cluster
|
||||
|
||||
Implementing Custom Backends
|
||||
----------------------------
|
||||
|
||||
To create a custom backend, inherit from ``Backend`` and implement all abstract
|
||||
methods:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.backends.base import Backend
|
||||
from typing import Any
|
||||
|
||||
class MyBackend(Backend):
|
||||
async def get(self, key: str) -> dict[str, Any] | None:
|
||||
# Retrieve state from your storage
|
||||
pass
|
||||
|
||||
async def set(self, key: str, value: dict[str, Any], *, ttl: float) -> None:
|
||||
# Store state with expiration
|
||||
pass
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
# Remove a key
|
||||
pass
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
# Check if key exists
|
||||
pass
|
||||
|
||||
async def increment(self, key: str, amount: int = 1) -> int:
|
||||
# Atomically increment (important for accuracy)
|
||||
pass
|
||||
|
||||
async def clear(self) -> None:
|
||||
# Clear all data
|
||||
pass
|
||||
|
||||
async def close(self) -> None:
|
||||
# Clean up connections
|
||||
pass
|
||||
|
||||
The ``value`` dictionary contains algorithm-specific state. Your backend should
|
||||
serialize it appropriately (JSON works well for most cases).
|
||||
245
docs/api/config.rst
Normal file
245
docs/api/config.rst
Normal file
@@ -0,0 +1,245 @@
|
||||
Configuration API
|
||||
=================
|
||||
|
||||
Configuration classes and loaders for rate limiting.
|
||||
|
||||
RateLimitConfig
|
||||
---------------
|
||||
|
||||
.. py:class:: RateLimitConfig(limit, window_size=60.0, algorithm=Algorithm.SLIDING_WINDOW_COUNTER, key_prefix="ratelimit", key_extractor=default_key_extractor, burst_size=None, include_headers=True, error_message="Rate limit exceeded", status_code=429, skip_on_error=False, cost=1, exempt_when=None, on_blocked=None)
|
||||
|
||||
Configuration for a rate limit rule.
|
||||
|
||||
:param limit: Maximum requests allowed in the window. Must be positive.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds. Must be positive.
|
||||
:type window_size: float
|
||||
:param algorithm: Rate limiting algorithm to use.
|
||||
:type algorithm: Algorithm
|
||||
:param key_prefix: Prefix for the rate limit key.
|
||||
:type key_prefix: str
|
||||
:param key_extractor: Function to extract client identifier from request.
|
||||
:type key_extractor: Callable[[Request], str]
|
||||
:param burst_size: Maximum burst size for token/leaky bucket.
|
||||
:type burst_size: int | None
|
||||
:param include_headers: Whether to include rate limit headers.
|
||||
:type include_headers: bool
|
||||
:param error_message: Error message when rate limited.
|
||||
:type error_message: str
|
||||
:param status_code: HTTP status code when rate limited.
|
||||
:type status_code: int
|
||||
:param skip_on_error: Skip rate limiting on backend errors.
|
||||
:type skip_on_error: bool
|
||||
:param cost: Cost per request.
|
||||
:type cost: int
|
||||
:param exempt_when: Function to check if request is exempt.
|
||||
:type exempt_when: Callable[[Request], bool] | None
|
||||
:param on_blocked: Callback when request is blocked.
|
||||
:type on_blocked: Callable[[Request, Any], Any] | None
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import RateLimitConfig, Algorithm
|
||||
|
||||
config = RateLimitConfig(
|
||||
limit=100,
|
||||
window_size=60,
|
||||
algorithm=Algorithm.TOKEN_BUCKET,
|
||||
burst_size=20,
|
||||
)
|
||||
|
||||
GlobalConfig
|
||||
------------
|
||||
|
||||
.. py:class:: GlobalConfig(backend=None, enabled=True, default_limit=100, default_window_size=60.0, default_algorithm=Algorithm.SLIDING_WINDOW_COUNTER, key_prefix="fastapi_traffic", include_headers=True, error_message="Rate limit exceeded. Please try again later.", status_code=429, skip_on_error=False, exempt_ips=set(), exempt_paths=set(), headers_prefix="X-RateLimit")
|
||||
|
||||
Global configuration for the rate limiter.
|
||||
|
||||
:param backend: Storage backend for rate limit data.
|
||||
:type backend: Backend | None
|
||||
:param enabled: Whether rate limiting is enabled.
|
||||
:type enabled: bool
|
||||
:param default_limit: Default maximum requests per window.
|
||||
:type default_limit: int
|
||||
:param default_window_size: Default time window in seconds.
|
||||
:type default_window_size: float
|
||||
:param default_algorithm: Default rate limiting algorithm.
|
||||
:type default_algorithm: Algorithm
|
||||
:param key_prefix: Global prefix for all rate limit keys.
|
||||
:type key_prefix: str
|
||||
:param include_headers: Include rate limit headers by default.
|
||||
:type include_headers: bool
|
||||
:param error_message: Default error message.
|
||||
:type error_message: str
|
||||
:param status_code: Default HTTP status code.
|
||||
:type status_code: int
|
||||
:param skip_on_error: Skip rate limiting on backend errors.
|
||||
:type skip_on_error: bool
|
||||
:param exempt_ips: IP addresses exempt from rate limiting.
|
||||
:type exempt_ips: set[str]
|
||||
:param exempt_paths: URL paths exempt from rate limiting.
|
||||
:type exempt_paths: set[str]
|
||||
:param headers_prefix: Prefix for rate limit headers.
|
||||
:type headers_prefix: str
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import GlobalConfig, RateLimiter
|
||||
|
||||
config = GlobalConfig(
|
||||
enabled=True,
|
||||
default_limit=100,
|
||||
exempt_paths={"/health", "/docs"},
|
||||
exempt_ips={"127.0.0.1"},
|
||||
)
|
||||
|
||||
limiter = RateLimiter(config=config)
|
||||
|
||||
ConfigLoader
|
||||
------------
|
||||
|
||||
.. py:class:: ConfigLoader(prefix="FASTAPI_TRAFFIC")
|
||||
|
||||
Load rate limit configuration from various sources.
|
||||
|
||||
:param prefix: Environment variable prefix.
|
||||
:type prefix: str
|
||||
|
||||
.. py:method:: load_rate_limit_config_from_env(env_vars=None, **overrides)
|
||||
|
||||
Load RateLimitConfig from environment variables.
|
||||
|
||||
:param env_vars: Dictionary of environment variables. Uses os.environ if None.
|
||||
:type env_vars: dict[str, str] | None
|
||||
:param overrides: Values to override after loading.
|
||||
:returns: Loaded configuration.
|
||||
:rtype: RateLimitConfig
|
||||
|
||||
.. py:method:: load_rate_limit_config_from_json(file_path, **overrides)
|
||||
|
||||
Load RateLimitConfig from a JSON file.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:type file_path: str | Path
|
||||
:param overrides: Values to override after loading.
|
||||
:returns: Loaded configuration.
|
||||
:rtype: RateLimitConfig
|
||||
|
||||
.. py:method:: load_rate_limit_config_from_env_file(file_path, **overrides)
|
||||
|
||||
Load RateLimitConfig from a .env file.
|
||||
|
||||
:param file_path: Path to the .env file.
|
||||
:type file_path: str | Path
|
||||
:param overrides: Values to override after loading.
|
||||
:returns: Loaded configuration.
|
||||
:rtype: RateLimitConfig
|
||||
|
||||
.. py:method:: load_global_config_from_env(env_vars=None, **overrides)
|
||||
|
||||
Load GlobalConfig from environment variables.
|
||||
|
||||
.. py:method:: load_global_config_from_json(file_path, **overrides)
|
||||
|
||||
Load GlobalConfig from a JSON file.
|
||||
|
||||
.. py:method:: load_global_config_from_env_file(file_path, **overrides)
|
||||
|
||||
Load GlobalConfig from a .env file.
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import ConfigLoader
|
||||
|
||||
loader = ConfigLoader()
|
||||
|
||||
# From environment
|
||||
config = loader.load_rate_limit_config_from_env()
|
||||
|
||||
# From JSON file
|
||||
config = loader.load_rate_limit_config_from_json("config.json")
|
||||
|
||||
# From .env file
|
||||
config = loader.load_rate_limit_config_from_env_file(".env")
|
||||
|
||||
# With overrides
|
||||
config = loader.load_rate_limit_config_from_json(
|
||||
"config.json",
|
||||
limit=200, # Override the limit
|
||||
)
|
||||
|
||||
Convenience Functions
|
||||
---------------------
|
||||
|
||||
.. py:function:: load_rate_limit_config(file_path, **overrides)
|
||||
|
||||
Load RateLimitConfig with automatic format detection.
|
||||
|
||||
:param file_path: Path to config file (.json or .env).
|
||||
:type file_path: str | Path
|
||||
:returns: Loaded configuration.
|
||||
:rtype: RateLimitConfig
|
||||
|
||||
.. py:function:: load_rate_limit_config_from_env(**overrides)
|
||||
|
||||
Load RateLimitConfig from environment variables.
|
||||
|
||||
:returns: Loaded configuration.
|
||||
:rtype: RateLimitConfig
|
||||
|
||||
.. py:function:: load_global_config(file_path, **overrides)
|
||||
|
||||
Load GlobalConfig with automatic format detection.
|
||||
|
||||
:param file_path: Path to config file (.json or .env).
|
||||
:type file_path: str | Path
|
||||
:returns: Loaded configuration.
|
||||
:rtype: GlobalConfig
|
||||
|
||||
.. py:function:: load_global_config_from_env(**overrides)
|
||||
|
||||
Load GlobalConfig from environment variables.
|
||||
|
||||
:returns: Loaded configuration.
|
||||
:rtype: GlobalConfig
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import (
|
||||
load_rate_limit_config,
|
||||
load_rate_limit_config_from_env,
|
||||
)
|
||||
|
||||
# Auto-detect format
|
||||
config = load_rate_limit_config("config.json")
|
||||
config = load_rate_limit_config(".env")
|
||||
|
||||
# From environment
|
||||
config = load_rate_limit_config_from_env()
|
||||
|
||||
default_key_extractor
|
||||
---------------------
|
||||
|
||||
.. py:function:: default_key_extractor(request)
|
||||
|
||||
Extract client IP as the default rate limit key.
|
||||
|
||||
Checks in order:
|
||||
|
||||
1. ``X-Forwarded-For`` header (first IP)
|
||||
2. ``X-Real-IP`` header
|
||||
3. Direct connection IP
|
||||
4. Falls back to "unknown"
|
||||
|
||||
:param request: The incoming request.
|
||||
:type request: Request
|
||||
:returns: Client identifier string.
|
||||
:rtype: str
|
||||
154
docs/api/decorator.rst
Normal file
154
docs/api/decorator.rst
Normal file
@@ -0,0 +1,154 @@
|
||||
Decorator API
|
||||
=============
|
||||
|
||||
The ``@rate_limit`` decorator is the primary way to add rate limiting to your
|
||||
FastAPI endpoints.
|
||||
|
||||
rate_limit
|
||||
----------
|
||||
|
||||
.. py:function:: rate_limit(limit, window_size=60.0, *, algorithm=Algorithm.SLIDING_WINDOW_COUNTER, key_prefix="ratelimit", key_extractor=default_key_extractor, burst_size=None, include_headers=True, error_message="Rate limit exceeded", status_code=429, skip_on_error=False, cost=1, exempt_when=None, on_blocked=None)
|
||||
|
||||
Apply rate limiting to a FastAPI endpoint.
|
||||
|
||||
:param limit: Maximum number of requests allowed in the window.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds. Defaults to 60.
|
||||
:type window_size: float
|
||||
:param algorithm: Rate limiting algorithm to use.
|
||||
:type algorithm: Algorithm
|
||||
:param key_prefix: Prefix for the rate limit key.
|
||||
:type key_prefix: str
|
||||
:param key_extractor: Function to extract client identifier from request.
|
||||
:type key_extractor: Callable[[Request], str]
|
||||
:param burst_size: Maximum burst size for token bucket/leaky bucket algorithms.
|
||||
:type burst_size: int | None
|
||||
:param include_headers: Whether to include rate limit headers in response.
|
||||
:type include_headers: bool
|
||||
:param error_message: Error message when rate limit is exceeded.
|
||||
:type error_message: str
|
||||
:param status_code: HTTP status code when rate limit is exceeded.
|
||||
:type status_code: int
|
||||
:param skip_on_error: Skip rate limiting if backend errors occur.
|
||||
:type skip_on_error: bool
|
||||
:param cost: Cost of each request (default 1).
|
||||
:type cost: int
|
||||
:param exempt_when: Function to determine if request should be exempt.
|
||||
:type exempt_when: Callable[[Request], bool] | None
|
||||
:param on_blocked: Callback when a request is blocked.
|
||||
:type on_blocked: Callable[[Request, Any], Any] | None
|
||||
:returns: Decorated function with rate limiting applied.
|
||||
:rtype: Callable
|
||||
|
||||
**Basic usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_traffic import rate_limit
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/api/data")
|
||||
@rate_limit(100, 60) # 100 requests per minute
|
||||
async def get_data(request: Request):
|
||||
return {"data": "here"}
|
||||
|
||||
**With algorithm:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import rate_limit, Algorithm
|
||||
|
||||
@app.get("/api/burst")
|
||||
@rate_limit(100, 60, algorithm=Algorithm.TOKEN_BUCKET, burst_size=20)
|
||||
async def burst_endpoint(request: Request):
|
||||
return {"status": "ok"}
|
||||
|
||||
**With custom key extractor:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def get_api_key(request: Request) -> str:
|
||||
return request.headers.get("X-API-Key", "anonymous")
|
||||
|
||||
@app.get("/api/data")
|
||||
@rate_limit(1000, 3600, key_extractor=get_api_key)
|
||||
async def api_endpoint(request: Request):
|
||||
return {"data": "here"}
|
||||
|
||||
**With exemption:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def is_admin(request: Request) -> bool:
|
||||
return getattr(request.state, "is_admin", False)
|
||||
|
||||
@app.get("/api/admin")
|
||||
@rate_limit(100, 60, exempt_when=is_admin)
|
||||
async def admin_endpoint(request: Request):
|
||||
return {"admin": "data"}
|
||||
|
||||
RateLimitDependency
|
||||
-------------------
|
||||
|
||||
.. py:class:: RateLimitDependency(limit, window_size=60.0, *, algorithm=Algorithm.SLIDING_WINDOW_COUNTER, key_prefix="ratelimit", key_extractor=default_key_extractor, burst_size=None, error_message="Rate limit exceeded", status_code=429, skip_on_error=False, cost=1, exempt_when=None)
|
||||
:no-index:
|
||||
|
||||
FastAPI dependency for rate limiting. Returns rate limit info that can be
|
||||
used in your endpoint. See :doc:`dependency` for full documentation.
|
||||
|
||||
:param limit: Maximum number of requests allowed in the window.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds.
|
||||
:type window_size: float
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import FastAPI, Depends, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
|
||||
app = FastAPI()
|
||||
rate_dep = RateLimitDependency(limit=100, window_size=60)
|
||||
|
||||
@app.get("/api/data")
|
||||
async def get_data(request: Request, rate_info=Depends(rate_dep)):
|
||||
return {
|
||||
"data": "here",
|
||||
"remaining_requests": rate_info.remaining,
|
||||
"reset_at": rate_info.reset_at,
|
||||
}
|
||||
|
||||
The dependency returns a ``RateLimitInfo`` object with:
|
||||
|
||||
- ``limit``: The configured limit
|
||||
- ``remaining``: Remaining requests in the current window
|
||||
- ``reset_at``: Unix timestamp when the window resets
|
||||
- ``retry_after``: Seconds until retry (if rate limited)
|
||||
|
||||
create_rate_limit_response
|
||||
--------------------------
|
||||
|
||||
.. py:function:: create_rate_limit_response(exc, *, include_headers=True)
|
||||
|
||||
Create a standard rate limit response from a RateLimitExceeded exception.
|
||||
|
||||
:param exc: The RateLimitExceeded exception.
|
||||
:type exc: RateLimitExceeded
|
||||
:param include_headers: Whether to include rate limit headers.
|
||||
:type include_headers: bool
|
||||
:returns: A JSONResponse with rate limit information.
|
||||
:rtype: Response
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import RateLimitExceeded
|
||||
from fastapi_traffic.core.decorator import create_rate_limit_response
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def handler(request: Request, exc: RateLimitExceeded):
|
||||
return create_rate_limit_response(exc)
|
||||
473
docs/api/dependency.rst
Normal file
473
docs/api/dependency.rst
Normal file
@@ -0,0 +1,473 @@
|
||||
Dependency Injection API
|
||||
========================
|
||||
|
||||
If you're already using FastAPI's dependency injection system, you'll feel right
|
||||
at home with ``RateLimitDependency``. It plugs directly into ``Depends``, giving
|
||||
you rate limiting that works just like any other dependency—plus you get access
|
||||
to rate limit info right inside your endpoint.
|
||||
|
||||
RateLimitDependency
|
||||
-------------------
|
||||
|
||||
.. py:class:: RateLimitDependency(limit, window_size=60.0, *, algorithm=Algorithm.SLIDING_WINDOW_COUNTER, key_prefix="ratelimit", key_extractor=default_key_extractor, burst_size=None, error_message="Rate limit exceeded", status_code=429, skip_on_error=False, cost=1, exempt_when=None)
|
||||
|
||||
This is the main class you'll use for dependency-based rate limiting. Create
|
||||
an instance, pass it to ``Depends()``, and you're done.
|
||||
|
||||
:param limit: Maximum number of requests allowed in the window.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds. Defaults to 60.
|
||||
:type window_size: float
|
||||
:param algorithm: Rate limiting algorithm to use.
|
||||
:type algorithm: Algorithm
|
||||
:param key_prefix: Prefix for the rate limit key.
|
||||
:type key_prefix: str
|
||||
:param key_extractor: Function to extract client identifier from request.
|
||||
:type key_extractor: Callable[[Request], str]
|
||||
:param burst_size: Maximum burst size for token bucket/leaky bucket algorithms.
|
||||
:type burst_size: int | None
|
||||
:param error_message: Error message when rate limit is exceeded.
|
||||
:type error_message: str
|
||||
:param status_code: HTTP status code when rate limit is exceeded.
|
||||
:type status_code: int
|
||||
:param skip_on_error: Skip rate limiting if backend errors occur.
|
||||
:type skip_on_error: bool
|
||||
:param cost: Cost of each request (default 1).
|
||||
:type cost: int
|
||||
:param exempt_when: Function to determine if request should be exempt.
|
||||
:type exempt_when: Callable[[Request], bool] | None
|
||||
|
||||
**Returns:** A ``RateLimitInfo`` object with details about the current rate limit state.
|
||||
|
||||
RateLimitInfo
|
||||
-------------
|
||||
|
||||
When the dependency runs, it hands you back a ``RateLimitInfo`` object. Here's
|
||||
what's inside:
|
||||
|
||||
.. py:class:: RateLimitInfo
|
||||
|
||||
:param limit: The configured request limit.
|
||||
:type limit: int
|
||||
:param remaining: Remaining requests in the current window.
|
||||
:type remaining: int
|
||||
:param reset_at: Unix timestamp when the window resets.
|
||||
:type reset_at: float
|
||||
:param retry_after: Seconds until retry is allowed (if rate limited).
|
||||
:type retry_after: float | None
|
||||
:param window_size: The configured window size in seconds.
|
||||
:type window_size: float
|
||||
|
||||
.. py:method:: to_headers() -> dict[str, str]
|
||||
|
||||
Converts the rate limit info into standard HTTP headers. Handy if you want
|
||||
to add these headers to your response manually.
|
||||
|
||||
:returns: A dictionary with ``X-RateLimit-Limit``, ``X-RateLimit-Remaining``,
|
||||
``X-RateLimit-Reset``, and ``Retry-After`` (when applicable).
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
Before you can use the dependency, you need to set up the rate limiter. The
|
||||
cleanest way is with FastAPI's lifespan context manager:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi_traffic import MemoryBackend, RateLimiter
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await limiter.initialize()
|
||||
set_limiter(limiter)
|
||||
yield
|
||||
await limiter.close()
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
Here's the simplest way to get started. Create a dependency instance and inject
|
||||
it with ``Depends``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Create the rate limit dependency
|
||||
rate_limit_dep = RateLimitDependency(limit=100, window_size=60)
|
||||
|
||||
@app.get("/api/data")
|
||||
async def get_data(
|
||||
request: Request,
|
||||
rate_info=Depends(rate_limit_dep),
|
||||
):
|
||||
return {
|
||||
"data": "here",
|
||||
"remaining_requests": rate_info.remaining,
|
||||
"reset_at": rate_info.reset_at,
|
||||
}
|
||||
|
||||
Using Type Aliases
|
||||
------------------
|
||||
|
||||
If you're using the same rate limit across multiple endpoints, type aliases
|
||||
with ``Annotated`` make your code much cleaner:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated, TypeAlias
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
rate_limit_dep = RateLimitDependency(limit=100, window_size=60)
|
||||
|
||||
# Create a type alias for cleaner signatures
|
||||
RateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(rate_limit_dep)]
|
||||
|
||||
@app.get("/api/data")
|
||||
async def get_data(request: Request, rate_info: RateLimit):
|
||||
return {
|
||||
"data": "here",
|
||||
"remaining": rate_info.remaining,
|
||||
}
|
||||
|
||||
Tiered Rate Limits
|
||||
------------------
|
||||
|
||||
This is where dependency injection really shines. You can apply different rate
|
||||
limits based on who's making the request—free users get 10 requests per minute,
|
||||
pro users get 100, and enterprise gets 1000:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated, TypeAlias
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Define tier-specific limits
|
||||
free_tier_limit = RateLimitDependency(
|
||||
limit=10,
|
||||
window_size=60,
|
||||
key_prefix="free",
|
||||
)
|
||||
|
||||
pro_tier_limit = RateLimitDependency(
|
||||
limit=100,
|
||||
window_size=60,
|
||||
key_prefix="pro",
|
||||
)
|
||||
|
||||
enterprise_tier_limit = RateLimitDependency(
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
key_prefix="enterprise",
|
||||
)
|
||||
|
||||
def get_user_tier(request: Request) -> str:
|
||||
"""Get user tier from header (in real app, from JWT/database)."""
|
||||
return request.headers.get("X-User-Tier", "free")
|
||||
|
||||
TierDep: TypeAlias = Annotated[str, Depends(get_user_tier)]
|
||||
|
||||
async def tiered_rate_limit(
|
||||
request: Request,
|
||||
tier: TierDep,
|
||||
) -> RateLimitInfo:
|
||||
"""Apply different rate limits based on user tier."""
|
||||
if tier == "enterprise":
|
||||
return await enterprise_tier_limit(request)
|
||||
elif tier == "pro":
|
||||
return await pro_tier_limit(request)
|
||||
else:
|
||||
return await free_tier_limit(request)
|
||||
|
||||
TieredRateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(tiered_rate_limit)]
|
||||
|
||||
@app.get("/api/resource")
|
||||
async def get_resource(request: Request, rate_info: TieredRateLimit):
|
||||
tier = get_user_tier(request)
|
||||
return {
|
||||
"tier": tier,
|
||||
"remaining": rate_info.remaining,
|
||||
"limit": rate_info.limit,
|
||||
}
|
||||
|
||||
Custom Key Extraction
|
||||
---------------------
|
||||
|
||||
By default, rate limits are tracked by IP address. But what if you want to rate
|
||||
limit by API key instead? Just pass a custom ``key_extractor``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
def api_key_extractor(request: Request) -> str:
|
||||
"""Extract API key for rate limiting."""
|
||||
api_key = request.headers.get("X-API-Key", "anonymous")
|
||||
return f"api:{api_key}"
|
||||
|
||||
api_rate_limit = RateLimitDependency(
|
||||
limit=100,
|
||||
window_size=3600, # 100 requests per hour
|
||||
key_extractor=api_key_extractor,
|
||||
)
|
||||
|
||||
@app.get("/api/resource")
|
||||
async def api_resource(
|
||||
request: Request,
|
||||
rate_info=Depends(api_rate_limit),
|
||||
):
|
||||
return {
|
||||
"data": "Resource data",
|
||||
"requests_remaining": rate_info.remaining,
|
||||
}
|
||||
|
||||
Multiple Rate Limits
|
||||
--------------------
|
||||
|
||||
Sometimes you need layered protection—say, 10 requests per minute *and* 100
|
||||
requests per hour. Dependencies make this easy to compose:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated, Any, TypeAlias
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
per_minute_limit = RateLimitDependency(
|
||||
limit=10,
|
||||
window_size=60,
|
||||
key_prefix="minute",
|
||||
)
|
||||
|
||||
per_hour_limit = RateLimitDependency(
|
||||
limit=100,
|
||||
window_size=3600,
|
||||
key_prefix="hour",
|
||||
)
|
||||
|
||||
PerMinuteLimit: TypeAlias = Annotated[RateLimitInfo, Depends(per_minute_limit)]
|
||||
PerHourLimit: TypeAlias = Annotated[RateLimitInfo, Depends(per_hour_limit)]
|
||||
|
||||
async def combined_rate_limit(
|
||||
request: Request,
|
||||
minute_info: PerMinuteLimit,
|
||||
hour_info: PerHourLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Apply both per-minute and per-hour limits."""
|
||||
return {
|
||||
"minute": {
|
||||
"limit": minute_info.limit,
|
||||
"remaining": minute_info.remaining,
|
||||
},
|
||||
"hour": {
|
||||
"limit": hour_info.limit,
|
||||
"remaining": hour_info.remaining,
|
||||
},
|
||||
}
|
||||
|
||||
CombinedRateLimit: TypeAlias = Annotated[dict[str, Any], Depends(combined_rate_limit)]
|
||||
|
||||
@app.get("/api/combined")
|
||||
async def combined_endpoint(
|
||||
request: Request,
|
||||
rate_info: CombinedRateLimit,
|
||||
):
|
||||
return {
|
||||
"message": "Success",
|
||||
"rate_limits": rate_info,
|
||||
}
|
||||
|
||||
Exemption Logic
|
||||
---------------
|
||||
|
||||
Need to let certain requests bypass rate limiting entirely? Maybe internal
|
||||
services or admin users? Use the ``exempt_when`` parameter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
def is_internal_request(request: Request) -> bool:
|
||||
"""Check if request is from internal service."""
|
||||
internal_token = request.headers.get("X-Internal-Token")
|
||||
return internal_token == "internal-secret-token"
|
||||
|
||||
internal_exempt_limit = RateLimitDependency(
|
||||
limit=10,
|
||||
window_size=60,
|
||||
exempt_when=is_internal_request,
|
||||
)
|
||||
|
||||
@app.get("/api/internal")
|
||||
async def internal_endpoint(
|
||||
request: Request,
|
||||
rate_info=Depends(internal_exempt_limit),
|
||||
):
|
||||
is_internal = is_internal_request(request)
|
||||
return {
|
||||
"message": "Success",
|
||||
"is_internal": is_internal,
|
||||
"rate_limit": None if is_internal else {
|
||||
"remaining": rate_info.remaining,
|
||||
},
|
||||
}
|
||||
|
||||
Exception Handling
|
||||
------------------
|
||||
|
||||
When a request exceeds the rate limit, a ``RateLimitExceeded`` exception is
|
||||
raised. You'll want to catch this and return a proper response:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_traffic import RateLimitExceeded
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(
|
||||
request: Request,
|
||||
exc: RateLimitExceeded,
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": exc.message,
|
||||
"retry_after": exc.retry_after,
|
||||
},
|
||||
)
|
||||
|
||||
Or if you prefer, there's a built-in helper that does the work for you:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_traffic import RateLimitExceeded
|
||||
from fastapi_traffic.core.decorator import create_rate_limit_response
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
||||
return create_rate_limit_response(exc, include_headers=True)
|
||||
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
Here's everything put together in a working example you can copy and run:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Annotated, TypeAlias
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from fastapi_traffic import (
|
||||
MemoryBackend,
|
||||
RateLimiter,
|
||||
RateLimitExceeded,
|
||||
)
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
# Initialize backend and limiter
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await limiter.initialize()
|
||||
set_limiter(limiter)
|
||||
yield
|
||||
await limiter.close()
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
# Exception handler
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(
|
||||
request: Request,
|
||||
exc: RateLimitExceeded,
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
"error": "rate_limit_exceeded",
|
||||
"retry_after": exc.retry_after,
|
||||
},
|
||||
)
|
||||
|
||||
# Create dependency
|
||||
api_rate_limit = RateLimitDependency(limit=100, window_size=60)
|
||||
ApiRateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(api_rate_limit)]
|
||||
|
||||
@app.get("/api/data")
|
||||
async def get_data(request: Request, rate_info: ApiRateLimit):
|
||||
return {
|
||||
"data": "Your data here",
|
||||
"rate_limit": {
|
||||
"limit": rate_info.limit,
|
||||
"remaining": rate_info.remaining,
|
||||
"reset_at": rate_info.reset_at,
|
||||
},
|
||||
}
|
||||
|
||||
Decorator vs Dependency
|
||||
-----------------------
|
||||
|
||||
Not sure which approach to use? Here's a quick guide:
|
||||
|
||||
**Go with the ``@rate_limit`` decorator if:**
|
||||
|
||||
- You just want to slap a rate limit on an endpoint and move on
|
||||
- You don't care about the remaining request count inside your endpoint
|
||||
- You're applying the same limit to a bunch of endpoints
|
||||
|
||||
**Go with ``RateLimitDependency`` if:**
|
||||
|
||||
- You want to show users how many requests they have left
|
||||
- You need different limits for different user tiers
|
||||
- You're stacking multiple rate limits (per-minute + per-hour)
|
||||
- You're already using FastAPI's dependency system and want consistency
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
- :doc:`decorator` - Decorator-based rate limiting
|
||||
- :doc:`middleware` - Global middleware rate limiting
|
||||
- :doc:`config` - Configuration options
|
||||
- :doc:`exceptions` - Exception handling
|
||||
165
docs/api/exceptions.rst
Normal file
165
docs/api/exceptions.rst
Normal file
@@ -0,0 +1,165 @@
|
||||
Exceptions API
|
||||
==============
|
||||
|
||||
Custom exceptions raised by FastAPI Traffic.
|
||||
|
||||
FastAPITrafficError
|
||||
-------------------
|
||||
|
||||
.. py:exception:: FastAPITrafficError
|
||||
|
||||
Base exception for all FastAPI Traffic errors.
|
||||
|
||||
All other exceptions in this library inherit from this class, so you can
|
||||
catch all FastAPI Traffic errors with a single handler:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.exceptions import FastAPITrafficError
|
||||
|
||||
@app.exception_handler(FastAPITrafficError)
|
||||
async def handle_traffic_error(request: Request, exc: FastAPITrafficError):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": str(exc)},
|
||||
)
|
||||
|
||||
RateLimitExceeded
|
||||
-----------------
|
||||
|
||||
.. py:exception:: RateLimitExceeded(message="Rate limit exceeded", *, retry_after=None, limit_info=None)
|
||||
|
||||
Raised when a rate limit has been exceeded.
|
||||
|
||||
:param message: Error message.
|
||||
:type message: str
|
||||
:param retry_after: Seconds until the client can retry.
|
||||
:type retry_after: float | None
|
||||
:param limit_info: Detailed rate limit information.
|
||||
:type limit_info: RateLimitInfo | None
|
||||
|
||||
.. py:attribute:: message
|
||||
:type: str
|
||||
|
||||
The error message.
|
||||
|
||||
.. py:attribute:: retry_after
|
||||
:type: float | None
|
||||
|
||||
Seconds until the client can retry. May be None if not calculable.
|
||||
|
||||
.. py:attribute:: limit_info
|
||||
:type: RateLimitInfo | None
|
||||
|
||||
Detailed information about the rate limit state.
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_traffic import RateLimitExceeded
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
||||
headers = {}
|
||||
if exc.limit_info:
|
||||
headers = exc.limit_info.to_headers()
|
||||
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": exc.message,
|
||||
"retry_after": exc.retry_after,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
BackendError
|
||||
------------
|
||||
|
||||
.. py:exception:: BackendError(message="Backend operation failed", *, original_error=None)
|
||||
|
||||
Raised when a backend operation fails.
|
||||
|
||||
:param message: Error message.
|
||||
:type message: str
|
||||
:param original_error: The original exception that caused this error.
|
||||
:type original_error: Exception | None
|
||||
|
||||
.. py:attribute:: message
|
||||
:type: str
|
||||
|
||||
The error message.
|
||||
|
||||
.. py:attribute:: original_error
|
||||
:type: Exception | None
|
||||
|
||||
The underlying exception, if any.
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import BackendError
|
||||
|
||||
@app.exception_handler(BackendError)
|
||||
async def backend_error_handler(request: Request, exc: BackendError):
|
||||
# Log the original error for debugging
|
||||
if exc.original_error:
|
||||
logger.error("Backend error: %s", exc.original_error)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"error": "service_unavailable"},
|
||||
)
|
||||
|
||||
This exception is raised when:
|
||||
|
||||
- Redis connection fails
|
||||
- SQLite database is locked or corrupted
|
||||
- Any other backend storage operation fails
|
||||
|
||||
ConfigurationError
|
||||
------------------
|
||||
|
||||
.. py:exception:: ConfigurationError
|
||||
|
||||
Raised when there is a configuration error.
|
||||
|
||||
This exception is raised when:
|
||||
|
||||
- Invalid values in configuration files
|
||||
- Missing required configuration
|
||||
- Type conversion failures
|
||||
- Unknown configuration fields
|
||||
|
||||
**Usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import ConfigLoader, ConfigurationError
|
||||
|
||||
loader = ConfigLoader()
|
||||
|
||||
try:
|
||||
config = loader.load_rate_limit_config_from_json("config.json")
|
||||
except ConfigurationError as e:
|
||||
print(f"Configuration error: {e}")
|
||||
# Use default configuration
|
||||
config = RateLimitConfig(limit=100, window_size=60)
|
||||
|
||||
Exception Hierarchy
|
||||
-------------------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
FastAPITrafficError
|
||||
├── RateLimitExceeded
|
||||
├── BackendError
|
||||
└── ConfigurationError
|
||||
|
||||
All exceptions inherit from ``FastAPITrafficError``, which inherits from
|
||||
Python's built-in ``Exception``.
|
||||
118
docs/api/middleware.rst
Normal file
118
docs/api/middleware.rst
Normal file
@@ -0,0 +1,118 @@
|
||||
Middleware API
|
||||
==============
|
||||
|
||||
Middleware for applying rate limiting globally across your application.
|
||||
|
||||
RateLimitMiddleware
|
||||
-------------------
|
||||
|
||||
.. py:class:: RateLimitMiddleware(app, *, limit=100, window_size=60.0, algorithm=Algorithm.SLIDING_WINDOW_COUNTER, backend=None, key_prefix="middleware", include_headers=True, error_message="Rate limit exceeded. Please try again later.", status_code=429, skip_on_error=False, exempt_paths=None, exempt_ips=None, key_extractor=default_key_extractor)
|
||||
|
||||
Middleware for global rate limiting across all endpoints.
|
||||
|
||||
:param app: The ASGI application.
|
||||
:type app: ASGIApp
|
||||
:param limit: Maximum requests per window.
|
||||
:type limit: int
|
||||
:param window_size: Time window in seconds.
|
||||
:type window_size: float
|
||||
:param algorithm: Rate limiting algorithm.
|
||||
:type algorithm: Algorithm
|
||||
:param backend: Storage backend. Defaults to MemoryBackend.
|
||||
:type backend: Backend | None
|
||||
:param key_prefix: Prefix for rate limit keys.
|
||||
:type key_prefix: str
|
||||
:param include_headers: Include rate limit headers in response.
|
||||
:type include_headers: bool
|
||||
:param error_message: Error message when rate limited.
|
||||
:type error_message: str
|
||||
:param status_code: HTTP status code when rate limited.
|
||||
:type status_code: int
|
||||
:param skip_on_error: Skip rate limiting on backend errors.
|
||||
:type skip_on_error: bool
|
||||
:param exempt_paths: Paths to exempt from rate limiting.
|
||||
:type exempt_paths: set[str] | None
|
||||
:param exempt_ips: IP addresses to exempt from rate limiting.
|
||||
:type exempt_ips: set[str] | None
|
||||
:param key_extractor: Function to extract client identifier.
|
||||
:type key_extractor: Callable[[Request], str]
|
||||
|
||||
**Basic usage:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi_traffic.middleware import RateLimitMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
RateLimitMiddleware,
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
)
|
||||
|
||||
**With exemptions:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
app.add_middleware(
|
||||
RateLimitMiddleware,
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
exempt_paths={"/health", "/docs"},
|
||||
exempt_ips={"127.0.0.1"},
|
||||
)
|
||||
|
||||
**With custom backend:**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic import SQLiteBackend
|
||||
|
||||
backend = SQLiteBackend("rate_limits.db")
|
||||
|
||||
app.add_middleware(
|
||||
RateLimitMiddleware,
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
SlidingWindowMiddleware
|
||||
-----------------------
|
||||
|
||||
.. py:class:: SlidingWindowMiddleware(app, *, limit=100, window_size=60.0, **kwargs)
|
||||
|
||||
Convenience middleware using the sliding window algorithm.
|
||||
|
||||
Accepts all the same parameters as ``RateLimitMiddleware``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.middleware import SlidingWindowMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
SlidingWindowMiddleware,
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
)
|
||||
|
||||
TokenBucketMiddleware
|
||||
---------------------
|
||||
|
||||
.. py:class:: TokenBucketMiddleware(app, *, limit=100, window_size=60.0, **kwargs)
|
||||
|
||||
Convenience middleware using the token bucket algorithm.
|
||||
|
||||
Accepts all the same parameters as ``RateLimitMiddleware``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from fastapi_traffic.middleware import TokenBucketMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
TokenBucketMiddleware,
|
||||
limit=1000,
|
||||
window_size=60,
|
||||
)
|
||||
Reference in New Issue
Block a user