312 lines
8.3 KiB
Python
312 lines
8.3 KiB
Python
"""
|
|
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))
|