""" 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", ]