Repository: meltano/sdk
Pull request #2220 (2024-02-05 20:15:21)

refactor: Deprecated `singer_sdk.authenticators.BasicAuthenticator` in favor of `requests.auth.HTTPBasicAuth`

Closes https://github.com/meltano/sdk/issues/2040

<!-- readthedocs-preview meltano-sdk start -->
----
📚 Documentation preview 📚: https://meltano-sdk--2220.org.readthedocs.build/en/2220/

<!-- readthedocs-preview meltano-sdk end -->

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/meltano/sdk/branches/edgarrmondragon/refactor/deprecate-basic-authenticator)

### Merging #2220 will **not alter performance**

<sub>Comparing <code>edgarrmondragon/refactor/deprecate-basic-authenticator</code> (c90a52b) with <code>main</code> (eda4fd6)</sub>



### Summary

`✅ 6` untouched benchmarks





codecov[bot]: ## [Codecov](https://app.codecov.io/gh/meltano/sdk/pull/2220?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`2f597fe`)](https://app.codecov.io/gh/meltano/sdk/commit/2f597fef4f61b97431d6df2c6a516f0fd23a41c1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) 88.47% compared to head [(`a672412`)](https://app.codecov.io/gh/meltano/sdk/pull/2220?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) 88.47%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2220   +/-   ##
=======================================
  Coverage   88.47%   88.47%           
=======================================
  Files          54       54           
  Lines        4720     4720           
  Branches      919      919           
=======================================
  Hits         4176     4176           
  Misses        383      383           
  Partials      161      161           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/meltano/sdk/pull/2220?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano).


Patch:
diff --git a/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/rest-client.py b/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/rest-client.py
index 26c1447c..16ad9d23 100644
--- a/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/rest-client.py
+++ b/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/rest-client.py
@@ -22,7 +22,7 @@ from singer_sdk.pagination import BaseAPIPaginator  # noqa: TCH002
 from singer_sdk.streams import {{ cookiecutter.stream_type }}Stream
 
 {% elif cookiecutter.auth_method == "Basic Auth" -%}
-from singer_sdk.authenticators import BasicAuthenticator
+from requests.auth import HTTPBasicAuth
 from singer_sdk.helpers.jsonpath import extract_jsonpath
 from singer_sdk.pagination import BaseAPIPaginator  # noqa: TCH002
 from singer_sdk.streams import {{ cookiecutter.stream_type }}Stream
@@ -110,14 +110,13 @@ class {{ cookiecutter.source_name }}Stream({{ cookiecutter.stream_type }}Stream)
 {%- elif cookiecutter.auth_method == "Basic Auth" %}
 
     @property
-    def authenticator(self) -> BasicAuthenticator:
+    def authenticator(self) -> HTTPBasicAuth:
         """Return a new authenticator object.
 
         Returns:
             An authenticator instance.
         """
-        return BasicAuthenticator.create_for_stream(
-            self,
+        return HTTPBasicAuth(
             username=self.config.get("username", ""),
             password=self.config.get("password", ""),
         )
diff --git a/pyproject.toml b/pyproject.toml
index d22e6c69..5de8456b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -249,6 +249,10 @@ jsonl = "singer_sdk.contrib.batch_encoder_jsonl:JSONLinesBatcher"
 parquet = "singer_sdk.contrib.batch_encoder_parquet:ParquetBatcher"
 
 [tool.ruff]
+extend-exclude = [
+    "cookiecutter/*",
+    "*simpleeval*",
+]
 line-length = 88
 src = ["samples", "singer_sdk", "tests"]
 target-version = "py38"
@@ -257,10 +261,6 @@ target-version = "py38"
 docstring-code-format = true
 
 [tool.ruff.lint]
-exclude = [
-    "cookiecutter/*",
-    "*simpleeval*",
-]
 ignore = [
     "ANN101",  # Missing type annotation for `self` in method
     "ANN102",  # Missing type annotation for `cls` in class method
diff --git a/singer_sdk/authenticators.py b/singer_sdk/authenticators.py
index 82e0556b..faf34e85 100644
--- a/singer_sdk/authenticators.py
+++ b/singer_sdk/authenticators.py
@@ -5,6 +5,7 @@ from __future__ import annotations
 import base64
 import math
 import typing as t
+import warnings
 from datetime import timedelta
 from types import MappingProxyType
 from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
@@ -297,7 +298,10 @@ class BearerTokenAuthenticator(APIAuthenticatorBase):
 class BasicAuthenticator(APIAuthenticatorBase):
     """Implements basic authentication for REST Streams.
 
-    This Authenticator implements basic authentication by concatinating a
+    .. deprecated:: 0.36.0
+       Use :class:`requests.auth.HTTPBasicAuth` instead.
+
+    This Authenticator implements basic authentication by concatenating a
     username and password then base64 encoding the string. The resulting
     token will be merged with any HTTP headers specified on the stream.
     """
@@ -316,6 +320,13 @@ class BasicAuthenticator(APIAuthenticatorBase):
             password: API password.
         """
         super().__init__(stream=stream)
+        warnings.warn(
+            "BasicAuthenticator is deprecated. Use "
+            "requests.auth.HTTPBasicAuth instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
         credentials = f"{username}:{password}".encode()
         auth_token = base64.b64encode(credentials).decode("ascii")
         auth_credentials = {"Authorization": f"Basic {auth_token}"}
diff --git a/singer_sdk/helpers/_flattening.py b/singer_sdk/helpers/_flattening.py
index 2a3e194d..9dcbf128 100644
--- a/singer_sdk/helpers/_flattening.py
+++ b/singer_sdk/helpers/_flattening.py
@@ -33,7 +33,7 @@ def get_flattening_options(
     Returns:
         A new FlatteningOptions object or None if flattening is disabled.
     """
-    if plugin_config.get("flattening_enabled"):
+    if plugin_config.get("flattening_enabled", False):
         return FlatteningOptions(max_level=int(plugin_config["flattening_max_depth"]))
 
     return None


Test patch:
diff --git a/tests/core/rest/test_authenticators.py b/tests/core/rest/test_authenticators.py
index 7e1da91e..d95d69ed 100644
--- a/tests/core/rest/test_authenticators.py
+++ b/tests/core/rest/test_authenticators.py
@@ -21,7 +21,11 @@ from cryptography.hazmat.primitives.serialization import (
 )
 from requests.auth import HTTPProxyAuth, _basic_auth_str
 
-from singer_sdk.authenticators import OAuthAuthenticator, OAuthJWTAuthenticator
+from singer_sdk.authenticators import (
+    BasicAuthenticator,
+    OAuthAuthenticator,
+    OAuthJWTAuthenticator,
+)
 
 if t.TYPE_CHECKING:
     import requests_mock
@@ -218,3 +222,13 @@ def test_requests_library_auth(rest_tap: Tap):
 
     assert isinstance(stream.authenticator, HTTPProxyAuth)
     assert r.headers["Proxy-Authorization"] == _basic_auth_str("username", "password")
+
+
+def test_basic_auth_deprecation_warning(rest_tap: Tap):
+    """Validate that a warning is emitted when using BasicAuthenticator."""
+    stream: RESTStream = rest_tap.streams["some_stream"]
+    with pytest.deprecated_call(match="BasicAuthenticator is deprecated") as recorder:
+        BasicAuthenticator(stream=stream, username="username", password="password")  # noqa: S106
+
+    assert len(recorder.list) == 1
+    assert recorder.list[0].filename.endswith("test_authenticators.py")