From 52db6ec33c6b3fcb0784fbebd4b26e07491b207d Mon Sep 17 00:00:00 2001 From: "Edgar R. M" Date: Wed, 24 Aug 2022 09:50:14 -0500 Subject: [PATCH] feat: Allow authenticating more generic requests (#842) * feat: Allow authenticating more generic requests * Add missing rst for base authenticator * Mutate un-prepared request * Fix external link in docs * Update singer_sdk/authenticators.py Co-authored-by: Will Da Silva Co-authored-by: Will Da Silva --- ...dk.authenticators.APIAuthenticatorBase.rst | 7 +++ docs/reference.rst | 1 + singer_sdk/authenticators.py | 12 ++++ singer_sdk/streams/rest.py | 59 +++++++++++++------ 4 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 docs/classes/singer_sdk.authenticators.APIAuthenticatorBase.rst diff --git a/docs/classes/singer_sdk.authenticators.APIAuthenticatorBase.rst b/docs/classes/singer_sdk.authenticators.APIAuthenticatorBase.rst new file mode 100644 index 000000000..b0d129a50 --- /dev/null +++ b/docs/classes/singer_sdk.authenticators.APIAuthenticatorBase.rst @@ -0,0 +1,7 @@ +singer_sdk.authenticators.APIAuthenticatorBase +============================================== + +.. currentmodule:: singer_sdk.authenticators + +.. autoclass:: APIAuthenticatorBase + :members: \ No newline at end of file diff --git a/docs/reference.rst b/docs/reference.rst index 1641d357c..f4c517268 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -56,6 +56,7 @@ Authenticator Classes :toctree: classes :template: class.rst + authenticators.APIAuthenticatorBase authenticators.APIKeyAuthenticator authenticators.BasicAuthenticator authenticators.BearerTokenAuthenticator diff --git a/singer_sdk/authenticators.py b/singer_sdk/authenticators.py index 270ac0634..468b607e8 100644 --- a/singer_sdk/authenticators.py +++ b/singer_sdk/authenticators.py @@ -95,6 +95,18 @@ def auth_params(self) -> dict: """ return self._auth_params or {} + def authenticate_request(self, request: requests.Request) -> None: + """Authenticate a request. + + Args: + request: A `request object`_. + + .. _request object: + https://requests.readthedocs.io/en/latest/api/#requests.Request + """ + request.headers.update(self.auth_headers) + request.params.update(self.auth_params) + class SimpleAuthenticator(APIAuthenticatorBase): """DEPRECATED: Please use a more specific authenticator. diff --git a/singer_sdk/streams/rest.py b/singer_sdk/streams/rest.py index 456ec5cd8..dd302d04f 100644 --- a/singer_sdk/streams/rest.py +++ b/singer_sdk/streams/rest.py @@ -124,7 +124,7 @@ def requests_session(self) -> requests.Session: The `requests.Session`_ object for HTTP requests. .. _requests.Session: - https://docs.python-requests.org/en/latest/api/#request-sessions + https://requests.readthedocs.io/en/latest/api/#request-sessions """ if not self._requests_session: self._requests_session = requests.Session() @@ -159,7 +159,7 @@ def validate_response(self, response: requests.Response) -> None: RetriableAPIError: If the request is retriable. .. _requests.Response: - https://docs.python-requests.org/en/latest/api/#requests.Response + https://requests.readthedocs.io/en/latest/api/#requests.Response """ if ( response.status_code in self.extra_retry_statuses @@ -264,10 +264,39 @@ def get_url_params( """ return {} + def build_prepared_request( + self, + *args: Any, + **kwargs: Any, + ) -> requests.PreparedRequest: + """Build a generic but authenticated request. + + Uses the authenticator instance to mutate the request with authentication. + + Args: + *args: Arguments to pass to `requests.Request`_. + **kwargs: Keyword arguments to pass to `requests.Request`_. + + Returns: + A `requests.PreparedRequest`_ object. + + .. _requests.PreparedRequest: + https://requests.readthedocs.io/en/latest/api/#requests.PreparedRequest + .. _requests.Request: + https://requests.readthedocs.io/en/latest/api/#requests.Request + """ + request = requests.Request(*args, **kwargs) + + if self.authenticator: + authenticator = self.authenticator + authenticator.authenticate_request(request) + + return self.requests_session.prepare_request(request) + def prepare_request( self, context: dict | None, next_page_token: _TToken | None ) -> requests.PreparedRequest: - """Prepare a request object. + """Prepare a request object for this stream. If partitioning is supported, the `context` object will contain the partition definitions. Pagination information can be parsed from `next_page_token` if @@ -288,21 +317,13 @@ def prepare_request( request_data = self.prepare_request_payload(context, next_page_token) headers = self.http_headers - authenticator = self.authenticator - if authenticator: - headers.update(authenticator.auth_headers or {}) - params.update(authenticator.auth_params or {}) - - request = self.requests_session.prepare_request( - requests.Request( - method=http_method, - url=url, - params=params, - headers=headers, - json=request_data, - ), + return self.build_prepared_request( + method=http_method, + url=url, + params=params, + headers=headers, + json=request_data, ) - return request def request_records(self, context: dict | None) -> Iterable[dict]: """Request records from REST endpoint(s), returning response records. @@ -435,7 +456,7 @@ def get_next_page_token( Reference value to retrieve next page. .. _requests.Response: - https://docs.python-requests.org/en/latest/api/#requests.Response + https://requests.readthedocs.io/en/latest/api/#requests.Response """ if self.next_page_token_jsonpath: all_matches = extract_jsonpath( @@ -504,7 +525,7 @@ def parse_response(self, response: requests.Response) -> Iterable[dict]: One item for every item found in the response. .. _requests.Response: - https://docs.python-requests.org/en/latest/api/#requests.Response + https://requests.readthedocs.io/en/latest/api/#requests.Response """ yield from extract_jsonpath(self.records_jsonpath, input=response.json())