1717T = TypeVar ("T" )
1818
1919
20+ def _parse_and_raise (
21+ content : bytes ,
22+ status_code : int ,
23+ headers : Mapping [str , str ],
24+ ) -> dict [str , Any ]:
25+ """Parse JSON response body and raise on HTTP errors.
26+
27+ Shared by both sync and async request classes to avoid duplicating
28+ the JSON-decode, error-extraction, and rate-limit-retry logic.
29+
30+ Returns:
31+ The parsed JSON dict (caller decides whether to unwrap ``data``).
32+
33+ Raises:
34+ OMOPHubError: On invalid JSON from a successful response.
35+ APIError / RateLimitError / etc.: On HTTP error status codes.
36+ """
37+ request_id = headers .get ("X-Request-Id" ) or headers .get ("x-request-id" )
38+
39+ try :
40+ data = json .loads (content ) if content else {}
41+ except json .JSONDecodeError as exc :
42+ if status_code >= 400 :
43+ raise_for_status (
44+ status_code ,
45+ f"Request failed with status { status_code } " ,
46+ request_id = request_id ,
47+ )
48+ raise OMOPHubError (
49+ f"Invalid JSON response: { content [:200 ].decode (errors = 'replace' )} "
50+ ) from exc
51+
52+ if status_code >= 400 :
53+ error_response : ErrorResponse = data # type: ignore[assignment]
54+ error = error_response .get ("error" , {})
55+ message = error .get ("message" , f"Request failed with status { status_code } " )
56+ error_code = error .get ("code" )
57+ details = error .get ("details" )
58+
59+ retry_after = None
60+ if status_code == 429 :
61+ retry_after_header = headers .get ("Retry-After" ) or headers .get (
62+ "retry-after"
63+ )
64+ if retry_after_header :
65+ with contextlib .suppress (ValueError ):
66+ retry_after = int (retry_after_header )
67+
68+ raise_for_status (
69+ status_code ,
70+ message ,
71+ request_id = request_id ,
72+ error_code = error_code ,
73+ details = details ,
74+ retry_after = retry_after ,
75+ )
76+
77+ return data # type: ignore[return-value]
78+
79+
2080class Request (Generic [T ]):
2181 """Handles API request execution and response parsing."""
2282
@@ -50,50 +110,8 @@ def _parse_response(
50110 status_code : int ,
51111 headers : Mapping [str , str ],
52112 ) -> T :
53- """Parse API response and handle errors."""
54- request_id = headers .get ("X-Request-Id" ) or headers .get ("x-request-id" )
55-
56- try :
57- data = json .loads (content ) if content else {}
58- except json .JSONDecodeError as exc :
59- if status_code >= 400 :
60- raise_for_status (
61- status_code ,
62- f"Request failed with status { status_code } " ,
63- request_id = request_id ,
64- )
65- raise OMOPHubError (
66- f"Invalid JSON response: { content [:200 ].decode (errors = 'replace' )} "
67- ) from exc
68-
69- # Handle error responses
70- if status_code >= 400 :
71- error_response : ErrorResponse = data # type: ignore[assignment]
72- error = error_response .get ("error" , {})
73- message = error .get ("message" , f"Request failed with status { status_code } " )
74- error_code = error .get ("code" )
75- details = error .get ("details" )
76-
77- # Check for rate limit retry-after
78- retry_after = None
79- if status_code == 429 :
80- retry_after_header = headers .get ("Retry-After" ) or headers .get (
81- "retry-after"
82- )
83- if retry_after_header :
84- with contextlib .suppress (ValueError ):
85- retry_after = int (retry_after_header )
86-
87- raise_for_status (
88- status_code ,
89- message ,
90- request_id = request_id ,
91- error_code = error_code ,
92- details = details ,
93- retry_after = retry_after ,
94- )
95-
96- # Return successful response data
113+ """Parse API response, raise on errors, return the ``data`` field."""
114+ data = _parse_and_raise (content , status_code , headers )
97115 response : APIResponse = data # type: ignore[assignment]
98116 return response .get ("data" , data )
99117
@@ -103,55 +121,8 @@ def _parse_response_raw(
103121 status_code : int ,
104122 headers : Mapping [str , str ],
105123 ) -> dict [str , Any ]:
106- """Parse API response and return full response dict with meta.
107-
108- Unlike _parse_response which extracts just the 'data' field,
109- this method returns the complete response including 'meta' for pagination.
110- """
111- request_id = headers .get ("X-Request-Id" ) or headers .get ("x-request-id" )
112-
113- try :
114- data = json .loads (content ) if content else {}
115- except json .JSONDecodeError as exc :
116- if status_code >= 400 :
117- raise_for_status (
118- status_code ,
119- f"Request failed with status { status_code } " ,
120- request_id = request_id ,
121- )
122- raise OMOPHubError (
123- f"Invalid JSON response: { content [:200 ].decode (errors = 'replace' )} "
124- ) from exc
125-
126- # Handle error responses
127- if status_code >= 400 :
128- error_response : ErrorResponse = data # type: ignore[assignment]
129- error = error_response .get ("error" , {})
130- message = error .get ("message" , f"Request failed with status { status_code } " )
131- error_code = error .get ("code" )
132- details = error .get ("details" )
133-
134- # Check for rate limit retry-after
135- retry_after = None
136- if status_code == 429 :
137- retry_after_header = headers .get ("Retry-After" ) or headers .get (
138- "retry-after"
139- )
140- if retry_after_header :
141- with contextlib .suppress (ValueError ):
142- retry_after = int (retry_after_header )
143-
144- raise_for_status (
145- status_code ,
146- message ,
147- request_id = request_id ,
148- error_code = error_code ,
149- details = details ,
150- retry_after = retry_after ,
151- )
152-
153- # Return full response dict (includes 'data' and 'meta')
154- return data
124+ """Parse API response, raise on errors, return the full dict with ``meta``."""
125+ return _parse_and_raise (content , status_code , headers )
155126
156127 def get (
157128 self ,
@@ -238,50 +209,8 @@ def _parse_response(
238209 status_code : int ,
239210 headers : Mapping [str , str ],
240211 ) -> T :
241- """Parse API response and handle errors."""
242- request_id = headers .get ("X-Request-Id" ) or headers .get ("x-request-id" )
243-
244- try :
245- data = json .loads (content ) if content else {}
246- except json .JSONDecodeError as exc :
247- if status_code >= 400 :
248- raise_for_status (
249- status_code ,
250- f"Request failed with status { status_code } " ,
251- request_id = request_id ,
252- )
253- raise OMOPHubError (
254- f"Invalid JSON response: { content [:200 ].decode (errors = 'replace' )} "
255- ) from exc
256-
257- # Handle error responses
258- if status_code >= 400 :
259- error_response : ErrorResponse = data # type: ignore[assignment]
260- error = error_response .get ("error" , {})
261- message = error .get ("message" , f"Request failed with status { status_code } " )
262- error_code = error .get ("code" )
263- details = error .get ("details" )
264-
265- # Check for rate limit retry-after
266- retry_after = None
267- if status_code == 429 :
268- retry_after_header = headers .get ("Retry-After" ) or headers .get (
269- "retry-after"
270- )
271- if retry_after_header :
272- with contextlib .suppress (ValueError ):
273- retry_after = int (retry_after_header )
274-
275- raise_for_status (
276- status_code ,
277- message ,
278- request_id = request_id ,
279- error_code = error_code ,
280- details = details ,
281- retry_after = retry_after ,
282- )
283-
284- # Return successful response data
212+ """Parse API response, raise on errors, return the ``data`` field."""
213+ data = _parse_and_raise (content , status_code , headers )
285214 response : APIResponse = data # type: ignore[assignment]
286215 return response .get ("data" , data )
287216
@@ -291,55 +220,8 @@ def _parse_response_raw(
291220 status_code : int ,
292221 headers : Mapping [str , str ],
293222 ) -> dict [str , Any ]:
294- """Parse API response and return full response dict with meta.
295-
296- Unlike _parse_response which extracts just the 'data' field,
297- this method returns the complete response including 'meta' for pagination.
298- """
299- request_id = headers .get ("X-Request-Id" ) or headers .get ("x-request-id" )
300-
301- try :
302- data = json .loads (content ) if content else {}
303- except json .JSONDecodeError as exc :
304- if status_code >= 400 :
305- raise_for_status (
306- status_code ,
307- f"Request failed with status { status_code } " ,
308- request_id = request_id ,
309- )
310- raise OMOPHubError (
311- f"Invalid JSON response: { content [:200 ].decode (errors = 'replace' )} "
312- ) from exc
313-
314- # Handle error responses
315- if status_code >= 400 :
316- error_response : ErrorResponse = data # type: ignore[assignment]
317- error = error_response .get ("error" , {})
318- message = error .get ("message" , f"Request failed with status { status_code } " )
319- error_code = error .get ("code" )
320- details = error .get ("details" )
321-
322- # Check for rate limit retry-after
323- retry_after = None
324- if status_code == 429 :
325- retry_after_header = headers .get ("Retry-After" ) or headers .get (
326- "retry-after"
327- )
328- if retry_after_header :
329- with contextlib .suppress (ValueError ):
330- retry_after = int (retry_after_header )
331-
332- raise_for_status (
333- status_code ,
334- message ,
335- request_id = request_id ,
336- error_code = error_code ,
337- details = details ,
338- retry_after = retry_after ,
339- )
340-
341- # Return full response dict (includes 'data' and 'meta')
342- return data
223+ """Parse API response, raise on errors, return the full dict with ``meta``."""
224+ return _parse_and_raise (content , status_code , headers )
343225
344226 async def get (
345227 self ,
0 commit comments