From 12c8d84af72fd3b9be97e09607e8e56cc8ed2ffc Mon Sep 17 00:00:00 2001 From: Pavel Kolesnikov Date: Thu, 7 May 2026 11:12:58 -0700 Subject: [PATCH] Add support for HTTP_PROXY/HTTPS_PROXY variables --- .../gooddata-sdk/src/gooddata_sdk/client.py | 18 ++++++ packages/gooddata-sdk/src/gooddata_sdk/sdk.py | 6 ++ packages/gooddata-sdk/tests/test_client.py | 64 ++++++++++++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/client.py b/packages/gooddata-sdk/src/gooddata_sdk/client.py index 80ff83925..355d60408 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/client.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/client.py @@ -26,6 +26,7 @@ def __init__( extra_user_agent: str | None = None, executions_cancellable: bool = False, ssl_ca_cert: str | None = None, + proxy: str | None = None, ) -> None: """Take url, token for connecting to GoodData.CN. @@ -38,6 +39,10 @@ def __init__( `executions_cancellable` is a flag that sets all executions computed through this client as cancellable. In case a request for a result is interrupted, the GD server will try to free resources like killing sql queries related to the given execution. + + `proxy` is optional URL of an HTTP(S) proxy (e.g. ``http://proxy:8080``). + When not set, the standard ``HTTPS_PROXY`` / ``https_proxy`` / ``HTTP_PROXY`` / + ``http_proxy`` environment variables are checked automatically. """ self._hostname = host self._token = token @@ -53,7 +58,20 @@ def __init__( f"ssl_ca_cert file path specified but the file does not exist. Path: {ssl_ca_cert_path}." ) + if proxy is None: + import os + + proxy = ( + os.environ.get("HTTPS_PROXY") + or os.environ.get("https_proxy") + or os.environ.get("HTTP_PROXY") + or os.environ.get("http_proxy") + or None + ) + self._api_config = api_client.Configuration(host=host, ssl_ca_cert=ssl_ca_cert) + if proxy: + self._api_config.proxy = proxy self._api_client = api_client.ApiClient( configuration=self._api_config, header_name="Authorization", diff --git a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py index 003840083..5fdd57810 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py @@ -49,6 +49,7 @@ def create( *, ssl_ca_cert: str | None = None, executions_cancellable: bool = False, + proxy: str | None = None, **custom_headers_: str | None, ) -> GoodDataSdk: """ @@ -56,6 +57,10 @@ def create( Custom headers are filtered. Headers with None value are removed. It simplifies usage because headers can be created directly from optional values. + ``proxy`` is an optional HTTP(S) proxy URL (e.g. ``http://proxy:8080``). + When not set, the standard ``HTTPS_PROXY`` / ``https_proxy`` / ``HTTP_PROXY`` + / ``http_proxy`` environment variables are checked automatically. + This is preferred way of creating GoodDataSdk, when no tweaks are needed. """ filtered_headers = {key: value for key, value in custom_headers_.items() if value is not None} @@ -66,6 +71,7 @@ def create( extra_user_agent=extra_user_agent_, executions_cancellable=executions_cancellable, ssl_ca_cert=ssl_ca_cert, + proxy=proxy, ) return cls(client) diff --git a/packages/gooddata-sdk/tests/test_client.py b/packages/gooddata-sdk/tests/test_client.py index 9327b87f3..dac94a308 100644 --- a/packages/gooddata-sdk/tests/test_client.py +++ b/packages/gooddata-sdk/tests/test_client.py @@ -1,5 +1,8 @@ # (C) 2021 GoodData Corporation -from gooddata_sdk import GoodDataApiClient +import os +from unittest import mock + +from gooddata_sdk import GoodDataApiClient, GoodDataSdk def test_http_headers_precedence(): @@ -12,3 +15,62 @@ def test_http_headers_precedence(): agent = c._api_client.default_headers["User-Agent"] assert agent.startswith("gooddata") assert agent.endswith("yes") + + +class TestProxy: + """Proxy configuration for the underlying urllib3 pool manager.""" + + @mock.patch.dict(os.environ, {}, clear=True) + def test_no_proxy_when_env_empty(self): + c = GoodDataApiClient("https://example.com", "token") + assert c._api_config.proxy is None + + def test_explicit_proxy(self): + c = GoodDataApiClient("https://example.com", "token", proxy="http://myproxy:8080") + assert c._api_config.proxy == "http://myproxy:8080" + + @mock.patch.dict(os.environ, {"HTTPS_PROXY": "http://envproxy:3128"}, clear=True) + def test_proxy_from_https_proxy_env(self): + c = GoodDataApiClient("https://example.com", "token") + assert c._api_config.proxy == "http://envproxy:3128" + + @mock.patch.dict(os.environ, {"https_proxy": "http://lower:3128"}, clear=True) + def test_proxy_from_lowercase_https_proxy_env(self): + c = GoodDataApiClient("https://example.com", "token") + assert c._api_config.proxy == "http://lower:3128" + + @mock.patch.dict(os.environ, {"HTTP_PROXY": "http://httpproxy:3128"}, clear=True) + def test_proxy_from_http_proxy_env(self): + c = GoodDataApiClient("https://example.com", "token") + assert c._api_config.proxy == "http://httpproxy:3128" + + @mock.patch.dict( + os.environ, + { + "HTTPS_PROXY": "http://preferred:3128", + "HTTP_PROXY": "http://fallback:3128", + }, + clear=True, + ) + def test_https_proxy_takes_precedence_over_http_proxy(self): + c = GoodDataApiClient("https://example.com", "token") + assert c._api_config.proxy == "http://preferred:3128" + + @mock.patch.dict(os.environ, {"HTTPS_PROXY": "http://envproxy:3128"}, clear=True) + def test_explicit_proxy_overrides_env(self): + c = GoodDataApiClient("https://example.com", "token", proxy="http://explicit:8080") + assert c._api_config.proxy == "http://explicit:8080" + + def test_sdk_create_with_explicit_proxy(self): + sdk = GoodDataSdk.create("https://example.com", "token", proxy="http://myproxy:8080") + assert sdk.client._api_config.proxy == "http://myproxy:8080" + + @mock.patch.dict(os.environ, {"HTTPS_PROXY": "http://envproxy:3128"}, clear=True) + def test_sdk_create_picks_up_env_proxy(self): + sdk = GoodDataSdk.create("https://example.com", "token") + assert sdk.client._api_config.proxy == "http://envproxy:3128" + + @mock.patch.dict(os.environ, {}, clear=True) + def test_sdk_create_no_proxy_when_env_empty(self): + sdk = GoodDataSdk.create("https://example.com", "token") + assert sdk.client._api_config.proxy is None