-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexceptions.py
More file actions
191 lines (164 loc) · 6.93 KB
/
exceptions.py
File metadata and controls
191 lines (164 loc) · 6.93 KB
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""SharpAPI exceptions and canonical error-code registry.
The codes here mirror the canonical set the SharpAPI server emits.
Keep this file in sync when new codes are added upstream.
"""
from __future__ import annotations
class SharpAPIError(Exception):
"""Base exception for all SharpAPI errors."""
def __init__(self, message: str, code: str | None = None, status: int | None = None):
super().__init__(message)
self.code = code
self.status = status
class AuthenticationError(SharpAPIError):
"""API key is missing, invalid, expired, disabled, or token is rejected.
Raised for HTTP 401 responses and any of the auth-related error codes
(``missing_api_key``, ``invalid_api_key``, ``expired_api_key``,
``disabled_api_key``, ``invalid_token``, ``unauthorized``).
"""
class TierRestrictedError(SharpAPIError):
"""Feature not available on current tier (403)."""
def __init__(
self,
message: str,
code: str | None = None,
status: int | None = None,
required_tier: str | None = None,
):
super().__init__(message, code, status)
self.required_tier = required_tier
class RateLimitedError(SharpAPIError):
"""Too many requests (429) — rate-limited, backpressure, or concurrent cap."""
def __init__(
self,
message: str,
code: str | None = None,
status: int | None = None,
retry_after: float | None = None,
):
super().__init__(message, code, status)
self.retry_after = retry_after
class ValidationError(SharpAPIError):
"""Invalid request parameters (400)."""
class StreamError(SharpAPIError):
"""Error during SSE or WebSocket streaming."""
# =============================================================================
# Canonical error-code registry
#
# Mirrors the canonical SharpAPI server error-code set. When upstream adds
# a new code, add it here too and update the matching description. Each
# code maps to the Python exception class that ``handle_errors`` (in
# ``_base.py``) raises for it.
# =============================================================================
# HTTP error codes — emitted via REST handlers (httputil.WriteJSONError).
BACKPRESSURE = "backpressure"
CONCURRENT_REQUEST_CAP = "concurrent_request_cap"
DISABLED_API_KEY = "disabled_api_key"
EXPIRED_API_KEY = "expired_api_key"
GONE = "gone"
INTERNAL_ERROR = "internal_error"
INVALID_API_KEY = "invalid_api_key"
INVALID_TOKEN = "invalid_token"
METHOD_NOT_ALLOWED = "method_not_allowed"
MISSING_API_KEY = "missing_api_key"
NOT_FOUND = "not_found"
NOT_READY = "not_ready"
OFFSET_TOO_LARGE = "offset_too_large"
RATE_LIMITED = "rate_limited"
SERVICE_UNAVAILABLE = "service_unavailable"
TIER_RESTRICTED = "tier_restricted"
TOO_MANY_STREAMS = "too_many_streams"
UNAUTHORIZED = "unauthorized"
UNKNOWN_ENDPOINT = "unknown_endpoint"
UPSTREAM_ERROR = "upstream_error"
VALIDATION_ERROR = "validation_error"
# WebSocket frame error codes — emitted in "error" message frames.
WS_ALREADY_AUTHENTICATED = "already_authenticated"
WS_INVALID_MESSAGE = "invalid_message"
WS_MISSING_CHANNELS = "missing_channels"
WS_MISSING_TOKEN = "missing_token"
WS_NOT_AUTHENTICATED = "not_authenticated"
WS_UNKNOWN_MESSAGE_TYPE = "unknown_message_type"
#: Human-readable descriptions for every canonical code.
ERROR_CODE_DESCRIPTIONS: dict[str, str] = {
# HTTP
BACKPRESSURE: "Server is shedding load; retry shortly.",
CONCURRENT_REQUEST_CAP: "Too many in-flight requests for this API key.",
DISABLED_API_KEY: "API key has been disabled.",
EXPIRED_API_KEY: "API key has expired.",
GONE: "Resource is no longer available.",
INTERNAL_ERROR: "Unexpected server error.",
INVALID_API_KEY: "API key is invalid.",
INVALID_TOKEN: "Bearer token is invalid or malformed.",
METHOD_NOT_ALLOWED: "HTTP method not allowed on this endpoint.",
MISSING_API_KEY: "No API key provided.",
NOT_FOUND: "Resource not found.",
NOT_READY: "A required backing store is not yet ready to serve this request; retry shortly.",
OFFSET_TOO_LARGE: (
"offset exceeds the per-endpoint maximum; "
"use cursor-based pagination or advance `since`."
),
RATE_LIMITED: "Rate limit exceeded; see Retry-After header.",
SERVICE_UNAVAILABLE: "Service is temporarily unavailable.",
TIER_RESTRICTED: "Current subscription tier does not include this feature.",
TOO_MANY_STREAMS: "Maximum concurrent WebSocket/SSE streams exceeded.",
UNAUTHORIZED: "Authentication required.",
UNKNOWN_ENDPOINT: "Endpoint does not exist.",
UPSTREAM_ERROR: "Upstream data source error.",
VALIDATION_ERROR: "Request parameters failed validation.",
# WebSocket
WS_ALREADY_AUTHENTICATED: "Auth frame sent on an already-authenticated connection.",
WS_INVALID_MESSAGE: "Malformed WebSocket frame.",
WS_MISSING_CHANNELS: "Subscribe frame had no channels.",
WS_MISSING_TOKEN: "Auth frame had no token.",
WS_NOT_AUTHENTICATED: "Action requires authentication first.",
WS_UNKNOWN_MESSAGE_TYPE: "Unknown WebSocket message type.",
}
#: Map each canonical code to the SharpAPIError subclass ``handle_errors`` raises.
ERROR_CODE_TO_EXCEPTION: dict[str, type[SharpAPIError]] = {
# Auth family → AuthenticationError
MISSING_API_KEY: AuthenticationError,
INVALID_API_KEY: AuthenticationError,
EXPIRED_API_KEY: AuthenticationError,
DISABLED_API_KEY: AuthenticationError,
INVALID_TOKEN: AuthenticationError,
UNAUTHORIZED: AuthenticationError,
# Tier
TIER_RESTRICTED: TierRestrictedError,
# Rate / load shedding
RATE_LIMITED: RateLimitedError,
BACKPRESSURE: RateLimitedError,
CONCURRENT_REQUEST_CAP: RateLimitedError,
TOO_MANY_STREAMS: RateLimitedError,
# Validation
VALIDATION_ERROR: ValidationError,
OFFSET_TOO_LARGE: ValidationError,
# Streaming frames
WS_ALREADY_AUTHENTICATED: StreamError,
WS_INVALID_MESSAGE: StreamError,
WS_MISSING_CHANNELS: StreamError,
WS_MISSING_TOKEN: StreamError,
WS_NOT_AUTHENTICATED: StreamError,
WS_UNKNOWN_MESSAGE_TYPE: StreamError,
# Everything else falls through to SharpAPIError
GONE: SharpAPIError,
INTERNAL_ERROR: SharpAPIError,
METHOD_NOT_ALLOWED: SharpAPIError,
NOT_FOUND: SharpAPIError,
NOT_READY: SharpAPIError,
SERVICE_UNAVAILABLE: SharpAPIError,
UNKNOWN_ENDPOINT: SharpAPIError,
UPSTREAM_ERROR: SharpAPIError,
}
# Deprecated aliases. ``bad_request`` and ``invalid_request`` were both
# collapsed into ``validation_error`` server-side. Kept here so that older
# API responses (or user code still checking these strings) resolve
# correctly. Will be removed after 2026-10.
DEPRECATED_CODE_ALIASES: dict[str, str] = {
"bad_request": VALIDATION_ERROR,
"invalid_request": VALIDATION_ERROR,
}
def canonical_code(code: str | None) -> str | None:
"""Return the canonical code, resolving deprecated aliases."""
if code is None:
return None
return DEPRECATED_CODE_ALIASES.get(code, code)