44import logging
55import os
66import pathlib
7- import re
87import sys
98import urllib .parse
109from enum import Enum
@@ -47,11 +46,13 @@ class ConfigAttribute:
4746 # name and transform are discovered from Config.__new__
4847 name : str = None
4948 transform : type = str
49+ _custom_transform = None
5050
51- def __init__ (self , env : str = None , auth : str = None , sensitive : bool = False ):
51+ def __init__ (self , env : str = None , auth : str = None , sensitive : bool = False , transform = None ):
5252 self .env = env
5353 self .auth = auth
5454 self .sensitive = sensitive
55+ self ._custom_transform = transform
5556
5657 def __get__ (self , cfg : "Config" , owner ):
5758 if not cfg :
@@ -65,6 +66,19 @@ def __repr__(self) -> str:
6566 return f"<ConfigAttribute '{ self .name } ' { self .transform .__name__ } >"
6667
6768
69+ def _parse_scopes (value ):
70+ """Parse scopes into a deduplicated, sorted list."""
71+ if value is None :
72+ return None
73+ if isinstance (value , list ):
74+ result = sorted (set (s for s in value if s ))
75+ return result if result else None
76+ if isinstance (value , str ):
77+ parsed = sorted (set (s .strip () for s in value .split ("," ) if s .strip ()))
78+ return parsed if parsed else None
79+ return None
80+
81+
6882def with_product (product : str , product_version : str ):
6983 """[INTERNAL API] Change the product name and version used in the User-Agent header."""
7084 useragent .with_product (product , product_version )
@@ -134,12 +148,12 @@ class Config:
134148 disable_experimental_files_api_client : bool = ConfigAttribute (
135149 env = "DATABRICKS_DISABLE_EXPERIMENTAL_FILES_API_CLIENT"
136150 )
137- # TODO: Expose these via environment variables too.
138- scopes : str = ConfigAttribute ()
151+
152+ scopes : List [ str ] = ConfigAttribute (transform = _parse_scopes )
139153 authorization_details : str = ConfigAttribute ()
140154
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 .
155+ # disable_oauth_refresh_token controls whether a refresh token should be requested
156+ # during the U2M authentication flow ( default to false) .
143157 disable_oauth_refresh_token : bool = ConfigAttribute (env = "DATABRICKS_DISABLE_OAUTH_REFRESH_TOKEN" )
144158
145159 files_ext_client_download_streaming_chunk_size : int = 2 * 1024 * 1024 # 2 MiB
@@ -270,7 +284,6 @@ def __init__(
270284 self ._known_file_config_loader ()
271285 self ._fix_host_if_needed ()
272286 self ._validate ()
273- self ._sort_scopes ()
274287 self .init_auth ()
275288 self ._init_product (product , product_version )
276289 except ValueError as e :
@@ -559,7 +572,7 @@ def attributes(cls) -> Iterable[ConfigAttribute]:
559572 if type (v ) != ConfigAttribute :
560573 continue
561574 v .name = name
562- v .transform = anno .get (name , str )
575+ v .transform = v . _custom_transform if v . _custom_transform else anno .get (name , str )
563576 attrs .append (v )
564577 cls ._attributes = attrs
565578 return cls ._attributes
@@ -672,16 +685,6 @@ def _validate(self):
672685 names = " and " .join (sorted (auths_used ))
673686 raise ValueError (f"validate: more than one authorization method configured: { names } " )
674687
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-
685688 def init_auth (self ):
686689 try :
687690 self ._header_factory = self ._credentials_strategy (self )
@@ -706,26 +709,14 @@ def get_scopes(self) -> List[str]:
706709
707710 Returns ["all-apis"] if no scopes configured.
708711 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.
714712 """
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" ]
713+ return self .scopes if self .scopes else ["all-apis" ]
721714
722715 def get_scopes_as_string (self ) -> str :
723716 """Get OAuth scopes as a space-separated string.
724717
725718 Returns "all-apis" if no scopes configured.
726719 """
727- if self .scopes and isinstance (self .scopes , str ):
728- return self .scopes
729720 return " " .join (self .get_scopes ())
730721
731722 def __repr__ (self ):
0 commit comments