338 lines
7.5 KiB
Python
338 lines
7.5 KiB
Python
"""
|
|
API response wrapper for Code of Conquest.
|
|
|
|
Provides standardized JSON response format for all API endpoints.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Optional
|
|
from flask import jsonify, Response
|
|
from app.config import get_config
|
|
|
|
|
|
def api_response(
|
|
result: Any = None,
|
|
status: int = 200,
|
|
error: Optional[Dict[str, Any]] = None,
|
|
meta: Optional[Dict[str, Any]] = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a standardized API response.
|
|
|
|
Args:
|
|
result: The response data (or None if error)
|
|
status: HTTP status code
|
|
error: Error information (dict with 'code', 'message', 'details')
|
|
meta: Metadata (pagination, etc.)
|
|
request_id: Optional request ID for tracking
|
|
|
|
Returns:
|
|
Response: Flask JSON response
|
|
|
|
Example:
|
|
>>> return api_response(
|
|
... result={"user_id": "123"},
|
|
... status=200
|
|
... )
|
|
|
|
>>> return api_response(
|
|
... error={
|
|
... "code": "INVALID_INPUT",
|
|
... "message": "Email is required",
|
|
... "details": {"field": "email"}
|
|
... },
|
|
... status=400
|
|
... )
|
|
"""
|
|
config = get_config()
|
|
|
|
response_data = {
|
|
"app": config.app.name,
|
|
"version": config.app.version,
|
|
"status": status,
|
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
"request_id": request_id,
|
|
"result": result,
|
|
"error": error,
|
|
"meta": meta
|
|
}
|
|
|
|
return jsonify(response_data), status
|
|
|
|
|
|
def success_response(
|
|
result: Any = None,
|
|
status: int = 200,
|
|
meta: Optional[Dict[str, Any]] = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a success response.
|
|
|
|
Args:
|
|
result: The response data
|
|
status: HTTP status code (default 200)
|
|
meta: Optional metadata
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response
|
|
|
|
Example:
|
|
>>> return success_response({"character_id": "123"})
|
|
"""
|
|
return api_response(
|
|
result=result,
|
|
status=status,
|
|
meta=meta,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def error_response(
|
|
code: str,
|
|
message: str,
|
|
status: int = 400,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create an error response.
|
|
|
|
Args:
|
|
code: Error code (e.g., "INVALID_INPUT")
|
|
message: Human-readable error message
|
|
status: HTTP status code (default 400)
|
|
details: Optional additional error details
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response
|
|
|
|
Example:
|
|
>>> return error_response(
|
|
... code="NOT_FOUND",
|
|
... message="Character not found",
|
|
... status=404
|
|
... )
|
|
"""
|
|
error = {
|
|
"code": code,
|
|
"message": message,
|
|
"details": details or {}
|
|
}
|
|
|
|
return api_response(
|
|
error=error,
|
|
status=status,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def created_response(
|
|
result: Any = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a 201 Created response.
|
|
|
|
Args:
|
|
result: The created resource data
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response with status 201
|
|
|
|
Example:
|
|
>>> return created_response({"character_id": "123"})
|
|
"""
|
|
return success_response(
|
|
result=result,
|
|
status=201,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def accepted_response(
|
|
result: Any = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a 202 Accepted response (for async operations).
|
|
|
|
Args:
|
|
result: Job information or status
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response with status 202
|
|
|
|
Example:
|
|
>>> return accepted_response({"job_id": "abc123"})
|
|
"""
|
|
return success_response(
|
|
result=result,
|
|
status=202,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def no_content_response(request_id: Optional[str] = None) -> Response:
|
|
"""
|
|
Create a 204 No Content response.
|
|
|
|
Args:
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response with status 204
|
|
|
|
Example:
|
|
>>> return no_content_response()
|
|
"""
|
|
return success_response(
|
|
result=None,
|
|
status=204,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def paginated_response(
|
|
items: list,
|
|
page: int,
|
|
limit: int,
|
|
total: int,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a paginated response.
|
|
|
|
Args:
|
|
items: List of items for current page
|
|
page: Current page number
|
|
limit: Items per page
|
|
total: Total number of items
|
|
request_id: Optional request ID
|
|
|
|
Returns:
|
|
Response: Flask JSON response with pagination metadata
|
|
|
|
Example:
|
|
>>> return paginated_response(
|
|
... items=[{"id": "1"}, {"id": "2"}],
|
|
... page=1,
|
|
... limit=20,
|
|
... total=100
|
|
... )
|
|
"""
|
|
pages = (total + limit - 1) // limit # Ceiling division
|
|
|
|
meta = {
|
|
"page": page,
|
|
"limit": limit,
|
|
"total": total,
|
|
"pages": pages
|
|
}
|
|
|
|
return success_response(
|
|
result=items,
|
|
meta=meta,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
# Common error responses
|
|
|
|
def unauthorized_response(
|
|
message: str = "Unauthorized",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""401 Unauthorized response."""
|
|
return error_response(
|
|
code="UNAUTHORIZED",
|
|
message=message,
|
|
status=401,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def forbidden_response(
|
|
message: str = "Forbidden",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""403 Forbidden response."""
|
|
return error_response(
|
|
code="FORBIDDEN",
|
|
message=message,
|
|
status=403,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def not_found_response(
|
|
message: str = "Resource not found",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""404 Not Found response."""
|
|
return error_response(
|
|
code="NOT_FOUND",
|
|
message=message,
|
|
status=404,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def validation_error_response(
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None,
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""400 Bad Request for validation errors."""
|
|
return error_response(
|
|
code="INVALID_INPUT",
|
|
message=message,
|
|
status=400,
|
|
details=details,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def rate_limit_exceeded_response(
|
|
message: str = "Rate limit exceeded",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""429 Too Many Requests response."""
|
|
return error_response(
|
|
code="RATE_LIMIT_EXCEEDED",
|
|
message=message,
|
|
status=429,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def internal_error_response(
|
|
message: str = "Internal server error",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""500 Internal Server Error response."""
|
|
return error_response(
|
|
code="INTERNAL_ERROR",
|
|
message=message,
|
|
status=500,
|
|
request_id=request_id
|
|
)
|
|
|
|
|
|
def premium_required_response(
|
|
message: str = "This feature requires a premium subscription",
|
|
request_id: Optional[str] = None
|
|
) -> Response:
|
|
"""403 Forbidden for premium-only features."""
|
|
return error_response(
|
|
code="PREMIUM_REQUIRED",
|
|
message=message,
|
|
status=403,
|
|
request_id=request_id
|
|
)
|