Skip to content

Commit 2fa1e6e

Browse files
committed
feat: Add client-level proxy configuration support
1 parent b594309 commit 2fa1e6e

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

databricks/sdk/_base_client.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import logging
3+
import os
34
import urllib.parse
45
from abc import ABC, abstractmethod
56
from datetime import timedelta
@@ -53,6 +54,10 @@ def __init__(
5354
debug_headers: Optional[bool] = False,
5455
clock: Optional[Clock] = None,
5556
streaming_buffer_size: int = 1024 * 1024,
57+
proxy_url: Optional[str] = None,
58+
proxy_username: Optional[str] = None,
59+
proxy_password: Optional[str] = None,
60+
proxy_auth_type: Optional[str] = None,
5661
): # 1MB
5762
"""
5863
:param debug_truncate_bytes:
@@ -84,6 +89,45 @@ def __init__(
8489
self._session.auth = self._authenticate
8590
self._streaming_buffer_size = streaming_buffer_size
8691

92+
self._proxy_url = proxy_url or os.environ.get("DATABRICKS_PROXY_URL")
93+
self._proxy_username = proxy_username or os.environ.get("DATABRICKS_PROXY_USERNAME")
94+
self._proxy_password = proxy_password or os.environ.get("DATABRICKS_PROXY_PASSWORD")
95+
self._proxy_auth_type = proxy_auth_type or os.environ.get("DATABRICKS_PROXY_AUTH_TYPE")
96+
97+
if self._proxy_url:
98+
p_url = self._proxy_url
99+
if "://" not in p_url:
100+
p_url = "http://" + p_url
101+
102+
if self._proxy_username:
103+
parsed = urllib.parse.urlparse(p_url)
104+
if "@" in parsed.netloc:
105+
_, host_port = parsed.netloc.rsplit("@", 1)
106+
else:
107+
host_port = parsed.netloc
108+
109+
user = urllib.parse.quote(self._proxy_username)
110+
if self._proxy_password:
111+
password = urllib.parse.quote(self._proxy_password)
112+
user_part = f"{user}:{password}@"
113+
else:
114+
user_part = f"{user}@"
115+
116+
netloc = f"{user_part}{host_port}"
117+
p_url = urllib.parse.urlunparse((
118+
parsed.scheme,
119+
netloc,
120+
parsed.path,
121+
parsed.params,
122+
parsed.query,
123+
parsed.fragment
124+
))
125+
126+
self._session.proxies = {
127+
"http": p_url,
128+
"https": p_url,
129+
}
130+
87131
# We don't use `max_retries` from HTTPAdapter to align with a more production-ready
88132
# retry strategy established in the Databricks SDK for Go. See _is_retryable and
89133
# @retried for more details.

databricks/sdk/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ class Config:
140140
serverless_compute_id: str = ConfigAttribute(env="DATABRICKS_SERVERLESS_COMPUTE_ID")
141141
skip_verify: bool = ConfigAttribute()
142142
http_timeout_seconds: float = ConfigAttribute()
143+
proxy_url: str = ConfigAttribute(env="DATABRICKS_PROXY_URL")
144+
proxy_username: str = ConfigAttribute(env="DATABRICKS_PROXY_USERNAME")
145+
proxy_password: str = ConfigAttribute(env="DATABRICKS_PROXY_PASSWORD", sensitive=True)
146+
proxy_auth_type: str = ConfigAttribute(env="DATABRICKS_PROXY_AUTH_TYPE")
143147
debug_truncate_bytes: int = ConfigAttribute(env="DATABRICKS_DEBUG_TRUNCATE_BYTES")
144148
debug_headers: bool = ConfigAttribute(env="DATABRICKS_DEBUG_HEADERS")
145149
rate_limit: int = ConfigAttribute(env="DATABRICKS_RATE_LIMIT")

databricks/sdk/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ def __init__(self, cfg: Config):
3737
http_timeout_seconds=cfg.http_timeout_seconds,
3838
extra_error_customizers=[_AddDebugErrorCustomizer(cfg)],
3939
clock=cfg.clock,
40+
proxy_url=cfg.proxy_url,
41+
proxy_username=cfg.proxy_username,
42+
proxy_password=cfg.proxy_password,
43+
proxy_auth_type=cfg.proxy_auth_type,
4044
)
4145

4246
@property

tests/test_proxy.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import os
2+
import urllib.parse
3+
import pytest
4+
from databricks.sdk.config import Config
5+
from databricks.sdk._base_client import _BaseClient
6+
from databricks.sdk.core import ApiClient
7+
8+
def test_config_proxy_attributes():
9+
cfg = Config(
10+
host="https://test.databricks.com",
11+
token="test-token",
12+
proxy_url="http://proxy.example.com:8080",
13+
proxy_username="user123",
14+
proxy_password="secretpassword",
15+
proxy_auth_type="BASIC",
16+
)
17+
assert cfg.proxy_url == "http://proxy.example.com:8080"
18+
assert cfg.proxy_username == "user123"
19+
assert cfg.proxy_password == "secretpassword"
20+
assert cfg.proxy_auth_type == "BASIC"
21+
assert "proxy_password=***" in cfg.debug_string()
22+
23+
def test_config_proxy_from_env(monkeypatch):
24+
monkeypatch.setenv("DATABRICKS_PROXY_URL", "http://proxy.env.com:8080")
25+
monkeypatch.setenv("DATABRICKS_PROXY_USERNAME", "envuser")
26+
monkeypatch.setenv("DATABRICKS_PROXY_PASSWORD", "envpass")
27+
monkeypatch.setenv("DATABRICKS_PROXY_AUTH_TYPE", "BASIC")
28+
29+
cfg = Config(host="https://test.databricks.com", token="test-token")
30+
assert cfg.proxy_url == "http://proxy.env.com:8080"
31+
assert cfg.proxy_username == "envuser"
32+
assert cfg.proxy_password == "envpass"
33+
assert cfg.proxy_auth_type == "BASIC"
34+
35+
def test_base_client_proxy_url_construction():
36+
# Test proxy with username and password containing special characters that need encoding
37+
client = _BaseClient(
38+
proxy_url="http://proxy.example.com:8080",
39+
proxy_username="user@domain",
40+
proxy_password="pass:word",
41+
)
42+
# The constructed proxy URL should have encoded credentials
43+
expected_url = "http://user%40domain:pass%3Aword@proxy.example.com:8080"
44+
assert client._session.proxies == {
45+
"http": expected_url,
46+
"https": expected_url,
47+
}
48+
49+
def test_base_client_proxy_no_credentials():
50+
client = _BaseClient(proxy_url="proxy.example.com:8080")
51+
# Scheme should be prepended
52+
expected_url = "http://proxy.example.com:8080"
53+
assert client._session.proxies == {
54+
"http": expected_url,
55+
"https": expected_url,
56+
}
57+
58+
def test_base_client_proxy_from_env(monkeypatch):
59+
monkeypatch.setenv("DATABRICKS_PROXY_URL", "http://envproxy.com:8080")
60+
monkeypatch.setenv("DATABRICKS_PROXY_USERNAME", "envuser")
61+
monkeypatch.setenv("DATABRICKS_PROXY_PASSWORD", "envpass")
62+
63+
client = _BaseClient()
64+
expected_url = "http://envuser:envpass@envproxy.com:8080"
65+
assert client._session.proxies == {
66+
"http": expected_url,
67+
"https": expected_url,
68+
}

0 commit comments

Comments
 (0)