generated from john/python-template
86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
"""Shared error taxonomy and helpers for runtime boundaries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import UTC
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from uuid import uuid4
|
|
|
|
|
|
class ErrorCategory(StrEnum):
|
|
"""Stable error categories defined by docs/error_handling.md."""
|
|
|
|
VALIDATION = "validation_error"
|
|
USER_INPUT = "user_input_error"
|
|
NOT_FOUND = "not_found_error"
|
|
CONFLICT = "conflict_error"
|
|
EXTERNAL_PROVIDER = "external_provider_error"
|
|
PROCESSING = "processing_error"
|
|
INFRA_TRANSIENT = "infrastructure_transient_error"
|
|
INFRA_PERSISTENT = "infrastructure_persistent_error"
|
|
INTERNAL_UNEXPECTED = "internal_unexpected_error"
|
|
|
|
|
|
def new_error_id() -> str:
|
|
"""Return a short, user-shareable error reference id."""
|
|
return uuid4().hex[:8]
|
|
|
|
|
|
class AppError(RuntimeError):
|
|
"""Base application error carrying user-safe handling metadata."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
category: ErrorCategory = ErrorCategory.INTERNAL_UNEXPECTED,
|
|
suggestion: str = "Retry once. If it persists, review logs and report the error reference id.",
|
|
retriable: bool = False,
|
|
error_id: str | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.category = category
|
|
self.suggestion = suggestion
|
|
self.retriable = retriable
|
|
self.error_id = error_id or new_error_id()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ErrorEnvelope:
|
|
"""Serializable API/UI error payload."""
|
|
|
|
error_id: str
|
|
category: str
|
|
message: str
|
|
suggestion: str
|
|
timestamp: str
|
|
|
|
|
|
def build_error_envelope(error: AppError) -> ErrorEnvelope:
|
|
"""Build an API-safe response envelope from an AppError."""
|
|
return ErrorEnvelope(
|
|
error_id=error.error_id,
|
|
category=error.category.value,
|
|
message=error.message,
|
|
suggestion=error.suggestion,
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
)
|
|
|
|
|
|
def classify_unexpected_error(exc: Exception, *, operation: str) -> AppError:
|
|
"""Normalize unknown exceptions into internal_unexpected_error."""
|
|
return AppError(
|
|
f"Unexpected error during {operation}: {exc}",
|
|
category=ErrorCategory.INTERNAL_UNEXPECTED,
|
|
suggestion="Retry once. If it persists, review logs and report the error reference id.",
|
|
retriable=False,
|
|
)
|
|
|
|
|
|
def format_error_detail(error: AppError) -> str:
|
|
"""Return a compact persisted failure string for transcript.error_detail."""
|
|
return f"[{error.category.value}] {error.message} | suggestion={error.suggestion} | error_id={error.error_id}"
|