py2docfx 0.1.19rc2173760__py3-none-any.whl → 0.1.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. py2docfx/__main__.py +2 -2
  2. py2docfx/convert_prepare/arg_parser.py +1 -1
  3. py2docfx/convert_prepare/get_source.py +40 -8
  4. py2docfx/convert_prepare/git.py +1 -1
  5. py2docfx/docfx_yaml/logger.py +23 -19
  6. py2docfx/venv/basevenv/Lib/site-packages/certifi/__init__.py +1 -1
  7. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/authorization_code.py +1 -1
  8. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/broker.py +79 -0
  9. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/chained.py +8 -2
  10. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/default.py +145 -54
  11. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/imds.py +25 -1
  12. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/vscode.py +163 -144
  13. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/workload_identity.py +23 -12
  14. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/__init__.py +2 -0
  15. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/pipeline.py +4 -2
  16. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/utils.py +85 -0
  17. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_version.py +1 -1
  18. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/authorization_code.py +1 -1
  19. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/default.py +112 -56
  20. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/imds.py +27 -1
  21. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/on_behalf_of.py +1 -1
  22. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/vscode.py +15 -67
  23. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/workload_identity.py +17 -13
  24. py2docfx/venv/venv1/Lib/site-packages/certifi/__init__.py +1 -1
  25. py2docfx/venv/venv1/Lib/site-packages/cryptography/__about__.py +1 -1
  26. py2docfx/venv/venv1/Lib/site-packages/msal/__main__.py +2 -0
  27. py2docfx/venv/venv1/Lib/site-packages/msal/application.py +63 -21
  28. py2docfx/venv/venv1/Lib/site-packages/msal/broker.py +5 -2
  29. py2docfx/venv/venv1/Lib/site-packages/msal/exceptions.py +19 -5
  30. py2docfx/venv/venv1/Lib/site-packages/msal/managed_identity.py +50 -10
  31. py2docfx/venv/venv1/Lib/site-packages/msal/sku.py +1 -1
  32. py2docfx/venv/venv1/Lib/site-packages/msal/throttled_http_client.py +4 -1
  33. {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/METADATA +1 -1
  34. {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/RECORD +36 -38
  35. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/linux_vscode_adapter.py +0 -100
  36. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/macos_vscode_adapter.py +0 -34
  37. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/win_vscode_adapter.py +0 -77
  38. {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/WHEEL +0 -0
  39. {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/top_level.txt +0 -0
py2docfx/__main__.py CHANGED
@@ -106,8 +106,8 @@ async def main(argv) -> int:
106
106
 
107
107
  # create log folder and package log folder
108
108
  log_folder = os.path.join(PACKAGE_ROOT, LOG_FOLDER)
109
- os.makedirs(log_folder)
110
- os.makedirs(os.path.join(log_folder, "package_logs"))
109
+ os.makedirs(log_folder, exist_ok=True)
110
+ os.makedirs(os.path.join(log_folder, "package_logs"), exist_ok=True)
111
111
 
112
112
  py2docfxLogger.decide_global_log_level(verbose, show_warning)
113
113
 
@@ -162,7 +162,7 @@ def parse_command_line_args(argv) -> (
162
162
  ado_token, output_root, verbose, show_warning)
163
163
  elif args.param_json:
164
164
  (package_info_list, required_packages) = load_command_params(args.param_json)
165
- return (package_info_list, required_packages, github_token,
165
+ return (list(package_info_list), list(required_packages), github_token,
166
166
  ado_token, output_root, verbose, show_warning)
167
167
  else:
168
168
  package_info = PackageInfo()
@@ -41,10 +41,11 @@ def update_package_info(executable: str, pkg: PackageInfo, source_folder: str):
41
41
 
42
42
  setattr(pkg, attr, attr_val)
43
43
  else:
44
- folder = next(
45
- f for f in all_files if path.isdir(f) and f.endswith(".dist-info")
46
- )
47
- if folder:
44
+ # Find .dist-info folders
45
+ dist_info_folders = [f for f in all_files if path.isdir(f) and f.endswith(".dist-info")]
46
+
47
+ if dist_info_folders:
48
+ folder = dist_info_folders[0] # Take the first one if multiple exist
48
49
  if path.exists(path.join(folder, "METADATA")):
49
50
  with open(
50
51
  path.join(folder, "METADATA"), "r", encoding="utf-8"
@@ -97,6 +98,9 @@ async def get_source(executable: str, pkg: PackageInfo, cnt: int, vststoken=None
97
98
  sys.path.insert(0, source_folder)
98
99
  elif pkg.install_type == PackageInfo.InstallType.PYPI:
99
100
  full_name = pkg.get_combined_name_version()
101
+ # Ensure the dist directory exists
102
+ os.makedirs(dist_dir, exist_ok=True)
103
+
100
104
  await pip_utils.download(
101
105
  full_name,
102
106
  dist_dir,
@@ -104,25 +108,53 @@ async def get_source(executable: str, pkg: PackageInfo, cnt: int, vststoken=None
104
108
  prefer_source_distribution=pkg.prefer_source_distribution,
105
109
  )
106
110
  # unpack the downloaded wheel file.
107
- downloaded_dist_file = path.join(dist_dir, os.listdir(dist_dir)[0])
111
+ dist_files = os.listdir(dist_dir)
112
+ if not dist_files:
113
+ msg = f"No files downloaded to {dist_dir} for package {pkg.name}"
114
+ py2docfx_logger.error(msg)
115
+ raise FileNotFoundError(f"No files found in {dist_dir}")
116
+
117
+ downloaded_dist_file = path.join(dist_dir, dist_files[0])
108
118
  await pack.unpack_dist(pkg.name, downloaded_dist_file)
109
119
  os.remove(downloaded_dist_file)
120
+ dist_files = os.listdir(dist_dir)
121
+ if not dist_files:
122
+ msg = f"No files found in {dist_dir} after unpacking for package {pkg.name}"
123
+ py2docfx_logger.error(msg)
124
+ raise FileNotFoundError(f"No files found in {dist_dir} after unpacking")
125
+
110
126
  source_folder = path.join(
111
127
  path.dirname(downloaded_dist_file),
112
- os.listdir(dist_dir)[0]
128
+ dist_files[0]
113
129
  )
114
130
  elif pkg.install_type == PackageInfo.InstallType.DIST_FILE:
131
+ # Ensure the dist directory exists
132
+ os.makedirs(dist_dir, exist_ok=True)
133
+
115
134
  await pip_utils.download(pkg.location, dist_dir, prefer_source_distribution=False)
116
135
  # unpack the downloaded dist file.
117
- downloaded_dist_file = path.join(dist_dir, os.listdir(dist_dir)[0])
136
+ dist_files = os.listdir(dist_dir)
137
+ if not dist_files:
138
+ msg = f"No files downloaded to {dist_dir} for package {pkg.name}"
139
+ py2docfx_logger.error(msg)
140
+ raise FileNotFoundError(f"No files found in {dist_dir}")
141
+
142
+ downloaded_dist_file = path.join(dist_dir, dist_files[0])
118
143
  await pack.unpack_dist(pkg.name, downloaded_dist_file)
119
144
  os.remove(downloaded_dist_file)
145
+
146
+ # Check again after unpacking
147
+ dist_files = os.listdir(dist_dir)
148
+ if not dist_files:
149
+ msg = f"No files found in {dist_dir} after unpacking for package {pkg.name}"
150
+ py2docfx_logger.error(msg)
151
+ raise FileNotFoundError(f"No files found in {dist_dir} after unpacking")
120
152
  if downloaded_dist_file.endswith(".tar.gz"):
121
153
  downloaded_dist_file = downloaded_dist_file.rsplit(".", maxsplit=1)[
122
154
  0]
123
155
  source_folder = path.join(
124
156
  path.dirname(downloaded_dist_file),
125
- os.listdir(dist_dir)[0]
157
+ dist_files[0] if dist_files else ""
126
158
  )
127
159
  else:
128
160
  msg = f"Unknown install type: {pkg.install_type}"
@@ -25,7 +25,7 @@ async def clone(repo_location, branch, folder, extra_token=None):
25
25
  raise ValueError(msg)
26
26
  else:
27
27
  # Remove http(s):// from url to record. Further avoid dup-clone.
28
- pureURL = re.sub("^\s*https?://", "", repo_location)
28
+ pureURL = re.sub(r"^\s*https?://", "", repo_location)
29
29
 
30
30
  if pureURL not in repoMap:
31
31
  branch = convertBranch(repo_location, branch, extra_token)
@@ -77,16 +77,18 @@ def counts_errors_warnings(log_file_path):
77
77
  warning_count += 1
78
78
  return warning_count, error_count
79
79
 
80
- def get_warning_error_count():
81
- main_log_file_path = os.path.join("logs", "log.txt")
82
- warning_count, error_count = counts_errors_warnings(main_log_file_path)
83
-
84
- log_folder_path = os.path.join("logs", "package_logs")
85
- for log_file in os.listdir(log_folder_path):
86
- log_file_path = os.path.join(log_folder_path, log_file)
87
- warnings, errors = counts_errors_warnings(log_file_path)
88
- warning_count += warnings
89
- error_count += errors
80
+ def get_warning_error_count():
81
+ main_log_file_path = os.path.join("logs", "log.txt")
82
+ warning_count, error_count = counts_errors_warnings(main_log_file_path)
83
+
84
+ log_folder_path = os.path.join("logs", "package_logs")
85
+ # Check if the directory exists before trying to list its contents
86
+ if os.path.exists(log_folder_path) and os.path.isdir(log_folder_path):
87
+ for log_file in os.listdir(log_folder_path):
88
+ log_file_path = os.path.join(log_folder_path, log_file)
89
+ warnings, errors = counts_errors_warnings(log_file_path)
90
+ warning_count += warnings
91
+ error_count += errors
90
92
 
91
93
  return warning_count, error_count
92
94
 
@@ -118,15 +120,17 @@ def print_out_log_by_log_level(log_list, log_level):
118
120
  if log['level'] >= log_level and log['message'] not in ['', '\n', '\r\n']:
119
121
  print(log['message'])
120
122
 
121
- def output_log_by_log_level():
122
- log_level = get_log_level()
123
- main_log_file_path = os.path.join("logs", "log.txt")
124
- print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
125
-
126
- package_logs_folder = os.path.join("logs", "package_logs")
127
- for log_file in os.listdir(package_logs_folder):
128
- log_file_path = os.path.join(package_logs_folder, log_file)
129
- print_out_log_by_log_level(parse_log(log_file_path), log_level)
123
+ def output_log_by_log_level():
124
+ log_level = get_log_level()
125
+ main_log_file_path = os.path.join("logs", "log.txt")
126
+ print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
127
+
128
+ package_logs_folder = os.path.join("logs", "package_logs")
129
+ # Check if the directory exists before trying to list its contents
130
+ if os.path.exists(package_logs_folder) and os.path.isdir(package_logs_folder):
131
+ for log_file in os.listdir(package_logs_folder):
132
+ log_file_path = os.path.join(package_logs_folder, log_file)
133
+ print_out_log_by_log_level(parse_log(log_file_path), log_level)
130
134
 
131
135
  async def run_async_subprocess(exe_path, cmd, logger, cwd=None):
132
136
  if cwd is None:
@@ -1,4 +1,4 @@
1
1
  from .core import contents, where
2
2
 
3
3
  __all__ = ["contents", "where"]
4
- __version__ = "2025.07.14"
4
+ __version__ = "2025.08.03"
@@ -127,7 +127,7 @@ class AuthorizationCodeCredential(GetTokenMixin):
127
127
  return token
128
128
 
129
129
  token = None
130
- for refresh_token in self._client.get_cached_refresh_tokens(scopes):
130
+ for refresh_token in self._client.get_cached_refresh_tokens(scopes, **kwargs):
131
131
  if "secret" in refresh_token:
132
132
  token = self._client.obtain_token_by_refresh_token(scopes, refresh_token["secret"], **kwargs)
133
133
  if token:
@@ -0,0 +1,79 @@
1
+ # ------------------------------------
2
+ # Copyright (c) Microsoft Corporation.
3
+ # Licensed under the MIT License.
4
+ # ------------------------------------
5
+ import sys
6
+ from typing import Any
7
+
8
+ import msal
9
+ from azure.core.credentials import AccessToken, AccessTokenInfo, SupportsTokenInfo
10
+ from .._exceptions import CredentialUnavailableError
11
+ from .._internal.utils import get_broker_credential, is_wsl
12
+
13
+
14
+ class BrokerCredential(SupportsTokenInfo):
15
+ """A broker credential that handles prerequisite checking and falls back appropriately.
16
+
17
+ This credential checks if the azure-identity-broker package is available and the platform
18
+ is supported. If both conditions are met, it uses the real broker credential. Otherwise,
19
+ it raises CredentialUnavailableError with an appropriate message.
20
+ """
21
+
22
+ def __init__(self, **kwargs: Any) -> None:
23
+
24
+ self._tenant_id = kwargs.pop("tenant_id", None)
25
+ self._client_id = kwargs.pop("client_id", None)
26
+ self._broker_credential = None
27
+ self._unavailable_message = None
28
+
29
+ # Check prerequisites and initialize the appropriate credential
30
+ broker_credential_class = get_broker_credential()
31
+ if broker_credential_class and (sys.platform.startswith("win") or is_wsl()):
32
+ # The silent auth flow for brokered auth is available on Windows/WSL with the broker package
33
+ try:
34
+ broker_credential_args = {
35
+ "tenant_id": self._tenant_id,
36
+ "parent_window_handle": msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE,
37
+ "use_default_broker_account": True,
38
+ "disable_interactive_fallback": True,
39
+ **kwargs,
40
+ }
41
+ if self._client_id:
42
+ broker_credential_args["client_id"] = self._client_id
43
+ self._broker_credential = broker_credential_class(**broker_credential_args)
44
+ except Exception as ex: # pylint: disable=broad-except
45
+ self._unavailable_message = f"InteractiveBrowserBrokerCredential initialization failed: {ex}"
46
+ else:
47
+ # Determine the specific reason for unavailability
48
+ if broker_credential_class is None:
49
+ self._unavailable_message = (
50
+ "InteractiveBrowserBrokerCredential unavailable. "
51
+ "The 'azure-identity-broker' package is required to use brokered authentication."
52
+ )
53
+ else:
54
+ self._unavailable_message = (
55
+ "InteractiveBrowserBrokerCredential unavailable. "
56
+ "Brokered authentication is only supported on Windows and WSL platforms."
57
+ )
58
+
59
+ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
60
+ if self._broker_credential:
61
+ return self._broker_credential.get_token(*scopes, **kwargs)
62
+ raise CredentialUnavailableError(message=self._unavailable_message)
63
+
64
+ def get_token_info(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
65
+ if self._broker_credential:
66
+ return self._broker_credential.get_token_info(*scopes, **kwargs)
67
+ raise CredentialUnavailableError(message=self._unavailable_message)
68
+
69
+ def __enter__(self) -> "BrokerCredential":
70
+ if self._broker_credential:
71
+ self._broker_credential.__enter__()
72
+ return self
73
+
74
+ def __exit__(self, *args):
75
+ if self._broker_credential:
76
+ self._broker_credential.__exit__(*args)
77
+
78
+ def close(self) -> None:
79
+ self.__exit__()
@@ -23,10 +23,16 @@ _LOGGER = logging.getLogger(__name__)
23
23
  def _get_error_message(history):
24
24
  attempts = []
25
25
  for credential, error in history:
26
+ # Check if credential has a custom name (for DACErrorReporter instances)
27
+ if hasattr(credential, "_credential_name"):
28
+ credential_name = credential._credential_name # pylint: disable=protected-access
29
+ else:
30
+ credential_name = credential.__class__.__name__
31
+
26
32
  if error:
27
- attempts.append("{}: {}".format(credential.__class__.__name__, error))
33
+ attempts.append("{}: {}".format(credential_name, error))
28
34
  else:
29
- attempts.append(credential.__class__.__name__)
35
+ attempts.append(credential_name)
30
36
  return """
31
37
  Attempted credentials:\n\t{}""".format(
32
38
  "\n\t".join(attempts)
@@ -6,10 +6,18 @@ import logging
6
6
  import os
7
7
  from typing import List, Any, Optional, cast
8
8
 
9
- from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, SupportsTokenInfo, TokenCredential
9
+ from azure.core.credentials import (
10
+ AccessToken,
11
+ AccessTokenInfo,
12
+ TokenRequestOptions,
13
+ SupportsTokenInfo,
14
+ TokenCredential,
15
+ )
16
+ from .. import CredentialUnavailableError
10
17
  from .._constants import EnvironmentVariables
11
- from .._internal import get_default_authority, normalize_authority, within_dac
18
+ from .._internal.utils import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
12
19
  from .azure_powershell import AzurePowerShellCredential
20
+ from .broker import BrokerCredential
13
21
  from .browser import InteractiveBrowserCredential
14
22
  from .chained import ChainedTokenCredential
15
23
  from .environment import EnvironmentCredential
@@ -23,6 +31,32 @@ from .workload_identity import WorkloadIdentityCredential
23
31
  _LOGGER = logging.getLogger(__name__)
24
32
 
25
33
 
34
+ class FailedDACCredential:
35
+ """This acts as a substitute for a credential that has failed to initialize in the DAC chain.
36
+
37
+ This allows instantiation errors to be reported in ChainTokenCredential if all token requests fail.
38
+ """
39
+
40
+ def __init__(self, credential_name: str, error: str) -> None:
41
+ self._error = error
42
+ self._credential_name = credential_name
43
+
44
+ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
45
+ raise CredentialUnavailableError(self._error)
46
+
47
+ def get_token_info(self, *scopes, options: Optional[TokenRequestOptions] = None, **kwargs: Any) -> AccessTokenInfo:
48
+ raise CredentialUnavailableError(self._error)
49
+
50
+ def __enter__(self) -> "FailedDACCredential":
51
+ return self
52
+
53
+ def __exit__(self, *args: Any) -> None:
54
+ pass
55
+
56
+ def close(self) -> None:
57
+ pass
58
+
59
+
26
60
  class DefaultAzureCredential(ChainedTokenCredential):
27
61
  """A credential capable of handling most Azure SDK authentication scenarios. For more information, See
28
62
  `Usage guidance for DefaultAzureCredential
@@ -42,6 +76,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
42
76
  5. The identity currently logged in to the Azure CLI.
43
77
  6. The identity currently logged in to Azure PowerShell.
44
78
  7. The identity currently logged in to the Azure Developer CLI.
79
+ 8. Brokered authentication. On Windows and WSL only, this uses the default account logged in via
80
+ Web Account Manager (WAM) if the `azure-identity-broker` package is installed.
45
81
 
46
82
  This default behavior is configurable with keyword arguments.
47
83
 
@@ -64,9 +100,13 @@ class DefaultAzureCredential(ChainedTokenCredential):
64
100
  **False**.
65
101
  :keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see
66
102
  :class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**.
103
+ :keyword bool exclude_broker_credential: Whether to exclude the broker credential from the credential chain.
104
+ Defaults to **False**.
67
105
  :keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through
68
106
  :class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable
69
107
  AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
108
+ :keyword str broker_tenant_id: The tenant ID to use when using brokered authentication. Defaults to the value of
109
+ environment variable AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
70
110
  :keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value
71
111
  of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used.
72
112
  :keyword str workload_identity_client_id: The client ID of an identity assigned to the pod. Defaults to the value
@@ -75,14 +115,15 @@ class DefaultAzureCredential(ChainedTokenCredential):
75
115
  Defaults to the value of environment variable AZURE_TENANT_ID, if any.
76
116
  :keyword str interactive_browser_client_id: The client ID to be used in interactive browser credential. If not
77
117
  specified, users will authenticate to an Azure development application.
118
+ :keyword str broker_client_id: The client ID to be used in brokered authentication. If not specified, users will
119
+ authenticate to an Azure development application.
78
120
  :keyword str shared_cache_username: Preferred username for :class:`~azure.identity.SharedTokenCacheCredential`.
79
121
  Defaults to the value of environment variable AZURE_USERNAME, if any.
80
122
  :keyword str shared_cache_tenant_id: Preferred tenant for :class:`~azure.identity.SharedTokenCacheCredential`.
81
123
  Defaults to the value of environment variable AZURE_TENANT_ID, if any.
82
124
  :keyword str visual_studio_code_tenant_id: Tenant ID to use when authenticating with
83
- :class:`~azure.identity.VisualStudioCodeCredential`. Defaults to the "Azure: Tenant" setting in VS Code's user
84
- settings or, when that setting has no value, the "organizations" tenant, which supports only Azure Active
85
- Directory work or school accounts.
125
+ :class:`~azure.identity.VisualStudioCodeCredential`. Defaults to the tenant specified in the authentication
126
+ record file used by the Azure Resources extension.
86
127
  :keyword int process_timeout: The timeout in seconds to use for developer credentials that run
87
128
  subprocesses (e.g. AzureCliCredential, AzurePowerShellCredential). Defaults to **10** seconds.
88
129
 
@@ -101,18 +142,10 @@ class DefaultAzureCredential(ChainedTokenCredential):
101
142
  raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.")
102
143
 
103
144
  authority = kwargs.pop("authority", None)
104
-
105
- vscode_tenant_id = kwargs.pop(
106
- "visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
107
- )
108
- vscode_args = dict(kwargs)
109
- if authority:
110
- vscode_args["authority"] = authority
111
- if vscode_tenant_id:
112
- vscode_args["tenant_id"] = vscode_tenant_id
113
-
114
145
  authority = normalize_authority(authority) if authority else get_default_authority()
115
146
 
147
+ vscode_tenant_id = kwargs.pop("visual_studio_code_tenant_id", None)
148
+
116
149
  interactive_browser_tenant_id = kwargs.pop(
117
150
  "interactive_browser_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
118
151
  )
@@ -126,6 +159,9 @@ class DefaultAzureCredential(ChainedTokenCredential):
126
159
  )
127
160
  interactive_browser_client_id = kwargs.pop("interactive_browser_client_id", None)
128
161
 
162
+ broker_tenant_id = kwargs.pop("broker_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID))
163
+ broker_client_id = kwargs.pop("broker_client_id", None)
164
+
129
165
  shared_cache_username = kwargs.pop("shared_cache_username", os.environ.get(EnvironmentVariables.AZURE_USERNAME))
130
166
  shared_cache_tenant_id = kwargs.pop(
131
167
  "shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
@@ -133,52 +169,97 @@ class DefaultAzureCredential(ChainedTokenCredential):
133
169
 
134
170
  process_timeout = kwargs.pop("process_timeout", 10)
135
171
 
136
- token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower()
137
- exclude_workload_identity_credential = kwargs.pop("exclude_workload_identity_credential", False)
138
- exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
139
- exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
140
- exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
141
- exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", True)
142
- exclude_developer_cli_credential = kwargs.pop("exclude_developer_cli_credential", False)
143
- exclude_cli_credential = kwargs.pop("exclude_cli_credential", False)
144
- exclude_interactive_browser_credential = kwargs.pop("exclude_interactive_browser_credential", True)
145
- exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False)
146
-
147
- if token_credentials_env == "dev":
148
- # In dev mode, use only developer credentials
149
- exclude_environment_credential = True
150
- exclude_managed_identity_credential = True
151
- exclude_workload_identity_credential = True
152
- elif token_credentials_env == "prod":
153
- # In prod mode, use only production credentials
154
- exclude_shared_token_cache_credential = True
155
- exclude_visual_studio_code_credential = True
156
- exclude_cli_credential = True
157
- exclude_developer_cli_credential = True
158
- exclude_powershell_credential = True
159
- exclude_interactive_browser_credential = True
160
- elif token_credentials_env != "":
161
- # If the environment variable is set to something other than dev or prod, raise an error
162
- raise ValueError(
163
- f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. "
164
- "Valid values are 'dev' or 'prod'."
165
- )
172
+ # Define credential configuration mapping
173
+ credential_config = {
174
+ "environment": {
175
+ "exclude_param": "exclude_environment_credential",
176
+ "env_name": "environmentcredential",
177
+ "default_exclude": False,
178
+ },
179
+ "workload_identity": {
180
+ "exclude_param": "exclude_workload_identity_credential",
181
+ "env_name": "workloadidentitycredential",
182
+ "default_exclude": False,
183
+ },
184
+ "managed_identity": {
185
+ "exclude_param": "exclude_managed_identity_credential",
186
+ "env_name": "managedidentitycredential",
187
+ "default_exclude": False,
188
+ },
189
+ "shared_token_cache": {
190
+ "exclude_param": "exclude_shared_token_cache_credential",
191
+ "default_exclude": False,
192
+ },
193
+ "visual_studio_code": {
194
+ "exclude_param": "exclude_visual_studio_code_credential",
195
+ "env_name": "visualstudiocodecredential",
196
+ "default_exclude": False,
197
+ },
198
+ "cli": {
199
+ "exclude_param": "exclude_cli_credential",
200
+ "env_name": "azureclicredential",
201
+ "default_exclude": False,
202
+ },
203
+ "developer_cli": {
204
+ "exclude_param": "exclude_developer_cli_credential",
205
+ "env_name": "azuredeveloperclicredential",
206
+ "default_exclude": False,
207
+ },
208
+ "powershell": {
209
+ "exclude_param": "exclude_powershell_credential",
210
+ "env_name": "azurepowershellcredential",
211
+ "default_exclude": False,
212
+ },
213
+ "interactive_browser": {
214
+ "exclude_param": "exclude_interactive_browser_credential",
215
+ "env_name": "interactivebrowsercredential",
216
+ "default_exclude": True,
217
+ },
218
+ "broker": {
219
+ "exclude_param": "exclude_broker_credential",
220
+ "default_exclude": False,
221
+ },
222
+ }
223
+
224
+ # Extract user-provided exclude flags and set defaults
225
+ exclude_flags = {}
226
+ user_excludes = {}
227
+ for cred_key, config in credential_config.items():
228
+ param_name = cast(str, config["exclude_param"])
229
+ user_excludes[cred_key] = kwargs.pop(param_name, None)
230
+ exclude_flags[cred_key] = config["default_exclude"]
231
+
232
+ # Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides
233
+ exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes)
234
+
235
+ # Extract individual exclude flags for backward compatibility
236
+ exclude_environment_credential = exclude_flags["environment"]
237
+ exclude_workload_identity_credential = exclude_flags["workload_identity"]
238
+ exclude_managed_identity_credential = exclude_flags["managed_identity"]
239
+ exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"]
240
+ exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"]
241
+ exclude_cli_credential = exclude_flags["cli"]
242
+ exclude_developer_cli_credential = exclude_flags["developer_cli"]
243
+ exclude_powershell_credential = exclude_flags["powershell"]
244
+ exclude_interactive_browser_credential = exclude_flags["interactive_browser"]
245
+ exclude_broker_credential = exclude_flags["broker"]
166
246
 
167
247
  credentials: List[SupportsTokenInfo] = []
168
248
  within_dac.set(True)
169
249
  if not exclude_environment_credential:
170
250
  credentials.append(EnvironmentCredential(authority=authority, _within_dac=True, **kwargs))
171
251
  if not exclude_workload_identity_credential:
172
- if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS):
173
- client_id = workload_identity_client_id
252
+ try:
174
253
  credentials.append(
175
254
  WorkloadIdentityCredential(
176
- client_id=cast(str, client_id),
255
+ client_id=cast(str, workload_identity_client_id),
177
256
  tenant_id=workload_identity_tenant_id,
178
- token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
257
+ token_file_path=os.environ.get(EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE),
179
258
  **kwargs,
180
259
  )
181
260
  )
261
+ except ValueError as ex:
262
+ credentials.append(FailedDACCredential("WorkloadIdentityCredential", error=str(ex)))
182
263
  if not exclude_managed_identity_credential:
183
264
  credentials.append(
184
265
  ManagedIdentityCredential(
@@ -197,7 +278,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
197
278
  except Exception as ex: # pylint:disable=broad-except
198
279
  _LOGGER.info("Shared token cache is unavailable: '%s'", ex)
199
280
  if not exclude_visual_studio_code_credential:
200
- credentials.append(VisualStudioCodeCredential(**vscode_args))
281
+ credentials.append(VisualStudioCodeCredential(tenant_id=vscode_tenant_id))
201
282
  if not exclude_cli_credential:
202
283
  credentials.append(AzureCliCredential(process_timeout=process_timeout))
203
284
  if not exclude_powershell_credential:
@@ -213,6 +294,12 @@ class DefaultAzureCredential(ChainedTokenCredential):
213
294
  )
214
295
  else:
215
296
  credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs))
297
+ if not exclude_broker_credential:
298
+ broker_credential_args = {"tenant_id": broker_tenant_id, **kwargs}
299
+ if broker_client_id:
300
+ broker_credential_args["client_id"] = broker_client_id
301
+ credentials.append(BrokerCredential(**broker_credential_args))
302
+
216
303
  within_dac.set(False)
217
304
  super(DefaultAzureCredential, self).__init__(*credentials)
218
305
 
@@ -245,8 +332,10 @@ class DefaultAzureCredential(ChainedTokenCredential):
245
332
  )
246
333
  return token
247
334
  within_dac.set(True)
248
- token = super().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs)
249
- within_dac.set(False)
335
+ try:
336
+ token = super().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs)
337
+ finally:
338
+ within_dac.set(False)
250
339
  return token
251
340
 
252
341
  def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo:
@@ -274,6 +363,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
274
363
  return token_info
275
364
 
276
365
  within_dac.set(True)
277
- token_info = cast(SupportsTokenInfo, super()).get_token_info(*scopes, options=options)
278
- within_dac.set(False)
366
+ try:
367
+ token_info = cast(SupportsTokenInfo, super()).get_token_info(*scopes, options=options)
368
+ finally:
369
+ within_dac.set(False)
279
370
  return token_info
@@ -6,9 +6,11 @@ import os
6
6
  import json
7
7
  from typing import Any, Optional, Dict
8
8
 
9
+ from azure.core.pipeline import PipelineResponse
9
10
  from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
10
11
  from azure.core.pipeline.transport import HttpRequest
11
12
  from azure.core.credentials import AccessTokenInfo
13
+ from azure.core.pipeline.policies import RetryPolicy
12
14
 
13
15
  from .. import CredentialUnavailableError
14
16
  from .._constants import EnvironmentVariables
@@ -31,6 +33,28 @@ PIPELINE_SETTINGS = {
31
33
  }
32
34
 
33
35
 
36
+ class ImdsRetryPolicy(RetryPolicy):
37
+ """Custom retry policy for IMDS credential with extended retry duration for 410 responses.
38
+
39
+ This policy ensures that specifically for 410 status codes, the total exponential backoff duration
40
+ is at least 70 seconds to handle temporary IMDS endpoint unavailability.
41
+ For other status codes, it uses the standard retry behavior.
42
+ """
43
+
44
+ def __init__(self, **kwargs: Any) -> None:
45
+ # Increased backoff factor to ensure at least 70 seconds retry duration for 410 responses.
46
+ # Five retries, with each retry sleeping for [0.0s, 5.0s, 10.0s, 20.0s, 40.0s] between attempts (75s total)
47
+ self.backoff_factor_for_410 = 2.5
48
+ super().__init__(**kwargs)
49
+
50
+ def is_retry(self, settings: Dict[str, Any], response: PipelineResponse[Any, Any]) -> bool:
51
+ if response.http_response.status_code == 410:
52
+ settings["backoff"] = self.backoff_factor_for_410
53
+ else:
54
+ settings["backoff"] = self.backoff_factor
55
+ return super().is_retry(settings, response)
56
+
57
+
34
58
  def _get_request(scope: str, identity_config: Dict) -> HttpRequest:
35
59
  url = (
36
60
  os.environ.get(EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST, IMDS_AUTHORITY).strip("/")
@@ -58,7 +82,7 @@ def _check_forbidden_response(ex: HttpResponseError) -> None:
58
82
 
59
83
  class ImdsCredential(MsalManagedIdentityClient):
60
84
  def __init__(self, **kwargs: Any) -> None:
61
- super(ImdsCredential, self).__init__(**kwargs)
85
+ super().__init__(retry_policy_class=ImdsRetryPolicy, **dict(PIPELINE_SETTINGS, **kwargs))
62
86
  self._config = kwargs
63
87
 
64
88
  if EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST in os.environ: