""" Standardized API response helpers. Provides consistent API response formatting across all endpoints. """ import time from typing import Any, Optional, Dict, Tuple from flask import jsonify, request from src.web_interface.error_handler import create_error_response, create_success_response from src.web_interface.errors import ErrorCode, WebInterfaceError def success_response( data: Any = None, message: Optional[str] = None, metadata: Optional[Dict] = None ): """ Create a standardized success response. Args: data: Response data message: Optional success message metadata: Optional metadata (timing, version, etc.) Returns: Flask jsonify response """ response_data = create_success_response(data, message, metadata) # Timing is merged into whatever the caller passed, without inventing a # metadata block for responses that have neither. enriched = dict(metadata) if metadata is not None else {} if hasattr(request, 'start_time'): # request_logging stamps start_time from perf_counter, not the wall clock. enriched['response_time_ms'] = int((time.perf_counter() - request.start_time) * 1000) if metadata is not None or enriched: response_data['metadata'] = enriched return jsonify(response_data) def error_response( error_code: ErrorCode, message: str, details: Optional[str] = None, context: Optional[Dict] = None, suggested_fixes: Optional[list] = None, status_code: int = 500 ): """ Create a standardized error response. Args: error_code: Error code message: Error message details: Optional detailed error information context: Optional context dictionary suggested_fixes: Optional list of suggested fixes status_code: HTTP status code Returns: Flask jsonify response with status code """ return create_error_response( error_code=error_code, message=message, details=details, context=context, suggested_fixes=suggested_fixes, status_code=status_code ) def exception_error_response( exc: Exception, error_code: ErrorCode, *, with_context: bool = True, status_code: int = 500 ): """ error_response() for a caught exception, built by WebInterfaceError. The message is the code's fixed, user-facing one -- never the exception text. `details` comes from the exception's own `context` dict when it has one, and `context` records the exception type. with_context=False leaves the context out, as the operation-history routes always have. Args: exc: The exception being reported error_code: Error code with_context: Whether to include the context (exception type) status_code: HTTP status code Returns: Flask jsonify response with status code """ error = WebInterfaceError.from_exception(exc, error_code) return error_response( error.error_code, error.message, details=error.details, context=error.context if with_context else None, status_code=status_code ) def validate_request_json(required_fields: list, data: Optional[Dict] = None) -> Tuple[Optional[Dict], Optional[Any]]: """ Validate request JSON has required fields. Args: required_fields: List of required field names data: Optional data dict (if None, reads from request) Returns: Tuple of (data_dict, error_response) or (data_dict, None) if valid """ if data is None: data = request.get_json(silent=True) if not data: return None, error_response( ErrorCode.INVALID_INPUT, "Request body must be valid JSON", status_code=400 ) # A JSON array passes the check above, and ``field in data`` then tests # list membership: ["plugin_id"] "had" every required field and the # handler's data['plugin_id'] raised TypeError -- a 500, not a 400. if not isinstance(data, dict): return None, error_response( ErrorCode.INVALID_INPUT, "Request body must be a JSON object", status_code=400 ) missing_fields = [field for field in required_fields if field not in data] if missing_fields: return None, error_response( ErrorCode.INVALID_INPUT, f"Missing required fields: {', '.join(missing_fields)}", context={'missing_fields': missing_fields}, status_code=400 ) return data, None