mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-07 23:56:36 +00:00
* ci: mypy ratchet -- keep type-clean modules clean mypy-clean.txt lists the 71 modules under src/ that type-check clean; scripts/check_types.py runs mypy (--follow-imports=silent) on exactly those files and fails on any error or a missing/unsorted/duplicate entry. A new "Type check (mypy ratchet)" CI job runs it with mypy 1.20.2 and pinned stubs; the manual pre-commit mypy hook now runs the same script (a local hook, so mypy sees the installed requirements like CI does). 35 modules were made clean with annotation-only fixes: hints, typing.cast, TYPE_CHECKING imports, implicit-Optional defaults made explicit, and annotations widened (never guards removed) where mypy called a defensive isinstance check unreachable. No runtime behaviour change. mypy.ini: numpy and orjson are treated as Any (follow_imports=skip, also for stubs). numpy 2.3+ stubs use 3.12 `type` statements that mypy won't parse at python_version 3.10, and orjson is optional, so seeing its stubs made the result depend on whether it was installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: annotate check_types.py's list-form mypy subprocess Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
4.5 KiB
Python
153 lines
4.5 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.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: 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
|