kaxil commented on code in PR #75810:
URL: https://github.com/apache/airflow/pull/75810#discussion_r4434069474


##########
providers/snowflake/tests/unit/snowflake/utils/test__rest_auth.py:
##########
@@ -227,6 +372,234 @@ def 
test_build_auth_headers_contains_only_auth_headers(self, mock_conn_params):
         assert headers["X-Snowflake-Authorization-Token-Type"] == 
"PROGRAMMATIC_ACCESS_TOKEN"
 
 
+class TestWifDestinations:
+    @pytest.mark.parametrize(
+        "host",
+        [
+            "attacker.example",
+            "evil-snowflakecomputing.com",
+            "a.snowflakecomputing.com.evil",
+            "[email protected]",
+            "a.snowflakecomputing.com:443",
+            "a.snowflakecomputing.com/path",
+            "a.snowflakecomputing.com?query",
+            "a.snowflakecomputing.com#fragment",
+            "a..snowflakecomputing.com",
+            "a.snowflakecomputing.com..",
+            "a.snowflakecomputing.com\n",
+            "a.snowflakecomputing.com%00",
+            "https://a.snowflakecomputing.com";,
+            "a\\.snowflakecomputing.com",
+        ],
+    )
+    @pytest.mark.parametrize("cloud", ["AWS", "AZURE", "GCP", "OIDC"])
+    def test_rejects_before_fetch_or_file_read(self, host, cloud):
+        hook = mock.create_autospec(SnowflakeHook, instance=True)
+        hook._get_conn_params.return_value = {
+            **CONN_PARAMS_WIF,
+            "host": host,
+            "workload_identity_provider": cloud,
+            "token_file_path": "/does/not/exist",
+        }
+        provider = SnowflakeRestTokenProvider(hook, private_key_loader=lambda: 
None)
+        with mock.patch(
+            FETCH_TOKEN.format(cloud.lower()) if cloud != "OIDC" else 
f"{MODULE_PATH}.Path.read_text",
+            autospec=True,
+        ) as fetch:
+            with pytest.raises(ValueError, match="recognized Snowflake host"):
+                provider.get_token()
+            fetch.assert_not_called()
+
+    @pytest.mark.parametrize(
+        "conn_config",
+        [
+            {"host": "a.snowflakecomputing.com"},
+            {"host": "A.snowflakecomputing.cn."},
+            {"host": "a.snowflakecomputing.mil"},
+            {"host": "private.example"},
+            {"host": "a.private.example"},
+            {"host": "myorg-my_account.snowflakecomputing.com"},
+            {"account": "myorg-my_account"},
+            {"account": "_my_account_"},
+            {"account": "_"},
+        ],
+    )
+    @mock.patch(FETCH_TOKEN.format("gcp"), autospec=True, 
return_value="id-token")
+    def test_allowed_destinations(self, fetch, conn_config, monkeypatch):
+        monkeypatch.setenv("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES", " 
private.example,other.example ")
+        hook = mock.create_autospec(SnowflakeHook, instance=True)
+        hook._get_conn_params.return_value = {**CONN_PARAMS_WIF, **conn_config}
+        provider = SnowflakeRestTokenProvider(hook, private_key_loader=lambda: 
None)
+        assert provider.get_token().token == "WIF.GCP.id-token"
+
+    @mock.patch(FETCH_TOKEN.format("gcp"), autospec=True)
+    def test_validates_cached_token_destination(self, fetch, monkeypatch):
+        monkeypatch.setenv("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES", 
"private.example")
+        fetch.return_value = _make_jwt(exp=int(time.time()) + 3600)
+        hook = mock.create_autospec(SnowflakeHook, instance=True)
+        hook._get_conn_params.return_value = {**CONN_PARAMS_WIF, "host": 
"private.example"}
+        provider = SnowflakeRestTokenProvider(hook, private_key_loader=lambda: 
None)
+        provider.get_token()
+        monkeypatch.delenv("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES")
+        with pytest.raises(ValueError, match="recognized Snowflake host"):
+            provider.get_token()
+        fetch.assert_called_once()
+
+    def test_connection_cannot_extend_allowlist(self, monkeypatch):
+        monkeypatch.delenv("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES", 
raising=False)
+        hook = mock.create_autospec(SnowflakeHook, instance=True)
+        hook._get_conn_params.return_value = {
+            **CONN_PARAMS_WIF,
+            "host": "evil.example",
+            "SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES": "evil.example",
+        }
+        with pytest.raises(ValueError, match="recognized Snowflake host"):
+            SnowflakeRestTokenProvider(hook, private_key_loader=lambda: 
None).get_token()
+
+
+class TestFetchIdentityTokens:
+    @pytest.mark.parametrize(
+        ("aws_region", "default_region", "metadata_region", "expected"),
+        [
+            ("eu-west-1", "eu-west-2", "eu-west-3", "eu-west-1"),
+            (None, "eu-west-2", "eu-west-3", "eu-west-2"),
+            (None, None, "eu-west-3", "eu-west-3"),
+            ("us-east-1", None, None, "us-east-1"),
+        ],
+    )
+    @mock.patch("botocore.utils.InstanceMetadataRegionFetcher", autospec=True)
+    def test_aws_requests_regional_web_identity_token(
+        self, metadata, aws_region, default_region, metadata_region, expected, 
monkeypatch
+    ):
+        import boto3
+        from botocore.stub import Stubber
+
+        for name, value in (("AWS_REGION", aws_region), ("AWS_DEFAULT_REGION", 
default_region)):
+            if value:
+                monkeypatch.setenv(name, value)
+            else:
+                monkeypatch.delenv(name, raising=False)
+        monkeypatch.setenv("AWS_STS_REGIONAL_ENDPOINTS", "legacy")
+        metadata.return_value.retrieve_region.return_value = metadata_region
+        real_session = boto3.Session
+        clients = []
+
+        def create_session(*args, **kwargs):
+            session = real_session(
+                *args, aws_access_key_id="testing", 
aws_secret_access_key="testing", **kwargs
+            )
+            real_client = session.client
+
+            def create_client(*args, **kwargs):
+                client = real_client(*args, **kwargs)
+                clients.append(client)
+                stubber = Stubber(client)
+                stubber.add_response(
+                    "get_web_identity_token",
+                    {
+                        "WebIdentityToken": "aws-jwt",
+                        "Expiration": datetime(2024, 1, 1, tzinfo=UTC),
+                    },
+                    {"Audience": ["snowflakecomputing.com"], 
"SigningAlgorithm": "ES384"},
+                )
+                stubber.activate()
+                return client
+
+            session.client = create_client
+            return session
+
+        with mock.patch("boto3.Session", autospec=True, 
side_effect=create_session):
+            assert _fetch_aws_identity_token() == "aws-jwt"
+        assert clients[0].meta.region_name == expected
+        assert clients[0].meta.endpoint_url == 
f"https://sts.{expected}.amazonaws.com";
+        if aws_region or default_region:
+            metadata.assert_not_called()
+        else:
+            metadata.return_value.retrieve_region.assert_called_once_with()
+
+    @mock.patch("botocore.utils.InstanceMetadataRegionFetcher", autospec=True)
+    @mock.patch("boto3.Session", autospec=True)
+    def test_aws_missing_region_raises(self, session, metadata, monkeypatch):
+        monkeypatch.delenv("AWS_REGION", raising=False)
+        monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
+        metadata.return_value.retrieve_region.return_value = None
+        with pytest.raises(ValueError, match="AWS_REGION or 
AWS_DEFAULT_REGION"):
+            _fetch_aws_identity_token()
+        session.assert_not_called()
+
+    @mock.patch("boto3.Session", autospec=True)
+    def test_aws_old_boto3_raises(self, session, monkeypatch):
+        monkeypatch.setenv("AWS_REGION", "eu-west-1")
+        session.return_value.client.return_value = mock.Mock(spec=[])
+        with pytest.raises(AirflowOptionalProviderFeatureException, 
match="sts:GetWebIdentityToken"):
+            _fetch_aws_identity_token()
+
+    @pytest.mark.parametrize(
+        ("fetch", "module", "installation"),
+        [
+            pytest.param(_fetch_aws_identity_token, "boto3", "Install boto3.", 
id="aws"),
+            pytest.param(
+                _fetch_azure_identity_token,
+                "azure.identity",
+                "Install apache-airflow-providers-snowflake[microsoft.azure].",
+                id="azure",
+            ),
+            pytest.param(
+                _fetch_gcp_identity_token, "google.auth.compute_engine", 
"Install google-auth.", id="gcp"
+            ),
+        ],
+    )
+    def test_missing_cloud_library_raises(self, fetch, module, installation):
+        with mock.patch.dict(sys.modules, {module: None}):
+            with pytest.raises(AirflowOptionalProviderFeatureException) as exc:
+                fetch()
+        assert installation in str(exc.value)
+
+    @pytest.mark.parametrize("workload", [False, True])
+    @pytest.mark.parametrize("fails", [False, True])
+    @mock.patch("azure.identity.ManagedIdentityCredential", autospec=True)
+    @mock.patch("azure.identity.WorkloadIdentityCredential", autospec=True)
+    def test_azure_credential_selection_and_cleanup(self, aks, managed, 
workload, fails, monkeypatch):
+        from azure.core.credentials import AccessToken
+
+        for name in ("AZURE_CLIENT_ID", "AZURE_TENANT_ID", 
"AZURE_FEDERATED_TOKEN_FILE"):
+            if workload:
+                monkeypatch.setenv(name, "configured")
+            else:
+                monkeypatch.delenv(name, raising=False)
+        monkeypatch.setenv("MANAGED_IDENTITY_CLIENT_ID", "mi-client-id")
+        selected, unused = (aks, managed) if workload else (managed, aks)
+        selected.return_value.get_token.return_value = 
AccessToken("azure-token", 3600)
+        if fails:
+            selected.return_value.get_token.side_effect = 
RuntimeError("unavailable")
+            with pytest.raises(RuntimeError, match="unavailable"):
+                _fetch_azure_identity_token()
+        else:
+            assert _fetch_azure_identity_token() == "azure-token"
+        if workload:
+            selected.assert_called_once_with()
+        else:
+            selected.assert_called_once_with(client_id="mi-client-id")
+        unused.assert_not_called()
+        selected.return_value.get_token.assert_called_once_with(
+            "api://fd3f753b-eed3-462c-b6a7-a4b5bb650aad/.default"
+        )
+        selected.return_value.close.assert_called_once_with()
+
+    @mock.patch("google.auth.compute_engine.IDTokenCredentials", autospec=True)

Review Comment:
   Patching `google.auth.compute_engine.IDTokenCredentials` imports the module, 
and google-auth isn't in this provider's dependencies or dev group: `uv export 
--package apache-airflow-providers-snowflake --all-extras` lists boto3 and 
azure-identity but no google-auth. The lowest-dependencies job runs `uv sync 
--all-extras` inside `providers/snowflake`, which removes it, so this test 
would fail there with `ModuleNotFoundError`. A 
`pytest.importorskip("google.auth")` or adding google-auth to the dev group 
would cover it.



##########
providers/snowflake/src/airflow/providers/snowflake/utils/_rest_auth.py:
##########
@@ -154,6 +175,61 @@ def get_token(self) -> SnowflakeRestToken:
             token = self._jwt_generator.get_token()
             return SnowflakeRestToken(token=token, token_type="KEYPAIR_JWT")
 
+    def _get_workload_identity_token(self, conn_config: dict[str, Any]) -> 
SnowflakeRestToken:
+        """
+        Return a Workload Identity Federation token, fetching a new one when 
the cached one expires.
+
+        The caller must hold ``self._lock``.
+        """
+        _validate_wif_host(conn_config)
+        if self._wif_token is not None and time.time() < 
self._wif_token_expires_at:
+            return self._wif_token
+
+        provider_name = str(conn_config["workload_identity_provider"]).upper()
+        if provider_name == "AWS":
+            credential = _fetch_aws_identity_token()
+        elif provider_name == "AZURE":
+            credential = _fetch_azure_identity_token()
+        elif provider_name == "GCP":
+            credential = _fetch_gcp_identity_token()
+        elif provider_name == "OIDC":
+            credential = self._get_oidc_token(conn_config)
+        else:
+            raise ValueError(
+                f"Unknown workload_identity_provider {provider_name!r}; 
expected one of "
+                "AWS, AZURE, GCP or OIDC."
+            )
+
+        token = SnowflakeRestToken(
+            token=f"WIF.{provider_name}.{credential}",
+            token_type="WORKLOAD_IDENTITY_FEDERATION",
+        )
+        expires_at = self._get_jwt_expiry(credential)
+        self._wif_token = token if expires_at else None
+        self._wif_token_expires_at = (expires_at or 0.0) - 
WIF_TOKEN_EXPIRY_BUFFER
+        return token
+
+    @staticmethod
+    def _get_oidc_token(conn_config: dict[str, Any]) -> str:
+        """Return the OIDC ID token from the connection ``token`` or 
``token_file_path`` extra."""
+        if token := conn_config.get("token"):

Review Comment:
   The connector does the reverse when both are set: `token_file_path` wins 
(its docstring says "the token in token_file_path will be used", and `__config` 
overwrites `_token` from the file). So with a projected token file plus a 
leftover static `token`, `SnowflakeHook` keeps using the rotating file while 
the REST hooks send the static token until it expires. Checking the file first, 
with a test that sets both, would keep them in step.



##########
providers/snowflake/src/airflow/providers/snowflake/utils/_rest_auth.py:
##########
@@ -163,6 +239,104 @@ def build_auth_headers(self) -> dict[str, str]:
         }
 
 
+def _validate_wif_host(conn_config: dict[str, Any]) -> None:
+    """Restrict ambient workload credentials to trusted Snowflake 
destinations."""
+    host = 
get_cortex_base_url(conn_config).removeprefix("https://";).lower().removesuffix(".")
+    suffixes = ["snowflakecomputing.com", "snowflakecomputing.cn", 
"snowflakecomputing.mil"]
+    suffixes.extend(
+        entry.strip().lower().strip(".")
+        for entry in os.environ.get("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES", 
"").split(",")
+        if entry.strip()
+    )
+    dns_name = 
r"[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?(?:\.[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?)*"
+    if (
+        len(host) <= 253
+        and re.fullmatch(dns_name, host)
+        and any(
+            re.fullmatch(dns_name, suffix) and (host == suffix or 
host.endswith(f".{suffix}"))
+            for suffix in suffixes
+        )
+    ):
+        return
+    raise ValueError("Workload Identity Federation requires a recognized 
Snowflake host.")
+
+
+def _fetch_aws_identity_token() -> str:
+    """Return the JWT from regional STS for the workload's AWS identity."""
+    try:
+        import boto3
+        from botocore.session import get_session
+        from botocore.utils import InstanceMetadataRegionFetcher
+    except ImportError as e:
+        raise AirflowOptionalProviderFeatureException(
+            "Workload Identity Federation with the AWS provider requires 
boto3. Install boto3."
+        ) from e
+    region = (
+        os.environ.get("AWS_REGION")
+        or os.environ.get("AWS_DEFAULT_REGION")
+        or InstanceMetadataRegionFetcher().retrieve_region()
+    )
+    if not region:
+        raise ValueError(
+            "AWS workload identity requires a region: set AWS_REGION or 
AWS_DEFAULT_REGION, "
+            "or enable EC2 instance metadata region discovery."
+        )
+    session = get_session()
+    session.set_config_variable("sts_regional_endpoints", "regional")
+    sts = boto3.Session(botocore_session=session).client("sts", 
region_name=region)
+    if not hasattr(sts, "get_web_identity_token"):
+        raise AirflowOptionalProviderFeatureException(
+            "Workload Identity Federation with the AWS provider requires a 
boto3 version that "
+            "supports sts:GetWebIdentityToken. Upgrade boto3."
+        )
+    response = sts.get_web_identity_token(Audience=[WIF_AUDIENCE], 
SigningAlgorithm="ES384")
+    return response["WebIdentityToken"]
+
+
+def _fetch_azure_identity_token() -> str:
+    """
+    Return an Entra access token for Snowflake's application, issued to the 
workload's identity.
+
+    Uses the AKS workload identity when the pod has one (the Azure Workload 
Identity webhook sets
+    ``AZURE_CLIENT_ID``, ``AZURE_TENANT_ID`` and 
``AZURE_FEDERATED_TOKEN_FILE``), and the managed
+    identity otherwise. A user-assigned managed identity is selected with
+    ``MANAGED_IDENTITY_CLIENT_ID``.
+    """
+    try:
+        from azure.identity import ManagedIdentityCredential, 
WorkloadIdentityCredential
+    except ImportError as e:
+        raise AirflowOptionalProviderFeatureException(
+            "Workload Identity Federation with the AZURE provider requires 
azure-identity. "
+            "Install apache-airflow-providers-snowflake[microsoft.azure]."
+        ) from e
+    credential: ManagedIdentityCredential | WorkloadIdentityCredential
+    if all(
+        os.environ.get(name) for name in ("AZURE_CLIENT_ID", 
"AZURE_TENANT_ID", "AZURE_FEDERATED_TOKEN_FILE")

Review Comment:
   The connector's AKS check also requires the `AZURE_FEDERATED_TOKEN_FILE` 
path to exist (`create_azure_attestation` in `wif_util.py`) and falls back to 
the managed identity when it doesn't. Adding `os.path.exists` here keeps both 
picking the same credential. The Azure test only sets all three variables or 
none, so a case with the variables set and no file would pin it.



##########
providers/snowflake/src/airflow/providers/snowflake/utils/_rest_auth.py:
##########
@@ -163,6 +239,104 @@ def build_auth_headers(self) -> dict[str, str]:
         }
 
 
+def _validate_wif_host(conn_config: dict[str, Any]) -> None:
+    """Restrict ambient workload credentials to trusted Snowflake 
destinations."""
+    host = 
get_cortex_base_url(conn_config).removeprefix("https://";).lower().removesuffix(".")
+    suffixes = ["snowflakecomputing.com", "snowflakecomputing.cn", 
"snowflakecomputing.mil"]
+    suffixes.extend(
+        entry.strip().lower().strip(".")
+        for entry in os.environ.get("SNOWFLAKE_WIF_ALLOWED_HOST_SUFFIXES", 
"").split(",")
+        if entry.strip()
+    )
+    dns_name = 
r"[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?(?:\.[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?)*"
+    if (
+        len(host) <= 253
+        and re.fullmatch(dns_name, host)
+        and any(
+            re.fullmatch(dns_name, suffix) and (host == suffix or 
host.endswith(f".{suffix}"))
+            for suffix in suffixes
+        )
+    ):
+        return
+    raise ValueError("Workload Identity Federation requires a recognized 
Snowflake host.")
+
+
+def _fetch_aws_identity_token() -> str:
+    """Return the JWT from regional STS for the workload's AWS identity."""
+    try:
+        import boto3
+        from botocore.session import get_session
+        from botocore.utils import InstanceMetadataRegionFetcher
+    except ImportError as e:
+        raise AirflowOptionalProviderFeatureException(
+            "Workload Identity Federation with the AWS provider requires 
boto3. Install boto3."
+        ) from e
+    region = (
+        os.environ.get("AWS_REGION")
+        or os.environ.get("AWS_DEFAULT_REGION")
+        or InstanceMetadataRegionFetcher().retrieve_region()
+    )
+    if not region:
+        raise ValueError(
+            "AWS workload identity requires a region: set AWS_REGION or 
AWS_DEFAULT_REGION, "
+            "or enable EC2 instance metadata region discovery."
+        )
+    session = get_session()
+    session.set_config_variable("sts_regional_endpoints", "regional")
+    sts = boto3.Session(botocore_session=session).client("sts", 
region_name=region)
+    if not hasattr(sts, "get_web_identity_token"):
+        raise AirflowOptionalProviderFeatureException(
+            "Workload Identity Federation with the AWS provider requires a 
boto3 version that "
+            "supports sts:GetWebIdentityToken. Upgrade boto3."

Review Comment:
   `GetWebIdentityToken` was added to STS in botocore 1.41.0, so naming that 
version here and in the AWS docs bullet would tell users what to upgrade to.



##########
providers/snowflake/src/airflow/providers/snowflake/utils/_rest_auth.py:
##########
@@ -117,18 +140,16 @@ def get_token(self) -> SnowflakeRestToken:
                     )
                 return SnowflakeRestToken(token=pat, 
token_type="PROGRAMMATIC_ACCESS_TOKEN")
 
-            if self._private_key is None:
+            if not self._private_key_loaded:
                 self._private_key = self._private_key_loader()
+                self._private_key_loaded = True
             if self._private_key is None:
                 if conn_config.get("workload_identity_provider"):

Review Comment:
   This picks WIF from `workload_identity_provider` alone, while the connector 
only uses WIF when `authenticator` is `WORKLOAD_IDENTITY`, and 
`_add_workload_identity_config` copies the extra whatever the authenticator is. 
So a password connection with a leftover provider extra now authenticates as 
the worker's cloud identity through the REST hooks, but not through 
`SnowflakeHook`. The new docs bullets already tell users to set `authenticator` 
to `WORKLOAD_IDENTITY`, so checking it here would match them.



##########
providers/snowflake/tests/unit/snowflake/utils/test__rest_auth.py:
##########
@@ -156,12 +172,141 @@ def 
test_get_token_workload_identity_with_private_key_uses_key_pair(self, mock_c
         assert provider.get_token().token_type == "KEYPAIR_JWT"
 
     @mock.patch.object(SnowflakeHook, "_get_conn_params", autospec=True)
-    def test_get_token_workload_identity_without_private_key_raises(self, 
mock_conn_params):
-        mock_conn_params.return_value = {**CONN_PARAMS_KEYPAIR, 
"workload_identity_provider": "AWS"}
+    @mock.patch(FETCH_TOKEN.format("gcp"), autospec=True)
+    def test_private_key_absence_cached_across_wif_refresh(self, fetch, 
conn_params, time_machine):
+        time_machine.move_to("2024-01-01T00:00:00Z", tick=False)
+        conn_params.return_value = CONN_PARAMS_WIF
+        fetch.return_value = _make_jwt(exp=int(time.time()) + 3600)
+        loader = mock.Mock(spec=SnowflakeHook().get_private_key, 
return_value=None)
+        provider = SnowflakeRestTokenProvider(SnowflakeHook(), 
private_key_loader=loader)
+        provider.get_token()
+        provider.get_token()
+        time_machine.shift(timedelta(hours=1))
+        provider.get_token()
+        loader.assert_called_once_with()
+        assert fetch.call_count == 2
+
+    @mock.patch.object(SnowflakeHook, "_get_conn_params", autospec=True)
+    def test_private_key_loader_retries_after_error(self, conn_params):
+        conn_params.return_value = {**CONN_PARAMS_WIF, 
"workload_identity_provider": "OIDC", "token": "token"}
+        loader = mock.Mock(spec=SnowflakeHook().get_private_key, 
side_effect=[OSError("unavailable"), None])
+        provider = SnowflakeRestTokenProvider(SnowflakeHook(), 
private_key_loader=loader)
+        with pytest.raises(OSError, match="unavailable"):
+            provider.get_token()
+        assert provider.get_token().token == "WIF.OIDC.token"
+        assert loader.call_count == 2
+
+    @pytest.mark.parametrize("provider_name", ["GCP", "AZURE", "AWS", "gcp"])
+    @mock.patch.object(SnowflakeHook, "_get_conn_params", autospec=True)
+    def test_get_token_workload_identity(self, mock_conn_params, 
provider_name):
+        credential = _make_jwt(exp=int(time.time()) + 3600)
+        mock_conn_params.return_value = {**CONN_PARAMS_WIF, 
"workload_identity_provider": provider_name}
+        hook = SnowflakeHook(snowflake_conn_id="mock_conn_id")
+        provider = SnowflakeRestTokenProvider(hook, private_key_loader=lambda: 
None)
+
+        with mock.patch(FETCH_TOKEN.format(provider_name.lower()), 
return_value=credential) as fetch:

Review Comment:
   These patches of the `_fetch_*` helpers (also at 264, 288 and 300, and the 
new ones in `test_cortex_agent.py` and `test_sql_api.py`) could take 
`autospec=True` like the others in this file. The `boto3` and `botocore.stub` 
imports at 474 can also move to the top, since the connector always installs 
them.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to