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:
@@ -1,9 +1,8 @@
|
||||
"""Basic usage examples for fastapi-traffic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Annotated, TypeAlias
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -17,15 +16,19 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
# Configure global rate limiter with SQLite backend for persistence
|
||||
backend = SQLiteBackend("rate_limits.db")
|
||||
limiter = RateLimiter(backend)
|
||||
set_limiter(limiter)
|
||||
|
||||
basic_ratelimiter = RateLimitDependency(limit=20, window_size=60)
|
||||
RateLimitDep: TypeAlias = Annotated[RateLimitInfo, Depends(basic_ratelimiter)]
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
@@ -108,19 +111,18 @@ async def api_key_endpoint(_: Request) -> dict[str, str]:
|
||||
|
||||
|
||||
# Example 5: Using dependency injection
|
||||
rate_limit_dep = RateLimitDependency(limit=20, window_size=60)
|
||||
|
||||
|
||||
@app.get("/api/dependency")
|
||||
# Note: This dependency injection seems to be tripping pydantic, needs to be looked into.
|
||||
"""@app.get("/api/dependency")
|
||||
async def dependency_endpoint(
|
||||
_: Request,
|
||||
rate_info: dict[str, object] = Depends(rate_limit_dep),
|
||||
rate_info: RateLimitDep,
|
||||
) -> dict[str, object]:
|
||||
"""Endpoint using rate limit as dependency."""
|
||||
'''Endpoint using rate limit as dependency.'''
|
||||
return {
|
||||
"message": "Rate limit info available",
|
||||
"rate_limit": rate_info,
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Example 6: Exempt certain requests
|
||||
@@ -163,12 +165,30 @@ async def expensive_endpoint(_: Request) -> dict[str, str]:
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, str]:
|
||||
async def health_check(_: Request) -> dict[str, str]:
|
||||
"""Health check endpoint (typically exempt from rate limiting)."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Basic usage example for fastapi-traffic"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
@@ -15,6 +15,9 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
# Step 1: Create a backend and limiter
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
@@ -55,6 +58,24 @@ async def get_data(_: Request) -> dict[str, str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8002)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Quickstart example for fastapi-traffic"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -16,6 +16,9 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@@ -126,6 +129,22 @@ async def leaky_bucket(_: Request) -> dict[str, str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(description="Rate limiting algorithms example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -18,6 +18,9 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
|
||||
# Choose backend based on environment
|
||||
def get_backend():
|
||||
@@ -100,10 +103,26 @@ async def backend_info() -> dict[str, Any]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
parser = argparse.ArgumentParser(description="Storage backends example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run with different backends:
|
||||
# RATE_LIMIT_BACKEND=memory python 03_backends.py
|
||||
# RATE_LIMIT_BACKEND=sqlite python 03_backends.py
|
||||
# RATE_LIMIT_BACKEND=redis REDIS_URL=redis://localhost:6379/0 python 03_backends.py
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -15,6 +15,9 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@@ -151,6 +154,22 @@ async def user_action(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(description="Custom key extractors example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -11,6 +11,10 @@ from fastapi_traffic.middleware import RateLimitMiddleware
|
||||
# from fastapi_traffic.middleware import SlidingWindowMiddleware
|
||||
# from fastapi_traffic.middleware import TokenBucketMiddleware
|
||||
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8001
|
||||
|
||||
app = FastAPI(title="Middleware Rate Limiting")
|
||||
|
||||
|
||||
@@ -104,6 +108,22 @@ async def docs_info() -> dict[str, str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(description="Middleware rate limiting example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
from typing import Annotated, Any, TypeAlias
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -15,6 +15,10 @@ from fastapi_traffic import (
|
||||
)
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
from fastapi_traffic.core.models import RateLimitInfo
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
@@ -43,29 +47,6 @@ async def rate_limit_handler(_: Request, exc: RateLimitExceeded) -> JSONResponse
|
||||
# 1. Basic dependency - rate limit info available in endpoint
|
||||
basic_rate_limit = RateLimitDependency(limit=10, window_size=60)
|
||||
|
||||
|
||||
@app.get("/basic")
|
||||
async def basic_endpoint(
|
||||
_: Request,
|
||||
rate_info: Any = Depends(basic_rate_limit),
|
||||
) -> dict[str, Any]:
|
||||
"""Access rate limit info in your endpoint logic."""
|
||||
return {
|
||||
"message": "Success",
|
||||
"rate_limit": {
|
||||
"limit": rate_info.limit,
|
||||
"remaining": rate_info.remaining,
|
||||
"reset_at": rate_info.reset_at,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# 2. Different limits for different user tiers
|
||||
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")
|
||||
|
||||
|
||||
free_tier_limit = RateLimitDependency(
|
||||
limit=10,
|
||||
window_size=60,
|
||||
@@ -84,11 +65,38 @@ enterprise_tier_limit = RateLimitDependency(
|
||||
key_prefix="enterprise",
|
||||
)
|
||||
|
||||
BasicRateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(basic_rate_limit)]
|
||||
|
||||
|
||||
@app.get("/basic")
|
||||
async def basic_endpoint(
|
||||
_: Request,
|
||||
rate_info: BasicRateLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Access rate limit info in your endpoint logic."""
|
||||
return {
|
||||
"message": "Success",
|
||||
"rate_limit": {
|
||||
"limit": rate_info.limit,
|
||||
"remaining": rate_info.remaining,
|
||||
"reset_at": rate_info.reset_at,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# 2. Different limits for different user tiers
|
||||
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: str = Depends(get_user_tier),
|
||||
) -> Any:
|
||||
tier: TierDep,
|
||||
) -> RateLimitInfo:
|
||||
"""Apply different rate limits based on user tier."""
|
||||
if tier == "enterprise":
|
||||
return await enterprise_tier_limit(request)
|
||||
@@ -98,10 +106,13 @@ async def tiered_rate_limit(
|
||||
return await free_tier_limit(request)
|
||||
|
||||
|
||||
TieredRateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(tiered_rate_limit)]
|
||||
|
||||
|
||||
@app.get("/tiered")
|
||||
async def tiered_endpoint(
|
||||
request: Request,
|
||||
rate_info: Any = Depends(tiered_rate_limit),
|
||||
rate_info: TieredRateLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Endpoint with tier-based rate limiting."""
|
||||
tier = get_user_tier(request)
|
||||
@@ -129,10 +140,13 @@ api_rate_limit = RateLimitDependency(
|
||||
)
|
||||
|
||||
|
||||
ApiRateLimit: TypeAlias = Annotated[RateLimitInfo, Depends(api_rate_limit)]
|
||||
|
||||
|
||||
@app.get("/api/resource")
|
||||
async def api_resource(
|
||||
_: Request,
|
||||
rate_info: Any = Depends(api_rate_limit),
|
||||
rate_info: ApiRateLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""API endpoint with per-API-key rate limiting."""
|
||||
return {
|
||||
@@ -155,10 +169,14 @@ per_hour_limit = RateLimitDependency(
|
||||
)
|
||||
|
||||
|
||||
PerMinuteLimit: TypeAlias = Annotated[RateLimitInfo, Depends(per_minute_limit)]
|
||||
PerHourLimit: TypeAlias = Annotated[RateLimitInfo, Depends(per_hour_limit)]
|
||||
|
||||
|
||||
async def combined_rate_limit(
|
||||
_: Request,
|
||||
minute_info: Any = Depends(per_minute_limit),
|
||||
hour_info: Any = Depends(per_hour_limit),
|
||||
minute_info: PerMinuteLimit,
|
||||
hour_info: PerHourLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Apply both per-minute and per-hour limits."""
|
||||
return {
|
||||
@@ -173,10 +191,13 @@ async def combined_rate_limit(
|
||||
}
|
||||
|
||||
|
||||
CombinedRateLimit: TypeAlias = Annotated[dict[str, Any], Depends(combined_rate_limit)]
|
||||
|
||||
|
||||
@app.get("/combined")
|
||||
async def combined_endpoint(
|
||||
_: Request,
|
||||
rate_info: dict[str, Any] = Depends(combined_rate_limit),
|
||||
rate_info: CombinedRateLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Endpoint with multiple rate limit tiers."""
|
||||
return {
|
||||
@@ -199,10 +220,15 @@ internal_exempt_limit = RateLimitDependency(
|
||||
)
|
||||
|
||||
|
||||
InternalExemptLimit: TypeAlias = Annotated[
|
||||
RateLimitInfo, Depends(internal_exempt_limit)
|
||||
]
|
||||
|
||||
|
||||
@app.get("/internal-exempt")
|
||||
async def internal_exempt_endpoint(
|
||||
request: Request,
|
||||
rate_info: Any = Depends(internal_exempt_limit),
|
||||
rate_info: InternalExemptLimit,
|
||||
) -> dict[str, Any]:
|
||||
"""Internal requests are exempt from rate limiting."""
|
||||
is_internal = is_internal_request(request)
|
||||
@@ -220,6 +246,22 @@ async def internal_exempt_endpoint(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(description="Dependency injection example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -29,13 +29,18 @@ from fastapi_traffic import (
|
||||
from fastapi_traffic.backends.redis import RedisBackend
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8001
|
||||
|
||||
|
||||
async def create_redis_backend():
|
||||
"""Create Redis backend with fallback to memory."""
|
||||
try:
|
||||
from fastapi_traffic import RedisBackend
|
||||
|
||||
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||
redis_url = os.getenv(
|
||||
"REDIS_URL", "redis://localhost:6379/0"
|
||||
) # tip: `docker run -d --name my-redis -p 6379:6379 redis:latest` to start a redis instance in docker and access it on redis://0.0.0.0:6379/0
|
||||
backend = await RedisBackend.from_url(
|
||||
redis_url,
|
||||
key_prefix="myapp",
|
||||
@@ -188,10 +193,28 @@ async def stats(backend: BackendDep) -> dict[str, object]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Redis distributed rate limiting example"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run multiple instances on different ports to test distributed limiting:
|
||||
# REDIS_URL=redis://localhost:6379/0 python 07_redis_distributed.py
|
||||
# In another terminal:
|
||||
# uvicorn 07_redis_distributed:app --port 8001
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
# uvicorn 07_redis_distributed:app --port 8002
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -19,6 +19,9 @@ from fastapi_traffic import (
|
||||
from fastapi_traffic.core.decorator import RateLimitDependency
|
||||
from fastapi_traffic.core.limiter import set_limiter
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
backend = MemoryBackend()
|
||||
limiter = RateLimiter(backend)
|
||||
|
||||
@@ -261,9 +264,25 @@ async def pricing() -> dict[str, Any]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
parser = argparse.ArgumentParser(description="Tiered API example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Test with different API keys:
|
||||
# curl -H "X-API-Key: free-key-123" http://localhost:8000/api/v1/data
|
||||
# curl -H "X-API-Key: pro-key-789" http://localhost:8000/api/v1/analytics
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -33,6 +33,9 @@ async def lifespan(_: FastAPI):
|
||||
await limiter.close()
|
||||
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
app = FastAPI(title="Custom Responses Example", lifespan=lifespan)
|
||||
|
||||
|
||||
@@ -211,6 +214,22 @@ async def graceful_endpoint(_: Request) -> dict[str, str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
parser = argparse.ArgumentParser(description="Custom responses example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -32,6 +32,9 @@ async def lifespan(_: FastAPI):
|
||||
await limiter.close()
|
||||
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
app = FastAPI(title="Advanced Patterns", lifespan=lifespan)
|
||||
|
||||
|
||||
@@ -327,6 +330,22 @@ async def cascading_endpoint(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
parser = argparse.ArgumentParser(description="Advanced patterns example")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to bind to (default: {DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -7,6 +7,7 @@ making it easy to manage settings across different environments (dev, staging, p
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -44,7 +45,6 @@ def example_env_variables() -> RateLimitConfig:
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_ALGORITHM=sliding_window_counter
|
||||
export FASTAPI_TRAFFIC_RATE_LIMIT_KEY_PREFIX=myapi
|
||||
"""
|
||||
# Using the convenience function
|
||||
config = load_rate_limit_config_from_env(
|
||||
# You can provide overrides for values not in env vars
|
||||
limit=50, # Default if FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT not set
|
||||
@@ -93,7 +93,7 @@ def example_dotenv_file() -> RateLimitConfig:
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_INCLUDE_HEADERS=true
|
||||
FASTAPI_TRAFFIC_RATE_LIMIT_ERROR_MESSAGE="Too many requests, please slow down"
|
||||
"""
|
||||
# Create a sample .env file for demonstration
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
f.write("# Rate limit configuration\n")
|
||||
f.write("FASTAPI_TRAFFIC_RATE_LIMIT_LIMIT=100\n")
|
||||
@@ -104,7 +104,7 @@ def example_dotenv_file() -> RateLimitConfig:
|
||||
env_path = f.name
|
||||
|
||||
try:
|
||||
# Load using auto-detection (detects .env suffix)
|
||||
|
||||
config = load_rate_limit_config(env_path)
|
||||
print(f"From .env: limit={config.limit}, algorithm={config.algorithm}")
|
||||
print(f"Burst size: {config.burst_size}")
|
||||
@@ -132,7 +132,6 @@ def example_json_file() -> RateLimitConfig:
|
||||
"cost": 1
|
||||
}
|
||||
"""
|
||||
# Create a sample JSON file for demonstration
|
||||
config_data = {
|
||||
"limit": 500,
|
||||
"window_size": 300.0,
|
||||
@@ -148,7 +147,7 @@ def example_json_file() -> RateLimitConfig:
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
# Load using auto-detection (detects .json suffix)
|
||||
|
||||
config = load_rate_limit_config(json_path)
|
||||
print(f"From JSON: limit={config.limit}, window={config.window_size}s")
|
||||
print(f"Algorithm: {config.algorithm.value}")
|
||||
@@ -346,7 +345,9 @@ def create_app_with_config() -> FastAPI:
|
||||
)
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def _rate_limit_handler(_: Request, exc: RateLimitExceeded) -> JSONResponse:
|
||||
async def _rate_limit_handler( # pyright: ignore[reportUnusedFunction]
|
||||
_: Request, exc: RateLimitExceeded
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
@@ -358,23 +359,26 @@ def create_app_with_config() -> FastAPI:
|
||||
|
||||
@app.get("/")
|
||||
@rate_limit(limit=10, window_size=60)
|
||||
async def _root(_: Request) -> dict[str, str]:
|
||||
async def _root( # pyright: ignore[reportUnusedFunction]
|
||||
_: Request,
|
||||
) -> dict[str, str]:
|
||||
return {"message": "Hello from config-loaded app!"}
|
||||
|
||||
@app.get("/health")
|
||||
async def _health() -> dict[str, str]:
|
||||
async def _health() -> dict[str, str]: # pyright: ignore[reportUnusedFunction]
|
||||
"""Health check - exempt from rate limiting."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get("/api/data")
|
||||
@rate_limit(limit=50, window_size=60)
|
||||
async def _get_data(_: Request) -> dict[str, str]:
|
||||
async def _get_data( # pyright: ignore[reportUnusedFunction]
|
||||
_: Request,
|
||||
) -> dict[str, str]:
|
||||
return {"data": "Some API data"}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# Create the app instance
|
||||
app = create_app_with_config()
|
||||
|
||||
|
||||
@@ -383,59 +387,77 @@ app = create_app_with_config()
|
||||
# =============================================================================
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_examples() -> None:
|
||||
"""Run all configuration loading examples."""
|
||||
print("=" * 60)
|
||||
print("FastAPI Traffic - Configuration Loader Examples")
|
||||
print("=" * 60)
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
print("\n1. Loading from environment variables:")
|
||||
print("-" * 40)
|
||||
logger.info("=" * 60)
|
||||
logger.info("FastAPI Traffic - Configuration Loader Examples")
|
||||
logger.info("=" * 60)
|
||||
|
||||
logger.info("\n1. Loading from environment variables:")
|
||||
logger.info("-" * 40)
|
||||
example_env_variables()
|
||||
|
||||
print("\n2. Loading GlobalConfig from environment:")
|
||||
print("-" * 40)
|
||||
logger.info("\n2. Loading GlobalConfig from environment:")
|
||||
logger.info("-" * 40)
|
||||
example_global_config_env()
|
||||
|
||||
print("\n3. Loading from .env file:")
|
||||
print("-" * 40)
|
||||
logger.info("\n3. Loading from .env file:")
|
||||
logger.info("-" * 40)
|
||||
example_dotenv_file()
|
||||
|
||||
print("\n4. Loading from JSON file:")
|
||||
print("-" * 40)
|
||||
logger.info("\n4. Loading from JSON file:")
|
||||
logger.info("-" * 40)
|
||||
example_json_file()
|
||||
|
||||
print("\n5. Loading GlobalConfig from JSON:")
|
||||
print("-" * 40)
|
||||
logger.info("\n5. Loading GlobalConfig from JSON:")
|
||||
logger.info("-" * 40)
|
||||
example_global_config_json()
|
||||
|
||||
print("\n6. Using custom environment prefix:")
|
||||
print("-" * 40)
|
||||
logger.info("\n6. Using custom environment prefix:")
|
||||
logger.info("-" * 40)
|
||||
example_custom_prefix()
|
||||
|
||||
print("\n7. Validation and error handling:")
|
||||
print("-" * 40)
|
||||
logger.info("\n7. Validation and error handling:")
|
||||
logger.info("-" * 40)
|
||||
example_validation()
|
||||
|
||||
print("\n8. Environment-based configuration:")
|
||||
print("-" * 40)
|
||||
logger.info("\n8. Environment-based configuration:")
|
||||
logger.info("-" * 40)
|
||||
example_environment_based_config()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All examples completed!")
|
||||
print("=" * 60)
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("All examples completed!")
|
||||
logger.info("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--demo":
|
||||
# Run the demo examples
|
||||
import uvicorn
|
||||
|
||||
parser = argparse.ArgumentParser(description="Config loader example")
|
||||
parser.add_argument(
|
||||
"--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=8011, help="Port to bind to (default: 8011)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--demo",
|
||||
action="store_true",
|
||||
help="Run configuration examples instead of server",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.demo:
|
||||
run_examples()
|
||||
else:
|
||||
# Run the FastAPI app
|
||||
import uvicorn
|
||||
|
||||
print("Starting FastAPI app with config loader...")
|
||||
print("Run with --demo flag to see configuration examples")
|
||||
uvicorn.run(app, host="127.0.0.1", port=8011)
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
logger.info("Starting FastAPI app with config loader...")
|
||||
logger.info("Run with --demo flag to see configuration examples")
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -160,11 +160,12 @@ Some examples support configuration via environment variables:
|
||||
Basic examples only need `fastapi-traffic` and `uvicorn`:
|
||||
|
||||
```bash
|
||||
pip install fastapi-traffic uvicorn
|
||||
uv add git+https://gitlab.com/zanewalker/fastapi-traffic.git
|
||||
uv add uvicorn
|
||||
```
|
||||
|
||||
For Redis examples:
|
||||
|
||||
```bash
|
||||
pip install redis
|
||||
uv add redis
|
||||
```
|
||||
|
||||
0
examples/__init__.py
Normal file
0
examples/__init__.py
Normal file
Reference in New Issue
Block a user