From d3cfc22ed715b35196e79caa2640e97110458295 Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Fri, 13 Feb 2026 17:59:31 +0200 Subject: [PATCH 1/8] Add Redis client self-identification for Apache Airflow --- .../airflow/providers/redis/hooks/redis.py | 16 ++++++++ .../tests/unit/redis/hooks/test_redis.py | 39 +++++++++++++------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/providers/redis/src/airflow/providers/redis/hooks/redis.py b/providers/redis/src/airflow/providers/redis/hooks/redis.py index 1e9740768ba6e..0734193235ae1 100644 --- a/providers/redis/src/airflow/providers/redis/hooks/redis.py +++ b/providers/redis/src/airflow/providers/redis/hooks/redis.py @@ -24,6 +24,7 @@ from redis import Redis from airflow.providers.common.compat.sdk import BaseHook +from airflow.providers.redis import __version__ as provider_version DEFAULT_SSL_CERT_REQS = "required" ALLOWED_SSL_CERT_REQS = [DEFAULT_SSL_CERT_REQS, "optional", "none"] @@ -87,6 +88,20 @@ def get_conn(self): self.port, self.db, ) + + # Add driver info for client identification + # This allows Redis server to identify Airflow as the upstream driver. + driver_info_options = {} + try: + # Try to use DriverInfo class if available (redis-py 5.1.0+) + from redis import DriverInfo + + driver_info = DriverInfo().add_upstream_driver("apache-airflow", provider_version) + driver_info_options = {"driver_info": driver_info} + except ImportError: + # Fallback to lib_name parameter for older redis-py versions + driver_info_options = {"lib_name": f"redis-py(apache-airflow_v{provider_version})"} + self.redis = Redis( host=self.host, port=self.port, @@ -94,6 +109,7 @@ def get_conn(self): password=self.password, db=self.db, **ssl_args, + **driver_info_options, ) return self.redis diff --git a/providers/redis/tests/unit/redis/hooks/test_redis.py b/providers/redis/tests/unit/redis/hooks/test_redis.py index d232537b23ca7..3061bbfc2b2ca 100644 --- a/providers/redis/tests/unit/redis/hooks/test_redis.py +++ b/providers/redis/tests/unit/redis/hooks/test_redis.py @@ -63,20 +63,35 @@ def test_get_conn_with_extra_config(self, mock_get_connection, mock_redis): hook = RedisHook() hook.get_conn() - mock_redis.assert_called_once_with( - host=connection.host, - username=connection.login, - password=connection.password, - port=connection.port, - db=connection.extra_dejson["db"], - ssl=connection.extra_dejson["ssl"], - ssl_cert_reqs=connection.extra_dejson["ssl_cert_reqs"], - ssl_ca_certs=connection.extra_dejson["ssl_ca_certs"], - ssl_keyfile=connection.extra_dejson["ssl_keyfile"], - ssl_certfile=connection.extra_dejson["ssl_certfile"], - ssl_check_hostname=connection.extra_dejson["ssl_check_hostname"], + + call_kwargs = mock_redis.call_args[1] + assert call_kwargs["host"] == connection.host + assert call_kwargs["username"] == connection.login + assert call_kwargs["password"] == connection.password + assert call_kwargs["port"] == connection.port + assert call_kwargs["db"] == connection.extra_dejson["db"] + assert call_kwargs["ssl"] == connection.extra_dejson["ssl"] + assert call_kwargs["ssl_cert_reqs"] == connection.extra_dejson["ssl_cert_reqs"] + assert call_kwargs["ssl_ca_certs"] == connection.extra_dejson["ssl_ca_certs"] + assert call_kwargs["ssl_keyfile"] == connection.extra_dejson["ssl_keyfile"] + assert call_kwargs["ssl_certfile"] == connection.extra_dejson["ssl_certfile"] + assert call_kwargs["ssl_check_hostname"] == connection.extra_dejson["ssl_check_hostname"] + + # Verify driver info is present with correct value + # Check for either driver_info or lib_name parameter + assert "driver_info" in call_kwargs or "lib_name" in call_kwargs, ( + "Expected either 'driver_info' or 'lib_name' in Redis client call" ) + if "driver_info" in call_kwargs: + # Uses DriverInfo class + driver_info = call_kwargs["driver_info"] + assert hasattr(driver_info, "formatted_name"), "DriverInfo should have formatted_name attribute" + assert "apache-airflow" in driver_info.formatted_name + elif "lib_name" in call_kwargs: + # Uses lib_name parameter + assert "apache-airflow" in call_kwargs["lib_name"] + @pytest.mark.db_test def test_get_conn_password_stays_none(self): hook = RedisHook(redis_conn_id="redis_default") From 15ebc49ddc70f803676fa9511be25df1a3658673 Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Thu, 12 Mar 2026 12:53:21 +0200 Subject: [PATCH 2/8] Check lib_name parameter support at module import time --- .../src/airflow/providers/redis/hooks/redis.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/providers/redis/src/airflow/providers/redis/hooks/redis.py b/providers/redis/src/airflow/providers/redis/hooks/redis.py index 0734193235ae1..9d564efbf785a 100644 --- a/providers/redis/src/airflow/providers/redis/hooks/redis.py +++ b/providers/redis/src/airflow/providers/redis/hooks/redis.py @@ -19,6 +19,7 @@ from __future__ import annotations +import inspect from typing import Any from redis import Redis @@ -29,6 +30,10 @@ DEFAULT_SSL_CERT_REQS = "required" ALLOWED_SSL_CERT_REQS = [DEFAULT_SSL_CERT_REQS, "optional", "none"] +# Check at module import time what Redis client identification features are supported +_REDIS_PARAMS = inspect.signature(Redis.__init__).parameters +_SUPPORTS_LIB_NAME = "lib_name" in _REDIS_PARAMS + class RedisHook(BaseHook): """ @@ -89,18 +94,22 @@ def get_conn(self): self.db, ) - # Add driver info for client identification + # Add driver info for client identification if supported # This allows Redis server to identify Airflow as the upstream driver. + # See: https://redis.io/docs/latest/commands/client-setinfo/ driver_info_options = {} try: - # Try to use DriverInfo class if available (redis-py 5.1.0+) + # Try to use DriverInfo class if available from redis import DriverInfo driver_info = DriverInfo().add_upstream_driver("apache-airflow", provider_version) driver_info_options = {"driver_info": driver_info} except ImportError: - # Fallback to lib_name parameter for older redis-py versions - driver_info_options = {"lib_name": f"redis-py(apache-airflow_v{provider_version})"} + # Fallback to lib_name parameter if supported + if _SUPPORTS_LIB_NAME: + driver_info_options = { + "lib_name": f"redis-py(apache-airflow_v{provider_version})", + } self.redis = Redis( host=self.host, From f63c53fe7a811b24ea821312da63ce49a852c89b Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Tue, 7 Apr 2026 15:38:02 +0300 Subject: [PATCH 3/8] Fix test to handle redis-py versions without lib_name support --- providers/redis/tests/unit/redis/hooks/test_redis.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/providers/redis/tests/unit/redis/hooks/test_redis.py b/providers/redis/tests/unit/redis/hooks/test_redis.py index 3061bbfc2b2ca..03565f8726d6c 100644 --- a/providers/redis/tests/unit/redis/hooks/test_redis.py +++ b/providers/redis/tests/unit/redis/hooks/test_redis.py @@ -77,19 +77,12 @@ def test_get_conn_with_extra_config(self, mock_get_connection, mock_redis): assert call_kwargs["ssl_certfile"] == connection.extra_dejson["ssl_certfile"] assert call_kwargs["ssl_check_hostname"] == connection.extra_dejson["ssl_check_hostname"] - # Verify driver info is present with correct value - # Check for either driver_info or lib_name parameter - assert "driver_info" in call_kwargs or "lib_name" in call_kwargs, ( - "Expected either 'driver_info' or 'lib_name' in Redis client call" - ) - + # Verify driver info is present if the installed redis-py version supports it if "driver_info" in call_kwargs: - # Uses DriverInfo class driver_info = call_kwargs["driver_info"] assert hasattr(driver_info, "formatted_name"), "DriverInfo should have formatted_name attribute" assert "apache-airflow" in driver_info.formatted_name elif "lib_name" in call_kwargs: - # Uses lib_name parameter assert "apache-airflow" in call_kwargs["lib_name"] @pytest.mark.db_test From 911b91fb668c055bc7bf0201ebee61e596737dd2 Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Thu, 23 Apr 2026 15:11:34 +0300 Subject: [PATCH 4/8] Move DriverInfo import to module level --- .../airflow/providers/redis/hooks/redis.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/providers/redis/src/airflow/providers/redis/hooks/redis.py b/providers/redis/src/airflow/providers/redis/hooks/redis.py index 9d564efbf785a..0173d0e28a8b4 100644 --- a/providers/redis/src/airflow/providers/redis/hooks/redis.py +++ b/providers/redis/src/airflow/providers/redis/hooks/redis.py @@ -27,6 +27,11 @@ from airflow.providers.common.compat.sdk import BaseHook from airflow.providers.redis import __version__ as provider_version +try: + from redis import DriverInfo +except ImportError: + DriverInfo = None + DEFAULT_SSL_CERT_REQS = "required" ALLOWED_SSL_CERT_REQS = [DEFAULT_SSL_CERT_REQS, "optional", "none"] @@ -97,19 +102,14 @@ def get_conn(self): # Add driver info for client identification if supported # This allows Redis server to identify Airflow as the upstream driver. # See: https://redis.io/docs/latest/commands/client-setinfo/ - driver_info_options = {} - try: - # Try to use DriverInfo class if available - from redis import DriverInfo - + driver_info_options: dict[str, Any] = {} + if DriverInfo is not None: driver_info = DriverInfo().add_upstream_driver("apache-airflow", provider_version) driver_info_options = {"driver_info": driver_info} - except ImportError: - # Fallback to lib_name parameter if supported - if _SUPPORTS_LIB_NAME: - driver_info_options = { - "lib_name": f"redis-py(apache-airflow_v{provider_version})", - } + elif _SUPPORTS_LIB_NAME: + driver_info_options = { + "lib_name": f"redis-py(apache-airflow_v{provider_version})", + } self.redis = Redis( host=self.host, From 6e1f0d512c48fc2b2fad88035481f4320bc17cd7 Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Thu, 23 Apr 2026 15:14:05 +0300 Subject: [PATCH 5/8] Assert Redis client is called exactly once in test --- providers/redis/tests/unit/redis/hooks/test_redis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/providers/redis/tests/unit/redis/hooks/test_redis.py b/providers/redis/tests/unit/redis/hooks/test_redis.py index 03565f8726d6c..42c8e46acb096 100644 --- a/providers/redis/tests/unit/redis/hooks/test_redis.py +++ b/providers/redis/tests/unit/redis/hooks/test_redis.py @@ -64,6 +64,7 @@ def test_get_conn_with_extra_config(self, mock_get_connection, mock_redis): hook.get_conn() + mock_redis.assert_called_once() call_kwargs = mock_redis.call_args[1] assert call_kwargs["host"] == connection.host assert call_kwargs["username"] == connection.login From da21dd10912be2f4ae4094e6bc7e14c79b7b792d Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Thu, 23 Apr 2026 15:19:32 +0300 Subject: [PATCH 6/8] Add deterministic tests for Redis client identification scenarios --- .../tests/unit/redis/hooks/test_redis.py | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/providers/redis/tests/unit/redis/hooks/test_redis.py b/providers/redis/tests/unit/redis/hooks/test_redis.py index 42c8e46acb096..19b74aea1f40a 100644 --- a/providers/redis/tests/unit/redis/hooks/test_redis.py +++ b/providers/redis/tests/unit/redis/hooks/test_redis.py @@ -78,13 +78,51 @@ def test_get_conn_with_extra_config(self, mock_get_connection, mock_redis): assert call_kwargs["ssl_certfile"] == connection.extra_dejson["ssl_certfile"] assert call_kwargs["ssl_check_hostname"] == connection.extra_dejson["ssl_check_hostname"] - # Verify driver info is present if the installed redis-py version supports it - if "driver_info" in call_kwargs: - driver_info = call_kwargs["driver_info"] - assert hasattr(driver_info, "formatted_name"), "DriverInfo should have formatted_name attribute" - assert "apache-airflow" in driver_info.formatted_name - elif "lib_name" in call_kwargs: - assert "apache-airflow" in call_kwargs["lib_name"] + @mock.patch("airflow.providers.redis.hooks.redis.Redis") + @mock.patch("airflow.providers.redis.hooks.redis.RedisHook.get_connection") + def test_client_identification_with_driver_info(self, mock_get_connection, mock_redis): + """When DriverInfo is available, the Redis client is created with a driver_info kwarg.""" + mock_get_connection.return_value = Connection(host="h", port=1, login="u", password="p") + fake_driver_info = mock.MagicMock() + fake_driver_info.add_upstream_driver.return_value = fake_driver_info + with mock.patch( + "airflow.providers.redis.hooks.redis.DriverInfo", return_value=fake_driver_info + ) as mock_driver_info_cls: + RedisHook().get_conn() + + mock_driver_info_cls.assert_called_once_with() + fake_driver_info.add_upstream_driver.assert_called_once() + args, _ = fake_driver_info.add_upstream_driver.call_args + assert args[0] == "apache-airflow" + call_kwargs = mock_redis.call_args[1] + assert call_kwargs["driver_info"] is fake_driver_info + assert "lib_name" not in call_kwargs + + @mock.patch("airflow.providers.redis.hooks.redis.Redis") + @mock.patch("airflow.providers.redis.hooks.redis.RedisHook.get_connection") + @mock.patch("airflow.providers.redis.hooks.redis.DriverInfo", None) + @mock.patch("airflow.providers.redis.hooks.redis._SUPPORTS_LIB_NAME", True) + def test_client_identification_with_lib_name(self, mock_get_connection, mock_redis): + """When DriverInfo is unavailable but lib_name is supported, lib_name kwarg is passed.""" + mock_get_connection.return_value = Connection(host="h", port=1, login="u", password="p") + RedisHook().get_conn() + + call_kwargs = mock_redis.call_args[1] + assert "driver_info" not in call_kwargs + assert "apache-airflow" in call_kwargs["lib_name"] + + @mock.patch("airflow.providers.redis.hooks.redis.Redis") + @mock.patch("airflow.providers.redis.hooks.redis.RedisHook.get_connection") + @mock.patch("airflow.providers.redis.hooks.redis.DriverInfo", None) + @mock.patch("airflow.providers.redis.hooks.redis._SUPPORTS_LIB_NAME", False) + def test_client_identification_unsupported(self, mock_get_connection, mock_redis): + """When neither DriverInfo nor lib_name is supported, no identification kwarg is passed.""" + mock_get_connection.return_value = Connection(host="h", port=1, login="u", password="p") + RedisHook().get_conn() + + call_kwargs = mock_redis.call_args[1] + assert "driver_info" not in call_kwargs + assert "lib_name" not in call_kwargs @pytest.mark.db_test def test_get_conn_password_stays_none(self): From 29af132d98bda6be2434fd486f51690956c436d4 Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Thu, 23 Apr 2026 15:27:29 +0300 Subject: [PATCH 7/8] Use provider package name in Redis client identification --- .../redis/src/airflow/providers/redis/hooks/redis.py | 8 +++++--- providers/redis/tests/unit/redis/hooks/test_redis.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/providers/redis/src/airflow/providers/redis/hooks/redis.py b/providers/redis/src/airflow/providers/redis/hooks/redis.py index 0173d0e28a8b4..0f858cce22fe5 100644 --- a/providers/redis/src/airflow/providers/redis/hooks/redis.py +++ b/providers/redis/src/airflow/providers/redis/hooks/redis.py @@ -100,15 +100,17 @@ def get_conn(self): ) # Add driver info for client identification if supported - # This allows Redis server to identify Airflow as the upstream driver. + # This allows Redis server to identify the Redis provider as the upstream driver. # See: https://redis.io/docs/latest/commands/client-setinfo/ driver_info_options: dict[str, Any] = {} if DriverInfo is not None: - driver_info = DriverInfo().add_upstream_driver("apache-airflow", provider_version) + driver_info = DriverInfo().add_upstream_driver( + "apache-airflow-providers-redis", provider_version + ) driver_info_options = {"driver_info": driver_info} elif _SUPPORTS_LIB_NAME: driver_info_options = { - "lib_name": f"redis-py(apache-airflow_v{provider_version})", + "lib_name": f"redis-py(apache-airflow-providers-redis_v{provider_version})", } self.redis = Redis( diff --git a/providers/redis/tests/unit/redis/hooks/test_redis.py b/providers/redis/tests/unit/redis/hooks/test_redis.py index 19b74aea1f40a..1c779c1911ba9 100644 --- a/providers/redis/tests/unit/redis/hooks/test_redis.py +++ b/providers/redis/tests/unit/redis/hooks/test_redis.py @@ -93,7 +93,7 @@ def test_client_identification_with_driver_info(self, mock_get_connection, mock_ mock_driver_info_cls.assert_called_once_with() fake_driver_info.add_upstream_driver.assert_called_once() args, _ = fake_driver_info.add_upstream_driver.call_args - assert args[0] == "apache-airflow" + assert args[0] == "apache-airflow-providers-redis" call_kwargs = mock_redis.call_args[1] assert call_kwargs["driver_info"] is fake_driver_info assert "lib_name" not in call_kwargs @@ -109,7 +109,7 @@ def test_client_identification_with_lib_name(self, mock_get_connection, mock_red call_kwargs = mock_redis.call_args[1] assert "driver_info" not in call_kwargs - assert "apache-airflow" in call_kwargs["lib_name"] + assert "apache-airflow-providers-redis" in call_kwargs["lib_name"] @mock.patch("airflow.providers.redis.hooks.redis.Redis") @mock.patch("airflow.providers.redis.hooks.redis.RedisHook.get_connection") From cf8263638e0985d7188e7be26423a611417e6b3f Mon Sep 17 00:00:00 2001 From: Vasil Chomakov Date: Mon, 27 Apr 2026 13:32:56 +0300 Subject: [PATCH 8/8] Use getattr for optional DriverInfo import --- providers/redis/src/airflow/providers/redis/hooks/redis.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/providers/redis/src/airflow/providers/redis/hooks/redis.py b/providers/redis/src/airflow/providers/redis/hooks/redis.py index 0f858cce22fe5..7cae107e6ab5f 100644 --- a/providers/redis/src/airflow/providers/redis/hooks/redis.py +++ b/providers/redis/src/airflow/providers/redis/hooks/redis.py @@ -22,15 +22,13 @@ import inspect from typing import Any +import redis from redis import Redis from airflow.providers.common.compat.sdk import BaseHook from airflow.providers.redis import __version__ as provider_version -try: - from redis import DriverInfo -except ImportError: - DriverInfo = None +DriverInfo = getattr(redis, "DriverInfo", None) DEFAULT_SSL_CERT_REQS = "required" ALLOWED_SSL_CERT_REQS = [DEFAULT_SSL_CERT_REQS, "optional", "none"]