44import logging
55import os
66import pathlib
7+ import re
78import sys
89import urllib .parse
910from 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
0 commit comments