Skip to content

Commit 78914fa

Browse files
Enable unified host support without flag (#1358)
## 🥞 Stacked PR Use this [link](https://github.com/databricks/databricks-sdk-py/pull/1358/files) to review incremental changes. - [**stack/unified-host-ga**](#1358) [[Files changed](https://github.com/databricks/databricks-sdk-py/pull/1358/files)] --------- <!-- This template provides a recommended structure for PR descriptions. Adapt it freely — the goal is clarity, not rigid compliance. The three-section format (Summary / Why / What Changed) helps reviewers understand the change quickly and makes the PR easier to revisit later. --> ## Summary Remove the `experimental_is_unified_host` flag, which was not used anymore. Update README to document the new support, the updates to default authentication flow and auto detection. This PR includes no behavioral changes. Co-authored-by: Isaac ## How is this tested? N/A
1 parent 4bb4280 commit 78914fa

6 files changed

Lines changed: 67 additions & 30 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Release v0.103.0
44

55
### New Features and Improvements
6+
* Add support for unified hosts. A single configuration profile can now be used for both account-level and workspace-level operations when the host supports it and both `account_id` and `workspace_id` are available. The `experimental_is_unified_host` flag has been removed; unified host detection is now automatic.
67
* Accept `DATABRICKS_OIDC_TOKEN_FILEPATH` environment variable for consistency with other Databricks SDKs (Go, CLI, Terraform). The previous `DATABRICKS_OIDC_TOKEN_FILE` is still supported as an alias.
78

89
### Security
@@ -27,4 +28,4 @@
2728
* Add `cascade` field for `databricks.sdk.service.pipelines.DeletePipelineRequest`.
2829
* Add `default_branch` field for `databricks.sdk.service.postgres.ProjectSpec`.
2930
* Add `default_branch` field for `databricks.sdk.service.postgres.ProjectStatus`.
30-
* Add `ingress` and `ingress_dry_run` fields for `databricks.sdk.service.settings.AccountNetworkPolicy`.
31+
* Add `ingress` and `ingress_dry_run` fields for `databricks.sdk.service.settings.AccountNetworkPolicy`.

README.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ The conventional name for the variable that holds the workspace-level client of
9292
### In this section
9393

9494
- [Default authentication flow](#default-authentication-flow)
95+
- [Unified host support](#unified-host-support)
9596
- [Databricks native authentication](#databricks-native-authentication)
9697
- [Azure native authentication](#azure-native-authentication)
9798
- [Overriding .databrickscfg](#overriding-databrickscfg)
@@ -107,10 +108,19 @@ in the following order, until it succeeds:
107108

108109
1. [Databricks native authentication](#databricks-native-authentication)
109110
2. [Azure native authentication](#azure-native-authentication)
111+
3. [GCP native authentication](#google-cloud-platform-native-authentication)
110112
4. If the SDK is unsuccessful at this point, it returns an authentication error and stops running.
111113

112-
You can instruct the Databricks SDK for Python to use a specific authentication method by setting the `auth_type` argument
113-
as described in the following sections.
114+
Each authentication method requires specific configuration attributes (e.g., `token` for PAT auth, `azure_client_id` for Azure service principal auth). The SDK automatically detects the cloud provider and skips authentication methods whose required configuration attributes are not present. This means that Azure-specific methods like `azure-cli` are automatically skipped when connecting to an AWS or GCP workspace, and vice versa for GCP-specific methods.
115+
116+
To force a specific authentication method instead of relying on auto-detection, set the `auth_type` argument:
117+
118+
```python
119+
from databricks.sdk import WorkspaceClient
120+
# Force Azure CLI authentication — skip all other methods
121+
w = WorkspaceClient(host='https://mycompany.databricks.com', auth_type='azure-cli', cloud='AZURE')
122+
```
123+
This is useful when your environment has credentials for multiple authentication methods and you want to ensure a specific one is used or when auto detection is not accurate.
114124

115125
For each authentication method, the SDK searches for compatible authentication credentials in the following locations,
116126
in the following order. Once the SDK finds a compatible set of credentials that it can use, it stops searching:
@@ -125,6 +135,38 @@ in the following order. Once the SDK finds a compatible set of credentials that
125135

126136
Depending on the Databricks authentication method, the SDK uses the following information. Presented are the `WorkspaceClient` and `AccountClient` arguments (which have corresponding `.databrickscfg` file fields), their descriptions, and any corresponding environment variables.
127137

138+
### Unified host support
139+
140+
Certain Databricks host types support both account-level and workspace-level API operations from a single endpoint. When using such a unified host, a single configuration profile can be used to create both `WorkspaceClient` and `AccountClient` instances without changing the `host`.
141+
142+
For this to work, the following conditions must be met:
143+
144+
1. The host must support unified operations.
145+
2. Both `account_id` and `workspace_id` must be available — either set explicitly in the configuration or auto-discovered.
146+
147+
When both values are present, the SDK uses `workspace_id` to route workspace-level requests and `account_id` to route account-level requests, all through the same host.
148+
149+
```ini
150+
# .databrickscfg
151+
[unified]
152+
host = https://mycompany.databricks.com
153+
account_id = 00000000-0000-0000-0000-000000000000
154+
workspace_id = 1234567890
155+
```
156+
157+
```python
158+
from databricks.sdk import WorkspaceClient, AccountClient
159+
160+
# Both clients share the same host and profile
161+
w = WorkspaceClient(profile='unified')
162+
a = AccountClient(profile='unified')
163+
164+
# A WorkspaceClient for a different workspace under the same host and account
165+
w = WorkspaceClient(profile='unified', workspace_id='2345678901')
166+
```
167+
168+
If the host supports it, `account_id` and `workspace_id` may be auto-discovered, reducing the required explicit configuration.
169+
128170
### Databricks native authentication
129171

130172
By default, the Databricks SDK for Python initially tries [Databricks token authentication](https://docs.databricks.com/dev-tools/api/latest/authentication.html) (`auth_type='pat'` argument). If the SDK is unsuccessful, it then tries Workload Identity Federation (WIF). See [Supported WIF](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-federation-provider) for the supported JWT token providers.
@@ -133,10 +175,15 @@ By default, the Databricks SDK for Python initially tries [Databricks token auth
133175
- For Databricks OIDC authentication, you must provide the `host`, `client_id` and `token_audience` _(optional)_ either directly, through the corresponding environment variables, or in your `.databrickscfg` configuration file.
134176
- For Azure DevOps OIDC authentication, the `token_audience` is irrelevant as the audience is always set to `api://AzureADTokenExchange`. Also, the `System.AccessToken` pipeline variable required for OIDC request must be exposed as the `SYSTEM_ACCESSTOKEN` environment variable, following [Pipeline variables](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken)
135177

178+
During initialization, the SDK automatically resolves missing configuration fields (`account_id`, `workspace_id`, `cloud`, and `discovery_url`). Any explicitly provided values take precedence and are never overwritten. If the auto discovery fails, the SDK falls back to the explicit configuration. It is recommended to always set explicit configuration.
179+
136180
| Argument | Description | Environment variable |
137181
|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
138-
| `host` | _(String)_ The Databricks host URL for either the Databricks workspace endpoint or the Databricks accounts endpoint. | `DATABRICKS_HOST` |
139-
| `account_id` | _(String)_ The Databricks account ID for the Databricks accounts endpoint. Only has effect when `Host` is either `https://accounts.cloud.databricks.com/` _(AWS)_, `https://accounts.azuredatabricks.net/` _(Azure)_, or `https://accounts.gcp.databricks.com/` _(GCP)_. | `DATABRICKS_ACCOUNT_ID` |
182+
| `host` | _(String)_ The Databricks host URL for either the Databricks workspace endpoint or the Databricks accounts endpoint. | `DATABRICKS_HOST` |
183+
| `account_id` | _(String)_ The Databricks account ID for the Databricks accounts endpoint. Auto-discovered if not provided. | `DATABRICKS_ACCOUNT_ID` |
184+
| `workspace_id` | _(String)_ The Databricks workspace ID for the Databricks workspace endpoint. Auto-discovered if not provided. | `DATABRICKS_WORKSPACE_ID` |
185+
| `cloud` | _(String)_ The cloud provider for the Databricks workspace (`AWS`, `AZURE`, or `GCP`). Auto-discovered if not provided. When set, `is_aws`, `is_azure`, and `is_gcp` use this value directly instead of inferring from hostname. | `DATABRICKS_CLOUD` |
186+
| `discovery_url` | _(String)_ The OpenID Connect discovery URL. Auto-discovered if not provided. When set, OIDC endpoints are fetched directly from this URL instead of using the default host-based well-known endpoint logic. | `DATABRICKS_DISCOVERY_URL` |
140187
| `token` | _(String)_ The Databricks personal access token (PAT) _(AWS, Azure, and GCP)_ or Azure Active Directory (Azure AD) token _(Azure)_. | `DATABRICKS_TOKEN` |
141188
| `client_id` | _(String)_ The Databricks Service Principal Application ID. | `DATABRICKS_CLIENT_ID` |
142189
| `token_audience` | _(String)_ When using Workload Identity Federation, the audience to specify when fetching an ID token from the ID token supplier. | `TOKEN_AUDIENCE` |
@@ -528,7 +575,7 @@ useragent.with_partner("partner-xyz")
528575

529576
`with_product()` can be used to define the name and version of the product that is built with the Databricks SDK for Python. The product name has the same restrictions as the partner name above, and the product version must be a valid [SemVer](https://semver.org/). Subsequent calls to `with_product()` replace the original product with the new user-specified one.
530577

531-
```go
578+
```python
532579
from databricks.sdk import useragent
533580
useragent.with_product("databricks-example-product", "1.2.0")
534581
```

databricks/sdk/config.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,11 @@ class Config:
9191
account_id: str = ConfigAttribute(env="DATABRICKS_ACCOUNT_ID")
9292
workspace_id: str = ConfigAttribute(env="DATABRICKS_WORKSPACE_ID")
9393

94-
# Experimental flag to indicate if the host is a unified host (supports both workspace and account APIs)
95-
experimental_is_unified_host: bool = ConfigAttribute(env="DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST")
96-
97-
# [Experimental] Cloud provider. When set, is_aws/is_azure/is_gcp use this value directly
94+
# Cloud provider. When set, is_aws/is_azure/is_gcp use this value directly
9895
# instead of inferring from hostname. Populated automatically from /.well-known/databricks-config.
9996
cloud: Cloud = ConfigAttribute(env="DATABRICKS_CLOUD", transform=_parse_cloud)
10097

101-
# [Experimental] OpenID Connect discovery URL. When set, OIDC endpoints are fetched directly
98+
# OpenID Connect discovery URL. When set, OIDC endpoints are fetched directly
10299
# from this URL instead of the default host-type-based well-known endpoint logic.
103100
discovery_url: str = ConfigAttribute(env="DATABRICKS_DISCOVERY_URL")
104101

tests/integration/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def unified_config(env_or_skip) -> Config:
9595
config = Config()
9696
config.host = env_or_skip("UNIFIED_HOST")
9797
config.workspace_id = env_or_skip("TEST_WORKSPACE_ID")
98-
config.experimental_is_unified_host = True
98+
9999
config._fix_host_if_needed()
100100
return config
101101

tests/test_config.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,6 @@ def test_client_type_workspace():
317317
host="https://unified.databricks.com",
318318
workspace_id="test-workspace",
319319
account_id="test-account",
320-
experimental_is_unified_host=True,
321320
token="test-token",
322321
)
323322
assert config.client_type == ClientType.WORKSPACE
@@ -352,7 +351,6 @@ def test_is_account_client_does_not_raise_on_unified_host():
352351
"""Test that is_account_client raises ValueError when used with unified hosts."""
353352
config = Config(
354353
host="https://unified.databricks.com",
355-
experimental_is_unified_host=True,
356354
workspace_id="test-workspace",
357355
token="test-token",
358356
)
@@ -464,7 +462,6 @@ def test_workspace_org_id_header_on_unified_host(requests_mock):
464462
host="https://unified.databricks.com",
465463
account_id="test-account",
466464
workspace_id="test-workspace-123",
467-
experimental_is_unified_host=True,
468465
token="test-token",
469466
)
470467

@@ -487,7 +484,6 @@ def test_not_workspace_org_id_header_on_unified_host_on_account_endpoint(request
487484
host="https://unified.databricks.com",
488485
account_id="test-account",
489486
workspace_id="test-workspace-123",
490-
experimental_is_unified_host=True,
491487
token="test-token",
492488
)
493489

@@ -727,7 +723,7 @@ def test_databricks_oidc_endpoints_uses_discovery_url(requests_mock):
727723
"account_id": _DUMMY_ACCOUNT_ID,
728724
"workspace_id": _DUMMY_WORKSPACE_ID,
729725
},
730-
{"experimental_is_unified_host": True},
726+
{},
731727
{
732728
"account_id": _DUMMY_ACCOUNT_ID,
733729
"workspace_id": _DUMMY_WORKSPACE_ID,
@@ -738,7 +734,7 @@ def test_databricks_oidc_endpoints_uses_discovery_url(requests_mock):
738734
pytest.param(
739735
_DUMMY_ACC_HOST,
740736
{"oidc_endpoint": f"{_DUMMY_ACC_HOST}/oidc/accounts/{{account_id}}"},
741-
{"account_id": _DUMMY_ACCOUNT_ID, "experimental_is_unified_host": True},
737+
{"account_id": _DUMMY_ACCOUNT_ID},
742738
{
743739
"discovery_url": f"{_DUMMY_ACC_HOST}/oidc/accounts/{_DUMMY_ACCOUNT_ID}/.well-known/oauth-authorization-server"
744740
},
@@ -750,7 +746,6 @@ def test_databricks_oidc_endpoints_uses_discovery_url(requests_mock):
750746
{
751747
"account_id": _DUMMY_ACCOUNT_ID,
752748
"workspace_id": _DUMMY_WORKSPACE_ID,
753-
"experimental_is_unified_host": True,
754749
},
755750
{"account_id": _DUMMY_ACCOUNT_ID, "workspace_id": _DUMMY_WORKSPACE_ID},
756751
id="unified-does-not-overwrite-existing-fields",
@@ -771,7 +766,7 @@ def test_resolve_host_metadata_missing_account_id(mocker):
771766
return_value=oauth.HostMetadata.from_dict({"oidc_endpoint": f"{_DUMMY_ACC_HOST}/oidc/accounts/{{account_id}}"}),
772767
)
773768
with pytest.raises(ValueError, match="account_id is required to resolve discovery_url"):
774-
Config(host=_DUMMY_ACC_HOST, token="t", experimental_is_unified_host=True)
769+
Config(host=_DUMMY_ACC_HOST, token="t")
775770

776771

777772
def test_resolve_host_metadata_no_oidc_endpoint(mocker):
@@ -780,7 +775,7 @@ def test_resolve_host_metadata_no_oidc_endpoint(mocker):
780775
"databricks.sdk.config.get_host_metadata",
781776
return_value=oauth.HostMetadata.from_dict({"account_id": _DUMMY_ACCOUNT_ID}),
782777
)
783-
config = Config(host=_DUMMY_WS_HOST, token="t", experimental_is_unified_host=True)
778+
config = Config(host=_DUMMY_WS_HOST, token="t")
784779
assert config.account_id == _DUMMY_ACCOUNT_ID
785780
assert config.discovery_url is None
786781

@@ -791,7 +786,7 @@ def test_resolve_host_metadata_http_error(mocker):
791786
"databricks.sdk.config.get_host_metadata",
792787
side_effect=ValueError(f"Failed to fetch host metadata from {_DUMMY_WS_HOST}/.well-known/databricks-config"),
793788
)
794-
config = Config(host=_DUMMY_WS_HOST, token="t", experimental_is_unified_host=True)
789+
config = Config(host=_DUMMY_WS_HOST, token="t")
795790
assert config.account_id is None
796791
assert config.discovery_url is None
797792

@@ -887,7 +882,7 @@ def test_resolve_host_metadata_populates_cloud(mocker):
887882
}
888883
),
889884
)
890-
config = Config(host=_DUMMY_WS_HOST, token="t", experimental_is_unified_host=True)
885+
config = Config(host=_DUMMY_WS_HOST, token="t")
891886
assert config.cloud == Cloud.AWS
892887

893888

@@ -905,7 +900,6 @@ def test_resolve_host_metadata_cloud_not_overwritten(mocker):
905900
config = Config(
906901
host=_DUMMY_WS_HOST,
907902
token="t",
908-
experimental_is_unified_host=True,
909903
cloud="AWS",
910904
)
911905
assert config.cloud == Cloud.AWS
@@ -917,7 +911,7 @@ def test_resolve_host_metadata_cloud_missing_in_response(mocker):
917911
"databricks.sdk.config.get_host_metadata",
918912
return_value=oauth.HostMetadata.from_dict({"oidc_endpoint": f"{_DUMMY_WS_HOST}/oidc"}),
919913
)
920-
config = Config(host=_DUMMY_WS_HOST, token="t", experimental_is_unified_host=True)
914+
config = Config(host=_DUMMY_WS_HOST, token="t")
921915
assert config.cloud is None
922916

923917

@@ -941,7 +935,6 @@ def test_resolve_host_metadata_sets_token_audience_for_account_host(mocker):
941935
host=_DUMMY_ACC_HOST,
942936
token="t",
943937
account_id=_DUMMY_ACCOUNT_ID,
944-
experimental_is_unified_host=True,
945938
)
946939
assert config.token_audience == _DUMMY_ACCOUNT_ID
947940

@@ -958,7 +951,7 @@ def test_resolve_host_metadata_no_token_audience_for_workspace_host(mocker):
958951
}
959952
),
960953
)
961-
config = Config(host=_DUMMY_WS_HOST, token="t", experimental_is_unified_host=True)
954+
config = Config(host=_DUMMY_WS_HOST, token="t")
962955
assert config.token_audience is None
963956

964957

@@ -977,7 +970,6 @@ def test_resolve_host_metadata_does_not_overwrite_token_audience(mocker):
977970
host=_DUMMY_ACC_HOST,
978971
token="t",
979972
account_id=_DUMMY_ACCOUNT_ID,
980-
experimental_is_unified_host=True,
981973
token_audience="custom-audience",
982974
)
983975
assert config.token_audience == "custom-audience"

tests/test_credentials_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def test_account_client_passes_account_id(self, mocker):
321321
mock_cfg = Mock()
322322
mock_cfg.profile = None
323323
mock_cfg.host = "https://accounts.cloud.databricks.com"
324-
mock_cfg.experimental_is_unified_host = False
324+
325325
mock_cfg.account_id = "test-account-id"
326326
mock_cfg.client_type = ClientType.ACCOUNT
327327
mock_cfg.databricks_cli_path = "/path/to/databricks"
@@ -348,7 +348,7 @@ def test_profile_uses_profile_flag_with_host_fallback(self, mocker):
348348
mock_cfg = Mock()
349349
mock_cfg.profile = "my-profile"
350350
mock_cfg.host = "https://workspace.databricks.com"
351-
mock_cfg.experimental_is_unified_host = False
351+
352352
mock_cfg.databricks_cli_path = "/path/to/databricks"
353353
mock_cfg.disable_async_token_refresh = False
354354

0 commit comments

Comments
 (0)