""" Standardized Response Format Provides consistent response structures for all API endpoints. All responses follow a standardized format for error handling and data delivery. """ from typing import Any, Dict, List, Optional, TypeVar, Generic from pydantic import BaseModel, Field from datetime import datetime, timezone T = TypeVar('T') # ============================================================================ # STANDARD RESPONSE MODELS # ============================================================================ class ErrorDetail(BaseModel): """Standard error detail structure""" error: str = Field(..., description="Error type/code") message: str = Field(..., description="Human-readable error message") details: Optional[Dict[str, Any]] = Field(None, description="Additional error context") timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")) class SuccessResponse(BaseModel): """Standard success response""" success: bool = True message: Optional[str] = None data: Optional[Any] = None class PaginatedResponse(BaseModel): """Standard paginated response""" items: List[Any] total: int page: int page_size: int has_more: bool class ListResponse(BaseModel): """Standard list response with metadata""" items: List[Any] count: int # ============================================================================ # RESPONSE BUILDERS # ============================================================================ def success(data: Any = None, message: str = None) -> Dict[str, Any]: """Build a success response""" response = {"success": True} if message: response["message"] = message if data is not None: response["data"] = data return response def error( error_type: str, message: str, details: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Build an error response""" return { "success": False, "error": error_type, "message": message, "details": details or {}, "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") } def paginated( items: List[Any], total: int, page: int, page_size: int ) -> Dict[str, Any]: """Build a paginated response""" return { "items": items, "total": total, "page": page, "page_size": page_size, "has_more": (page * page_size) < total } def list_response(items: List[Any], key: str = "items") -> Dict[str, Any]: """Build a simple list response with custom key""" return { key: items, "count": len(items) } def message_response(message: str) -> Dict[str, str]: """Build a simple message response""" return {"message": message} def count_response(message: str, count: int) -> Dict[str, Any]: """Build a response with count of affected items""" return {"message": message, "count": count} def id_response(resource_id: int, message: str = "Resource created successfully") -> Dict[str, Any]: """Build a response with created resource ID""" return {"id": resource_id, "message": message} def offset_paginated( items: List[Any], total: int, limit: int, offset: int, key: str = "items", include_timestamp: bool = False, **extra_fields ) -> Dict[str, Any]: """ Build an offset-based paginated response (common pattern in existing routers). This is the standard paginated response format used across all endpoints that return lists of items with pagination support. Args: items: Items for current page total: Total count of all items limit: Page size (number of items per page) offset: Current offset (starting position) key: Key name for items list (default "items") include_timestamp: Whether to include timestamp field **extra_fields: Additional fields to include in response Returns: Dict with paginated response structure Example: return offset_paginated( items=media_items, total=total_count, limit=50, offset=0, key="media", stats={"images": 100, "videos": 50} ) """ response = { key: items, "total": total, "limit": limit, "offset": offset } if include_timestamp: response["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # Add any extra fields response.update(extra_fields) return response def batch_operation_response( succeeded: bool, processed: List[Any] = None, errors: List[Dict[str, Any]] = None, message: str = None ) -> Dict[str, Any]: """ Build a response for batch operations (batch delete, batch move, etc.). This provides a standardized format for operations that affect multiple items. Args: succeeded: Overall success status processed: List of successfully processed items errors: List of error details for failed items message: Optional message Returns: Dict with batch operation response structure Example: return batch_operation_response( succeeded=True, processed=["/path/to/file1.jpg", "/path/to/file2.jpg"], errors=[{"file": "/path/to/file3.jpg", "error": "File not found"}] ) """ processed = processed or [] errors = errors or [] response = { "success": succeeded, "processed_count": len(processed), "error_count": len(errors), "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") } if processed: response["processed"] = processed if errors: response["errors"] = errors if message: response["message"] = message return response # ============================================================================ # DATE/TIME UTILITIES # ============================================================================ def to_iso8601(dt: datetime) -> Optional[str]: """ Convert datetime to ISO 8601 format with UTC timezone. This is the standard format for all API responses. Example output: "2025-12-04T10:30:00Z" """ if dt is None: return None # Ensure timezone awareness if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) else: dt = dt.astimezone(timezone.utc) return dt.strftime("%Y-%m-%dT%H:%M:%SZ") def from_iso8601(date_string: str) -> Optional[datetime]: """ Parse ISO 8601 date string to datetime. Handles various formats including with and without timezone. """ if not date_string: return None # Common formats to try formats = [ "%Y-%m-%dT%H:%M:%SZ", # ISO with Z "%Y-%m-%dT%H:%M:%S.%fZ", # ISO with microseconds and Z "%Y-%m-%dT%H:%M:%S", # ISO without Z "%Y-%m-%dT%H:%M:%S.%f", # ISO with microseconds "%Y-%m-%d %H:%M:%S", # Space separator "%Y-%m-%d", # Date only ] for fmt in formats: try: dt = datetime.strptime(date_string, fmt) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt except ValueError: continue # Try parsing with fromisoformat (Python 3.7+) try: dt = datetime.fromisoformat(date_string.replace('Z', '+00:00')) return dt except ValueError: pass return None def format_timestamp( timestamp: Optional[str], output_format: str = "iso8601" ) -> Optional[str]: """ Format a timestamp string to the specified format. Args: timestamp: Input timestamp string output_format: "iso8601", "unix", or strftime format string Returns: Formatted timestamp string """ if not timestamp: return None dt = from_iso8601(timestamp) if not dt: return timestamp # Return original if parsing failed if output_format == "iso8601": return to_iso8601(dt) elif output_format == "unix": return str(int(dt.timestamp())) else: return dt.strftime(output_format) def now_iso8601() -> str: """Get current UTC time in ISO 8601 format""" return to_iso8601(datetime.now(timezone.utc))