airbyte_cdk.sources.declarative.auth.token_provider
1# 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved. 3# 4 5 6import datetime 7import threading 8from abc import abstractmethod 9from dataclasses import InitVar, dataclass, field 10from typing import Any, List, Mapping, Optional, Union 11 12import dpath 13from isodate import Duration 14 15from airbyte_cdk.sources.declarative.decoders.decoder import Decoder 16from airbyte_cdk.sources.declarative.decoders.json_decoder import JsonDecoder 17from airbyte_cdk.sources.declarative.exceptions import ReadException 18from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString 19from airbyte_cdk.sources.declarative.requesters.requester import Requester 20from airbyte_cdk.sources.http_logger import format_http_message 21from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository 22from airbyte_cdk.sources.types import Config 23from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now 24 25 26class TokenProvider: 27 @abstractmethod 28 def get_token(self) -> str: 29 pass 30 31 32@dataclass 33class SessionTokenProvider(TokenProvider): 34 login_requester: Requester 35 session_token_path: List[str] 36 expiration_duration: Optional[Union[datetime.timedelta, Duration]] 37 parameters: InitVar[Mapping[str, Any]] 38 message_repository: MessageRepository = NoopMessageRepository() 39 decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={})) 40 41 _next_expiration_time: Optional[AirbyteDateTime] = None 42 _token: Optional[str] = None 43 44 def __post_init__(self, parameters: Mapping[str, Any]) -> None: 45 self._lock = threading.Lock() 46 47 def get_token(self) -> str: 48 self._refresh_if_necessary() 49 if self._token is None: 50 raise ReadException("Failed to get session token, token is None") 51 return self._token 52 53 def _refresh_if_necessary(self) -> None: 54 if self.expiration_duration is None: 55 # Without an expiry nothing is cached and every call logs in, so there is nothing to lock 56 self._refresh() 57 elif self._token_has_expired(): 58 with self._lock: 59 if self._token_has_expired(): 60 self._refresh() 61 62 def _token_has_expired(self) -> bool: 63 return self._next_expiration_time is None or self._next_expiration_time < ab_datetime_now() 64 65 def _refresh(self) -> None: 66 response = self.login_requester.send_request( 67 log_formatter=lambda response: format_http_message( 68 response, 69 "Login request", 70 "Obtains session token", 71 None, 72 is_auxiliary=True, 73 type="AUTH", 74 ), 75 ) 76 if response is None: 77 raise ReadException("Failed to get session token, response got ignored by requester") 78 session_token = dpath.get(next(self.decoder.decode(response)), self.session_token_path) 79 # Set the token first: `get_token` reads it without the lock as soon as the expiry is fresh 80 self._token = session_token # type: ignore # Returned decoded response will be Mapping and therefore session_token will be str or None 81 if self.expiration_duration is not None: 82 self._next_expiration_time = ab_datetime_now() + self.expiration_duration 83 84 85@dataclass 86class InterpolatedStringTokenProvider(TokenProvider): 87 """Provides a token by interpolating a string with config values.""" 88 89 config: Config 90 api_token: Union[InterpolatedString, str] 91 parameters: Mapping[str, Any] 92 93 def __post_init__(self) -> None: 94 self._token = InterpolatedString.create(self.api_token, parameters=self.parameters) 95 96 def get_token(self) -> str: 97 return str(self._token.eval(self.config)) 98 99 100@dataclass 101class InterpolatedSessionTokenProvider(TokenProvider): 102 """Provides a token by interpolating a template with the session token. 103 104 This allows flexible token formatting, such as "Token {{ session_token }}" 105 for Django REST Framework APIs that expect "Authorization: Token <value>". 106 """ 107 108 config: Config 109 api_token: Union[InterpolatedString, str] 110 session_token_provider: TokenProvider 111 parameters: Mapping[str, Any] 112 113 def __post_init__(self) -> None: 114 self._token_template = InterpolatedString.create(self.api_token, parameters=self.parameters) 115 116 def get_token(self) -> str: 117 session_token = self.session_token_provider.get_token() 118 return str(self._token_template.eval(self.config, session_token=session_token))
class
TokenProvider:
33@dataclass 34class SessionTokenProvider(TokenProvider): 35 login_requester: Requester 36 session_token_path: List[str] 37 expiration_duration: Optional[Union[datetime.timedelta, Duration]] 38 parameters: InitVar[Mapping[str, Any]] 39 message_repository: MessageRepository = NoopMessageRepository() 40 decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={})) 41 42 _next_expiration_time: Optional[AirbyteDateTime] = None 43 _token: Optional[str] = None 44 45 def __post_init__(self, parameters: Mapping[str, Any]) -> None: 46 self._lock = threading.Lock() 47 48 def get_token(self) -> str: 49 self._refresh_if_necessary() 50 if self._token is None: 51 raise ReadException("Failed to get session token, token is None") 52 return self._token 53 54 def _refresh_if_necessary(self) -> None: 55 if self.expiration_duration is None: 56 # Without an expiry nothing is cached and every call logs in, so there is nothing to lock 57 self._refresh() 58 elif self._token_has_expired(): 59 with self._lock: 60 if self._token_has_expired(): 61 self._refresh() 62 63 def _token_has_expired(self) -> bool: 64 return self._next_expiration_time is None or self._next_expiration_time < ab_datetime_now() 65 66 def _refresh(self) -> None: 67 response = self.login_requester.send_request( 68 log_formatter=lambda response: format_http_message( 69 response, 70 "Login request", 71 "Obtains session token", 72 None, 73 is_auxiliary=True, 74 type="AUTH", 75 ), 76 ) 77 if response is None: 78 raise ReadException("Failed to get session token, response got ignored by requester") 79 session_token = dpath.get(next(self.decoder.decode(response)), self.session_token_path) 80 # Set the token first: `get_token` reads it without the lock as soon as the expiry is fresh 81 self._token = session_token # type: ignore # Returned decoded response will be Mapping and therefore session_token will be str or None 82 if self.expiration_duration is not None: 83 self._next_expiration_time = ab_datetime_now() + self.expiration_duration
SessionTokenProvider( login_requester: airbyte_cdk.Requester, session_token_path: List[str], expiration_duration: Union[datetime.timedelta, isodate.duration.Duration, NoneType], parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]], message_repository: airbyte_cdk.MessageRepository = <airbyte_cdk.sources.message.NoopMessageRepository object>, decoder: airbyte_cdk.Decoder = <factory>, _next_expiration_time: Optional[airbyte_cdk.utils.datetime_helpers.AirbyteDateTime] = None, _token: Optional[str] = None)
login_requester: airbyte_cdk.Requester
message_repository: airbyte_cdk.MessageRepository =
<airbyte_cdk.sources.message.NoopMessageRepository object>
decoder: airbyte_cdk.Decoder
86@dataclass 87class InterpolatedStringTokenProvider(TokenProvider): 88 """Provides a token by interpolating a string with config values.""" 89 90 config: Config 91 api_token: Union[InterpolatedString, str] 92 parameters: Mapping[str, Any] 93 94 def __post_init__(self) -> None: 95 self._token = InterpolatedString.create(self.api_token, parameters=self.parameters) 96 97 def get_token(self) -> str: 98 return str(self._token.eval(self.config))
Provides a token by interpolating a string with config values.
InterpolatedStringTokenProvider( config: Mapping[str, Any], api_token: Union[airbyte_cdk.InterpolatedString, str], parameters: Mapping[str, Any])
api_token: Union[airbyte_cdk.InterpolatedString, str]
101@dataclass 102class InterpolatedSessionTokenProvider(TokenProvider): 103 """Provides a token by interpolating a template with the session token. 104 105 This allows flexible token formatting, such as "Token {{ session_token }}" 106 for Django REST Framework APIs that expect "Authorization: Token <value>". 107 """ 108 109 config: Config 110 api_token: Union[InterpolatedString, str] 111 session_token_provider: TokenProvider 112 parameters: Mapping[str, Any] 113 114 def __post_init__(self) -> None: 115 self._token_template = InterpolatedString.create(self.api_token, parameters=self.parameters) 116 117 def get_token(self) -> str: 118 session_token = self.session_token_provider.get_token() 119 return str(self._token_template.eval(self.config, session_token=session_token))
Provides a token by interpolating a template with the session token.
This allows flexible token formatting, such as "Token {{ session_token }}"
for Django REST Framework APIs that expect "Authorization: Token
InterpolatedSessionTokenProvider( config: Mapping[str, Any], api_token: Union[airbyte_cdk.InterpolatedString, str], session_token_provider: TokenProvider, parameters: Mapping[str, Any])
api_token: Union[airbyte_cdk.InterpolatedString, str]
session_token_provider: TokenProvider