Skip to content

Commit 32e3131

Browse files
committed
Merge remote-tracking branch 'origin/main' into hosttype-metadata
2 parents 142c169 + 1971711 commit 32e3131

18 files changed

Lines changed: 417 additions & 2330 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Setup JFrog PyPI proxy
2+
description: Authenticate to JFrog via OIDC and configure uv to use the db-pypi proxy
3+
4+
runs:
5+
using: composite
6+
steps:
7+
- name: Setup JFrog CLI with OIDC
8+
id: jfrog
9+
uses: jfrog/setup-jfrog-cli@279b1f629f43dd5bc658d8361ac4802a7ef8d2d5 # v4.9.1
10+
env:
11+
JF_URL: https://databricks.jfrog.io
12+
with:
13+
oidc-provider-name: github-actions
14+
15+
- name: Configure uv for JFrog
16+
shell: bash
17+
run: |
18+
# Route uv package resolution through the JFrog PyPI proxy. Hardened
19+
# runners block direct access to pypi.org, so all index queries go
20+
# through this authenticated mirror instead.
21+
echo "UV_INDEX_URL=https://${{ steps.jfrog.outputs.oidc-user }}:${{ steps.jfrog.outputs.oidc-token }}@databricks.jfrog.io/artifactory/api/pypi/db-pypi/simple" >> "$GITHUB_ENV"

.github/workflows/push.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ on:
66
merge_group:
77
types: [checks_requested]
88

9+
permissions:
10+
id-token: write
11+
contents: read
12+
913
jobs:
1014
tests-ubuntu:
1115
uses: ./.github/workflows/test.yml
1216
strategy:
1317
fail-fast: false
1418
matrix:
15-
pyVersion: [ '3.8', '3.9', '3.10', '3.11', '3.12' ]
19+
pyVersion: [ '3.10', '3.11', '3.12' ]
1620
with:
1721
os: ubuntu-latest
1822
pyVersion: ${{ matrix.pyVersion }}
@@ -22,7 +26,7 @@ jobs:
2226
strategy:
2327
fail-fast: false
2428
matrix:
25-
pyVersion: [ '3.9', '3.10', '3.11', '3.12' ]
29+
pyVersion: [ '3.10', '3.11', '3.12' ]
2630
with:
2731
os: windows-latest
2832
pyVersion: ${{ matrix.pyVersion }}
@@ -39,9 +43,20 @@ jobs:
3943
with:
4044
version: "0.6.5"
4145

46+
- name: Setup JFrog PyPI proxy
47+
uses: ./.github/actions/setup-jfrog-pypi
48+
49+
- name: Re-lock for JFrog
50+
run: uv lock
51+
4252
- name: Format all files
4353
run: make dev fmt
4454

55+
- name: Restore lockfiles and fix proxy URLs
56+
run: |
57+
git checkout -- '*.lock'
58+
make fix-lockfile
59+
4560
- name: Fail on differences
4661
run: git diff --exit-code
4762

@@ -57,5 +72,11 @@ jobs:
5772
with:
5873
version: "0.6.5"
5974

75+
- name: Setup JFrog PyPI proxy
76+
uses: ./.github/actions/setup-jfrog-pypi
77+
78+
- name: Re-lock for JFrog
79+
run: uv lock
80+
6081
- name: Check MANIFEST.in
6182
run: make dev && uv run check-manifest .

.github/workflows/test.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
runs-on: ${{ inputs.os }}
18+
19+
permissions:
20+
id-token: write
21+
contents: read
22+
1823
steps:
1924
- name: Checkout
2025
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
@@ -28,6 +33,15 @@ jobs:
2833
version: "0.6.5"
2934
python-version: ${{ inputs.pyVersion }}
3035

36+
- name: Setup JFrog PyPI proxy
37+
uses: ./.github/actions/setup-jfrog-pypi
38+
39+
# Re-lock so uv.lock matches the JFrog-configured UV_INDEX_URL.
40+
# Keeps the same versions; only registry URLs change. Ephemeral (not committed).
41+
- name: Re-lock for JFrog
42+
shell: bash
43+
run: uv lock
44+
3145
- name: Run tests
3246
run: make dev test
3347

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,13 @@ benchmark:
3030
coverage: test
3131
open htmlcov/index.html
3232

33+
fix-lockfile:
34+
@# Replace JFrog proxy URLs with public equivalents in lockfiles.
35+
@# Prevents proxy URLs from being accidentally committed.
36+
find . -type f -name '*.lock' -not -path './.github/*' \
37+
-exec sed -i 's|databricks\.jfrog\.io/artifactory/api/pypi/db-pypi/simple|pypi.org/simple|g' {} +
38+
find . -type f -name '*.lock' -not -path './.github/*' \
39+
-exec sed -i 's|databricks\.jfrog\.io/artifactory/api/pypi/db-pypi/packages|files.pythonhosted.org|g' {} +
40+
3341
clean:
3442
rm -fr dist *.egg-info .pytest_cache build htmlcov .venv

NEXT_CHANGELOG.md

Lines changed: 5 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
@@ -11,6 +12,9 @@
1112

1213
### Documentation
1314

15+
### Breaking Changes
16+
* Drop support for Python 3.8 and 3.9. The minimum supported Python version is now 3.10, in line with the oldest supported Databricks Runtime LTS (DBR 13.3).
17+
1418
### Internal Changes
1519
* Replace the async-disabling mechanism on token refresh failure with a 1-minute retry backoff. Previously, a single failed async refresh would disable proactive token renewal until the token expired. Now, the SDK waits a short cooldown period and retries, improving resilience to transient errors.
1620
* Extract `_resolve_profile` to simplify config file loading and improve `__settings__` error messages.
@@ -25,4 +29,4 @@
2529
* Add `cascade` field for `databricks.sdk.service.pipelines.DeletePipelineRequest`.
2630
* Add `default_branch` field for `databricks.sdk.service.postgres.ProjectSpec`.
2731
* Add `default_branch` field for `databricks.sdk.service.postgres.ProjectStatus`.
28-
* Add `ingress` and `ingress_dry_run` fields for `databricks.sdk.service.settings.AccountNetworkPolicy`.
32+
* 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: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import pathlib
77
import re
8-
import sys
98
import urllib.parse
109
from typing import Dict, Iterable, List, Optional
1110

@@ -92,14 +91,11 @@ class Config:
9291
account_id: str = ConfigAttribute(env="DATABRICKS_ACCOUNT_ID")
9392
workspace_id: str = ConfigAttribute(env="DATABRICKS_WORKSPACE_ID")
9493

95-
# Experimental flag to indicate if the host is a unified host (supports both workspace and account APIs)
96-
experimental_is_unified_host: bool = ConfigAttribute(env="DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST")
97-
98-
# [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
9995
# instead of inferring from hostname. Populated automatically from /.well-known/databricks-config.
10096
cloud: Cloud = ConfigAttribute(env="DATABRICKS_CLOUD", transform=_parse_cloud)
10197

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

@@ -615,15 +611,9 @@ def attributes(cls) -> Iterable[ConfigAttribute]:
615611
"""Returns a list of Databricks SDK configuration metadata"""
616612
if hasattr(cls, "_attributes"):
617613
return cls._attributes
618-
if sys.version_info[1] >= 10:
619-
import inspect
614+
import inspect
620615

621-
anno = inspect.get_annotations(cls)
622-
else:
623-
# Python 3.7 compatibility: getting type hints require extra hop, as described in
624-
# "Accessing The Annotations Dict Of An Object In Python 3.9 And Older" section of
625-
# https://docs.python.org/3/howto/annotations.html
626-
anno = cls.__dict__["__annotations__"]
616+
anno = inspect.get_annotations(cls)
627617
attrs = []
628618
for name, v in cls.__dict__.items():
629619
if type(v) != ConfigAttribute:

databricks/sdk/credentials_provider.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,21 @@ def runtime_native_auth(cfg: "Config") -> Optional[CredentialsProvider]:
160160
# runtime and no config variables are set.
161161
from databricks.sdk.runtime import (init_runtime_legacy_auth,
162162
init_runtime_native_auth,
163+
init_runtime_native_unified,
163164
init_runtime_repl_auth)
164165

166+
# Try the unified provider first (returns host, account_id, workspace_id, inner).
167+
if init_runtime_native_unified is not None:
168+
host, account_id, workspace_id, inner = init_runtime_native_unified()
169+
if host is not None:
170+
cfg.host = host
171+
cfg.account_id = account_id
172+
cfg.workspace_id = workspace_id
173+
logger.debug("[init_runtime_native_unified] runtime native auth configured")
174+
return inner
175+
logger.debug("[init_runtime_native_unified] no host detected")
176+
177+
# Fall back to legacy providers (return host, inner).
165178
for init in [
166179
init_runtime_native_auth,
167180
init_runtime_repl_auth,

databricks/sdk/mixins/files.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import platform
1010
import re
1111
import shutil
12-
import sys
1312
import xml.etree.ElementTree as ET
1413
from abc import ABC, abstractmethod
1514
from collections import deque
@@ -430,10 +429,7 @@ def delete(self, *, recursive=False):
430429
_LocalPath(leaf.path).delete()
431430
self._path.rmdir()
432431
else:
433-
kw = {}
434-
if sys.version_info[:2] > (3, 7):
435-
kw["missing_ok"] = True
436-
self._path.unlink(**kw)
432+
self._path.unlink(missing_ok=True)
437433

438434
def __repr__(self) -> str:
439435
return f"<_LocalPath {self._path}>"

databricks/sdk/runtime/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
]
2424

2525
# DO NOT MOVE THE TRY-CATCH BLOCK BELOW AND DO NOT ADD THINGS BEFORE IT! WILL MAKE TEST FAIL.
26+
try:
27+
from dbruntime.sdk_credential_provider import init_runtime_native_unified
28+
29+
logger.debug("runtime SDK credential provider (unified) available")
30+
except ImportError:
31+
init_runtime_native_unified = None
32+
2633
try:
2734
# We don't want to expose additional entity to user namespace, so
2835
# a workaround here for exposing required information in notebook environment
@@ -34,6 +41,7 @@
3441
init_runtime_native_auth = None
3542

3643
globals()["init_runtime_native_auth"] = init_runtime_native_auth
44+
globals()["init_runtime_native_unified"] = init_runtime_native_unified
3745

3846

3947
def init_runtime_repl_auth():

0 commit comments

Comments
 (0)