Files
kontextual-engine/src/kontextual_engine/errors.py

203 lines
5.2 KiB
Python

"""Shared diagnostics and structured errors."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class Diagnostic:
"""A structured finding emitted by engine operations."""
severity: str
code: str
message: str
details: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"severity": self.severity,
"code": self.code,
"message": self.message,
"details": dict(self.details),
}
@dataclass(frozen=True)
class OperationFailure:
"""Structured operation failure suitable for API and batch envelopes."""
code: str
message: str
operation: str
correlation_id: str
details: dict[str, Any] = field(default_factory=dict)
remediation: str | None = None
def to_dict(self) -> dict[str, Any]:
data: dict[str, Any] = {
"code": self.code,
"message": self.message,
"operation": self.operation,
"correlation_id": self.correlation_id,
"details": dict(self.details),
}
if self.remediation:
data["remediation"] = self.remediation
return data
@dataclass(frozen=True)
class BatchItemResult:
"""One item result inside a batch operation envelope."""
item_id: str
operation: str
success: bool
result_ref: dict[str, Any] = field(default_factory=dict)
error: OperationFailure | None = None
@classmethod
def succeeded(
cls,
*,
item_id: str,
operation: str,
result_ref: dict[str, Any] | None = None,
) -> "BatchItemResult":
return cls(
item_id=item_id,
operation=operation,
success=True,
result_ref=dict(result_ref or {}),
)
@classmethod
def failed(
cls,
*,
item_id: str,
operation: str,
error: OperationFailure,
) -> "BatchItemResult":
return cls(item_id=item_id, operation=operation, success=False, error=error)
def to_dict(self) -> dict[str, Any]:
data: dict[str, Any] = {
"item_id": self.item_id,
"operation": self.operation,
"success": self.success,
}
if self.result_ref:
data["result_ref"] = dict(self.result_ref)
if self.error:
data["error"] = self.error.to_dict()
return data
@dataclass(frozen=True)
class BatchOperationResult:
"""Compact result envelope for batch operations with partial failures."""
operation: str
correlation_id: str
items: tuple[BatchItemResult, ...] = ()
audit_event_id: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "items", tuple(self.items))
@property
def total(self) -> int:
return len(self.items)
@property
def succeeded(self) -> int:
return sum(1 for item in self.items if item.success)
@property
def failed(self) -> int:
return self.total - self.succeeded
@property
def partial(self) -> bool:
return self.succeeded > 0 and self.failed > 0
@property
def outcome(self) -> str:
if self.partial:
return "partial"
if self.failed:
return "failed"
return "success"
def to_dict(self) -> dict[str, Any]:
data: dict[str, Any] = {
"operation": self.operation,
"correlation_id": self.correlation_id,
"outcome": self.outcome,
"total": self.total,
"succeeded": self.succeeded,
"failed": self.failed,
"partial": self.partial,
"items": [item.to_dict() for item in self.items],
}
if self.audit_event_id:
data["audit_event_id"] = self.audit_event_id
return data
class KontextualError(Exception):
"""Base class for explicit engine failures."""
code = "kontextual.error"
def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None:
super().__init__(message)
self.details = details or {}
def diagnostic(self, *, severity: str = "error") -> Diagnostic:
return Diagnostic(
severity=severity,
code=self.code,
message=str(self),
details=dict(self.details),
)
def to_operation_failure(
self,
*,
operation: str,
correlation_id: str,
remediation: str | None = None,
) -> OperationFailure:
return OperationFailure(
code=str(self.details.get("code") or self.code),
message=str(self),
operation=operation,
correlation_id=correlation_id,
details=dict(self.details),
remediation=remediation,
)
class NotFoundError(KontextualError):
code = "kontextual.not_found"
class DuplicateResourceError(KontextualError):
code = "kontextual.duplicate"
class ValidationError(KontextualError):
code = "kontextual.validation"
class AuthorizationError(KontextualError):
code = "kontextual.authorization"
class AdapterUnavailableError(KontextualError):
code = "kontextual.adapter_unavailable"