11import functools
22import getpass
3+ import json
34import logging
45from typing import TYPE_CHECKING , Callable , Optional , Type , cast
56from urllib .parse import urlparse
67
78import requests
8- from id import detect_credential # type: ignore
9+ from id import AmbientCredentialError # type: ignore
10+ from id import detect_credential
911
1012# keyring has an indirect dependency on PyCA cryptography, which has no
1113# pre-built wheels for ppc64le and s390x, see #1158.
@@ -61,24 +63,14 @@ def username(self) -> Optional[str]:
6163 @property
6264 @functools .lru_cache ()
6365 def password (self ) -> Optional [str ]:
64- if (
65- self .is_pypi ()
66- and self .username == "__token__"
67- and self .input .password is None
68- ):
69- logger .info (
70- "Trying to use trusted publishing (no token was explicitly provided)"
71- )
72- return self .make_trusted_publishing_token ()
73-
7466 return utils .get_userpass_value (
7567 self .input .password ,
7668 self .config ,
7769 key = "password" ,
78- prompt_strategy = self .password_from_keyring_or_prompt ,
70+ prompt_strategy = self .password_from_keyring_or_trusted_publishing_or_prompt ,
7971 )
8072
81- def make_trusted_publishing_token (self ) -> str :
73+ def make_trusted_publishing_token (self ) -> Optional [ str ] :
8274 # Trusted publishing (OpenID Connect): get one token from the CI
8375 # system, and exchange that for a PyPI token.
8476 repository_domain = cast (str , urlparse (self .system ).netloc )
@@ -91,7 +83,20 @@ def make_trusted_publishing_token(self) -> str:
9183 resp .raise_for_status ()
9284 audience = cast (str , resp .json ()["audience" ])
9385
94- oidc_token = detect_credential (audience )
86+ try :
87+ oidc_token = detect_credential (audience )
88+ except AmbientCredentialError as e :
89+ # If we get here, we're on a supported CI platform for trusted
90+ # publishing, and we have not been given any token, so we can error.
91+ raise exceptions .TrustedPublishingFailure (
92+ "Unable to retrieve an OIDC token from the CI platform for "
93+ f"trusted publishing { e } "
94+ )
95+
96+ if oidc_token is None :
97+ logger .debug ("This environment is not supported for trusted publishing" )
98+ return None # Fall back to prompting for a token (if possible)
99+
95100 logger .debug ("Got OIDC token for audience %s" , audience )
96101
97102 token_exchange_url = f"https://{ repository_domain } /_/oidc/mint-token"
@@ -101,9 +106,25 @@ def make_trusted_publishing_token(self) -> str:
101106 json = {"token" : oidc_token },
102107 timeout = 5 , # S113 wants a timeout
103108 )
104- mint_token_resp .raise_for_status ()
109+ try :
110+ mint_token_payload = mint_token_resp .json ()
111+ except json .JSONDecodeError :
112+ raise exceptions .TrustedPublishingFailure (
113+ "The token-minting request returned invalid JSON"
114+ )
115+
116+ if not mint_token_resp .ok :
117+ reasons = "\n " .join (
118+ f'* `{ error ["code" ]} `: { error ["description" ]} '
119+ for error in mint_token_payload ["errors" ]
120+ )
121+ raise exceptions .TrustedPublishingFailure (
122+ "The token request failed; the index server gave the following "
123+ f"reasons:\n \n { reasons } "
124+ )
125+
105126 logger .debug ("Minted upload token for trusted publishing" )
106- return cast (str , mint_token_resp . json () ["token" ])
127+ return cast (str , mint_token_payload ["token" ])
107128
108129 @property
109130 def system (self ) -> Optional [str ]:
@@ -147,12 +168,19 @@ def username_from_keyring_or_prompt(self) -> str:
147168
148169 return self .prompt ("username" , input )
149170
150- def password_from_keyring_or_prompt (self ) -> str :
171+ def password_from_keyring_or_trusted_publishing_or_prompt (self ) -> str :
151172 password = self .get_password_from_keyring ()
152173 if password :
153174 logger .info ("password set from keyring" )
154175 return password
155176
177+ if self .is_pypi () and self .username == "__token__" :
178+ logger .debug (
179+ "Trying to use trusted publishing (no token was explicitly provided)"
180+ )
181+ if (token := self .make_trusted_publishing_token ()) is not None :
182+ return token
183+
156184 # Prompt for API token when required.
157185 what = "API token" if self .is_pypi () else "password"
158186
0 commit comments