44from __future__ import annotations
55
66import json
7- from typing import Optional
7+ from typing import Any , Optional
8+
9+ INVALID_REQUEST_STATUS_CODES = (400 , 422 )
810
911
1012class ShadeError (Exception ):
@@ -74,7 +76,47 @@ class AuthenticationError(ShadeError):
7476
7577
7678class InvalidRequestError (ShadeError ):
77- """Raised when a request is malformed or rejected by validation."""
79+ """Raised on HTTP 400/422 responses for malformed or invalid parameters."""
80+
81+ def __init__ (
82+ self ,
83+ message : str ,
84+ status_code : Optional [int ] = None ,
85+ response_body : Optional [str ] = None ,
86+ param : Optional [str ] = None ,
87+ field_errors : Optional [dict [str , Any ]] = None ,
88+ ) -> None :
89+ super ().__init__ (message , status_code , response_body )
90+ parsed = _parse_error_response (response_body )
91+ self .param : Optional [str ] = param if param is not None else parsed .get ("param" )
92+ self .field_errors : dict [str , Any ] = (
93+ field_errors if field_errors is not None else parsed .get ("field_errors" , {})
94+ )
95+
96+ def __str__ (self ) -> str :
97+ message = self .message
98+ if self .param :
99+ message = f"{ message } (param: { self .param } )"
100+ if self .status_code is None :
101+ return message
102+ return f"{ message } (status code: { self .status_code } )"
103+
104+ @classmethod
105+ def from_response (
106+ cls ,
107+ status_code : int ,
108+ response_body : Optional [str ] = None ,
109+ ) -> "InvalidRequestError" :
110+ """Construct from a raw 400/422 API response body."""
111+ parsed = _parse_error_response (response_body )
112+ message = parsed .get ("message" ) or "Invalid request"
113+ return cls (
114+ message ,
115+ status_code = status_code ,
116+ response_body = response_body ,
117+ param = parsed .get ("param" ),
118+ field_errors = parsed .get ("field_errors" , {}),
119+ )
78120
79121
80122class NotFoundError (ShadeError ):
@@ -119,4 +161,46 @@ def _parse_body(response_body: Optional[str]) -> dict:
119161
120162
121163class NetworkError (ShadeError ):
122- """Raised when the SDK cannot complete a network request."""
164+ """Raised when the SDK cannot complete a network request."""
165+
166+
167+ def raise_for_invalid_request (
168+ status_code : int ,
169+ response_body : Optional [str ] = None ,
170+ ) -> None :
171+ """Raise InvalidRequestError when the API returns 400 or 422."""
172+ if status_code in INVALID_REQUEST_STATUS_CODES :
173+ raise InvalidRequestError .from_response (status_code , response_body )
174+
175+
176+ def _parse_body (response_body : Optional [str ]) -> dict [str , Any ]:
177+ if not response_body :
178+ return {}
179+ try :
180+ data = json .loads (response_body )
181+ return data if isinstance (data , dict ) else {}
182+ except (json .JSONDecodeError , ValueError ):
183+ return {}
184+
185+
186+ def _parse_error_response (response_body : Optional [str ]) -> dict [str , Any ]:
187+ data = _parse_body (response_body )
188+ error = data .get ("error" , {})
189+ if not isinstance (error , dict ):
190+ error = {}
191+
192+ field_errors = error .get ("field_errors" )
193+ if not isinstance (field_errors , dict ):
194+ field_errors = data .get ("field_errors" )
195+ if not isinstance (field_errors , dict ):
196+ field_errors = {}
197+
198+ message = error .get ("message" )
199+ if not message :
200+ message = data .get ("message" )
201+
202+ return {
203+ "message" : message ,
204+ "param" : error .get ("param" ),
205+ "field_errors" : field_errors ,
206+ }
0 commit comments