1- """SharpAPI exceptions."""
1+ """SharpAPI exceptions and canonical error-code registry.
2+
3+ The error codes here mirror ``pkg/errcodes/errcodes.go`` in sharp-api-go, which
4+ is the single source of truth for every code the API emits. Keep this file in
5+ sync when new codes are added upstream.
6+ """
27
38from __future__ import annotations
49
@@ -13,7 +18,12 @@ def __init__(self, message: str, code: str | None = None, status: int | None = N
1318
1419
1520class AuthenticationError (SharpAPIError ):
16- """API key is missing or invalid (401)."""
21+ """API key is missing, invalid, expired, disabled, or token is rejected.
22+
23+ Raised for HTTP 401 responses and any of the auth-related error codes
24+ (``missing_api_key``, ``invalid_api_key``, ``expired_api_key``,
25+ ``disabled_api_key``, ``invalid_token``, ``unauthorized``).
26+ """
1727
1828
1929class TierRestrictedError (SharpAPIError ):
@@ -31,7 +41,7 @@ def __init__(
3141
3242
3343class RateLimitedError (SharpAPIError ):
34- """Too many requests (429)."""
44+ """Too many requests (429) — rate-limited, backpressure, or concurrent cap ."""
3545
3646 def __init__ (
3747 self ,
@@ -49,4 +59,124 @@ class ValidationError(SharpAPIError):
4959
5060
5161class StreamError (SharpAPIError ):
52- """Error during SSE streaming."""
62+ """Error during SSE or WebSocket streaming."""
63+
64+
65+ # =============================================================================
66+ # Canonical error-code registry
67+ #
68+ # Mirrors sharp-api-go/pkg/errcodes/errcodes.go. When upstream adds a new code,
69+ # add it here too and update the matching description. Each code maps to the
70+ # Python exception class that ``handle_errors`` (in ``_base.py``) raises for it.
71+ # =============================================================================
72+
73+ # HTTP error codes — emitted via REST handlers (httputil.WriteJSONError).
74+ BACKPRESSURE = "backpressure"
75+ CONCURRENT_REQUEST_CAP = "concurrent_request_cap"
76+ DISABLED_API_KEY = "disabled_api_key"
77+ EXPIRED_API_KEY = "expired_api_key"
78+ GONE = "gone"
79+ INTERNAL_ERROR = "internal_error"
80+ INVALID_API_KEY = "invalid_api_key"
81+ INVALID_TOKEN = "invalid_token"
82+ METHOD_NOT_ALLOWED = "method_not_allowed"
83+ MISSING_API_KEY = "missing_api_key"
84+ NOT_FOUND = "not_found"
85+ RATE_LIMITED = "rate_limited"
86+ SERVICE_UNAVAILABLE = "service_unavailable"
87+ TIER_RESTRICTED = "tier_restricted"
88+ TOO_MANY_STREAMS = "too_many_streams"
89+ UNAUTHORIZED = "unauthorized"
90+ UNKNOWN_ENDPOINT = "unknown_endpoint"
91+ UPSTREAM_ERROR = "upstream_error"
92+ VALIDATION_ERROR = "validation_error"
93+
94+ # WebSocket frame error codes — emitted in "error" message frames.
95+ WS_ALREADY_AUTHENTICATED = "already_authenticated"
96+ WS_INVALID_MESSAGE = "invalid_message"
97+ WS_MISSING_CHANNELS = "missing_channels"
98+ WS_MISSING_TOKEN = "missing_token"
99+ WS_NOT_AUTHENTICATED = "not_authenticated"
100+ WS_UNKNOWN_MESSAGE_TYPE = "unknown_message_type"
101+
102+ #: Human-readable descriptions for every canonical code.
103+ ERROR_CODE_DESCRIPTIONS : dict [str , str ] = {
104+ # HTTP
105+ BACKPRESSURE : "Server is shedding load; retry shortly." ,
106+ CONCURRENT_REQUEST_CAP : "Too many in-flight requests for this API key." ,
107+ DISABLED_API_KEY : "API key has been disabled." ,
108+ EXPIRED_API_KEY : "API key has expired." ,
109+ GONE : "Resource is no longer available." ,
110+ INTERNAL_ERROR : "Unexpected server error." ,
111+ INVALID_API_KEY : "API key is invalid." ,
112+ INVALID_TOKEN : "Bearer token is invalid or malformed." ,
113+ METHOD_NOT_ALLOWED : "HTTP method not allowed on this endpoint." ,
114+ MISSING_API_KEY : "No API key provided." ,
115+ NOT_FOUND : "Resource not found." ,
116+ RATE_LIMITED : "Rate limit exceeded; see Retry-After header." ,
117+ SERVICE_UNAVAILABLE : "Service is temporarily unavailable." ,
118+ TIER_RESTRICTED : "Current subscription tier does not include this feature." ,
119+ TOO_MANY_STREAMS : "Maximum concurrent WebSocket/SSE streams exceeded." ,
120+ UNAUTHORIZED : "Authentication required." ,
121+ UNKNOWN_ENDPOINT : "Endpoint does not exist." ,
122+ UPSTREAM_ERROR : "Upstream data source error." ,
123+ VALIDATION_ERROR : "Request parameters failed validation." ,
124+ # WebSocket
125+ WS_ALREADY_AUTHENTICATED : "Auth frame sent on an already-authenticated connection." ,
126+ WS_INVALID_MESSAGE : "Malformed WebSocket frame." ,
127+ WS_MISSING_CHANNELS : "Subscribe frame had no channels." ,
128+ WS_MISSING_TOKEN : "Auth frame had no token." ,
129+ WS_NOT_AUTHENTICATED : "Action requires authentication first." ,
130+ WS_UNKNOWN_MESSAGE_TYPE : "Unknown WebSocket message type." ,
131+ }
132+
133+ #: Map each canonical code to the SharpAPIError subclass ``handle_errors`` raises.
134+ ERROR_CODE_TO_EXCEPTION : dict [str , type [SharpAPIError ]] = {
135+ # Auth family → AuthenticationError
136+ MISSING_API_KEY : AuthenticationError ,
137+ INVALID_API_KEY : AuthenticationError ,
138+ EXPIRED_API_KEY : AuthenticationError ,
139+ DISABLED_API_KEY : AuthenticationError ,
140+ INVALID_TOKEN : AuthenticationError ,
141+ UNAUTHORIZED : AuthenticationError ,
142+ # Tier
143+ TIER_RESTRICTED : TierRestrictedError ,
144+ # Rate / load shedding
145+ RATE_LIMITED : RateLimitedError ,
146+ BACKPRESSURE : RateLimitedError ,
147+ CONCURRENT_REQUEST_CAP : RateLimitedError ,
148+ TOO_MANY_STREAMS : RateLimitedError ,
149+ # Validation
150+ VALIDATION_ERROR : ValidationError ,
151+ # Streaming frames
152+ WS_ALREADY_AUTHENTICATED : StreamError ,
153+ WS_INVALID_MESSAGE : StreamError ,
154+ WS_MISSING_CHANNELS : StreamError ,
155+ WS_MISSING_TOKEN : StreamError ,
156+ WS_NOT_AUTHENTICATED : StreamError ,
157+ WS_UNKNOWN_MESSAGE_TYPE : StreamError ,
158+ # Everything else falls through to SharpAPIError
159+ GONE : SharpAPIError ,
160+ INTERNAL_ERROR : SharpAPIError ,
161+ METHOD_NOT_ALLOWED : SharpAPIError ,
162+ NOT_FOUND : SharpAPIError ,
163+ SERVICE_UNAVAILABLE : SharpAPIError ,
164+ UNKNOWN_ENDPOINT : SharpAPIError ,
165+ UPSTREAM_ERROR : SharpAPIError ,
166+ }
167+
168+ # Deprecated aliases. ``bad_request`` and ``invalid_request`` were both collapsed
169+ # into ``validation_error`` in sharp-api-go. Kept here so that older API
170+ # responses (or user code still checking these strings) resolve correctly.
171+ # TODO: remove after 2026-10.
172+ DEPRECATED_CODE_ALIASES : dict [str , str ] = {
173+ "bad_request" : VALIDATION_ERROR ,
174+ "invalid_request" : VALIDATION_ERROR ,
175+ }
176+
177+
178+ def canonical_code (code : str | None ) -> str | None :
179+ """Return the canonical code, resolving deprecated aliases."""
180+ if code is None :
181+ return None
182+ return DEPRECATED_CODE_ALIASES .get (code , code )
0 commit comments