airbyte_cdk.sources.streams.http.requests_native_auth

 1#
 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
 3#
 4
 5from .oauth import Oauth2Authenticator, SingleUseRefreshTokenOauth2Authenticator
 6from .protocols import ResponseAwareAuthenticator, TokenRotatingAuthenticator
 7from .token import BasicHttpAuthenticator, MultipleTokenAuthenticator, TokenAuthenticator
 8
 9__all__ = [
10    "Oauth2Authenticator",
11    "SingleUseRefreshTokenOauth2Authenticator",
12    "TokenAuthenticator",
13    "MultipleTokenAuthenticator",
14    "BasicHttpAuthenticator",
15    "ResponseAwareAuthenticator",
16    "TokenRotatingAuthenticator",
17]
 27class Oauth2Authenticator(AbstractOauth2Authenticator):
 28    """
 29    Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials.
 30    The generated access token is attached to each request via the Authorization header.
 31    If a connector_config is provided any mutation of it's value in the scope of this class will emit AirbyteControlConnectorConfigMessage.
 32    """
 33
 34    def __init__(
 35        self,
 36        token_refresh_endpoint: str,
 37        client_id: str,
 38        client_secret: str,
 39        refresh_token: str,
 40        client_id_name: str = "client_id",
 41        client_secret_name: str = "client_secret",
 42        refresh_token_name: str = "refresh_token",
 43        scopes: List[str] | None = None,
 44        token_expiry_date: AirbyteDateTime | None = None,
 45        token_expiry_date_format: str | None = None,
 46        access_token_name: str = "access_token",
 47        expires_in_name: str = "expires_in",
 48        refresh_request_body: Mapping[str, Any] | None = None,
 49        refresh_request_headers: Mapping[str, Any] | None = None,
 50        send_refresh_request_as_query_params: bool = False,
 51        grant_type_name: str = "grant_type",
 52        grant_type: str = "refresh_token",
 53        token_expiry_is_time_of_expiration: bool = False,
 54        refresh_token_error_status_codes: Tuple[int, ...] = (),
 55        refresh_token_error_key: str = "",
 56        refresh_token_error_values: Tuple[str, ...] = (),
 57    ) -> None:
 58        self._token_refresh_endpoint = token_refresh_endpoint
 59        self._client_secret_name = client_secret_name
 60        self._client_secret = client_secret
 61        self._client_id_name = client_id_name
 62        self._client_id = client_id
 63        self._refresh_token_name = refresh_token_name
 64        self._refresh_token = refresh_token
 65        self._scopes = scopes
 66        self._access_token_name = access_token_name
 67        self._expires_in_name = expires_in_name
 68        self._refresh_request_body = refresh_request_body
 69        self._refresh_request_headers = refresh_request_headers
 70        self._send_refresh_request_as_query_params = send_refresh_request_as_query_params
 71        self._grant_type_name = grant_type_name
 72        self._grant_type = grant_type
 73
 74        self._token_expiry_date = token_expiry_date or (ab_datetime_now() - timedelta(days=1))
 75        self._token_expiry_date_format = token_expiry_date_format
 76        self._token_expiry_is_time_of_expiration = token_expiry_is_time_of_expiration
 77        self._access_token = None
 78        super().__init__(
 79            refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values
 80        )
 81
 82    def get_token_refresh_endpoint(self) -> str:
 83        return self._token_refresh_endpoint
 84
 85    def get_client_id_name(self) -> str:
 86        return self._client_id_name
 87
 88    def get_client_id(self) -> str:
 89        return self._client_id
 90
 91    def get_client_secret_name(self) -> str:
 92        return self._client_secret_name
 93
 94    def get_client_secret(self) -> str:
 95        return self._client_secret
 96
 97    def get_refresh_token_name(self) -> str:
 98        return self._refresh_token_name
 99
100    def get_refresh_token(self) -> str:
101        return self._refresh_token
102
103    def get_access_token_name(self) -> str:
104        return self._access_token_name
105
106    def get_scopes(self) -> list[str]:
107        return self._scopes  # type: ignore[return-value]
108
109    def get_expires_in_name(self) -> str:
110        return self._expires_in_name
111
112    def get_refresh_request_body(self) -> Mapping[str, Any]:
113        return self._refresh_request_body  # type: ignore[return-value]
114
115    def get_refresh_request_headers(self) -> Mapping[str, Any]:
116        return self._refresh_request_headers  # type: ignore[return-value]
117
118    def should_send_refresh_request_as_query_params(self) -> bool:
119        return self._send_refresh_request_as_query_params
120
121    def get_grant_type_name(self) -> str:
122        return self._grant_type_name
123
124    def get_grant_type(self) -> str:
125        return self._grant_type
126
127    def get_token_expiry_date(self) -> AirbyteDateTime:
128        return self._token_expiry_date
129
130    def set_token_expiry_date(self, value: AirbyteDateTime) -> None:
131        # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`.
132        if not isinstance(value, datetime):
133            with suppress(ValueError, OverflowError):
134                value = self._parse_token_expiration_date(value)
135        self._token_expiry_date = value
136
137    @property
138    def token_expiry_is_time_of_expiration(self) -> bool:
139        return self._token_expiry_is_time_of_expiration
140
141    @property
142    def token_expiry_date_format(self) -> Optional[str]:
143        return self._token_expiry_date_format
144
145    @property
146    def access_token(self) -> str:
147        return self._access_token  # type: ignore[return-value]
148
149    @access_token.setter
150    def access_token(self, value: str) -> None:
151        self._access_token = value  # type: ignore[assignment]  # Incorrect type for assignment

Generates OAuth2.0 access tokens from an OAuth2.0 refresh token and client credentials. The generated access token is attached to each request via the Authorization header. If a connector_config is provided any mutation of it's value in the scope of this class will emit AirbyteControlConnectorConfigMessage.

Oauth2Authenticator( token_refresh_endpoint: str, client_id: str, client_secret: str, refresh_token: str, client_id_name: str = 'client_id', client_secret_name: str = 'client_secret', refresh_token_name: str = 'refresh_token', scopes: Optional[List[str]] = None, token_expiry_date: airbyte_cdk.utils.datetime_helpers.AirbyteDateTime | None = None, token_expiry_date_format: str | None = None, access_token_name: str = 'access_token', expires_in_name: str = 'expires_in', refresh_request_body: Optional[Mapping[str, Any]] = None, refresh_request_headers: Optional[Mapping[str, Any]] = None, send_refresh_request_as_query_params: bool = False, grant_type_name: str = 'grant_type', grant_type: str = 'refresh_token', token_expiry_is_time_of_expiration: bool = False, refresh_token_error_status_codes: Tuple[int, ...] = (), refresh_token_error_key: str = '', refresh_token_error_values: Tuple[str, ...] = ())
34    def __init__(
35        self,
36        token_refresh_endpoint: str,
37        client_id: str,
38        client_secret: str,
39        refresh_token: str,
40        client_id_name: str = "client_id",
41        client_secret_name: str = "client_secret",
42        refresh_token_name: str = "refresh_token",
43        scopes: List[str] | None = None,
44        token_expiry_date: AirbyteDateTime | None = None,
45        token_expiry_date_format: str | None = None,
46        access_token_name: str = "access_token",
47        expires_in_name: str = "expires_in",
48        refresh_request_body: Mapping[str, Any] | None = None,
49        refresh_request_headers: Mapping[str, Any] | None = None,
50        send_refresh_request_as_query_params: bool = False,
51        grant_type_name: str = "grant_type",
52        grant_type: str = "refresh_token",
53        token_expiry_is_time_of_expiration: bool = False,
54        refresh_token_error_status_codes: Tuple[int, ...] = (),
55        refresh_token_error_key: str = "",
56        refresh_token_error_values: Tuple[str, ...] = (),
57    ) -> None:
58        self._token_refresh_endpoint = token_refresh_endpoint
59        self._client_secret_name = client_secret_name
60        self._client_secret = client_secret
61        self._client_id_name = client_id_name
62        self._client_id = client_id
63        self._refresh_token_name = refresh_token_name
64        self._refresh_token = refresh_token
65        self._scopes = scopes
66        self._access_token_name = access_token_name
67        self._expires_in_name = expires_in_name
68        self._refresh_request_body = refresh_request_body
69        self._refresh_request_headers = refresh_request_headers
70        self._send_refresh_request_as_query_params = send_refresh_request_as_query_params
71        self._grant_type_name = grant_type_name
72        self._grant_type = grant_type
73
74        self._token_expiry_date = token_expiry_date or (ab_datetime_now() - timedelta(days=1))
75        self._token_expiry_date_format = token_expiry_date_format
76        self._token_expiry_is_time_of_expiration = token_expiry_is_time_of_expiration
77        self._access_token = None
78        super().__init__(
79            refresh_token_error_status_codes, refresh_token_error_key, refresh_token_error_values
80        )

If all of refresh_token_error_status_codes, refresh_token_error_key, and refresh_token_error_values are set, then http errors with such params will be wrapped in AirbyteTracedException.

def get_token_refresh_endpoint(self) -> str:
82    def get_token_refresh_endpoint(self) -> str:
83        return self._token_refresh_endpoint

Returns the endpoint to refresh the access token

def get_client_id_name(self) -> str:
85    def get_client_id_name(self) -> str:
86        return self._client_id_name

The client id name to authenticate

def get_client_id(self) -> str:
88    def get_client_id(self) -> str:
89        return self._client_id

The client id to authenticate

def get_client_secret_name(self) -> str:
91    def get_client_secret_name(self) -> str:
92        return self._client_secret_name

The client secret name to authenticate

def get_client_secret(self) -> str:
94    def get_client_secret(self) -> str:
95        return self._client_secret

The client secret to authenticate

def get_refresh_token_name(self) -> str:
97    def get_refresh_token_name(self) -> str:
98        return self._refresh_token_name

The refresh token name to authenticate

def get_refresh_token(self) -> str:
100    def get_refresh_token(self) -> str:
101        return self._refresh_token

The token used to refresh the access token when it expires

def get_access_token_name(self) -> str:
103    def get_access_token_name(self) -> str:
104        return self._access_token_name

Field to extract access token from in the response

def get_scopes(self) -> list[str]:
106    def get_scopes(self) -> list[str]:
107        return self._scopes  # type: ignore[return-value]

List of requested scopes

def get_expires_in_name(self) -> str:
109    def get_expires_in_name(self) -> str:
110        return self._expires_in_name

Returns the expires_in field name

def get_refresh_request_body(self) -> Mapping[str, Any]:
112    def get_refresh_request_body(self) -> Mapping[str, Any]:
113        return self._refresh_request_body  # type: ignore[return-value]

Returns the request body to set on the refresh request

def get_refresh_request_headers(self) -> Mapping[str, Any]:
115    def get_refresh_request_headers(self) -> Mapping[str, Any]:
116        return self._refresh_request_headers  # type: ignore[return-value]

Returns the request headers to set on the refresh request

def should_send_refresh_request_as_query_params(self) -> bool:
118    def should_send_refresh_request_as_query_params(self) -> bool:
119        return self._send_refresh_request_as_query_params

Returns True if the standard refresh args should be sent on the URL query string instead of in the request body.

Defaults to False so existing authenticators retain their previous behavior (params in body, no query params on the refresh URL). Subclasses can override this to opt into the URL-query-string shape required by OAuth providers like Gong.

def get_grant_type_name(self) -> str:
121    def get_grant_type_name(self) -> str:
122        return self._grant_type_name

Returns grant_type specified name for requesting access_token

def get_grant_type(self) -> str:
124    def get_grant_type(self) -> str:
125        return self._grant_type

Returns grant_type specified for requesting access_token

def get_token_expiry_date(self) -> airbyte_cdk.utils.datetime_helpers.AirbyteDateTime:
127    def get_token_expiry_date(self) -> AirbyteDateTime:
128        return self._token_expiry_date

Expiration date of the access token

def set_token_expiry_date(self, value: airbyte_cdk.utils.datetime_helpers.AirbyteDateTime) -> None:
130    def set_token_expiry_date(self, value: AirbyteDateTime) -> None:
131        # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`.
132        if not isinstance(value, datetime):
133            with suppress(ValueError, OverflowError):
134                value = self._parse_token_expiration_date(value)
135        self._token_expiry_date = value

Setter for access token expiration date

token_expiry_is_time_of_expiration: bool
137    @property
138    def token_expiry_is_time_of_expiration(self) -> bool:
139        return self._token_expiry_is_time_of_expiration

Indicates that the Token Expiry returns the date until which the token will be valid, not the amount of time it will be valid.

token_expiry_date_format: Optional[str]
141    @property
142    def token_expiry_date_format(self) -> Optional[str]:
143        return self._token_expiry_date_format

Format of the datetime; exists it if expires_in is returned as the expiration datetime instead of seconds until it expires

access_token: str
145    @property
146    def access_token(self) -> str:
147        return self._access_token  # type: ignore[return-value]

Returns the access token

class SingleUseRefreshTokenOauth2Authenticator(airbyte_cdk.sources.streams.http.requests_native_auth.Oauth2Authenticator):
154class SingleUseRefreshTokenOauth2Authenticator(Oauth2Authenticator):
155    """
156    Authenticator that should be used for API implementing single use refresh tokens:
157    when refreshing access token some API returns a new refresh token that needs to used in the next refresh flow.
158    This authenticator updates the configuration with new refresh token by emitting Airbyte control message from an observed mutation.
159    By default, this authenticator expects a connector config with a "credentials" field with the following nested fields: client_id,
160    client_secret, refresh_token. This behavior can be changed by defining custom config path (using dpath paths) in client_id_config_path,
161    client_secret_config_path, refresh_token_config_path constructor arguments.
162    """
163
164    def __init__(
165        self,
166        connector_config: Mapping[str, Any],
167        token_refresh_endpoint: str,
168        scopes: List[str] | None = None,
169        access_token_name: str = "access_token",
170        expires_in_name: str = "expires_in",
171        refresh_token_name: str = "refresh_token",
172        refresh_request_body: Mapping[str, Any] | None = None,
173        refresh_request_headers: Mapping[str, Any] | None = None,
174        send_refresh_request_as_query_params: bool = False,
175        grant_type_name: str = "grant_type",
176        grant_type: str = "refresh_token",
177        client_id_name: str = "client_id",
178        client_id: Optional[str] = None,
179        client_secret_name: str = "client_secret",
180        client_secret: Optional[str] = None,
181        access_token_config_path: Sequence[str] = ("credentials", "access_token"),
182        refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"),
183        token_expiry_date_config_path: Sequence[str] = ("credentials", "token_expiry_date"),
184        token_expiry_date_format: Optional[str] = None,
185        message_repository: MessageRepository = NoopMessageRepository(),
186        token_expiry_is_time_of_expiration: bool = False,
187        refresh_token_error_status_codes: Tuple[int, ...] = (),
188        refresh_token_error_key: str = "",
189        refresh_token_error_values: Tuple[str, ...] = (),
190    ) -> None:
191        """
192        Args:
193            connector_config (Mapping[str, Any]): The full connector configuration
194            token_refresh_endpoint (str): Full URL to the token refresh endpoint
195            scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None.
196            access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token".
197            expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in".
198            refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token".
199            refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None.
200            refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None.
201            send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (`grant_type`, `refresh_token`, client credentials when not in an `Authorization` header, scopes, plus any `refresh_request_body` extras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False.
202            grant_type (str, optional): OAuth grant type. Defaults to "refresh_token".
203            client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object.
204            client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object.
205            access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token").
206            refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token").
207            token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date").
208            token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration.
209            token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration
210            message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
211        """
212        self._connector_config = connector_config
213        self._client_id: str = self._get_config_value_by_path(
214            ("credentials", "client_id"), client_id
215        )
216        self._client_secret: str = self._get_config_value_by_path(
217            ("credentials", "client_secret"), client_secret
218        )
219        self._client_id_name = client_id_name
220        self._client_secret_name = client_secret_name
221        self._access_token_config_path = access_token_config_path
222        self._refresh_token_config_path = refresh_token_config_path
223        self._token_expiry_date_config_path = token_expiry_date_config_path
224        self._token_expiry_date_format = token_expiry_date_format
225        self._refresh_token_name = refresh_token_name
226        self._grant_type_name = grant_type_name
227        self._connector_config = connector_config
228        self.__message_repository = message_repository
229        super().__init__(
230            token_refresh_endpoint=token_refresh_endpoint,
231            client_id_name=self._client_id_name,
232            client_id=self._client_id,
233            client_secret_name=self._client_secret_name,
234            client_secret=self._client_secret,
235            refresh_token=self.get_refresh_token(),
236            refresh_token_name=self._refresh_token_name,
237            scopes=scopes,
238            token_expiry_date=self.get_token_expiry_date(),
239            access_token_name=access_token_name,
240            expires_in_name=expires_in_name,
241            refresh_request_body=refresh_request_body,
242            refresh_request_headers=refresh_request_headers,
243            send_refresh_request_as_query_params=send_refresh_request_as_query_params,
244            grant_type_name=self._grant_type_name,
245            grant_type=grant_type,
246            token_expiry_date_format=token_expiry_date_format,
247            token_expiry_is_time_of_expiration=token_expiry_is_time_of_expiration
248            or bool(token_expiry_date_format),
249            refresh_token_error_status_codes=refresh_token_error_status_codes,
250            refresh_token_error_key=refresh_token_error_key,
251            refresh_token_error_values=refresh_token_error_values,
252        )
253
254    @property
255    def access_token(self) -> str:
256        """
257        Retrieve the access token from the configuration.
258
259        Returns:
260            str: The access token.
261        """
262        return self._get_config_value_by_path(self._access_token_config_path)  # type: ignore[return-value]
263
264    @access_token.setter
265    def access_token(self, new_access_token: str) -> None:
266        """
267        Sets a new access token.
268
269        Args:
270            new_access_token (str): The new access token to be set.
271        """
272        self._set_config_value_by_path(self._access_token_config_path, new_access_token)
273
274    def get_refresh_token(self) -> str:
275        """
276        Retrieve the refresh token from the configuration.
277
278        This method fetches the refresh token using the configuration path specified
279        by `_refresh_token_config_path`.
280
281        Returns:
282            str: The refresh token as a string.
283        """
284        return self._get_config_value_by_path(self._refresh_token_config_path)  # type: ignore[return-value]
285
286    def set_refresh_token(self, new_refresh_token: str) -> None:
287        """
288        Updates the refresh token in the configuration.
289
290        Args:
291            new_refresh_token (str): The new refresh token to be set.
292        """
293        self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token)
294
295    def get_token_expiry_date(self) -> AirbyteDateTime:
296        """
297        Retrieves the token expiry date from the configuration.
298
299        This method fetches the token expiry date from the configuration using the specified path.
300        If the expiry date is an empty string, it returns the current date and time minus one day.
301        Otherwise, it parses the expiry date string into an AirbyteDateTime object.
302
303        Returns:
304            AirbyteDateTime: The parsed or calculated token expiry date.
305
306        Raises:
307            TypeError: If the result is not an instance of AirbyteDateTime.
308        """
309        expiry_date = self._get_config_value_by_path(self._token_expiry_date_config_path)
310        result = (
311            ab_datetime_now() - timedelta(days=1)
312            if expiry_date == ""
313            else ab_datetime_parse(str(expiry_date))
314        )
315        if isinstance(result, AirbyteDateTime):
316            return result
317        raise TypeError("Invalid datetime conversion")
318
319    def set_token_expiry_date(self, new_token_expiry_date: AirbyteDateTime) -> None:  # type: ignore[override]
320        """
321        Sets the token expiry date in the configuration.
322
323        Args:
324            new_token_expiry_date (AirbyteDateTime): The new expiry date for the token.
325        """
326        if not (
327            isinstance(new_token_expiry_date, datetime) or self.token_expiry_is_time_of_expiration
328        ):
329            # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`
330            # in seconds. Other values are stored as-is and parsed on read.
331            with suppress(ValueError, OverflowError):
332                new_token_expiry_date = self._parse_token_expiration_date(new_token_expiry_date)
333        self._set_config_value_by_path(
334            self._token_expiry_date_config_path, str(new_token_expiry_date)
335        )
336
337    def token_has_expired(self) -> bool:
338        """Returns True if the token is expired"""
339        return ab_datetime_now() > self.get_token_expiry_date()
340
341    def get_access_token(self) -> str:
342        """Retrieve new access and refresh token if the access token has expired.
343
344        This method uses double-checked locking to ensure thread-safe token refresh.
345        This is especially critical for single-use refresh tokens where concurrent
346        refresh attempts would cause failures as the refresh token is invalidated
347        after first use.
348
349        The new refresh token is persisted with the set_refresh_token function.
350
351        Returns:
352            str: The current access_token, updated if it was previously expired.
353        """
354        if self.token_has_expired():
355            with self._token_refresh_lock:
356                # Double-check after acquiring lock - another thread may have already refreshed
357                if self.token_has_expired():
358                    self.refresh_and_set_access_token()
359        return self.access_token
360
361    def refresh_and_set_access_token(self) -> None:
362        """Force refresh the access token and update internal state.
363
364        For single-use refresh tokens, this also persists the new refresh token
365        and emits a control message to update the connector config. If the
366        response omits a refresh token, the existing one is preserved.
367        """
368        token_before_waiting = self._current_access_token_or_none()
369        with self._token_refresh_lock:
370            if self._current_access_token_or_none() != token_before_waiting:
371                return
372            new_access_token, access_token_expires_in, new_refresh_token = (
373                self.refresh_access_token()
374            )
375            self.access_token = new_access_token
376            if new_refresh_token is not None:
377                self.set_refresh_token(new_refresh_token)
378            self.set_token_expiry_date(access_token_expires_in)
379            self._emit_control_message()
380
381    def refresh_access_token(self) -> Tuple[str, AirbyteDateTime, Optional[str]]:  # type: ignore[override]
382        """
383        Refreshes the access token by making a handled request and extracting the necessary token information.
384
385        Returns:
386            A tuple of (access_token, token_expiry_date, refresh_token). The refresh token
387            is `None` when the OAuth provider omits it from the response.
388        """
389        response_json = self._make_handled_request()
390        return (
391            self._extract_access_token(response_json),
392            self._extract_token_expiry_date(response_json),
393            self._extract_refresh_token(response_json),
394        )
395
396    def _set_config_value_by_path(self, config_path: Union[str, Sequence[str]], value: Any) -> None:
397        """
398        Set a value in the connector configuration at the specified path.
399
400        Args:
401            config_path (Union[str, Sequence[str]]): The path within the configuration where the value should be set.
402                This can be a string representing a single key or a sequence of strings representing a nested path.
403            value (Any): The value to set at the specified path in the configuration.
404
405        Returns:
406            None
407        """
408        dpath.new(self._connector_config, config_path, value)  # type: ignore[arg-type]
409
410    def _get_config_value_by_path(
411        self, config_path: Union[str, Sequence[str]], default: Optional[str] = None
412    ) -> str | Any:
413        """
414        Retrieve a value from the connector configuration using a specified path.
415
416        Args:
417            config_path (Union[str, Sequence[str]]): The path to the desired configuration value. This can be a string or a sequence of strings.
418            default (Optional[str], optional): The default value to return if the specified path does not exist in the configuration. Defaults to None.
419
420        Returns:
421            Any: The value from the configuration at the specified path, or the default value if the path does not exist.
422        """
423        return dpath.get(
424            self._connector_config,  # type: ignore[arg-type]
425            config_path,
426            default=default if default is not None else "",
427        )
428
429    def _emit_control_message(self) -> None:
430        """
431        Emits a control message based on the connector configuration.
432
433        Control messages for config updates (like refreshed tokens) must be printed directly
434        to stdout so the platform can process them immediately. The message repository is
435        also used to queue the message for any additional processing.
436
437        Note:
438            The function `emit_configuration_as_airbyte_control_message` prints directly to
439            stdout, which is required for the platform to detect and persist config changes.
440        """
441        # Always emit to stdout so the platform can process the config update immediately.
442        # This is critical for single-use refresh tokens where the new token must be persisted
443        # before subsequent operations try to use the old (now invalid) token.
444        emit_configuration_as_airbyte_control_message(self._connector_config)  # type: ignore[arg-type]
445
446        # Also emit to the message repository for any additional processing (e.g., logging)
447        if not isinstance(self._message_repository, NoopMessageRepository):
448            self._message_repository.emit_message(
449                create_connector_config_control_message(self._connector_config)  # type: ignore[arg-type]
450            )
451
452    @property
453    def _message_repository(self) -> MessageRepository:
454        """
455        Overriding AbstractOauth2Authenticator._message_repository to allow for HTTP request logs
456        """
457        return self.__message_repository

Authenticator that should be used for API implementing single use refresh tokens: when refreshing access token some API returns a new refresh token that needs to used in the next refresh flow. This authenticator updates the configuration with new refresh token by emitting Airbyte control message from an observed mutation. By default, this authenticator expects a connector config with a "credentials" field with the following nested fields: client_id, client_secret, refresh_token. This behavior can be changed by defining custom config path (using dpath paths) in client_id_config_path, client_secret_config_path, refresh_token_config_path constructor arguments.

SingleUseRefreshTokenOauth2Authenticator( connector_config: Mapping[str, Any], token_refresh_endpoint: str, scopes: Optional[List[str]] = None, access_token_name: str = 'access_token', expires_in_name: str = 'expires_in', refresh_token_name: str = 'refresh_token', refresh_request_body: Optional[Mapping[str, Any]] = None, refresh_request_headers: Optional[Mapping[str, Any]] = None, send_refresh_request_as_query_params: bool = False, grant_type_name: str = 'grant_type', grant_type: str = 'refresh_token', client_id_name: str = 'client_id', client_id: Optional[str] = None, client_secret_name: str = 'client_secret', client_secret: Optional[str] = None, access_token_config_path: Sequence[str] = ('credentials', 'access_token'), refresh_token_config_path: Sequence[str] = ('credentials', 'refresh_token'), token_expiry_date_config_path: Sequence[str] = ('credentials', 'token_expiry_date'), token_expiry_date_format: Optional[str] = None, message_repository: airbyte_cdk.MessageRepository = <airbyte_cdk.sources.message.NoopMessageRepository object>, token_expiry_is_time_of_expiration: bool = False, refresh_token_error_status_codes: Tuple[int, ...] = (), refresh_token_error_key: str = '', refresh_token_error_values: Tuple[str, ...] = ())
164    def __init__(
165        self,
166        connector_config: Mapping[str, Any],
167        token_refresh_endpoint: str,
168        scopes: List[str] | None = None,
169        access_token_name: str = "access_token",
170        expires_in_name: str = "expires_in",
171        refresh_token_name: str = "refresh_token",
172        refresh_request_body: Mapping[str, Any] | None = None,
173        refresh_request_headers: Mapping[str, Any] | None = None,
174        send_refresh_request_as_query_params: bool = False,
175        grant_type_name: str = "grant_type",
176        grant_type: str = "refresh_token",
177        client_id_name: str = "client_id",
178        client_id: Optional[str] = None,
179        client_secret_name: str = "client_secret",
180        client_secret: Optional[str] = None,
181        access_token_config_path: Sequence[str] = ("credentials", "access_token"),
182        refresh_token_config_path: Sequence[str] = ("credentials", "refresh_token"),
183        token_expiry_date_config_path: Sequence[str] = ("credentials", "token_expiry_date"),
184        token_expiry_date_format: Optional[str] = None,
185        message_repository: MessageRepository = NoopMessageRepository(),
186        token_expiry_is_time_of_expiration: bool = False,
187        refresh_token_error_status_codes: Tuple[int, ...] = (),
188        refresh_token_error_key: str = "",
189        refresh_token_error_values: Tuple[str, ...] = (),
190    ) -> None:
191        """
192        Args:
193            connector_config (Mapping[str, Any]): The full connector configuration
194            token_refresh_endpoint (str): Full URL to the token refresh endpoint
195            scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None.
196            access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token".
197            expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in".
198            refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token".
199            refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None.
200            refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None.
201            send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (`grant_type`, `refresh_token`, client credentials when not in an `Authorization` header, scopes, plus any `refresh_request_body` extras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False.
202            grant_type (str, optional): OAuth grant type. Defaults to "refresh_token".
203            client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object.
204            client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object.
205            access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token").
206            refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token").
207            token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date").
208            token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration.
209            token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration
210            message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
211        """
212        self._connector_config = connector_config
213        self._client_id: str = self._get_config_value_by_path(
214            ("credentials", "client_id"), client_id
215        )
216        self._client_secret: str = self._get_config_value_by_path(
217            ("credentials", "client_secret"), client_secret
218        )
219        self._client_id_name = client_id_name
220        self._client_secret_name = client_secret_name
221        self._access_token_config_path = access_token_config_path
222        self._refresh_token_config_path = refresh_token_config_path
223        self._token_expiry_date_config_path = token_expiry_date_config_path
224        self._token_expiry_date_format = token_expiry_date_format
225        self._refresh_token_name = refresh_token_name
226        self._grant_type_name = grant_type_name
227        self._connector_config = connector_config
228        self.__message_repository = message_repository
229        super().__init__(
230            token_refresh_endpoint=token_refresh_endpoint,
231            client_id_name=self._client_id_name,
232            client_id=self._client_id,
233            client_secret_name=self._client_secret_name,
234            client_secret=self._client_secret,
235            refresh_token=self.get_refresh_token(),
236            refresh_token_name=self._refresh_token_name,
237            scopes=scopes,
238            token_expiry_date=self.get_token_expiry_date(),
239            access_token_name=access_token_name,
240            expires_in_name=expires_in_name,
241            refresh_request_body=refresh_request_body,
242            refresh_request_headers=refresh_request_headers,
243            send_refresh_request_as_query_params=send_refresh_request_as_query_params,
244            grant_type_name=self._grant_type_name,
245            grant_type=grant_type,
246            token_expiry_date_format=token_expiry_date_format,
247            token_expiry_is_time_of_expiration=token_expiry_is_time_of_expiration
248            or bool(token_expiry_date_format),
249            refresh_token_error_status_codes=refresh_token_error_status_codes,
250            refresh_token_error_key=refresh_token_error_key,
251            refresh_token_error_values=refresh_token_error_values,
252        )
Arguments:
  • connector_config (Mapping[str, Any]): The full connector configuration
  • token_refresh_endpoint (str): Full URL to the token refresh endpoint
  • scopes (List[str], optional): List of OAuth scopes to pass in the refresh token request body. Defaults to None.
  • access_token_name (str, optional): Name of the access token field, used to parse the refresh token response. Defaults to "access_token".
  • expires_in_name (str, optional): Name of the name of the field that characterizes when the current access token will expire, used to parse the refresh token response. Defaults to "expires_in".
  • refresh_token_name (str, optional): Name of the name of the refresh token field, used to parse the refresh token response. Defaults to "refresh_token".
  • refresh_request_body (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request body. Defaults to None.
  • refresh_request_headers (Mapping[str, Any], optional): Custom key value pair that will be added to the refresh token request headers. Defaults to None.
  • send_refresh_request_as_query_params (bool, optional): When True, the standard refresh args (grant_type, refresh_token, client credentials when not in an Authorization header, scopes, plus any refresh_request_body extras) are sent on the URL query string and the request body is emitted empty. Use this for OAuth providers like Gong that document their refresh endpoint with refresh args on the URL query string. Defaults to False.
  • grant_type (str, optional): OAuth grant type. Defaults to "refresh_token".
  • client_id (Optional[str]): The client id to authenticate. If not specified, defaults to credentials.client_id in the config object.
  • client_secret (Optional[str]): The client secret to authenticate. If not specified, defaults to credentials.client_secret in the config object.
  • access_token_config_path (Sequence[str]): Dpath to the access_token field in the connector configuration. Defaults to ("credentials", "access_token").
  • refresh_token_config_path (Sequence[str]): Dpath to the refresh_token field in the connector configuration. Defaults to ("credentials", "refresh_token").
  • token_expiry_date_config_path (Sequence[str]): Dpath to the token_expiry_date field in the connector configuration. Defaults to ("credentials", "token_expiry_date").
  • token_expiry_date_format (Optional[str]): Date format of the token expiry date field (set by expires_in_name). If specified the token expiry date is interpreted as time of expiration, as with token_expiry_is_time_of_expiration, otherwise as number of seconds until expiration.
  • token_expiry_is_time_of_expiration bool: set True it if expires_in is returned as time of expiration instead of the number seconds until expiration
  • message_repository (MessageRepository): the message repository used to emit logs on HTTP requests and control message on config update
access_token: str
254    @property
255    def access_token(self) -> str:
256        """
257        Retrieve the access token from the configuration.
258
259        Returns:
260            str: The access token.
261        """
262        return self._get_config_value_by_path(self._access_token_config_path)  # type: ignore[return-value]

Retrieve the access token from the configuration.

Returns:

str: The access token.

def get_refresh_token(self) -> str:
274    def get_refresh_token(self) -> str:
275        """
276        Retrieve the refresh token from the configuration.
277
278        This method fetches the refresh token using the configuration path specified
279        by `_refresh_token_config_path`.
280
281        Returns:
282            str: The refresh token as a string.
283        """
284        return self._get_config_value_by_path(self._refresh_token_config_path)  # type: ignore[return-value]

Retrieve the refresh token from the configuration.

This method fetches the refresh token using the configuration path specified by _refresh_token_config_path.

Returns:

str: The refresh token as a string.

def set_refresh_token(self, new_refresh_token: str) -> None:
286    def set_refresh_token(self, new_refresh_token: str) -> None:
287        """
288        Updates the refresh token in the configuration.
289
290        Args:
291            new_refresh_token (str): The new refresh token to be set.
292        """
293        self._set_config_value_by_path(self._refresh_token_config_path, new_refresh_token)

Updates the refresh token in the configuration.

Arguments:
  • new_refresh_token (str): The new refresh token to be set.
def get_token_expiry_date(self) -> airbyte_cdk.utils.datetime_helpers.AirbyteDateTime:
295    def get_token_expiry_date(self) -> AirbyteDateTime:
296        """
297        Retrieves the token expiry date from the configuration.
298
299        This method fetches the token expiry date from the configuration using the specified path.
300        If the expiry date is an empty string, it returns the current date and time minus one day.
301        Otherwise, it parses the expiry date string into an AirbyteDateTime object.
302
303        Returns:
304            AirbyteDateTime: The parsed or calculated token expiry date.
305
306        Raises:
307            TypeError: If the result is not an instance of AirbyteDateTime.
308        """
309        expiry_date = self._get_config_value_by_path(self._token_expiry_date_config_path)
310        result = (
311            ab_datetime_now() - timedelta(days=1)
312            if expiry_date == ""
313            else ab_datetime_parse(str(expiry_date))
314        )
315        if isinstance(result, AirbyteDateTime):
316            return result
317        raise TypeError("Invalid datetime conversion")

Retrieves the token expiry date from the configuration.

This method fetches the token expiry date from the configuration using the specified path. If the expiry date is an empty string, it returns the current date and time minus one day. Otherwise, it parses the expiry date string into an AirbyteDateTime object.

Returns:

AirbyteDateTime: The parsed or calculated token expiry date.

Raises:
  • TypeError: If the result is not an instance of AirbyteDateTime.
def set_token_expiry_date( self, new_token_expiry_date: airbyte_cdk.utils.datetime_helpers.AirbyteDateTime) -> None:
319    def set_token_expiry_date(self, new_token_expiry_date: AirbyteDateTime) -> None:  # type: ignore[override]
320        """
321        Sets the token expiry date in the configuration.
322
323        Args:
324            new_token_expiry_date (AirbyteDateTime): The new expiry date for the token.
325        """
326        if not (
327            isinstance(new_token_expiry_date, datetime) or self.token_expiry_is_time_of_expiration
328        ):
329            # Overrides of `refresh_access_token` written before 6.45.5 return the raw `expires_in`
330            # in seconds. Other values are stored as-is and parsed on read.
331            with suppress(ValueError, OverflowError):
332                new_token_expiry_date = self._parse_token_expiration_date(new_token_expiry_date)
333        self._set_config_value_by_path(
334            self._token_expiry_date_config_path, str(new_token_expiry_date)
335        )

Sets the token expiry date in the configuration.

Arguments:
  • new_token_expiry_date (AirbyteDateTime): The new expiry date for the token.
def token_has_expired(self) -> bool:
337    def token_has_expired(self) -> bool:
338        """Returns True if the token is expired"""
339        return ab_datetime_now() > self.get_token_expiry_date()

Returns True if the token is expired

def get_access_token(self) -> str:
341    def get_access_token(self) -> str:
342        """Retrieve new access and refresh token if the access token has expired.
343
344        This method uses double-checked locking to ensure thread-safe token refresh.
345        This is especially critical for single-use refresh tokens where concurrent
346        refresh attempts would cause failures as the refresh token is invalidated
347        after first use.
348
349        The new refresh token is persisted with the set_refresh_token function.
350
351        Returns:
352            str: The current access_token, updated if it was previously expired.
353        """
354        if self.token_has_expired():
355            with self._token_refresh_lock:
356                # Double-check after acquiring lock - another thread may have already refreshed
357                if self.token_has_expired():
358                    self.refresh_and_set_access_token()
359        return self.access_token

Retrieve new access and refresh token if the access token has expired.

This method uses double-checked locking to ensure thread-safe token refresh. This is especially critical for single-use refresh tokens where concurrent refresh attempts would cause failures as the refresh token is invalidated after first use.

The new refresh token is persisted with the set_refresh_token function.

Returns:

str: The current access_token, updated if it was previously expired.

def refresh_and_set_access_token(self) -> None:
361    def refresh_and_set_access_token(self) -> None:
362        """Force refresh the access token and update internal state.
363
364        For single-use refresh tokens, this also persists the new refresh token
365        and emits a control message to update the connector config. If the
366        response omits a refresh token, the existing one is preserved.
367        """
368        token_before_waiting = self._current_access_token_or_none()
369        with self._token_refresh_lock:
370            if self._current_access_token_or_none() != token_before_waiting:
371                return
372            new_access_token, access_token_expires_in, new_refresh_token = (
373                self.refresh_access_token()
374            )
375            self.access_token = new_access_token
376            if new_refresh_token is not None:
377                self.set_refresh_token(new_refresh_token)
378            self.set_token_expiry_date(access_token_expires_in)
379            self._emit_control_message()

Force refresh the access token and update internal state.

For single-use refresh tokens, this also persists the new refresh token and emits a control message to update the connector config. If the response omits a refresh token, the existing one is preserved.

def refresh_access_token( self) -> Tuple[str, airbyte_cdk.utils.datetime_helpers.AirbyteDateTime, Optional[str]]:
381    def refresh_access_token(self) -> Tuple[str, AirbyteDateTime, Optional[str]]:  # type: ignore[override]
382        """
383        Refreshes the access token by making a handled request and extracting the necessary token information.
384
385        Returns:
386            A tuple of (access_token, token_expiry_date, refresh_token). The refresh token
387            is `None` when the OAuth provider omits it from the response.
388        """
389        response_json = self._make_handled_request()
390        return (
391            self._extract_access_token(response_json),
392            self._extract_token_expiry_date(response_json),
393            self._extract_refresh_token(response_json),
394        )

Refreshes the access token by making a handled request and extracting the necessary token information.

Returns:

A tuple of (access_token, token_expiry_date, refresh_token). The refresh token is None when the OAuth provider omits it from the response.

39class TokenAuthenticator(AbstractHeaderAuthenticator):
40    """
41    Builds auth header, based on the token provided.
42    The token is attached to each request via the `auth_header` header.
43    """
44
45    @property
46    def auth_header(self) -> str:
47        return self._auth_header
48
49    @property
50    def token(self) -> str:
51        return f"{self._auth_method} {self._token}"
52
53    def __init__(self, token: str, auth_method: str = "Bearer", auth_header: str = "Authorization"):
54        self._auth_header = auth_header
55        self._auth_method = auth_method
56        self._token = token

Builds auth header, based on the token provided. The token is attached to each request via the auth_header header.

TokenAuthenticator( token: str, auth_method: str = 'Bearer', auth_header: str = 'Authorization')
53    def __init__(self, token: str, auth_method: str = "Bearer", auth_header: str = "Authorization"):
54        self._auth_header = auth_header
55        self._auth_method = auth_method
56        self._token = token
auth_header: str
45    @property
46    def auth_header(self) -> str:
47        return self._auth_header

HTTP header to set on the requests

token: str
49    @property
50    def token(self) -> str:
51        return f"{self._auth_method} {self._token}"

The header value to set on outgoing HTTP requests

15class MultipleTokenAuthenticator(AbstractHeaderAuthenticator):
16    """
17    Builds auth header, based on the list of tokens provided.
18    Auth header is changed per each `get_auth_header` call, using each token in cycle.
19    The token is attached to each request via the `auth_header` header.
20    """
21
22    @property
23    def auth_header(self) -> str:
24        return self._auth_header
25
26    @property
27    def token(self) -> str:
28        return f"{self._auth_method} {next(self._tokens_iter)}"
29
30    def __init__(
31        self, tokens: List[str], auth_method: str = "Bearer", auth_header: str = "Authorization"
32    ):
33        self._auth_method = auth_method
34        self._auth_header = auth_header
35        self._tokens = tokens
36        self._tokens_iter = cycle(self._tokens)

Builds auth header, based on the list of tokens provided. Auth header is changed per each get_auth_header call, using each token in cycle. The token is attached to each request via the auth_header header.

MultipleTokenAuthenticator( tokens: List[str], auth_method: str = 'Bearer', auth_header: str = 'Authorization')
30    def __init__(
31        self, tokens: List[str], auth_method: str = "Bearer", auth_header: str = "Authorization"
32    ):
33        self._auth_method = auth_method
34        self._auth_header = auth_header
35        self._tokens = tokens
36        self._tokens_iter = cycle(self._tokens)
auth_header: str
22    @property
23    def auth_header(self) -> str:
24        return self._auth_header

HTTP header to set on the requests

token: str
26    @property
27    def token(self) -> str:
28        return f"{self._auth_method} {next(self._tokens_iter)}"

The header value to set on outgoing HTTP requests

59class BasicHttpAuthenticator(AbstractHeaderAuthenticator):
60    """
61    Builds auth based off the basic authentication scheme as defined by RFC 7617, which transmits credentials as USER ID/password pairs, encoded using bas64
62    https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme
63    """
64
65    @property
66    def auth_header(self) -> str:
67        return self._auth_header
68
69    @property
70    def token(self) -> str:
71        return f"{self._auth_method} {self._token}"
72
73    def __init__(
74        self,
75        username: str,
76        password: str = "",
77        auth_method: str = "Basic",
78        auth_header: str = "Authorization",
79    ):
80        auth_string = f"{username}:{password}".encode("utf8")
81        b64_encoded = base64.b64encode(auth_string).decode("utf8")
82        self._auth_header = auth_header
83        self._auth_method = auth_method
84        self._token = b64_encoded

Builds auth based off the basic authentication scheme as defined by RFC 7617, which transmits credentials as USER ID/password pairs, encoded using bas64 https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme

BasicHttpAuthenticator( username: str, password: str = '', auth_method: str = 'Basic', auth_header: str = 'Authorization')
73    def __init__(
74        self,
75        username: str,
76        password: str = "",
77        auth_method: str = "Basic",
78        auth_header: str = "Authorization",
79    ):
80        auth_string = f"{username}:{password}".encode("utf8")
81        b64_encoded = base64.b64encode(auth_string).decode("utf8")
82        self._auth_header = auth_header
83        self._auth_method = auth_method
84        self._token = b64_encoded
auth_header: str
65    @property
66    def auth_header(self) -> str:
67        return self._auth_header

HTTP header to set on the requests

token: str
69    @property
70    def token(self) -> str:
71        return f"{self._auth_method} {self._token}"

The header value to set on outgoing HTTP requests

@runtime_checkable
class ResponseAwareAuthenticator(typing.Protocol):
30@runtime_checkable
31class ResponseAwareAuthenticator(Protocol):
32    """An authenticator that wants to see responses, not just sign requests.
33
34    Authenticators tracking per-token quota need a feedback channel: without one they can only
35    guess at the server's view of the quota and cannot tell that a token has been rejected.
36    `HttpClient` calls `update_from_response` once per attempt on any authenticator that
37    implements this, skipping replayed cache hits -- a cached response carries stale rate-limit
38    headers and consumed no quota.
39
40    Implementations must not raise for a response they cannot interpret, and must be safe to
41    call from multiple threads.
42    """
43
44    def update_from_response(
45        self, request: requests.PreparedRequest, response: requests.Response
46    ) -> None:
47        pass

An authenticator that wants to see responses, not just sign requests.

Authenticators tracking per-token quota need a feedback channel: without one they can only guess at the server's view of the quota and cannot tell that a token has been rejected. HttpClient calls update_from_response once per attempt on any authenticator that implements this, skipping replayed cache hits -- a cached response carries stale rate-limit headers and consumed no quota.

Implementations must not raise for a response they cannot interpret, and must be safe to call from multiple threads.

ResponseAwareAuthenticator(*args, **kwargs)
1739def _no_init_or_replace_init(self, *args, **kwargs):
1740    cls = type(self)
1741
1742    if cls._is_protocol:
1743        raise TypeError('Protocols cannot be instantiated')
1744
1745    # Already using a custom `__init__`. No need to calculate correct
1746    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1747    if cls.__init__ is not _no_init_or_replace_init:
1748        return
1749
1750    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1751    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1752    # searches for a proper new `__init__` in the MRO. The new `__init__`
1753    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1754    # instantiation of the protocol subclass will thus use the new
1755    # `__init__` and no longer call `_no_init_or_replace_init`.
1756    for base in cls.__mro__:
1757        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1758        if init is not _no_init_or_replace_init:
1759            cls.__init__ = init
1760            break
1761    else:
1762        # should not happen
1763        cls.__init__ = object.__init__
1764
1765    cls.__init__(self, *args, **kwargs)
def update_from_response( self, request: requests.models.PreparedRequest, response: requests.models.Response) -> None:
44    def update_from_response(
45        self, request: requests.PreparedRequest, response: requests.Response
46    ) -> None:
47        pass
@runtime_checkable
class TokenRotatingAuthenticator(typing.Protocol):
50@runtime_checkable
51class TokenRotatingAuthenticator(Protocol):
52    """An authenticator holding several interchangeable credentials.
53
54    A rate-limit backoff is computed from the response alone, so it assumes the only way
55    forward is for that quota to come back. An authenticator with a spare credential knows
56    better. `HttpClient` asks before sleeping out a rate-limit window.
57
58    Implementations must only answer True when retrying immediately would actually use a
59    different credential -- otherwise the retry hammers the same rejected one.
60    """
61
62    def has_alternative_token(self, request: requests.PreparedRequest) -> bool:
63        pass

An authenticator holding several interchangeable credentials.

A rate-limit backoff is computed from the response alone, so it assumes the only way forward is for that quota to come back. An authenticator with a spare credential knows better. HttpClient asks before sleeping out a rate-limit window.

Implementations must only answer True when retrying immediately would actually use a different credential -- otherwise the retry hammers the same rejected one.

TokenRotatingAuthenticator(*args, **kwargs)
1739def _no_init_or_replace_init(self, *args, **kwargs):
1740    cls = type(self)
1741
1742    if cls._is_protocol:
1743        raise TypeError('Protocols cannot be instantiated')
1744
1745    # Already using a custom `__init__`. No need to calculate correct
1746    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1747    if cls.__init__ is not _no_init_or_replace_init:
1748        return
1749
1750    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1751    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1752    # searches for a proper new `__init__` in the MRO. The new `__init__`
1753    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1754    # instantiation of the protocol subclass will thus use the new
1755    # `__init__` and no longer call `_no_init_or_replace_init`.
1756    for base in cls.__mro__:
1757        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1758        if init is not _no_init_or_replace_init:
1759            cls.__init__ = init
1760            break
1761    else:
1762        # should not happen
1763        cls.__init__ = object.__init__
1764
1765    cls.__init__(self, *args, **kwargs)
def has_alternative_token(self, request: requests.models.PreparedRequest) -> bool:
62    def has_alternative_token(self, request: requests.PreparedRequest) -> bool:
63        pass