mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-06 23:35:08 +00:00
Whole-tree audit. Every symbol was checked against core, the plugin monorepo and all eight third-party plugins in plugins.json first. - Deprecate (removal 3.10.0) plugin-facing methods nothing calls: LogoDownloader bulk download, ConfigManager backup/secret wrappers, APIHelper extras, BackgroundDataService poll API, PluginManager / PluginStateManager info readers, and a few CacheManager, FontManager, BaseOddsManager, DynamicTeamResolver methods and PluginTestCase. plugin_api_usage.py learns their receiver names; DEPRECATIONS doc regenerated. - Remove core-internal dead code: CacheMetrics, Vegas status/stats plumbing, sync "new cycle" message (followers ignore unknown types), unused operation types, test-only PluginCatalog readers, IPC to_dict and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator callbacks, duplicate web response helpers. - Web UI: drop never-mounted json-file-manager.js, the example widget, utils/error_handler.js, four uncalled PluginAPI methods, and 29 escapeHtml shims (call window.LEDEscape directly). Public globals, BaseWidget and widget names unchanged. - Remove six one-off scripts (owner decision) and the unused markupsafe and pytest-mock pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
"""
|
|
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.errors import ErrorCode, WebInterfaceError
|
|
|
|
|
|
def success_response(
|
|
data: Any = None,
|
|
message: Optional[str] = None,
|
|
metadata: Optional[Dict] = None,
|
|
extra: Optional[Dict[str, Any]] = None
|
|
):
|
|
"""
|
|
Create a standardized success response.
|
|
|
|
Args:
|
|
data: Response data
|
|
message: Optional success message
|
|
metadata: Optional metadata (timing, version, etc.)
|
|
extra: Optional top-level fields beside ``status``/``data``, such as
|
|
``restart_required``; they cannot replace the standard keys
|
|
|
|
Returns:
|
|
Flask jsonify response
|
|
"""
|
|
response_data: Dict[str, Any] = {'status': 'success'}
|
|
# `is not None` rather than truthiness: "" and {} are values a caller
|
|
# chose to send, and dropping them would make the shape depend on the data.
|
|
if data is not None:
|
|
response_data['data'] = data
|
|
if message is not None:
|
|
response_data['message'] = message
|
|
if metadata is not None:
|
|
response_data['metadata'] = metadata
|
|
for key, value in (extra or {}).items():
|
|
response_data.setdefault(key, value)
|
|
|
|
# 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
|
|
"""
|
|
error = WebInterfaceError(
|
|
error_code=error_code,
|
|
message=message,
|
|
details=details,
|
|
context=context or {},
|
|
suggested_fixes=suggested_fixes
|
|
)
|
|
return jsonify(error.to_dict()), 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: Any = 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
|