55interact with these directly.
66
77The `init_session()` function can be used to specify an alternative root URL, and to
8- provide an authentication key (if required). If `init_session()` is not called, the
9- default root URL (see `API_ROOT` below) will be used, and no authentication keys will be
10- included when making API calls.
8+ provide an authentication key or bearer token (if required). If `init_session()` is not
9+ called, the default root URL (see `API_ROOT` below) will be used, and no authentication
10+ headers will be included when making API calls.
1111
1212Example: Initializing a session
1313
1717 # Specify an alternate URL and an auth key
1818 init_session(api_root="https://example.com/cwms-data", api_key="API_KEY")
1919
20+ # Specify an alternate URL and an OIDC bearer token
21+ init_session(api_root="https://example.com/cwms-data", token="ACCESS_TOKEN")
22+
2023Functions which make API calls that _may_ return a JSON response will return a `dict`
2124containing the deserialized data. If the API response does not include data, an empty
2225`dict` will be returned.
3437from typing import Any , Optional , cast
3538
3639from requests import Response , adapters
40+ from requests .exceptions import RetryError as RequestsRetryError
3741from requests_toolbelt import sessions # type: ignore
3842from requests_toolbelt .sessions import BaseUrlSession # type: ignore
3943from urllib3 .util .retry import Retry
5256 status_forcelist = [
5357 403 ,
5458 429 ,
55- 500 ,
5659 502 ,
5760 503 ,
5861 504 ,
5962 ], # Example: also retry on these HTTP status codes
6063 allowed_methods = ["GET" , "PUT" , "POST" , "PATCH" , "DELETE" ], # Methods to retry
64+ raise_on_status = False ,
6165)
6266SESSION = sessions .BaseUrlSession (base_url = API_ROOT )
6367adapter = adapters .HTTPAdapter (
@@ -137,21 +141,46 @@ class PermissionError(ApiError):
137141 """Raised when the CDA request is not authorized for the current caller."""
138142
139143
144+ def _unwrap_retry_error (error : RequestsRetryError ) -> Exception :
145+ """Return the original retry cause when requests wraps it in RetryError."""
146+
147+ current : Exception = error
148+ cause = error .__cause__
149+ while isinstance (cause , Exception ):
150+ current = cause
151+ cause = cause .__cause__
152+
153+ if current is error and error .args :
154+ first_arg = error .args [0 ]
155+ if isinstance (first_arg , Exception ):
156+ current = first_arg
157+ reason = getattr (current , "reason" , None )
158+ while isinstance (reason , Exception ):
159+ current = reason
160+ reason = getattr (current , "reason" , None )
161+
162+ return current
163+
164+
140165def init_session (
141166 * ,
142167 api_root : Optional [str ] = None ,
143168 api_key : Optional [str ] = None ,
169+ token : Optional [str ] = None ,
144170 pool_connections : int = 100 ,
145171) -> BaseUrlSession :
146- """Specify a root URL and authentication key for the CWMS Data API.
172+ """Specify a root URL and authentication credentials for the CWMS Data API.
147173
148174 This function can be used to change the root URL used when interacting with the CDA.
149- All API calls made after this function is called will use the specified URL. If an
150- authentication key is given it will be included in all future request headers.
175+ All API calls made after this function is called will use the specified URL. If
176+ authentication credentials are given they will be included in all future request
177+ headers.
151178
152179 Keyword Args:
153180 api_root (optional): The root URL for the CWMS Data API.
154181 api_key (optional): An authentication key.
182+ token (optional): A Keycloak access token. If both token and api_key are
183+ provided, token is used.
155184
156185 Returns:
157186 Returns the updated session object.
@@ -169,7 +198,16 @@ def init_session(
169198 max_retries = retry_strategy ,
170199 )
171200 SESSION .mount ("https://" , adapter )
172- if api_key :
201+ if token :
202+ if api_key :
203+ logging .warning (
204+ "Both token and api_key were provided to init_session(); using token for Authorization."
205+ )
206+ # Ensure we don't provide the bearer text twice
207+ if token .lower ().startswith ("bearer " ):
208+ token = token [7 :]
209+ SESSION .headers .update ({"Authorization" : "Bearer " + token })
210+ elif api_key :
173211 if api_key .startswith ("apikey " ):
174212 api_key = api_key .replace ("apikey " , "" )
175213 SESSION .headers .update ({"Authorization" : "apikey " + api_key })
@@ -292,11 +330,14 @@ def get(
292330 """
293331
294332 headers = {"Accept" : api_version_text (api_version )}
295- with SESSION .get (endpoint , params = params , headers = headers ) as response :
296- if not response .ok :
297- logging .error (f"CDA Error: response={ response } " )
298- raise ApiError (response )
299- return _process_response (response )
333+ try :
334+ with SESSION .get (endpoint , params = params , headers = headers ) as response :
335+ if not response .ok :
336+ logging .error (f"CDA Error: response={ response } " )
337+ raise ApiError (response )
338+ return _process_response (response )
339+ except RequestsRetryError as error :
340+ raise _unwrap_retry_error (error ) from None
300341
301342
302343def get_with_paging (
@@ -351,11 +392,16 @@ def _post_function(
351392 headers = {"accept" : "*/*" , "Content-Type" : api_version_text (api_version )}
352393 if isinstance (data , dict ) or isinstance (data , list ):
353394 data = json .dumps (data )
354- with SESSION .post (endpoint , params = params , headers = headers , data = data ) as response :
355- if not response .ok :
356- logging .error (f"CDA Error: response={ response } " )
357- raise ApiError (response )
358- return response
395+ try :
396+ with SESSION .post (
397+ endpoint , params = params , headers = headers , data = data
398+ ) as response :
399+ if not response .ok :
400+ logging .error (f"CDA Error: response={ response } " )
401+ raise ApiError (response )
402+ return response
403+ except RequestsRetryError as error :
404+ raise _unwrap_retry_error (error ) from None
359405
360406
361407def post (
@@ -445,10 +491,15 @@ def patch(
445491
446492 if data and isinstance (data , dict ) or isinstance (data , list ):
447493 data = json .dumps (data )
448- with SESSION .patch (endpoint , params = params , headers = headers , data = data ) as response :
449- if not response .ok :
450- logging .error (f"CDA Error: response={ response } " )
451- raise ApiError (response )
494+ try :
495+ with SESSION .patch (
496+ endpoint , params = params , headers = headers , data = data
497+ ) as response :
498+ if not response .ok :
499+ logging .error (f"CDA Error: response={ response } " )
500+ raise ApiError (response )
501+ except RequestsRetryError as error :
502+ raise _unwrap_retry_error (error ) from None
452503
453504
454505def delete (
@@ -472,7 +523,10 @@ def delete(
472523 """
473524
474525 headers = {"Accept" : api_version_text (api_version )}
475- with SESSION .delete (endpoint , params = params , headers = headers ) as response :
476- if not response .ok :
477- logging .error (f"CDA Error: response={ response } " )
478- raise ApiError (response )
526+ try :
527+ with SESSION .delete (endpoint , params = params , headers = headers ) as response :
528+ if not response .ok :
529+ logging .error (f"CDA Error: response={ response } " )
530+ raise ApiError (response )
531+ except RequestsRetryError as error :
532+ raise _unwrap_retry_error (error ) from None
0 commit comments