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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user