Spaces:
Running
Running
File size: 2,600 Bytes
6741fc6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | """Typed API errors.
Spec §7: *"API returns typed error responses, never stack traces."* Every failure
that reaches a client is one of these, serialised to a stable JSON envelope:
{"error": {"code": "...", "message": "...", "details": {...}}}
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any
class ErrorCode(StrEnum):
"""Stable, machine-readable error identifiers. Values are part of the API."""
UNSUPPORTED_MEDIA_TYPE = "unsupported_media_type"
FILE_TOO_LARGE = "file_too_large"
EMPTY_FILE = "empty_file"
MALFORMED_FILE = "malformed_file"
FORBIDDEN_ORIGIN = "forbidden_origin"
NOT_FOUND = "not_found"
INVALID_STATE_TRANSITION = "invalid_state_transition"
VALIDATION_ERROR = "validation_error"
RATE_LIMITED = "rate_limited"
UPSTREAM_UNAVAILABLE = "upstream_unavailable"
EXTRACTION_FAILED = "extraction_failed"
INTERNAL_ERROR = "internal_error"
class LedgerLensError(Exception):
"""Base class for every error the API deliberately surfaces."""
status_code: int = 500
code: ErrorCode = ErrorCode.INTERNAL_ERROR
def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None:
super().__init__(message)
self.message = message
self.details = details or {}
def to_payload(self) -> dict[str, Any]:
return {"error": {"code": str(self.code), "message": self.message, "details": self.details}}
class UnsupportedMediaTypeError(LedgerLensError):
status_code = 415
code = ErrorCode.UNSUPPORTED_MEDIA_TYPE
class FileTooLargeError(LedgerLensError):
status_code = 413
code = ErrorCode.FILE_TOO_LARGE
class EmptyFileError(LedgerLensError):
status_code = 400
code = ErrorCode.EMPTY_FILE
class MalformedFileError(LedgerLensError):
status_code = 400
code = ErrorCode.MALFORMED_FILE
class NotFoundError(LedgerLensError):
status_code = 404
code = ErrorCode.NOT_FOUND
class InvalidStateTransitionError(LedgerLensError):
status_code = 409
code = ErrorCode.INVALID_STATE_TRANSITION
class ValidationFailedError(LedgerLensError):
status_code = 422
code = ErrorCode.VALIDATION_ERROR
class UpstreamUnavailableError(LedgerLensError):
"""A dependency (Claude, database) failed after every retry was exhausted."""
status_code = 503
code = ErrorCode.UPSTREAM_UNAVAILABLE
class ExtractionFailedError(LedgerLensError):
"""The model could not produce a schema-valid extraction within the retry budget."""
status_code = 422
code = ErrorCode.EXTRACTION_FAILED
|