self.data is typed T | None on the dataclass but the Ok-branch invariant (enforced by __post_init__) guarantees it is T whenever self.ok is True. Cast explicitly instead of leaving mypy to infer T | None. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""
|
|
Result pattern for error handling without exceptions.
|
|
All service methods return Result[T] to avoid HTTP 500 errors.
|
|
"""
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Generic, TypeVar, cast
|
|
|
|
from .types import ErrorCode
|
|
|
|
T = TypeVar("T")
|
|
U = TypeVar("U")
|
|
|
|
@dataclass
|
|
class Result(Generic[T]):
|
|
"""
|
|
Result pattern for safe error handling.
|
|
|
|
Usage:
|
|
def get_metadata(path: str) -> Result[dict]:
|
|
if not os.path.exists(path):
|
|
return Result.Err("NOT_FOUND", f"File not found: {path}")
|
|
return Result.Ok({"filename": path})
|
|
"""
|
|
ok: bool
|
|
data: T | None = None
|
|
error: str | None = None
|
|
code: str = "OK" # OK, DEGRADED, TOOL_MISSING, DB_ERROR, UNSUPPORTED, etc.
|
|
meta: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.ok and self.error is not None:
|
|
raise ValueError("Result.Ok cannot have an error message")
|
|
if not self.ok and self.data is not None:
|
|
raise ValueError("Result.Err should not carry data")
|
|
if not self.ok and self.error is None:
|
|
raise ValueError("Result.Err requires an error message")
|
|
|
|
@staticmethod
|
|
def Ok(data: T, **meta: Any) -> "Result[T]":
|
|
"""Create a successful result with data and optional metadata."""
|
|
return Result(ok=True, data=data, code="OK", meta=meta)
|
|
|
|
@staticmethod
|
|
def Err(code: ErrorCode | str | Enum, error: str, **meta: Any) -> "Result[T]":
|
|
"""Create an error result with code, message, and optional metadata."""
|
|
try:
|
|
code_value = code.value if isinstance(code, Enum) else code
|
|
except Exception:
|
|
code_value = code
|
|
return Result(ok=False, error=error, code=str(code_value), meta=meta)
|
|
|
|
def map(self, fn: Callable[[T], U]) -> "Result[U]":
|
|
"""Map the data if ok, otherwise return self."""
|
|
if self.ok:
|
|
return Result.Ok(fn(cast(T, self.data)), **self.meta)
|
|
return cast(Result[U], self)
|
|
|
|
def unwrap(self) -> T:
|
|
"""Get data or raise ValueError if error."""
|
|
if self.ok:
|
|
return cast(T, self.data)
|
|
raise ValueError(f"[{self.code}] {self.error}")
|
|
|
|
def unwrap_or(self, default: T) -> T:
|
|
"""Get data or return default if error."""
|
|
return cast(T, self.data) if self.ok else default
|