@@ -127,7 +127,9 @@ def log_response(response, *args, **kwargs):
127127
128128class AtlanClient (BaseSettings ):
129129 base_url : Union [Literal ["INTERNAL" ], HttpUrl ]
130- api_key : str
130+ api_key : Optional [str ] = None
131+ oauth_client_id : Optional [str ] = None
132+ oauth_client_secret : Optional [str ] = None
131133 connect_timeout : float = 30.0 # 30 secs
132134 read_timeout : float = 900.0 # 15 mins
133135 retry : Retry = DEFAULT_RETRY
@@ -137,6 +139,7 @@ class AtlanClient(BaseSettings):
137139 _session : httpx .Client = PrivateAttr ()
138140 _request_params : dict = PrivateAttr ()
139141 _user_id : Optional [str ] = PrivateAttr (default = None )
142+ _oauth_token_manager : Optional [Any ] = PrivateAttr (default = None )
140143 _workflow_client : Optional [WorkflowClient ] = PrivateAttr (default = None )
141144 _credential_client : Optional [CredentialClient ] = PrivateAttr (default = None )
142145 _admin_client : Optional [AdminClient ] = PrivateAttr (default = None )
@@ -172,17 +175,24 @@ class Config:
172175
173176 def __init__ (self , ** data ):
174177 super ().__init__ (** data )
175- self ._request_params = (
176- {"headers" : {"authorization" : f"Bearer { self .api_key } " }}
177- if self .api_key and self .api_key .strip ()
178- else {"headers" : {}}
179- )
180178
181- # Build proxy/SSL configuration with environment variable fallback
179+ if self .oauth_client_id and self .oauth_client_secret :
180+ from pyatlan .client .oauth import OAuthTokenManager
181+
182+ self ._oauth_token_manager = OAuthTokenManager (
183+ base_url = str (self .base_url ),
184+ client_id = self .oauth_client_id ,
185+ client_secret = self .oauth_client_secret ,
186+ )
187+ self ._request_params = {"headers" : {}}
188+ else :
189+ self ._request_params = (
190+ {"headers" : {"authorization" : f"Bearer { self .api_key } " }}
191+ if self .api_key and self .api_key .strip ()
192+ else {"headers" : {}}
193+ )
194+
182195 transport_kwargs = self ._build_transport_proxy_config (data )
183- # Configure httpx client with custom transport that supports retry and proxy
184- # Note: We pass proxy/SSL config to the transport, not the client,
185- # so that retry logic properly respects these settings
186196 self ._session = httpx .Client (
187197 transport = PyatlanSyncTransport (retry = self .retry , ** transport_kwargs ),
188198 headers = {
@@ -691,7 +701,7 @@ def _call_api_internal(
691701 # Retry with impersonation (if _user_id is present)
692702 # on authentication failure (token may have expired)
693703 if (
694- self ._user_id
704+ ( self ._user_id or self . _oauth_token_manager )
695705 and not self ._401_has_retried .get ()
696706 and response .status_code
697707 == ErrorCode .AUTHENTICATION_PASSTHROUGH .http_error_code
@@ -813,15 +823,15 @@ def _create_params(
813823 self , api : API , query_params , request_obj , exclude_unset : bool = True
814824 ):
815825 params = copy .deepcopy (self ._request_params )
826+ if self ._oauth_token_manager :
827+ token = self ._oauth_token_manager .get_token ()
828+ params ["headers" ]["authorization" ] = f"Bearer { token } "
816829 params ["headers" ]["Accept" ] = api .consumes
817830 params ["headers" ]["content-type" ] = api .produces
818831 if query_params is not None :
819832 params ["params" ] = query_params
820833 if request_obj is not None :
821834 if isinstance (request_obj , AtlanObject ):
822- # Always use AtlanRequest, which accepts a Pydantic model instance and the client
823- # Behind the scenes, it handles retranslation tasks—such as converting
824- # human-readable Atlan tag names back into hashed IDs as required by the backend
825835 params ["data" ] = AtlanRequest (instance = request_obj , client = self ).json ()
826836 elif api .consumes == APPLICATION_ENCODED_FORM :
827837 params ["data" ] = request_obj
@@ -838,14 +848,21 @@ def _handle_401_token_refresh(
838848 download_file_path = None ,
839849 text_response = False ,
840850 ):
841- """
842- Handles token refresh and retries the API request upon a 401 Unauthorized response.
843- 1. Impersonates the user (if a user ID is available) to fetch a new token.
844- 2. Updates the authorization header with the refreshed token.
845- 3. Retries the API request with the new token.
851+ if self ._oauth_token_manager :
852+ self ._oauth_token_manager .invalidate_token ()
853+ token = self ._oauth_token_manager .get_token ()
854+ params ["headers" ]["authorization" ] = f"Bearer { token } "
855+ self ._401_has_retried .set (True )
856+ LOGGER .debug ("Successfully refreshed OAuth token after 401." )
857+ return self ._call_api_internal (
858+ api ,
859+ path ,
860+ params ,
861+ binary_data = binary_data ,
862+ download_file_path = download_file_path ,
863+ text_response = text_response ,
864+ )
846865
847- returns: HTTP response received after retrying the request with the refreshed token
848- """
849866 try :
850867 new_token = self .impersonate .user (user_id = self ._user_id )
851868 except Exception as e :
@@ -861,11 +878,6 @@ def _handle_401_token_refresh(
861878 self ._request_params ["headers" ]["authorization" ] = f"Bearer { self .api_key } "
862879 LOGGER .debug ("Successfully completed 401 automatic token refresh." )
863880
864- # Added a retry loop to ensure a token is active before retrying original request
865- # This helps ensure that when we fetch typedefs using the new token,
866- # the backend has fully recognized the token as valid.
867- # Without this delay, we occasionally get an empty response `[]` from the API,
868- # likely because the backend hasn’t fully propagated token validity yet.
869881 import time
870882
871883 retry_count = 1
@@ -879,10 +891,9 @@ def _handle_401_token_refresh(
879891 "Retrying to get typedefs (to ensure token is active) after token refresh failed: %s" ,
880892 e ,
881893 )
882- time .sleep (retry_count ) # Linear backoff
894+ time .sleep (retry_count )
883895 retry_count += 1
884896
885- # Retry the API call with the new token
886897 return self ._call_api_internal (
887898 api ,
888899 path ,
0 commit comments