py2docfx 0.1.11.dev1985872__py3-none-any.whl → 0.1.11.dev1989123__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 (49) hide show
  1. py2docfx/__main__.py +13 -8
  2. py2docfx/convert_prepare/environment.py +1 -1
  3. py2docfx/convert_prepare/generate_document.py +5 -5
  4. py2docfx/convert_prepare/get_source.py +2 -2
  5. py2docfx/convert_prepare/pack.py +4 -4
  6. py2docfx/convert_prepare/sphinx_caller.py +34 -8
  7. py2docfx/convert_prepare/tests/test_environment.py +0 -2
  8. py2docfx/convert_prepare/tests/test_generate_document.py +4 -2
  9. py2docfx/convert_prepare/tests/test_get_source.py +1 -0
  10. py2docfx/convert_prepare/tests/test_pack.py +3 -1
  11. py2docfx/convert_prepare/tests/test_params.py +0 -1
  12. py2docfx/convert_prepare/tests/test_sphinx_caller.py +8 -6
  13. py2docfx/convert_prepare/tests/test_subpackage.py +1 -0
  14. py2docfx/docfx_yaml/build_finished.py +1 -1
  15. py2docfx/docfx_yaml/logger.py +12 -11
  16. py2docfx/docfx_yaml/tests/roots/test-writer-uri/code_with_uri.py +0 -7
  17. py2docfx/docfx_yaml/tests/test_writer_uri.py +0 -4
  18. py2docfx/docfx_yaml/writer.py +1 -13
  19. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/authorization_code.py +1 -1
  20. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/azd_cli.py +20 -14
  21. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/azure_arc.py +1 -1
  22. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/azure_cli.py +36 -14
  23. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/azure_powershell.py +1 -1
  24. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/chained.py +2 -2
  25. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/default.py +4 -3
  26. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/imds.py +2 -2
  27. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/managed_identity.py +1 -1
  28. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/__init__.py +2 -0
  29. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/auth_code_redirect_handler.py +1 -1
  30. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/decorators.py +15 -7
  31. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/interactive.py +1 -1
  32. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/managed_identity_client.py +0 -1
  33. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/msal_client.py +1 -1
  34. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/msal_managed_identity_client.py +2 -1
  35. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/shared_token_cache.py +3 -3
  36. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/utils.py +17 -2
  37. py2docfx/venv/venv1/Lib/site-packages/azure/identity/_version.py +1 -1
  38. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/azd_cli.py +14 -11
  39. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/azure_cli.py +30 -12
  40. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/default.py +2 -2
  41. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/imds.py +3 -3
  42. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/managed_identity.py +1 -1
  43. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_internal/decorators.py +15 -7
  44. py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_internal/managed_identity_client.py +1 -1
  45. py2docfx/venv/venv1/Lib/site-packages/cryptography/__about__.py +1 -1
  46. {py2docfx-0.1.11.dev1985872.dist-info → py2docfx-0.1.11.dev1989123.dist-info}/METADATA +1 -1
  47. {py2docfx-0.1.11.dev1985872.dist-info → py2docfx-0.1.11.dev1989123.dist-info}/RECORD +49 -49
  48. {py2docfx-0.1.11.dev1985872.dist-info → py2docfx-0.1.11.dev1989123.dist-info}/WHEEL +0 -0
  49. {py2docfx-0.1.11.dev1985872.dist-info → py2docfx-0.1.11.dev1989123.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  import json
7
7
  import os
8
8
  import re
9
+ import logging
9
10
  import shutil
10
11
  import subprocess
11
12
  import sys
@@ -15,12 +16,22 @@ from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOpt
15
16
  from azure.core.exceptions import ClientAuthenticationError
16
17
 
17
18
  from .. import CredentialUnavailableError
18
- from .._internal import _scopes_to_resource, resolve_tenant, within_dac, validate_tenant_id, validate_scope
19
+ from .._internal import (
20
+ _scopes_to_resource,
21
+ resolve_tenant,
22
+ within_dac,
23
+ validate_tenant_id,
24
+ validate_scope,
25
+ validate_subscription,
26
+ )
19
27
  from .._internal.decorators import log_get_token
20
28
 
21
29
 
30
+ _LOGGER = logging.getLogger(__name__)
31
+
22
32
  CLI_NOT_FOUND = "Azure CLI not found on path"
23
- COMMAND_LINE = "az account get-access-token --output json --resource {}"
33
+ # COMMAND_LINE = "account get-access-token --output json --resource {}"
34
+ COMMAND_LINE = ["account", "get-access-token", "--output", "json"]
24
35
  EXECUTABLE_NAME = "az"
25
36
  NOT_LOGGED_IN = "Please run 'az login' to set up an account"
26
37
 
@@ -31,6 +42,8 @@ class AzureCliCredential:
31
42
  This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity.
32
43
 
33
44
  :keyword str tenant_id: Optional tenant to include in the token request.
45
+ :keyword str subscription: The name or ID of a subscription. Set this to acquire tokens for an account other
46
+ than the Azure CLI's current account.
34
47
  :keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
35
48
  for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
36
49
  acquire tokens for any tenant the application can access.
@@ -50,12 +63,17 @@ class AzureCliCredential:
50
63
  self,
51
64
  *,
52
65
  tenant_id: str = "",
66
+ subscription: Optional[str] = None,
53
67
  additionally_allowed_tenants: Optional[List[str]] = None,
54
68
  process_timeout: int = 10,
55
69
  ) -> None:
56
70
  if tenant_id:
57
71
  validate_tenant_id(tenant_id)
72
+ if subscription:
73
+ validate_subscription(subscription)
74
+
58
75
  self.tenant_id = tenant_id
76
+ self.subscription = subscription
59
77
  self._additionally_allowed_tenants = additionally_allowed_tenants or []
60
78
  self._process_timeout = process_timeout
61
79
 
@@ -135,7 +153,7 @@ class AzureCliCredential:
135
153
  validate_scope(scope)
136
154
 
137
155
  resource = _scopes_to_resource(*scopes)
138
- command = COMMAND_LINE.format(resource)
156
+ command_args = COMMAND_LINE + ["--resource", resource]
139
157
  tenant = resolve_tenant(
140
158
  default_tenant=self.tenant_id,
141
159
  tenant_id=tenant_id,
@@ -143,8 +161,11 @@ class AzureCliCredential:
143
161
  **kwargs,
144
162
  )
145
163
  if tenant:
146
- command += " --tenant " + tenant
147
- output = _run_command(command, self._process_timeout)
164
+ command_args += ["--tenant", tenant]
165
+
166
+ if self.subscription:
167
+ command_args += ["--subscription", self.subscription]
168
+ output = _run_command(command_args, self._process_timeout)
148
169
 
149
170
  token = parse_token(output)
150
171
  if not token:
@@ -211,15 +232,13 @@ def sanitize_output(output: str) -> str:
211
232
  return re.sub(r"\"accessToken\": \"(.*?)(\"|$)", "****", output)
212
233
 
213
234
 
214
- def _run_command(command: str, timeout: int) -> str:
235
+ def _run_command(command_args: List[str], timeout: int) -> str:
215
236
  # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
216
- if shutil.which(EXECUTABLE_NAME) is None:
237
+ az_path = shutil.which(EXECUTABLE_NAME)
238
+ if not az_path:
217
239
  raise CredentialUnavailableError(message=CLI_NOT_FOUND)
218
240
 
219
- if sys.platform.startswith("win"):
220
- args = ["cmd", "/c", command]
221
- else:
222
- args = ["/bin/sh", "-c", command]
241
+ args = [az_path] + command_args
223
242
  try:
224
243
  working_directory = get_safe_working_dir()
225
244
 
@@ -231,13 +250,16 @@ def _run_command(command: str, timeout: int) -> str:
231
250
  "timeout": timeout,
232
251
  "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
233
252
  }
253
+ _LOGGER.debug("Executing subprocess with the following arguments %s", args)
234
254
  return subprocess.check_output(args, **kwargs)
235
255
  except subprocess.CalledProcessError as ex:
236
256
  # non-zero return from shell
237
257
  # Fallback check in case the executable is not found while executing subprocess.
238
- if ex.returncode == 127 or ex.stderr.startswith("'az' is not recognized"):
258
+ if ex.returncode == 127 or (ex.stderr is not None and ex.stderr.startswith("'az' is not recognized")):
239
259
  raise CredentialUnavailableError(message=CLI_NOT_FOUND) from ex
240
- if ("az login" in ex.stderr or "az account set" in ex.stderr) and "AADSTS" not in ex.stderr:
260
+ if ex.stderr is not None and (
261
+ ("az login" in ex.stderr or "az account set" in ex.stderr) and "AADSTS" not in ex.stderr
262
+ ):
241
263
  raise CredentialUnavailableError(message=NOT_LOGGED_IN) from ex
242
264
 
243
265
  # return code is from the CLI -> propagate its output
@@ -252,7 +274,7 @@ def _run_command(command: str, timeout: int) -> str:
252
274
  # failed to execute 'cmd' or '/bin/sh'
253
275
  error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0]))
254
276
  raise error from ex
255
- except Exception as ex: # pylint:disable=broad-except
277
+ except Exception as ex:
256
278
  # could be a timeout, for example
257
279
  error = CredentialUnavailableError(message="Failed to invoke the Azure CLI")
258
280
  raise error from ex
@@ -192,7 +192,7 @@ def run_command_line(command_line: List[str], timeout: int) -> str:
192
192
  proc = start_process(command_line)
193
193
  stdout, stderr = proc.communicate(**kwargs)
194
194
 
195
- except Exception as ex: # pylint:disable=broad-except
195
+ except Exception as ex:
196
196
  # failed to execute "cmd" or "/bin/sh", or timed out; PowerShell and Az.Account may or may not be installed
197
197
  # (handling Exception here because subprocess.SubprocessError and .TimeoutExpired were added in 3.3)
198
198
  if proc and not proc.returncode:
@@ -37,8 +37,8 @@ class ChainedTokenCredential:
37
37
  """A sequence of credentials that is itself a credential.
38
38
 
39
39
  Its :func:`get_token` method calls ``get_token`` on each credential in the sequence, in order, returning the first
40
- valid token received. For more information, see
41
- https://aka.ms/azsdk/python/identity/credential-chains#chainedtokencredential-overview.
40
+ valid token received. For more information, see `ChainedTokenCredential overview
41
+ <"https://aka.ms/azsdk/python/identity/credential-chains#chainedtokencredential-overview">`__.
42
42
 
43
43
  :param credentials: credential instances to form the chain
44
44
  :type credentials: ~azure.core.credentials.TokenCredential
@@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__)
24
24
 
25
25
 
26
26
  class DefaultAzureCredential(ChainedTokenCredential):
27
- """A credential capable of handling most Azure SDK authentication scenarios. See
28
- https://aka.ms/azsdk/python/identity/credential-chains#usage-guidance-for-defaultazurecredential.
27
+ """A credential capable of handling most Azure SDK authentication scenarios. For more information, See
28
+ `Usage guidance for DefaultAzureCredential
29
+ <"https://aka.ms/azsdk/python/identity/credential-chains#usage-guidance-for-defaultazurecredential">`__.
29
30
 
30
31
  The identity it uses depends on the environment. When an access token is needed, it requests one using these
31
32
  identities in turn, stopping when one provides a token:
@@ -153,7 +154,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
153
154
  WorkloadIdentityCredential(
154
155
  client_id=cast(str, client_id),
155
156
  tenant_id=workload_identity_tenant_id,
156
- file=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
157
+ token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
157
158
  **kwargs
158
159
  )
159
160
  )
@@ -89,7 +89,7 @@ class ImdsCredential(MsalManagedIdentityClient):
89
89
  # IMDS responded
90
90
  _check_forbidden_response(ex)
91
91
  self._endpoint_available = True
92
- except Exception as ex: # pylint:disable=broad-except
92
+ except Exception as ex:
93
93
  error_message = (
94
94
  "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
95
95
  )
@@ -119,7 +119,7 @@ class ImdsCredential(MsalManagedIdentityClient):
119
119
  raise ClientAuthenticationError(message=ex.message, response=ex.response) from ex
120
120
  except json.decoder.JSONDecodeError as ex:
121
121
  raise CredentialUnavailableError(message="ManagedIdentityCredential authentication unavailable.") from ex
122
- except Exception as ex: # pylint:disable=broad-except
122
+ except Exception as ex:
123
123
  # if anything else was raised, assume the endpoint is unavailable
124
124
  error_message = "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
125
125
  raise CredentialUnavailableError(error_message) from ex
@@ -108,7 +108,7 @@ class ManagedIdentityCredential:
108
108
  self._credential = WorkloadIdentityCredential(
109
109
  tenant_id=os.environ[EnvironmentVariables.AZURE_TENANT_ID],
110
110
  client_id=workload_client_id,
111
- file=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
111
+ token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
112
112
  **kwargs,
113
113
  )
114
114
  else:
@@ -13,6 +13,7 @@ from .utils import (
13
13
  normalize_authority,
14
14
  resolve_tenant,
15
15
  validate_scope,
16
+ validate_subscription,
16
17
  validate_tenant_id,
17
18
  within_credential_chain,
18
19
  within_dac,
@@ -49,6 +50,7 @@ __all__ = [
49
50
  "normalize_authority",
50
51
  "resolve_tenant",
51
52
  "validate_scope",
53
+ "validate_subscription",
52
54
  "within_credential_chain",
53
55
  "within_dac",
54
56
  "wrap_exceptions",
@@ -28,7 +28,7 @@ class AuthCodeRedirectHandler(BaseHTTPRequestHandler):
28
28
 
29
29
  self.wfile.write(b"Authentication complete. You can close this window.")
30
30
 
31
- def log_message(self, format, *args): # pylint: disable=redefined-builtin,unused-argument
31
+ def log_message(self, format, *args): # pylint: disable=redefined-builtin
32
32
  pass # this prevents server dumping messages to stdout
33
33
 
34
34
 
@@ -22,24 +22,32 @@ def log_get_token(fn):
22
22
  try:
23
23
  token = fn(*args, **kwargs)
24
24
  _LOGGER.log(
25
- logging.DEBUG if within_credential_chain.get() else logging.INFO, "%s succeeded", fn.__qualname__
25
+ logging.DEBUG if within_credential_chain.get() else logging.INFO,
26
+ "%s succeeded",
27
+ fn.__qualname__,
26
28
  )
27
29
  if _LOGGER.isEnabledFor(logging.DEBUG):
28
30
  try:
29
- base64_meta_data = token.token.split(".")[1].encode("utf-8") + b"=="
30
- json_bytes = base64.decodebytes(base64_meta_data)
31
+ base64_meta_data = token.token.split(".")[1]
32
+ padding_needed = -len(base64_meta_data) % 4
33
+ if padding_needed:
34
+ base64_meta_data += "=" * padding_needed
35
+ json_bytes = base64.urlsafe_b64decode(base64_meta_data)
31
36
  json_string = json_bytes.decode("utf-8")
32
37
  json_dict = json.loads(json_string)
33
38
  upn = json_dict.get("upn", "unavailableUpn")
39
+ appid = json_dict.get("appid", "<unavailable>")
40
+ tid = json_dict.get("tid", "<unavailable>")
41
+ oid = json_dict.get("oid", "<unavailable>")
34
42
  log_string = (
35
- "[Authenticated account] Client ID: {}. Tenant ID: {}. User Principal Name: {}. "
36
- "Object ID (user): {}".format(json_dict["appid"], json_dict["tid"], upn, json_dict["oid"])
43
+ f"[Authenticated account] Client ID: {appid}. "
44
+ f"Tenant ID: {tid}. User Principal Name: {upn}. Object ID (user): {oid}"
37
45
  )
38
46
  _LOGGER.debug(log_string)
39
47
  except Exception as ex: # pylint: disable=broad-except
40
48
  _LOGGER.debug("Failed to log the account information: %s", ex, exc_info=True)
41
49
  return token
42
- except Exception as ex: # pylint: disable=broad-except
50
+ except Exception as ex:
43
51
  _LOGGER.log(
44
52
  logging.DEBUG if within_credential_chain.get() else logging.WARNING,
45
53
  "%s failed: %s",
@@ -67,7 +75,7 @@ def wrap_exceptions(fn):
67
75
  return fn(*args, **kwargs)
68
76
  except ClientAuthenticationError:
69
77
  raise
70
- except Exception as ex: # pylint:disable=broad-except
78
+ except Exception as ex:
71
79
  auth_error = ClientAuthenticationError(message="Authentication failed: {}".format(ex))
72
80
  raise auth_error from ex
73
81
 
@@ -224,7 +224,7 @@ class InteractiveCredential(MsalCredential, ABC):
224
224
 
225
225
  # this may be the first authentication, or the user may have authenticated a different identity
226
226
  self._auth_record = _build_auth_record(result)
227
- except Exception as ex: # pylint:disable=broad-except
227
+ except Exception as ex:
228
228
  _LOGGER.warning(
229
229
  "%s.%s failed: %s",
230
230
  self.__class__.__name__,
@@ -19,7 +19,6 @@ from .._internal.pipeline import build_pipeline
19
19
 
20
20
 
21
21
  class ManagedIdentityClientBase(abc.ABC):
22
- # pylint:disable=missing-client-constructor-parameter-credential
23
22
  def __init__(
24
23
  self,
25
24
  request_factory: Callable[[str, dict], HttpRequest],
@@ -7,7 +7,7 @@ from typing import Any, Dict, Optional, Union
7
7
 
8
8
  from azure.core.exceptions import ClientAuthenticationError
9
9
  from azure.core.pipeline.policies import ContentDecodePolicy
10
- from azure.core.pipeline.transport import ( # pylint:disable=unknown-option-value,no-legacy-azure-core-http-response-import
10
+ from azure.core.pipeline.transport import ( # pylint:disable=no-legacy-azure-core-http-response-import
11
11
  HttpRequest,
12
12
  HttpResponse,
13
13
  )
@@ -59,6 +59,7 @@ class MsalManagedIdentityClient(abc.ABC): # pylint:disable=client-accepts-api-v
59
59
  token_type=result.get("token_type", "Bearer"),
60
60
  refresh_on=refresh_on,
61
61
  )
62
+ error_desc = ""
62
63
  if result and "error" in result:
63
64
  error_desc = cast(str, result["error"])
64
65
  error_message = self.get_unavailable_message(error_desc)
@@ -186,7 +187,7 @@ class MsalManagedIdentityClient(abc.ABC): # pylint:disable=client-accepts-api-v
186
187
  exc_info=_LOGGER.isEnabledFor(logging.DEBUG),
187
188
  )
188
189
  raise ClientAuthenticationError(self.get_unavailable_message(str(ex))) from ex
189
- except Exception as ex: # pylint:disable=broad-except
190
+ except Exception as ex:
190
191
  _LOGGER.log(
191
192
  logging.DEBUG if within_credential_chain.get() else logging.WARNING,
192
193
  "%s.%s failed: %s",
@@ -88,7 +88,7 @@ class SharedTokenCacheBase(ABC): # pylint: disable=too-many-instance-attributes
88
88
  authority: Optional[str] = None,
89
89
  tenant_id: Optional[str] = None,
90
90
  **kwargs: Any
91
- ) -> None: # pylint:disable=unused-argument
91
+ ) -> None:
92
92
  self._authority = normalize_authority(authority) if authority else get_default_authority()
93
93
  environment = urlparse(self._authority).netloc
94
94
  self._environment_aliases = KNOWN_ALIASES.get(environment) or frozenset((environment,))
@@ -246,7 +246,7 @@ class SharedTokenCacheBase(ABC): # pylint: disable=too-many-instance-attributes
246
246
  return AccessTokenInfo(
247
247
  token["secret"], expires_on, token_type=token.get("token_type", "Bearer"), refresh_on=refresh_on
248
248
  )
249
- except Exception as ex: # pylint:disable=broad-except
249
+ except Exception as ex:
250
250
  message = "Error accessing cached data: {}".format(ex)
251
251
  raise CredentialUnavailableError(message=message) from ex
252
252
 
@@ -262,7 +262,7 @@ class SharedTokenCacheBase(ABC): # pylint: disable=too-many-instance-attributes
262
262
  msal.TokenCache.CredentialType.REFRESH_TOKEN, query={"home_account_id": account["home_account_id"]}
263
263
  )
264
264
  return [token["secret"] for token in cache_entries if "secret" in token]
265
- except Exception as ex: # pylint:disable=broad-except
265
+ except Exception as ex:
266
266
  message = "Error accessing cached data: {}".format(ex)
267
267
  raise CredentialUnavailableError(message=message) from ex
268
268
 
@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
20
20
 
21
21
  VALID_TENANT_ID_CHARACTERS = frozenset(ascii_letters + digits + "-.")
22
22
  VALID_SCOPE_CHARACTERS = frozenset(ascii_letters + digits + "_-.:/")
23
+ VALID_SUBSCRIPTION_CHARACTERS = frozenset(ascii_letters + digits + "_-. ")
23
24
 
24
25
 
25
26
  def normalize_authority(authority: str) -> str:
@@ -68,7 +69,21 @@ def validate_tenant_id(tenant_id: str) -> None:
68
69
  if not tenant_id or any(c not in VALID_TENANT_ID_CHARACTERS for c in tenant_id):
69
70
  raise ValueError(
70
71
  "Invalid tenant ID provided. You can locate your tenant ID by following the instructions here: "
71
- + "https://learn.microsoft.com/partner-center/find-ids-and-domain-names"
72
+ "https://learn.microsoft.com/partner-center/find-ids-and-domain-names"
73
+ )
74
+
75
+
76
+ def validate_subscription(subscription: str) -> None:
77
+ """Raise ValueError if subscription is empty or contains a character invalid for a subscription name/ID.
78
+
79
+ :param str subscription: subscription ID to validate
80
+ :raises: ValueError if subscription is empty or contains a character invalid for a subscription ID.
81
+ """
82
+ if not subscription or any(c not in VALID_SUBSCRIPTION_CHARACTERS for c in subscription):
83
+ raise ValueError(
84
+ f"Subscription '{subscription}' contains invalid characters. If this is the name of a subscription, use "
85
+ "its ID instead. You can locate your subscription by following the instructions listed here: "
86
+ "https://learn.microsoft.com/azure/azure-portal/get-subscription-tenant-id"
72
87
  )
73
88
 
74
89
 
@@ -77,7 +92,7 @@ def resolve_tenant(
77
92
  tenant_id: Optional[str] = None,
78
93
  *,
79
94
  additionally_allowed_tenants: Optional[List[str]] = None,
80
- **_
95
+ **_,
81
96
  ) -> str:
82
97
  """Returns the correct tenant for a token request given a credential's configuration.
83
98
 
@@ -2,4 +2,4 @@
2
2
  # Copyright (c) Microsoft Corporation.
3
3
  # Licensed under the MIT License.
4
4
  # ------------------------------------
5
- VERSION = "1.19.0"
5
+ VERSION = "1.20.0"
@@ -3,6 +3,7 @@
3
3
  # Licensed under the MIT License.
4
4
  # ------------------------------------
5
5
  import asyncio
6
+ import logging
6
7
  import os
7
8
  import shutil
8
9
  import sys
@@ -26,6 +27,9 @@ from ..._credentials.azd_cli import (
26
27
  from ..._internal import resolve_tenant, within_dac, validate_tenant_id, validate_scope
27
28
 
28
29
 
30
+ _LOGGER = logging.getLogger(__name__)
31
+
32
+
29
33
  class AzureDeveloperCliCredential(AsyncContextManager):
30
34
  """Authenticates by requesting a token from the Azure Developer CLI.
31
35
 
@@ -153,8 +157,9 @@ class AzureDeveloperCliCredential(AsyncContextManager):
153
157
  for scope in scopes:
154
158
  validate_scope(scope)
155
159
 
156
- commandString = " --scope ".join(scopes)
157
- command = COMMAND_LINE.format(commandString)
160
+ command_args = COMMAND_LINE.copy()
161
+ for scope in scopes:
162
+ command_args += ["--scope", scope]
158
163
  tenant = resolve_tenant(
159
164
  default_tenant=self.tenant_id,
160
165
  tenant_id=tenant_id,
@@ -163,8 +168,8 @@ class AzureDeveloperCliCredential(AsyncContextManager):
163
168
  )
164
169
 
165
170
  if tenant:
166
- command += " --tenant-id " + tenant
167
- output = await _run_command(command, self._process_timeout)
171
+ command_args += ["--tenant-id", tenant]
172
+ output = await _run_command(command_args, self._process_timeout)
168
173
 
169
174
  token = parse_token(output)
170
175
  if not token:
@@ -184,19 +189,17 @@ class AzureDeveloperCliCredential(AsyncContextManager):
184
189
  """Calling this method is unnecessary"""
185
190
 
186
191
 
187
- async def _run_command(command: str, timeout: int) -> str:
192
+ async def _run_command(command_args: List[str], timeout: int) -> str:
188
193
  # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
189
- if shutil.which(EXECUTABLE_NAME) is None:
194
+ azd_path = shutil.which(EXECUTABLE_NAME)
195
+ if not azd_path:
190
196
  raise CredentialUnavailableError(message=CLI_NOT_FOUND)
191
197
 
192
- if sys.platform.startswith("win"):
193
- args = ("cmd", "/c " + command)
194
- else:
195
- args = ("/bin/sh", "-c", command)
196
-
198
+ args = [azd_path] + command_args
197
199
  working_directory = get_safe_working_dir()
198
200
 
199
201
  try:
202
+ _LOGGER.debug("Executing subprocess with the following arguments %s", args)
200
203
  proc = await asyncio.create_subprocess_exec(
201
204
  *args,
202
205
  stdout=asyncio.subprocess.PIPE,
@@ -3,6 +3,7 @@
3
3
  # Licensed under the MIT License.
4
4
  # ------------------------------------
5
5
  import asyncio
6
+ import logging
6
7
  import os
7
8
  import shutil
8
9
  import sys
@@ -23,7 +24,17 @@ from ..._credentials.azure_cli import (
23
24
  parse_token,
24
25
  sanitize_output,
25
26
  )
26
- from ..._internal import _scopes_to_resource, resolve_tenant, within_dac, validate_tenant_id, validate_scope
27
+ from ..._internal import (
28
+ _scopes_to_resource,
29
+ resolve_tenant,
30
+ within_dac,
31
+ validate_tenant_id,
32
+ validate_scope,
33
+ validate_subscription,
34
+ )
35
+
36
+
37
+ _LOGGER = logging.getLogger(__name__)
27
38
 
28
39
 
29
40
  class AzureCliCredential(AsyncContextManager):
@@ -32,6 +43,8 @@ class AzureCliCredential(AsyncContextManager):
32
43
  This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity.
33
44
 
34
45
  :keyword str tenant_id: Optional tenant to include in the token request.
46
+ :keyword str subscription: The name or ID of a subscription. Set this to acquire tokens for an account other
47
+ than the Azure CLI's current account.
35
48
  :keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
36
49
  for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
37
50
  acquire tokens for any tenant the application can access.
@@ -51,12 +64,17 @@ class AzureCliCredential(AsyncContextManager):
51
64
  self,
52
65
  *,
53
66
  tenant_id: str = "",
67
+ subscription: Optional[str] = None,
54
68
  additionally_allowed_tenants: Optional[List[str]] = None,
55
69
  process_timeout: int = 10,
56
70
  ) -> None:
57
71
  if tenant_id:
58
72
  validate_tenant_id(tenant_id)
73
+ if subscription:
74
+ validate_subscription(subscription)
75
+
59
76
  self.tenant_id = tenant_id
77
+ self.subscription = subscription
60
78
  self._additionally_allowed_tenants = additionally_allowed_tenants or []
61
79
  self._process_timeout = process_timeout
62
80
 
@@ -131,7 +149,7 @@ class AzureCliCredential(AsyncContextManager):
131
149
  validate_scope(scope)
132
150
 
133
151
  resource = _scopes_to_resource(*scopes)
134
- command = COMMAND_LINE.format(resource)
152
+ command_args = COMMAND_LINE + ["--resource", resource]
135
153
  tenant = resolve_tenant(
136
154
  default_tenant=self.tenant_id,
137
155
  tenant_id=tenant_id,
@@ -140,8 +158,11 @@ class AzureCliCredential(AsyncContextManager):
140
158
  )
141
159
 
142
160
  if tenant:
143
- command += " --tenant " + tenant
144
- output = await _run_command(command, self._process_timeout)
161
+ command_args += ["--tenant", tenant]
162
+
163
+ if self.subscription:
164
+ command_args += ["--subscription", self.subscription]
165
+ output = await _run_command(command_args, self._process_timeout)
145
166
 
146
167
  token = parse_token(output)
147
168
  if not token:
@@ -161,19 +182,16 @@ class AzureCliCredential(AsyncContextManager):
161
182
  """Calling this method is unnecessary"""
162
183
 
163
184
 
164
- async def _run_command(command: str, timeout: int) -> str:
185
+ async def _run_command(command_args: List[str], timeout: int) -> str:
165
186
  # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
166
- if shutil.which(EXECUTABLE_NAME) is None:
187
+ az_path = shutil.which(EXECUTABLE_NAME)
188
+ if not az_path:
167
189
  raise CredentialUnavailableError(message=CLI_NOT_FOUND)
168
190
 
169
- if sys.platform.startswith("win"):
170
- args = ("cmd", "/c " + command)
171
- else:
172
- args = ("/bin/sh", "-c", command)
173
-
191
+ args = [az_path] + command_args
174
192
  working_directory = get_safe_working_dir()
175
-
176
193
  try:
194
+ _LOGGER.debug("Executing subprocess with the following arguments %s", args)
177
195
  proc = await asyncio.create_subprocess_exec(
178
196
  *args,
179
197
  stdout=asyncio.subprocess.PIPE,
@@ -89,7 +89,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
89
89
  :caption: Create a DefaultAzureCredential.
90
90
  """
91
91
 
92
- def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements, too-many-locals
92
+ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements
93
93
  if "tenant_id" in kwargs:
94
94
  raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.")
95
95
 
@@ -145,7 +145,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
145
145
  WorkloadIdentityCredential(
146
146
  client_id=cast(str, client_id),
147
147
  tenant_id=workload_identity_tenant_id,
148
- file=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
148
+ token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
149
149
  **kwargs
150
150
  )
151
151
  )
@@ -37,7 +37,7 @@ class ImdsCredential(AsyncContextManager, GetTokenMixin):
37
37
  async def _acquire_token_silently(self, *scopes: str, **kwargs: Any) -> Optional[AccessTokenInfo]:
38
38
  return self._client.get_cached_token(*scopes)
39
39
 
40
- async def _request_token(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo: # pylint:disable=unused-argument
40
+ async def _request_token(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
41
41
 
42
42
  if within_credential_chain.get() and not self._endpoint_available:
43
43
  # If within a chain (e.g. DefaultAzureCredential), we do a quick check to see if the IMDS endpoint
@@ -49,7 +49,7 @@ class ImdsCredential(AsyncContextManager, GetTokenMixin):
49
49
  # IMDS responded
50
50
  _check_forbidden_response(ex)
51
51
  self._endpoint_available = True
52
- except Exception as ex: # pylint:disable=broad-except
52
+ except Exception as ex:
53
53
  error_message = (
54
54
  "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
55
55
  )
@@ -78,7 +78,7 @@ class ImdsCredential(AsyncContextManager, GetTokenMixin):
78
78
  _check_forbidden_response(ex)
79
79
  # any other error is unexpected
80
80
  raise ClientAuthenticationError(message=ex.message, response=ex.response) from ex
81
- except Exception as ex: # pylint:disable=broad-except
81
+ except Exception as ex:
82
82
  # if anything else was raised, assume the endpoint is unavailable
83
83
  error_message = "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
84
84
  raise CredentialUnavailableError(error_message) from ex
@@ -96,7 +96,7 @@ class ManagedIdentityCredential(AsyncContextManager):
96
96
  self._credential = WorkloadIdentityCredential(
97
97
  tenant_id=os.environ[EnvironmentVariables.AZURE_TENANT_ID],
98
98
  client_id=workload_client_id,
99
- file=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
99
+ token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE],
100
100
  **kwargs
101
101
  )
102
102
  else: