File size: 3,875 Bytes
921d377 | 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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | """
Typed errors for the interactive service.
Every error carries a stable ``code`` so the router can map it to
the right HTTP status without leaking internal details, and so the
frontend can branch on the code string instead of parsing messages.
Design rule: the service NEVER raises raw ``Exception``. Unexpected
exceptions are caught at the router boundary and wrapped in
``InteractiveError(code='internal', β¦)`` so observability stays
tidy. See ``router.py`` handlers.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
class InteractiveError(Exception):
"""Base β all interactive-service errors inherit from this."""
#: Default HTTP status when the router maps this to a response.
http_status: int = 500
#: Stable machine-readable code. Frontend branches on this.
code: str = "internal"
def __init__(
self,
message: str,
*,
code: Optional[str] = None,
http_status: Optional[int] = None,
data: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(message)
self.message = message
if code is not None:
self.code = code
if http_status is not None:
self.http_status = http_status
self.data = dict(data or {})
def to_dict(self) -> Dict[str, Any]:
return {
"ok": False,
"code": self.code,
"error": self.message,
"data": self.data,
}
# ββ Configuration / feature-flag ββββββββββββββββββββββββββββββββββ
class ServiceDisabledError(InteractiveError):
http_status = 503
code = "service_disabled"
# ββ Input validation ββββββββββββββββββββββββββββββββββββββββββββββ
class InvalidInputError(InteractiveError):
http_status = 400
code = "invalid_input"
# ββ Authorization βββββββββββββββββββββββββββββββββββββββββββββββββ
class NotAuthenticatedError(InteractiveError):
http_status = 401
code = "not_authenticated"
class NotAuthorizedError(InteractiveError):
"""Authenticated but the resource doesn't belong to the caller."""
http_status = 404 # probe-safe β 404 hides existence
code = "not_found"
# ββ Resource lookup βββββββββββββββββββββββββββββββββββββββββββββββ
class NotFoundError(InteractiveError):
http_status = 404
code = "not_found"
# ββ Structural / graph ββββββββββββββββββββββββββββββββββββββββββββ
class GraphError(InteractiveError):
http_status = 422
code = "graph_invalid"
class CapacityError(InteractiveError):
"""Branching graph or similar structure exceeds configured cap."""
http_status = 422
code = "capacity_exceeded"
# ββ Policy ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class PolicyBlockError(InteractiveError):
http_status = 403
code = "policy_blocked"
class ConsentRequiredError(InteractiveError):
http_status = 403
code = "consent_required"
# ββ Runtime βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class InvalidStateError(InteractiveError):
http_status = 409
code = "invalid_state"
class CooldownError(InteractiveError):
http_status = 429
code = "cooldown"
|