| """
|
| 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_VERSION = "1.0.0"
|
| SUPPORTED_VERSIONS = ["1.0.0", "1.0.0-beta", "0.9.0"]
|
|
|
|
|
| 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:
|
|
|
| client_version = request.headers.get("Accept-Version")
|
|
|
|
|
| if not client_version:
|
| client_version = self._extract_version_from_path(request.url.path)
|
|
|
|
|
| if not client_version:
|
| client_version = self.default_version
|
|
|
|
|
| is_supported, version_to_use = self._validate_version(client_version)
|
|
|
|
|
| request.state.api_version = version_to_use
|
| request.state.client_version = client_version
|
|
|
|
|
| response = await call_next(request)
|
|
|
|
|
| response.headers["X-API-Version"] = version_to_use
|
| response.headers["X-Supported-Versions"] = ",".join(SUPPORTED_VERSIONS)
|
|
|
|
|
| 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)
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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",
|
| ]
|
|
|