Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions src/huggingface_hub/hf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
BadRequestError,
GatedRepoError,
HfHubHTTPError,
LocalTokenNotFoundError,
RemoteEntryNotFoundError,
RepositoryNotFoundError,
RevisionNotFoundError,
Expand Down Expand Up @@ -1694,6 +1695,9 @@ def __init__(
self.headers = headers
self._thread_pool: Optional[ThreadPoolExecutor] = None

# /whoami-v2 is the only endpoint for which we may want to cache results
self._whoami_cache: dict[str, dict] = {}

def run_as_future(self, fn: Callable[..., R], *args, **kwargs) -> Future[R]:
"""
Run a method in the background and return a Future instance.
Expand Down Expand Up @@ -1735,39 +1739,76 @@ def run_as_future(self, fn: Callable[..., R], *args, **kwargs) -> Future[R]:
return self._thread_pool.submit(fn, *args, **kwargs)

@validate_hf_hub_args
def whoami(self, token: Union[bool, str, None] = None) -> dict:
def whoami(self, token: Union[bool, str, None] = None, *, cache: bool = False) -> dict:
"""
Call HF API to know "whoami".

If passing `cache=True`, the result will be cached for subsequent calls for the duration of the Python process. This is useful if you plan to call
`whoami` multiple times as this endpoint is heavily rate-limited for security reasons.

Args:
token (`bool` or `str`, *optional*):
A valid user access token (string). Defaults to the locally saved
token, which is the recommended method for authentication (see
https://huggingface.co/docs/huggingface_hub/quick-start#authentication).
To disable authentication, pass `False`.
cache (`bool`, *optional*):
Whether to cache the result of the `whoami` call for subsequent calls.
If an error occurs during the first call, it won't be cached.
Defaults to `False`.
"""
# Get the effective token using the helper function get_token
effective_token = token or self.token or get_token() or True
token = token or self.token
if token is False:
raise ValueError("Cannot use `token=False` with `whoami` method as it requires authentication.")
Comment on lines +1762 to +1763
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change in behavior, right? Previously, if token=False, we could still get the token from get_token(), but now we raise an exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, since token=False explicitly means "do not retrieve the token locally". The breaking change is that previously it would raise an HTTP 401 unauthorized issue, and now a ValueError. I don't believe though that it's better like this to fail early.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I had to step away earlier but to clarify, I do agree that this change is the way to go and is inline with the docstring. But what I was saying was that previously, you would actually use the locally-saved token, even if a user passed token=False, because token or self.token or get_token() would keep going until a truthy value was found, so the logic would still fall back to a local token via get_token().

Unless I'm misunderstanding something, this would be a breaking change -- although I do agree that this is the right way to go. In fact, maybe a nit but I'd say we go even farther, and replace token = self.token with token = self.token if token is None else token, because again, this would mean that if a user does whoami(token=False), but has a token saved to the class would end up using that token, which is contrary to the docstring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^agree that we should do token = self.token if token is None else token since token can be False (as we do in _build_hf_headers).

if token is True or token is None:
effective_token = get_token()
if effective_token is None:
raise LocalTokenNotFoundError(
"Token is required to call the /whoami-v2 endpoint, but no token found. You must provide a token or be logged in to "
"Hugging Face with `hf auth login` or `huggingface_hub.login`. See https://huggingface.co/settings/tokens."
)
token = effective_token
Comment on lines +1764 to +1771
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) not sure if we need an intermediate variable here

Suggested change
if token is True or token is None:
effective_token = get_token()
if effective_token is None:
raise LocalTokenNotFoundError(
"Token is required to call the /whoami-v2 endpoint, but no token found. You must provide a token or be logged in to "
"Hugging Face with `hf auth login` or `huggingface_hub.login`. See https://huggingface.co/settings/tokens."
)
token = effective_token
if token is True or token is None:
token = get_token()
if token is None:
raise LocalTokenNotFoundError(
"Token is required to call the /whoami-v2 endpoint, but no token found. You must provide a token or be logged in to "
"Hugging Face with `hf auth login` or `huggingface_hub.login`. See https://huggingface.co/settings/tokens."
)


if cache:
if token in self._whoami_cache:
return self._whoami_cache[token]
Comment on lines +1773 to +1775
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an excuse to use more Walrus operators in the codebase :)

Suggested change
if cache:
if token in self._whoami_cache:
return self._whoami_cache[token]
if cache and (cached_token := self._whoami_cache.get(token)):
return cached_token


# Call Hub
output = self._inner_whoami(token=token)

# Cache result and return
if cache:
self._whoami_cache[token] = output
return output

def _inner_whoami(self, token: str) -> dict:
r = get_session().get(
f"{self.endpoint}/api/whoami-v2",
headers=self._build_hf_headers(token=effective_token),
headers=self._build_hf_headers(token=token),
)
try:
hf_raise_for_status(r)
except HfHubHTTPError as e:
if e.response.status_code == 401:
error_message = "Invalid user token."
# Check which token is the effective one and generate the error message accordingly
if effective_token == _get_token_from_google_colab():
if token == _get_token_from_google_colab():
error_message += " The token from Google Colab vault is invalid. Please update it from the UI."
elif effective_token == _get_token_from_environment():
elif token == _get_token_from_environment():
error_message += (
" The token from HF_TOKEN environment variable is invalid. "
"Note that HF_TOKEN takes precedence over `hf auth login`."
)
elif effective_token == _get_token_from_file():
elif token == _get_token_from_file():
error_message += " The token stored is invalid. Please run `hf auth login` to update it."
raise HfHubHTTPError(error_message, response=e.response) from e
if e.response.status_code == 429:
error_message = (
"You've hit the rate limit for the /whoami-v2 endpoint, which is intentionally strict for security reasons."
" If you're calling it often, consider caching the response with `whoami(..., cache=True)`."
)
raise HfHubHTTPError(error_message, response=e.response) from e
raise
return r.json()

Expand Down
43 changes: 36 additions & 7 deletions tests/test_hf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,26 +170,55 @@ def test_file_exists(self):
class HfApiEndpointsTest(HfApiCommonTest):
def test_whoami_with_passing_token(self):
info = self._api.whoami(token=self._token)
self.assertEqual(info["name"], USER)
self.assertEqual(info["fullname"], FULL_NAME)
self.assertIsInstance(info["orgs"], list)
assert info["name"] == USER
assert info["fullname"] == FULL_NAME
assert isinstance(info["orgs"], list)
valid_org = [org for org in info["orgs"] if org["name"] == "valid_org"][0]
self.assertEqual(valid_org["fullname"], "Dummy Org")
assert valid_org["fullname"] == "Dummy Org"

@patch("huggingface_hub.utils._headers.get_token", return_value=TOKEN)
@patch("huggingface_hub.hf_api.get_token", return_value=TOKEN)
def test_whoami_with_implicit_token_from_login(self, mock_get_token: Mock) -> None:
"""Test using `whoami` after a `hf auth login`."""
with patch.object(self._api, "token", None): # no default token
info = self._api.whoami()
self.assertEqual(info["name"], USER)
assert info["name"] == USER

@patch("huggingface_hub.utils._headers.get_token")
def test_whoami_with_implicit_token_from_hf_api(self, mock_get_token: Mock) -> None:
"""Test using `whoami` with token from the HfApi client."""
info = self._api.whoami()
self.assertEqual(info["name"], USER)
assert info["name"] == USER
mock_get_token.assert_not_called()

def test_whoami_with_caching(self) -> None:
# Don't use class instance to avoid cache sharing
api = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN)
assert api._whoami_cache == {}

assert api.whoami(cache=True)["name"] == USER

# Value in cache
assert len(api._whoami_cache) == 1
assert TOKEN in api._whoami_cache
mocked_value = Mock()
api._whoami_cache[TOKEN] = mocked_value

# Call again => use cache
assert api.whoami(cache=True) == mocked_value

# Cache not shared between HfApi instances
api_bis = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN)
assert api_bis._whoami_cache == {}
assert api_bis.whoami(cache=True)["name"] == USER

def test_whoami_rate_limit_suggest_caching(self) -> None:
with patch("huggingface_hub.hf_api.hf_raise_for_status") as mock:
mock.side_effect = HfHubHTTPError(message="Fake error.", response=Mock(status_code=429))
with pytest.raises(
HfHubHTTPError, match=r".*consider caching the response with `whoami\(..., cache=True\)`.*"
):
self._api.whoami()

def test_delete_repo_error_message(self):
# test for #751
# See https://github.com/huggingface/huggingface_hub/issues/751
Expand Down
Loading