This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 251cd91bb24 Migrate google + pagerduty + anthropic providers to 
`httpx2` (#72111)
251cd91bb24 is described below

commit 251cd91bb24a335f9ec320ec1b3f2929f9e4577c
Author: Zach <[email protected]>
AuthorDate: Wed Sep 9 15:10:21 2026 -0400

    Migrate google + pagerduty + anthropic providers to `httpx2` (#72111)
    
    * Migrate google + pagerduty providers to `httpx2`
    
    * Add a prek check that keeps providers on `httpx2`
    
    The `httpx` package is unmaintained and Airflow is moving to the `httpx2` 
fork,
    so providers already migrated must not drift back.
    
    The check exempts `airbyte`, `common/ai`, `weaviate` and `microsoft/azure`,
    whose third-party SDKs are still built on `httpx`. Their objects cross that
    boundary, and the two packages' classes are distinct, so switching those
    providers would break at runtime rather than fail loudly.
    
    * Address feedback on ast-grep pre-commit hook
---
 providers/.ast-grep/rules/no-httpx-import.yml      | 43 ++++++++++++++++++++++
 providers/.ast-grep/sgconfig.yml                   | 28 ++++++++++++++
 providers/.pre-commit-config.yaml                  |  9 +++++
 .../src/airflow/providers/airbyte/hooks/airbyte.py |  3 ++
 providers/google/README.rst                        |  2 +-
 providers/google/docs/changelog.rst                |  7 ++++
 providers/google/docs/index.rst                    |  2 +-
 providers/google/pyproject.toml                    |  2 +-
 .../providers/google/cloud/hooks/cloud_sql.py      | 10 +----
 .../unit/google/cloud/hooks/test_cloud_sql.py      | 25 +++++++++++++
 .../providers/microsoft/azure/hooks/msgraph.py     |  3 ++
 providers/pagerduty/README.rst                     |  2 +-
 providers/pagerduty/docs/changelog.rst             |  5 +++
 providers/pagerduty/docs/index.rst                 |  2 +-
 providers/pagerduty/pyproject.toml                 |  2 +-
 .../unit/pagerduty/hooks/test_pagerduty_events.py  | 10 ++---
 .../airflow/providers/weaviate/hooks/weaviate.py   |  3 ++
 uv.lock                                            |  6 +--
 18 files changed, 142 insertions(+), 22 deletions(-)

diff --git a/providers/.ast-grep/rules/no-httpx-import.yml 
b/providers/.ast-grep/rules/no-httpx-import.yml
new file mode 100644
index 00000000000..a40a96131d1
--- /dev/null
+++ b/providers/.ast-grep/rules/no-httpx-import.yml
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+id: no-httpx-import
+language: python
+severity: error
+message: Import 'httpx2' instead of 'httpx' -- see 
https://github.com/apache/airflow/issues/70522
+note: >-
+  The 'airbyte', 'common/ai', 'weaviate' and 'microsoft/azure' providers are 
exempt
+  because the third-party SDKs they hand these objects to are themselves built 
on
+  'httpx', and 'httpx2' classes are distinct from their 'httpx' counterparts.
+files:
+  - "**/*.py"
+ignores:
+  - "airbyte/**"
+  - "common/ai/**"
+  - "weaviate/**"
+  - "microsoft/azure/**"
+rule:
+  all:
+    - kind: dotted_name
+    - regex: ^httpx($|\.)
+    - any:
+        - inside:
+            kind: import_statement
+            stopBy: end
+        - inside:
+            kind: import_from_statement
+            field: module_name
diff --git a/providers/.ast-grep/sgconfig.yml b/providers/.ast-grep/sgconfig.yml
new file mode 100644
index 00000000000..ff99acc8c13
--- /dev/null
+++ b/providers/.ast-grep/sgconfig.yml
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+# NOTE: this file configures `ast-grep`, which is used for enforcing custom 
lint / auto-formatting
+# across a number of different languages.  Currently, we use this in 
pre-commit hook to avoid regressions of
+# reintroducing `httpx` in provider packages which currently don't use `httpx`.
+#
+# If new ast-grep rules are required, they may be added in the rules directory 
listed below.
+#
+# See also:
+#   https://github.com/ast-grep/ast-grep
+#   https://ast-grep.github.io/guide/introduction
+ruleDirs:
+  - rules
diff --git a/providers/.pre-commit-config.yaml 
b/providers/.pre-commit-config.yaml
index 4c624358aae..c8ff25f217d 100644
--- a/providers/.pre-commit-config.yaml
+++ b/providers/.pre-commit-config.yaml
@@ -280,3 +280,12 @@ repos:
         types: [python]
         files: ^.*/src/airflow/providers/.*\.py$
         require_serial: true
+  - repo: https://github.com/boidolr/ast-grep-pre-commit
+    rev: 0.45.2
+    hooks:
+      - id: ast-grep
+        name: Check providers import httpx2 rather than httpx
+        types: [python]
+        # Pinned to the providers config rather than relying on ast-grep 
walking up
+        # from the working directory, so a future root sgconfig.yml cannot 
shadow it.
+        args: ["--config", "./.ast-grep/sgconfig.yml"]
diff --git a/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py 
b/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
index dd7cc045626..6f4fbe3ae55 100644
--- a/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
+++ b/providers/airbyte/src/airflow/providers/airbyte/hooks/airbyte.py
@@ -20,6 +20,9 @@ from __future__ import annotations
 import time
 from typing import Any, TypeVar
 
+# Stays on httpx (not httpx2): the client built below is handed to 
airbyte-api, which builds
+# and sends httpx.Request objects through it, and the two packages' classes 
are distinct.
+# Migrate once airbyte-api moves to httpx2; tracked at 
https://github.com/apache/airflow/issues/70522
 import httpx
 from airbyte_api import AirbyteAPI
 from airbyte_api.api import CancelJobRequest, GetJobRequest
diff --git a/providers/google/README.rst b/providers/google/README.rst
index 40545c6684f..288c25c0e52 100644
--- a/providers/google/README.rst
+++ b/providers/google/README.rst
@@ -122,7 +122,7 @@ PIP package                                 Version required
 ``google-cloud-run``                        ``>=0.10.0``
 ``google-cloud-batch``                      ``>=0.13.0``
 ``grpcio-gcp``                              ``>=0.2.2``
-``httpx``                                   ``>=0.25.0``
+``httpx2``                                  ``>=2.0.0``
 ``looker-sdk``                              ``>=22.4.0,!=24.18.0``
 ``pandas-gbq``                              ``>=0.7.0``
 ``pandas``                                  ``>=2.1.2; python_version < 
"3.13"``
diff --git a/providers/google/docs/changelog.rst 
b/providers/google/docs/changelog.rst
index c1e4de295eb..fc11e72def1 100644
--- a/providers/google/docs/changelog.rst
+++ b/providers/google/docs/changelog.rst
@@ -27,6 +27,13 @@
 Changelog
 ---------
 
+.. note::
+    This provider now depends on ``httpx2`` instead of ``httpx``, and 
``httpx2`` verifies TLS
+    against the operating system trust store rather than the ``certifi`` 
bundle. If
+    ``CloudSqlProxyRunner`` downloads the ``cloud-sql-proxy`` binary through a 
proxy presenting
+    a private CA, install that CA into the OS trust store or point 
``SSL_CERT_FILE`` (or
+    ``SSL_CERT_DIR``) at it -- adding it to ``certifi`` alone no longer has 
any effect.
+
 22.4.0
 ......
 
diff --git a/providers/google/docs/index.rst b/providers/google/docs/index.rst
index db9c84efeae..424f9ec2858 100644
--- a/providers/google/docs/index.rst
+++ b/providers/google/docs/index.rst
@@ -175,7 +175,7 @@ PIP package                                 Version required
 ``google-cloud-run``                        ``>=0.10.0``
 ``google-cloud-batch``                      ``>=0.13.0``
 ``grpcio-gcp``                              ``>=0.2.2``
-``httpx``                                   ``>=0.25.0``
+``httpx2``                                  ``>=2.0.0``
 ``looker-sdk``                              ``>=22.4.0,!=24.18.0``
 ``pandas-gbq``                              ``>=0.7.0``
 ``pandas``                                  ``>=2.1.2; python_version < 
"3.13"``
diff --git a/providers/google/pyproject.toml b/providers/google/pyproject.toml
index 07f2b310091..4a4386de52f 100644
--- a/providers/google/pyproject.toml
+++ b/providers/google/pyproject.toml
@@ -132,7 +132,7 @@ dependencies = [
     "google-cloud-run>=0.10.0",
     "google-cloud-batch>=0.13.0",
     "grpcio-gcp>=0.2.2",
-    "httpx>=0.25.0",
+    "httpx2>=2.0.0",
     # looker-sdk 24.18.0 has issues in import looker_sdk.rtl,  No module named 
looker_sdk.rtl
     # See https://github.com/looker-open-source/sdk-codegen/issues/1518
     "looker-sdk>=22.4.0,!=24.18.0",
diff --git 
a/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py 
b/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
index 57889e96c2b..0f672570f33 100644
--- a/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
+++ b/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
@@ -34,14 +34,13 @@ import subprocess
 import time
 import uuid
 from collections.abc import Sequence
-from inspect import signature
 from pathlib import Path
 from subprocess import PIPE, Popen
 from tempfile import NamedTemporaryFile, _TemporaryFileWrapper, gettempdir
 from typing import TYPE_CHECKING, Any, cast
 from urllib.parse import quote_plus
 
-import httpx
+import httpx2
 from aiohttp import ClientSession
 from gcloud.aio.auth import AioSession, Token
 from googleapiclient.discovery import Resource, build
@@ -608,12 +607,7 @@ class CloudSqlProxyRunner(LoggingMixin):
         download_url = self._get_sql_proxy_download_url()
         proxy_path_tmp = self.sql_proxy_path + ".tmp"
         self.log.info("Downloading cloud_sql_proxy from %s to %s", 
download_url, proxy_path_tmp)
-        # httpx has a breaking API change (follow_redirects vs allow_redirects)
-        # and this should work with both versions (cf. issue #20088)
-        if "follow_redirects" in signature(httpx.get).parameters.keys():
-            response = httpx.get(download_url, follow_redirects=True)
-        else:
-            response = httpx.get(download_url, allow_redirects=True)  # type: 
ignore[call-arg]
+        response = httpx2.get(download_url, follow_redirects=True)
         # Downloading to .tmp file first to avoid case where partially 
downloaded
         # binary is used by parallel operator which uses the same fixed binary 
path
         with open(proxy_path_tmp, "wb") as file:
diff --git a/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py 
b/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
index 518f509e522..955add00379 100644
--- a/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
+++ b/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
@@ -1918,6 +1918,31 @@ class TestCloudSqlProxyRunner:
         with pytest.raises(ValueError, match="The sql_proxy_version should 
match the regular expression"):
             runner._get_sql_proxy_download_url()
 
+    @mock.patch("airflow.providers.google.cloud.hooks.cloud_sql.httpx2.get")
+    def test_download_sql_proxy_follows_redirects(self, mock_get, tmp_path):
+        """The download URL redirects, so the request must opt in to following 
them."""
+        mock_get.return_value = mock.Mock(status_code=200, content=b"binary")
+        runner = CloudSqlProxyRunner(
+            path_prefix=str(tmp_path / "12345678"),
+            instance_specification="project:us-east-1:instance",
+        )
+
+        runner._download_sql_proxy_if_needed()
+
+        mock_get.assert_called_once_with(runner._get_sql_proxy_download_url(), 
follow_redirects=True)
+        assert runner.sql_proxy_was_downloaded is True
+
+    @mock.patch("airflow.providers.google.cloud.hooks.cloud_sql.httpx2.get")
+    def test_download_sql_proxy_raises_on_error_status(self, mock_get, 
tmp_path):
+        mock_get.return_value = mock.Mock(status_code=404, content=b"", 
reason_phrase="Not Found")
+        runner = CloudSqlProxyRunner(
+            path_prefix=str(tmp_path / "12345678"),
+            instance_specification="project:us-east-1:instance",
+        )
+
+        with pytest.raises(AirflowException, match="Status code = 404. Reason 
= Not Found"):
+            runner._download_sql_proxy_if_needed()
+
     def test_cloud_sql_proxy_runner_adds_enable_iam_login_flag(self):
         runner = CloudSqlProxyRunner(
             path_prefix="12345678",
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
index a389c8725be..b9a74f0f97b 100644
--- 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
@@ -44,6 +44,9 @@ from types import TracebackType
 from typing import TYPE_CHECKING, Any, cast
 from urllib.parse import quote, urljoin, urlparse
 
+# Stays on httpx (not httpx2): the client below is handed to kiota_http, which 
builds and
+# consumes httpx objects, and the two packages' classes are distinct.
+# Migrate once msgraph-core/kiota_http move to httpx2; tracked at 
https://github.com/apache/airflow/issues/70522
 import httpx
 from azure.core.credentials_async import AsyncTokenCredential
 from azure.identity.aio import CertificateCredential, ClientSecretCredential
diff --git a/providers/pagerduty/README.rst b/providers/pagerduty/README.rst
index 0165433426b..ac7d5fe4d8a 100644
--- a/providers/pagerduty/README.rst
+++ b/providers/pagerduty/README.rst
@@ -56,7 +56,7 @@ PIP package                                 Version required
 ``apache-airflow``                          ``>=2.11.0``
 ``apache-airflow-providers-common-compat``  ``>=1.10.1``
 ``apache-airflow-providers-http``
-``pagerduty``                               ``>=2.3.0``
+``pagerduty``                               ``>=7.0.0``
 ==========================================  ==================
 
 The changelog for the provider package can be found in the
diff --git a/providers/pagerduty/docs/changelog.rst 
b/providers/pagerduty/docs/changelog.rst
index eb94a468257..64070e03eba 100644
--- a/providers/pagerduty/docs/changelog.rst
+++ b/providers/pagerduty/docs/changelog.rst
@@ -27,6 +27,11 @@
 Changelog
 ---------
 
+.. note::
+    The minimum required version of the ``pagerduty`` SDK is now ``7.0.0``, 
which replaced
+    ``httpx`` with ``httpx2``. Deployments pinning an older SDK must upgrade 
it alongside this
+    provider. The provider's own hooks and notifier are unchanged.
+
 5.2.6
 .....
 
diff --git a/providers/pagerduty/docs/index.rst 
b/providers/pagerduty/docs/index.rst
index 0f6ea4906db..e25dfb369e8 100644
--- a/providers/pagerduty/docs/index.rst
+++ b/providers/pagerduty/docs/index.rst
@@ -95,7 +95,7 @@ PIP package                                 Version required
 ``apache-airflow``                          ``>=2.11.0``
 ``apache-airflow-providers-common-compat``  ``>=1.10.1``
 ``apache-airflow-providers-http``
-``pagerduty``                               ``>=2.3.0``
+``pagerduty``                               ``>=7.0.0``
 ==========================================  ==================
 
 Downloading official packages
diff --git a/providers/pagerduty/pyproject.toml 
b/providers/pagerduty/pyproject.toml
index 47d4fcb3337..c61f8e073df 100644
--- a/providers/pagerduty/pyproject.toml
+++ b/providers/pagerduty/pyproject.toml
@@ -62,7 +62,7 @@ dependencies = [
     "apache-airflow>=2.11.0",
     "apache-airflow-providers-common-compat>=1.10.1",
     "apache-airflow-providers-http",
-    "pagerduty>=2.3.0",
+    "pagerduty>=7.0.0",
 ]
 
 [dependency-groups]
diff --git 
a/providers/pagerduty/tests/unit/pagerduty/hooks/test_pagerduty_events.py 
b/providers/pagerduty/tests/unit/pagerduty/hooks/test_pagerduty_events.py
index 6e86fb489d4..c139878d886 100644
--- a/providers/pagerduty/tests/unit/pagerduty/hooks/test_pagerduty_events.py
+++ b/providers/pagerduty/tests/unit/pagerduty/hooks/test_pagerduty_events.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 from unittest import mock
 from unittest.mock import patch
 
-import httpx
+import httpx2
 import pagerduty
 import pytest
 from pagerduty import EventsApiV2Client
@@ -91,10 +91,10 @@ class TestPagerdutyEventsHook:
             "message": "Change event processed",
             "status": "success",
         }
-        mock_response = httpx.Response(
+        mock_response = httpx2.Response(
             status_code=202,
             json=mock_response_body,
-            request=httpx.Request("POST", 
"https://events.pagerduty.com/v2/change/enqueue";),
+            request=httpx2.Request("POST", 
"https://events.pagerduty.com/v2/change/enqueue";),
         )
 
         mock_response.ok = True
@@ -114,10 +114,10 @@ class TestPagerdutyEventsHook:
             "message": "Event processed",
             "dedup_key": dedup_key,
         }
-        mock_response = httpx.Response(
+        mock_response = httpx2.Response(
             status_code=202,
             json=mock_response_body,
-            request=httpx.Request("POST", 
"https://events.pagerduty.com/v2/enqueue";),
+            request=httpx2.Request("POST", 
"https://events.pagerduty.com/v2/enqueue";),
         )
         mock_response.ok = True
         mock_request.return_value = mock_response
diff --git 
a/providers/weaviate/src/airflow/providers/weaviate/hooks/weaviate.py 
b/providers/weaviate/src/airflow/providers/weaviate/hooks/weaviate.py
index 10dce9bed09..6757f616854 100644
--- a/providers/weaviate/src/airflow/providers/weaviate/hooks/weaviate.py
+++ b/providers/weaviate/src/airflow/providers/weaviate/hooks/weaviate.py
@@ -69,6 +69,9 @@ REQUESTS_EXCEPTIONS_TYPES = (
 
 def check_http_error_is_retryable(exc: BaseException):
     try:
+        # Stays on httpx (not httpx2): ``exc`` is raised inside 
weaviate-client, which is built
+        # on httpx, so an httpx2.ConnectError would never match and retries 
would silently stop.
+        # Migrate once weaviate-client moves to httpx2; tracked at 
https://github.com/apache/airflow/issues/70522
         import httpx
 
         if isinstance(exc, httpx.ConnectError):
diff --git a/uv.lock b/uv.lock
index a39119a8d0f..a609f7ac284 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5892,7 +5892,7 @@ dependencies = [
     { name = "google-cloud-workflows" },
     { name = "google-genai" },
     { name = "grpcio-gcp" },
-    { name = "httpx" },
+    { name = "httpx2" },
     { name = "immutabledict" },
     { name = "looker-sdk" },
     { name = "pandas", version = "2.3.3", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.11'" },
@@ -6092,7 +6092,7 @@ requires-dist = [
     { name = "google-cloud-workflows", specifier = ">=1.10.0" },
     { name = "google-genai", specifier = ">=2.8.0" },
     { name = "grpcio-gcp", specifier = ">=0.2.2" },
-    { name = "httpx", specifier = ">=0.25.0" },
+    { name = "httpx2", specifier = ">=2.0.0" },
     { name = "immutabledict", specifier = ">=4.2.0" },
     { name = "looker-sdk", specifier = ">=22.4.0,!=24.18.0" },
     { name = "pandas", marker = "python_full_version < '3.13'", specifier = 
">=2.1.2" },
@@ -7432,7 +7432,7 @@ requires-dist = [
     { name = "apache-airflow", editable = "." },
     { name = "apache-airflow-providers-common-compat", editable = 
"providers/common/compat" },
     { name = "apache-airflow-providers-http", editable = "providers/http" },
-    { name = "pagerduty", specifier = ">=2.3.0" },
+    { name = "pagerduty", specifier = ">=7.0.0" },
 ]
 
 [package.metadata.requires-dev]

Reply via email to