@@ -88,12 +88,66 @@ def __init__(
8888 code : str | None = None ,
8989 status_code : int | None = None ,
9090 url : str | None = None ,
91+ data : Any = None ,
9192 ) -> None :
9293 super ().__init__ (message )
9394 self .message = message
9495 self .code = code
9596 self .status_code = status_code
9697 self .url = url
98+ self .data = data
99+
100+
101+ class AuthorizationRequiredError (APIError ):
102+ """Raised when a secret or connection has not been authenticated yet.
103+
104+ ``url`` is the address the user must visit to authorize.
105+ """
106+
107+ def __init__ (
108+ self ,
109+ message : str ,
110+ * ,
111+ status_code : int | None = None ,
112+ url : str | None = None ,
113+ data : Any = None ,
114+ ) -> None :
115+ super ().__init__ (
116+ message ,
117+ code = "AUTHORIZATION_REQUIRED" ,
118+ status_code = status_code ,
119+ url = url ,
120+ data = data ,
121+ )
122+
123+
124+ def _handle_proxy_response (response : httpx .Response ) -> httpx .Response :
125+ """Surface a CBK control response from a proxied request.
126+
127+ Successful and non-JSON error responses are returned untouched; a CBK
128+ ``authorization_required`` signal is raised as an AuthorizationRequiredError
129+ carrying the authorize URL, while a genuine upstream error is returned as-is.
130+ """
131+ if response .status_code < 400 :
132+ return response
133+
134+ if "application/json" not in response .headers .get ("content-type" , "" ):
135+ return response
136+
137+ try :
138+ data = response .json ()
139+ except ValueError :
140+ return response
141+
142+ if isinstance (data , dict ) and data .get ("error" ) == "authorization_required" :
143+ raise AuthorizationRequiredError (
144+ data .get ("message" ) or "authorization required" ,
145+ status_code = response .status_code ,
146+ url = data .get ("url" ),
147+ data = data ,
148+ )
149+
150+ return response
97151
98152
99153class Response (Generic [T , U ]):
@@ -272,6 +326,34 @@ async def request(
272326
273327 return response
274328
329+ async def proxy (
330+ self ,
331+ path : str ,
332+ * ,
333+ method : str | None = None ,
334+ query : Any = None ,
335+ record : Any = None ,
336+ headers : Mapping [str , str ] | None = None ,
337+ endpoint : str | None = None ,
338+ ) -> httpx .Response :
339+ """Proxy a request and return the upstream response.
340+
341+ Successful and upstream-error responses pass through untouched; a CBK
342+ ``authorization_required`` signal is raised as an
343+ AuthorizationRequiredError carrying the URL the user must visit.
344+ """
345+ response = await self .request (
346+ path ,
347+ method = method ,
348+ query = query ,
349+ record = record ,
350+ headers = headers ,
351+ endpoint = endpoint ,
352+ raw = True ,
353+ )
354+
355+ return _handle_proxy_response (response )
356+
275357 def stream (
276358 self ,
277359 path : str ,
@@ -371,18 +453,29 @@ async def raise_for_status(self, response: httpx.Response) -> None:
371453
372454 message = f"HTTP Error: { response .reason_phrase } "
373455 code = f"ERROR_{ response .status_code } "
456+ data : Any = None
374457
375458 try :
376459 data = response .json ()
377- message = data .get ("message" ) or message
378- code = data .get ("code" ) or code
460+ if isinstance (data , dict ):
461+ message = data .get ("message" ) or message
462+ code = data .get ("code" ) or code
379463 except ValueError :
380464 body = await response .aread ()
381465 message = body .decode () or f"HTTP Error: { response .status_code } "
382466
467+ if isinstance (data , dict ) and data .get ("error" ) == "authorization_required" :
468+ raise AuthorizationRequiredError (
469+ message ,
470+ status_code = response .status_code ,
471+ url = data .get ("url" ),
472+ data = data ,
473+ )
474+
383475 raise APIError (
384476 message ,
385477 code = code ,
386478 status_code = response .status_code ,
387479 url = str (response .url ),
480+ data = data ,
388481 )
0 commit comments