aegislm / backend /api /middleware /api_versioning.py
ACA050's picture
Upload 50 files
1a4aa87 verified
"""
API Versioning Middleware
Ensures consistent API versioning across all endpoints.
"""
import re
from typing import Optional, Callable
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from backend.logging.logger import get_logger
# Current API version
CURRENT_VERSION = "1.0.0"
SUPPORTED_VERSIONS = ["1.0.0", "1.0.0-beta", "0.9.0"]
# Version compatibility matrix
VERSION_COMPATIBILITY = {
"1.0.0": {
"min_client": "1.0.0",
"features": ["evaluation", "certification", "monitoring", "leaderboard", "risk_passport"],
"breaking": [],
},
"1.0.0-beta": {
"min_client": "0.9.0",
"features": ["evaluation", "certification", "monitoring", "leaderboard"],
"breaking": ["risk_passport"],
},
"0.9.0": {
"min_client": "0.9.0",
"features": ["evaluation", "certification"],
"breaking": ["monitoring", "leaderboard", "risk_passport"],
},
}
class APIVersioningMiddleware(BaseHTTPMiddleware):
"""
Middleware to handle API versioning.
- Extracts version from Accept-Version header or URL path
- Validates version compatibility
- Adds version headers to responses
- Handles deprecation warnings
"""
def __init__(self, app, default_version: str = CURRENT_VERSION):
super().__init__(app)
self.default_version = default_version
self.logger = get_logger("api.versioning")
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Extract version from header
client_version = request.headers.get("Accept-Version")
# Extract version from URL path if header not present
if not client_version:
client_version = self._extract_version_from_path(request.url.path)
# Use default if no version specified
if not client_version:
client_version = self.default_version
# Validate version
is_supported, version_to_use = self._validate_version(client_version)
# Add version info to request state
request.state.api_version = version_to_use
request.state.client_version = client_version
# Process request
response = await call_next(request)
# Add version headers to response
response.headers["X-API-Version"] = version_to_use
response.headers["X-Supported-Versions"] = ",".join(SUPPORTED_VERSIONS)
# Add deprecation warning if needed
if self._is_deprecated(version_to_use):
response.headers["X-API-Deprecated"] = "true"
response.headers["X-API-Deprecation-Date"] = self._get_deprecation_date(version_to_use)
# Return appropriate response
if not is_supported:
return JSONResponse(
status_code=400,
content={
"error": "Unsupported API Version",
"message": f"Version {client_version} is not supported",
"supported_versions": SUPPORTED_VERSIONS,
"current_version": CURRENT_VERSION,
},
headers={
"X-API-Version": version_to_use,
"X-Supported-Versions": ",".join(SUPPORTED_VERSIONS),
}
)
return response
def _extract_version_from_path(self, path: str) -> Optional[str]:
"""Extract version from URL path like /api/v1/..."""
match = re.match(r'/api/(v\d+(?:\.\d+)?(?:-[a-z]+)?)', path)
if match:
version = match.group(1).replace("v", "")
return version
return None
def _validate_version(self, version: str) -> tuple[bool, str]:
"""Validate if version is supported, return (is_supported, effective_version)"""
if version in SUPPORTED_VERSIONS:
return True, version
# Try to find compatible version
# For now, just return current version if not found
return False, CURRENT_VERSION
def _is_deprecated(self, version: str) -> bool:
"""Check if version is deprecated"""
return version in ["0.9.0"]
def _get_deprecation_date(self, version: str) -> str:
"""Get deprecation date for version"""
deprecation_dates = {
"0.9.0": "2024-12-31",
}
return deprecation_dates.get(version, "Unknown")
def get_api_version(request: Request) -> str:
"""
Get the API version for a request.
Args:
request: FastAPI request object
Returns:
API version string
"""
return getattr(request.state, "api_version", CURRENT_VERSION)
def require_feature(request: Request, feature: str) -> bool:
"""
Check if a feature is available for the client's API version.
Args:
request: FastAPI request object
feature: Feature name to check
Returns:
True if feature is available
"""
version = get_api_version(request)
config = VERSION_COMPATIBILITY.get(version, VERSION_COMPATIBILITY[CURRENT_VERSION])
return feature in config.get("features", [])
__all__ = [
"APIVersioningMiddleware",
"CURRENT_VERSION",
"SUPPORTED_VERSIONS",
"VERSION_COMPATIBILITY",
"get_api_version",
"require_feature",
]