Skip to content

Commit b50398b

Browse files
config changes
1 parent 0bd7d5c commit b50398b

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

databricks/sdk/config.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import os
66
import pathlib
7+
import re
78
import sys
89
import urllib.parse
910
from typing import Dict, Iterable, List, Optional
@@ -117,6 +118,10 @@ class Config:
117118
scopes: str = ConfigAttribute()
118119
authorization_details: str = ConfigAttribute()
119120

121+
# Controls whether the offline_access scope is requested during U2M OAuth authentication.
122+
# offline_access is requested by default, causing a refresh token to be included in the OAuth token.
123+
disable_oauth_refresh_token: bool = ConfigAttribute(env="DATABRICKS_DISABLE_OAUTH_REFRESH_TOKEN")
124+
120125
files_ext_client_download_streaming_chunk_size: int = 2 * 1024 * 1024 # 2 MiB
121126

122127
# When downloading a file, the maximum number of attempts to retry downloading the whole file. Default is no limit.
@@ -245,6 +250,7 @@ def __init__(
245250
self._known_file_config_loader()
246251
self._fix_host_if_needed()
247252
self._validate()
253+
self._sort_scopes()
248254
self.init_auth()
249255
self._init_product(product, product_version)
250256
except ValueError as e:
@@ -579,6 +585,16 @@ def _validate(self):
579585
names = " and ".join(sorted(auths_used))
580586
raise ValueError(f"validate: more than one authorization method configured: {names}")
581587

588+
def _sort_scopes(self):
589+
"""Sort scopes in-place for better de-duplication in the refresh token cache.
590+
Delimiter is set to a single whitespace after sorting."""
591+
if self.scopes and isinstance(self.scopes, str):
592+
# Split on whitespaces and commas, sort, and rejoin
593+
parsed = [s for s in re.split(r"[\s,]+", self.scopes) if s]
594+
if parsed:
595+
parsed.sort()
596+
self.scopes = " ".join(parsed)
597+
582598
def init_auth(self):
583599
try:
584600
self._header_factory = self._credentials_strategy(self)
@@ -598,6 +614,33 @@ def _init_product(self, product, product_version):
598614
else:
599615
self._product_info = None
600616

617+
def get_scopes(self) -> List[str]:
618+
"""Get OAuth scopes with proper defaulting.
619+
620+
Returns ["all-apis"] if no scopes configured.
621+
This is the single source of truth for scope defaulting across all OAuth methods.
622+
623+
Parses string scopes by splitting on whitespaces and commas.
624+
625+
Returns:
626+
List of scope strings.
627+
"""
628+
if self.scopes and isinstance(self.scopes, str):
629+
parsed = [s for s in re.split(r"[\s,]+", self.scopes) if s]
630+
if not parsed: # Empty string case
631+
return ["all-apis"]
632+
return parsed
633+
return ["all-apis"]
634+
635+
def get_scopes_as_string(self) -> str:
636+
"""Get OAuth scopes as a space-separated string.
637+
638+
Returns "all-apis" if no scopes configured.
639+
"""
640+
if self.scopes and isinstance(self.scopes, str):
641+
return self.scopes
642+
return " ".join(self.get_scopes())
643+
601644
def __repr__(self):
602645
return f"<{self.debug_string()}>"
603646

tests/test_config.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,42 @@ def test_oauth_token_reuses_existing_provider(mocker):
260260
# Both calls should work and use the same provider instance
261261
assert token1 == token2 == mock_token
262262
assert mock_oauth_provider.oauth_token.call_count == 2
263+
264+
265+
def test_disable_oauth_refresh_token_from_env(monkeypatch, mocker):
266+
mocker.patch("databricks.sdk.config.Config.init_auth")
267+
monkeypatch.setenv("DATABRICKS_DISABLE_OAUTH_REFRESH_TOKEN", "true")
268+
config = Config(host="https://test.databricks.com")
269+
assert config.disable_oauth_refresh_token is True
270+
271+
272+
def test_disable_oauth_refresh_token_defaults_to_false(mocker):
273+
mocker.patch("databricks.sdk.config.Config.init_auth")
274+
config = Config(host="https://test.databricks.com")
275+
assert config.disable_oauth_refresh_token is None # ConfigAttribute returns None when not set
276+
277+
278+
def test_config_file_scopes_empty_defaults_to_all_apis(monkeypatch, mocker):
279+
"""Test that empty scopes in config file defaults to all-apis."""
280+
mocker.patch("databricks.sdk.config.Config.init_auth")
281+
monkeypatch.setenv("HOME", str(pathlib.Path(__tests__) / "testdata"))
282+
config = Config(profile="scope-empty")
283+
assert config.get_scopes() == ["all-apis"]
284+
285+
286+
def test_config_file_scopes_single(monkeypatch, mocker):
287+
"""Test single scope from config file."""
288+
mocker.patch("databricks.sdk.config.Config.init_auth")
289+
monkeypatch.setenv("HOME", str(pathlib.Path(__tests__) / "testdata"))
290+
config = Config(profile="scope-single")
291+
assert config.get_scopes() == ["clusters:read"]
292+
293+
294+
def test_config_file_scopes_multiple_sorted(monkeypatch, mocker):
295+
"""Test multiple scopes from config file are sorted."""
296+
mocker.patch("databricks.sdk.config.Config.init_auth")
297+
monkeypatch.setenv("HOME", str(pathlib.Path(__tests__) / "testdata"))
298+
config = Config(profile="scope-multiple")
299+
# Should be sorted alphabetically
300+
expected = ["clusters", "files:read", "iam:read", "jobs", "mlflow", "model-serving:read", "pipelines"]
301+
assert config.get_scopes() == expected

tests/testdata/.databrickscfg

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,15 @@ google_credentials = paw48590aw8e09t8apu
3838

3939
[pat.with.dot]
4040
host = https://dbc-XXXXXXXX-YYYY.cloud.databricks.com/
41-
token = PT0+IC9kZXYvdXJhbmRvbSA8PT0KYFZ
41+
token = PT0+IC9kZXYvdXJhbmRvbSA8PT0KYFZ
42+
43+
[scope-empty]
44+
host = https://example.cloud.databricks.com
45+
46+
[scope-single]
47+
host = https://example.cloud.databricks.com
48+
scopes = clusters
49+
50+
[scope-multiple]
51+
host = https://example.cloud.databricks.com
52+
scopes = clusters, jobs, pipelines, iam:read, files:read, mlflow, model-serving:read

0 commit comments

Comments
 (0)