airbyte_cdk.sources.streams.http.requests_native_auth.protocols

Optional capabilities an authenticator can offer the HTTP client.

Authenticators are normally write-only from the client's point of view: they sign a request and that is the end of the conversation. An authenticator managing a quota, or several interchangeable credentials, needs more than that -- it has to learn what the server said, and it knows things about credential availability that the retry logic cannot work out on its own.

These protocols are how it says so. Both are optional and dispatched structurally: HttpClient checks the authenticator against the protocol and skips one that does not satisfy it, so implementing one does not require inheriting from anything or importing the client.

Define the methods on the class or set them on the instance. Dispatch is an isinstance check, and from Python 3.12 those resolve protocol members with inspect.getattr_static, which does not run __getattr__ -- so a delegating authenticator that only exposes the method dynamically satisfies the protocol on 3.10/3.11 and is silently skipped on 3.12+. Presence is all that is checked either way: an implementation with the wrong signature still dispatches, and then fails at the call.

 1#
 2# Copyright (c) 2026 Airbyte, Inc., all rights reserved.
 3#
 4
 5"""Optional capabilities an authenticator can offer the HTTP client.
 6
 7Authenticators are normally write-only from the client's point of view: they sign a request and
 8that is the end of the conversation. An authenticator managing a quota, or several
 9interchangeable credentials, needs more than that -- it has to learn what the server said, and
10it knows things about credential availability that the retry logic cannot work out on its own.
11
12These protocols are how it says so. Both are optional and dispatched structurally: `HttpClient`
13checks the authenticator against the protocol and skips one that does not satisfy it, so
14implementing one does not require inheriting from anything or importing the client.
15
16Define the methods on the class or set them on the instance. Dispatch is an `isinstance` check,
17and from Python 3.12 those resolve protocol members with `inspect.getattr_static`, which does
18not run `__getattr__` -- so a delegating authenticator that only exposes the method dynamically
19satisfies the protocol on 3.10/3.11 and is silently skipped on 3.12+. Presence is all that is
20checked either way: an implementation with the wrong signature still dispatches, and then fails
21at the call.
22"""
23
24from typing import Protocol, runtime_checkable
25
26import requests
27
28
29@runtime_checkable
30class ResponseAwareAuthenticator(Protocol):
31    """An authenticator that wants to see responses, not just sign requests.
32
33    Authenticators tracking per-token quota need a feedback channel: without one they can only
34    guess at the server's view of the quota and cannot tell that a token has been rejected.
35    `HttpClient` calls `update_from_response` once per attempt on any authenticator that
36    implements this, skipping replayed cache hits -- a cached response carries stale rate-limit
37    headers and consumed no quota.
38
39    Implementations must not raise for a response they cannot interpret, and must be safe to
40    call from multiple threads.
41    """
42
43    def update_from_response(
44        self, request: requests.PreparedRequest, response: requests.Response
45    ) -> None:
46        pass
47
48
49@runtime_checkable
50class TokenRotatingAuthenticator(Protocol):
51    """An authenticator holding several interchangeable credentials.
52
53    A rate-limit backoff is computed from the response alone, so it assumes the only way
54    forward is for that quota to come back. An authenticator with a spare credential knows
55    better. `HttpClient` asks before sleeping out a rate-limit window.
56
57    Implementations must only answer True when retrying immediately would actually use a
58    different credential -- otherwise the retry hammers the same rejected one.
59    """
60
61    def has_alternative_token(self, request: requests.PreparedRequest) -> bool:
62        pass
@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)
1431def _no_init_or_replace_init(self, *args, **kwargs):
1432    cls = type(self)
1433
1434    if cls._is_protocol:
1435        raise TypeError('Protocols cannot be instantiated')
1436
1437    # Already using a custom `__init__`. No need to calculate correct
1438    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1439    if cls.__init__ is not _no_init_or_replace_init:
1440        return
1441
1442    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1443    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1444    # searches for a proper new `__init__` in the MRO. The new `__init__`
1445    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1446    # instantiation of the protocol subclass will thus use the new
1447    # `__init__` and no longer call `_no_init_or_replace_init`.
1448    for base in cls.__mro__:
1449        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1450        if init is not _no_init_or_replace_init:
1451            cls.__init__ = init
1452            break
1453    else:
1454        # should not happen
1455        cls.__init__ = object.__init__
1456
1457    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)
1431def _no_init_or_replace_init(self, *args, **kwargs):
1432    cls = type(self)
1433
1434    if cls._is_protocol:
1435        raise TypeError('Protocols cannot be instantiated')
1436
1437    # Already using a custom `__init__`. No need to calculate correct
1438    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1439    if cls.__init__ is not _no_init_or_replace_init:
1440        return
1441
1442    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1443    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1444    # searches for a proper new `__init__` in the MRO. The new `__init__`
1445    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1446    # instantiation of the protocol subclass will thus use the new
1447    # `__init__` and no longer call `_no_init_or_replace_init`.
1448    for base in cls.__mro__:
1449        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1450        if init is not _no_init_or_replace_init:
1451            cls.__init__ = init
1452            break
1453    else:
1454        # should not happen
1455        cls.__init__ = object.__init__
1456
1457    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