44import logging
55import os
66import pathlib
7+ import re
78import sys
89import urllib .parse
910from enum import Enum
@@ -137,6 +138,10 @@ class Config:
137138 scopes : str = ConfigAttribute ()
138139 authorization_details : str = ConfigAttribute ()
139140
141+ # Controls whether the offline_access scope is requested during U2M OAuth authentication.
142+ # offline_access is requested by default, causing a refresh token to be included in the OAuth token.
143+ disable_oauth_refresh_token : bool = ConfigAttribute (env = "DATABRICKS_DISABLE_OAUTH_REFRESH_TOKEN" )
144+
140145 files_ext_client_download_streaming_chunk_size : int = 2 * 1024 * 1024 # 2 MiB
141146
142147 # When downloading a file, the maximum number of attempts to retry downloading the whole file. Default is no limit.
@@ -265,6 +270,7 @@ def __init__(
265270 self ._known_file_config_loader ()
266271 self ._fix_host_if_needed ()
267272 self ._validate ()
273+ self ._sort_scopes ()
268274 self .init_auth ()
269275 self ._init_product (product , product_version )
270276 except ValueError as e :
@@ -666,6 +672,16 @@ def _validate(self):
666672 names = " and " .join (sorted (auths_used ))
667673 raise ValueError (f"validate: more than one authorization method configured: { names } " )
668674
675+ def _sort_scopes (self ):
676+ """Sort scopes in-place for better de-duplication in the refresh token cache.
677+ Delimiter is set to a single whitespace after sorting."""
678+ if self .scopes and isinstance (self .scopes , str ):
679+ # Split on whitespaces and commas, sort, and rejoin
680+ parsed = [s for s in re .split (r"[\s,]+" , self .scopes ) if s ]
681+ if parsed :
682+ parsed .sort ()
683+ self .scopes = " " .join (parsed )
684+
669685 def init_auth (self ):
670686 try :
671687 self ._header_factory = self ._credentials_strategy (self )
@@ -685,6 +701,33 @@ def _init_product(self, product, product_version):
685701 else :
686702 self ._product_info = None
687703
704+ def get_scopes (self ) -> List [str ]:
705+ """Get OAuth scopes with proper defaulting.
706+
707+ Returns ["all-apis"] if no scopes configured.
708+ This is the single source of truth for scope defaulting across all OAuth methods.
709+
710+ Parses string scopes by splitting on whitespaces and commas.
711+
712+ Returns:
713+ List of scope strings.
714+ """
715+ if self .scopes and isinstance (self .scopes , str ):
716+ parsed = [s for s in re .split (r"[\s,]+" , self .scopes ) if s ]
717+ if not parsed : # Empty string case
718+ return ["all-apis" ]
719+ return parsed
720+ return ["all-apis" ]
721+
722+ def get_scopes_as_string (self ) -> str :
723+ """Get OAuth scopes as a space-separated string.
724+
725+ Returns "all-apis" if no scopes configured.
726+ """
727+ if self .scopes and isinstance (self .scopes , str ):
728+ return self .scopes
729+ return " " .join (self .get_scopes ())
730+
688731 def __repr__ (self ):
689732 return f"<{ self .debug_string ()} >"
690733
0 commit comments