Files
TimeTracker/app/utils/api_responses.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

222 lines
5.3 KiB
Python

"""
Consistent API response helpers.
Provides standardized response formats for all API endpoints.
"""
from typing import Any, Dict, List, Optional
from flask import Response, jsonify
from marshmallow import ValidationError
def success_response(
data: Any = None, message: Optional[str] = None, status_code: int = 200, meta: Optional[Dict[str, Any]] = None
) -> Response:
"""
Create a successful API response.
Args:
data: Response data
message: Optional success message
status_code: HTTP status code
meta: Optional metadata
Returns:
Flask JSON response
"""
response = {
"success": True,
}
if message:
response["message"] = message
if data is not None:
response["data"] = data
if meta:
response["meta"] = meta
return jsonify(response), status_code
def error_response(
message: str,
error_code: Optional[str] = None,
status_code: int = 400,
errors: Optional[Dict[str, List[str]]] = None,
details: Optional[Dict[str, Any]] = None,
) -> Response:
"""
Create an error API response.
Args:
message: Error message
error_code: Optional error code
status_code: HTTP status code
errors: Optional field-specific errors
details: Optional additional error details
Returns:
Flask JSON response
"""
# error = user-facing message (backward compat); error_code = machine-readable
response = {
"success": False,
"error": message,
"message": message,
"error_code": error_code or "error",
}
if errors:
response["errors"] = errors
if details:
response["details"] = details
return jsonify(response), status_code
def validation_error_response(errors: Dict[str, List[str]], message: str = "Validation failed") -> Response:
"""
Create a validation error response.
Args:
errors: Field-specific validation errors
message: Error message
Returns:
Flask JSON response
"""
return error_response(message=message, error_code="validation_error", status_code=400, errors=errors)
def not_found_response(resource: str = "Resource", resource_id: Optional[Any] = None) -> Response:
"""
Create a not found error response.
Args:
resource: Resource type name
resource_id: Optional resource ID
Returns:
Flask JSON response
"""
message = f"{resource} not found"
if resource_id is not None:
message = f"{resource} with ID {resource_id} not found"
return error_response(message=message, error_code="not_found", status_code=404)
def unauthorized_response(message: str = "Authentication required") -> Response:
"""
Create an unauthorized error response.
Args:
message: Error message
Returns:
Flask JSON response
"""
return error_response(message=message, error_code="unauthorized", status_code=401)
def forbidden_response(message: str = "Insufficient permissions") -> Response:
"""
Create a forbidden error response.
Args:
message: Error message
Returns:
Flask JSON response
"""
return error_response(message=message, error_code="forbidden", status_code=403)
def paginated_response(
items: List[Any], page: int, per_page: int, total: int, message: Optional[str] = None
) -> Response:
"""
Create a paginated response.
Args:
items: List of items for current page
page: Current page number
per_page: Items per page
total: Total number of items
message: Optional message
Returns:
Flask JSON response
"""
pages = (total + per_page - 1) // per_page if total > 0 else 0
pagination = {
"page": page,
"per_page": per_page,
"total": total,
"pages": pages,
"has_next": page < pages,
"has_prev": page > 1,
"next_page": page + 1 if page < pages else None,
"prev_page": page - 1 if page > 1 else None,
}
return success_response(data=items, message=message, meta={"pagination": pagination})
def handle_validation_error(error: ValidationError) -> Response:
"""
Handle Marshmallow validation errors.
Args:
error: ValidationError instance
Returns:
Flask JSON response
"""
errors = {}
if isinstance(error.messages, dict):
errors = error.messages
elif isinstance(error.messages, list):
errors = {"_general": error.messages}
return validation_error_response(errors=errors)
def created_response(data: Any, message: Optional[str] = None, location: Optional[str] = None) -> Response:
"""
Create a 201 Created response.
Args:
data: Created resource data
message: Optional success message
location: Optional resource location URL
Returns:
Flask JSON response
"""
response_data = {"success": True, "data": data}
if message:
response_data["message"] = message
response = jsonify(response_data)
response.status_code = 201
if location:
response.headers["Location"] = location
return response
def no_content_response() -> Response:
"""
Create a 204 No Content response.
Returns:
Flask response
"""
return "", 204