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
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.
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)
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.
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)