Skip to content

Commit ee6a7d9

Browse files
Wauplinabidlabshanouticelina
authored
Add optional cache to whoami (#3568)
* Add optional cache to whoami * Update src/huggingface_hub/hf_api.py Co-authored-by: Abubakar Abid <[email protected]> * Update src/huggingface_hub/hf_api.py Co-authored-by: Abubakar Abid <[email protected]> * Fix token=False not working * add test for token=False * Update src/huggingface_hub/hf_api.py Co-authored-by: célina <[email protected]> * Update src/huggingface_hub/hf_api.py Co-authored-by: célina <[email protected]> * code quality --------- Co-authored-by: Abubakar Abid <[email protected]> Co-authored-by: célina <[email protected]>
1 parent 5e9ad43 commit ee6a7d9

File tree

2 files changed

+94
-13
lines changed

2 files changed

+94
-13
lines changed

src/huggingface_hub/hf_api.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
BadRequestError,
7676
GatedRepoError,
7777
HfHubHTTPError,
78+
LocalTokenNotFoundError,
7879
RemoteEntryNotFoundError,
7980
RepositoryNotFoundError,
8081
RevisionNotFoundError,
@@ -1694,6 +1695,9 @@ def __init__(
16941695
self.headers = headers
16951696
self._thread_pool: Optional[ThreadPoolExecutor] = None
16961697

1698+
# /whoami-v2 is the only endpoint for which we may want to cache results
1699+
self._whoami_cache: dict[str, dict] = {}
1700+
16971701
def run_as_future(self, fn: Callable[..., R], *args, **kwargs) -> Future[R]:
16981702
"""
16991703
Run a method in the background and return a Future instance.
@@ -1735,39 +1739,74 @@ def run_as_future(self, fn: Callable[..., R], *args, **kwargs) -> Future[R]:
17351739
return self._thread_pool.submit(fn, *args, **kwargs)
17361740

17371741
@validate_hf_hub_args
1738-
def whoami(self, token: Union[bool, str, None] = None) -> dict:
1742+
def whoami(self, token: Union[bool, str, None] = None, *, cache: bool = False) -> dict:
17391743
"""
17401744
Call HF API to know "whoami".
17411745
1746+
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
1747+
`whoami` multiple times as this endpoint is heavily rate-limited for security reasons.
1748+
17421749
Args:
17431750
token (`bool` or `str`, *optional*):
17441751
A valid user access token (string). Defaults to the locally saved
17451752
token, which is the recommended method for authentication (see
17461753
https://huggingface.co/docs/huggingface_hub/quick-start#authentication).
17471754
To disable authentication, pass `False`.
1755+
cache (`bool`, *optional*):
1756+
Whether to cache the result of the `whoami` call for subsequent calls.
1757+
If an error occurs during the first call, it won't be cached.
1758+
Defaults to `False`.
17481759
"""
17491760
# Get the effective token using the helper function get_token
1750-
effective_token = token or self.token or get_token() or True
1761+
token = self.token if token is None else token
1762+
if token is False:
1763+
raise ValueError("Cannot use `token=False` with `whoami` method as it requires authentication.")
1764+
if token is True or token is None:
1765+
token = get_token()
1766+
if token is None:
1767+
raise LocalTokenNotFoundError(
1768+
"Token is required to call the /whoami-v2 endpoint, but no token found. You must provide a token or be logged in to "
1769+
"Hugging Face with `hf auth login` or `huggingface_hub.login`. See https://huggingface.co/settings/tokens."
1770+
)
1771+
1772+
if cache and (cached_token := self._whoami_cache.get(token)):
1773+
return cached_token
1774+
1775+
# Call Hub
1776+
output = self._inner_whoami(token=token)
1777+
1778+
# Cache result and return
1779+
if cache:
1780+
self._whoami_cache[token] = output
1781+
return output
1782+
1783+
def _inner_whoami(self, token: str) -> dict:
17511784
r = get_session().get(
17521785
f"{self.endpoint}/api/whoami-v2",
1753-
headers=self._build_hf_headers(token=effective_token),
1786+
headers=self._build_hf_headers(token=token),
17541787
)
17551788
try:
17561789
hf_raise_for_status(r)
17571790
except HfHubHTTPError as e:
17581791
if e.response.status_code == 401:
17591792
error_message = "Invalid user token."
17601793
# Check which token is the effective one and generate the error message accordingly
1761-
if effective_token == _get_token_from_google_colab():
1794+
if token == _get_token_from_google_colab():
17621795
error_message += " The token from Google Colab vault is invalid. Please update it from the UI."
1763-
elif effective_token == _get_token_from_environment():
1796+
elif token == _get_token_from_environment():
17641797
error_message += (
17651798
" The token from HF_TOKEN environment variable is invalid. "
17661799
"Note that HF_TOKEN takes precedence over `hf auth login`."
17671800
)
1768-
elif effective_token == _get_token_from_file():
1801+
elif token == _get_token_from_file():
17691802
error_message += " The token stored is invalid. Please run `hf auth login` to update it."
17701803
raise HfHubHTTPError(error_message, response=e.response) from e
1804+
if e.response.status_code == 429:
1805+
error_message = (
1806+
"You've hit the rate limit for the /whoami-v2 endpoint, which is intentionally strict for security reasons."
1807+
" If you're calling it often, consider caching the response with `whoami(..., cache=True)`."
1808+
)
1809+
raise HfHubHTTPError(error_message, response=e.response) from e
17711810
raise
17721811
return r.json()
17731812

tests/test_hf_api.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,68 @@ def test_file_exists(self):
170170
class HfApiEndpointsTest(HfApiCommonTest):
171171
def test_whoami_with_passing_token(self):
172172
info = self._api.whoami(token=self._token)
173-
self.assertEqual(info["name"], USER)
174-
self.assertEqual(info["fullname"], FULL_NAME)
175-
self.assertIsInstance(info["orgs"], list)
173+
assert info["name"] == USER
174+
assert info["fullname"] == FULL_NAME
175+
assert isinstance(info["orgs"], list)
176176
valid_org = [org for org in info["orgs"] if org["name"] == "valid_org"][0]
177-
self.assertEqual(valid_org["fullname"], "Dummy Org")
177+
assert valid_org["fullname"] == "Dummy Org"
178178

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

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

193+
def test_whoami_with_caching(self) -> None:
194+
# Don't use class instance to avoid cache sharing
195+
api = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN)
196+
assert api._whoami_cache == {}
197+
198+
assert api.whoami(cache=True)["name"] == USER
199+
200+
# Value in cache
201+
assert len(api._whoami_cache) == 1
202+
assert TOKEN in api._whoami_cache
203+
mocked_value = Mock()
204+
api._whoami_cache[TOKEN] = mocked_value
205+
206+
# Call again => use cache
207+
assert api.whoami(cache=True) == mocked_value
208+
209+
# Cache not shared between HfApi instances
210+
api_bis = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN)
211+
assert api_bis._whoami_cache == {}
212+
assert api_bis.whoami(cache=True)["name"] == USER
213+
214+
def test_whoami_rate_limit_suggest_caching(self) -> None:
215+
with patch("huggingface_hub.hf_api.hf_raise_for_status") as mock:
216+
mock.side_effect = HfHubHTTPError(message="Fake error.", response=Mock(status_code=429))
217+
with pytest.raises(
218+
HfHubHTTPError, match=r".*consider caching the response with `whoami\(..., cache=True\)`.*"
219+
):
220+
self._api.whoami()
221+
222+
def test_whoami_with_token_false(self):
223+
"""Test that using `token=False` raises an error.
224+
225+
Regression test for https://github.com/huggingface/huggingface_hub/pull/3568#discussion_r2557248898.
226+
227+
Before the fix, local token was used even when `token=False` was passed (which is not intended).
228+
"""
229+
with self.assertRaises(ValueError):
230+
self._api.whoami(token=False)
231+
232+
with self.assertRaises(ValueError):
233+
HfApi(token=False).whoami()
234+
193235
def test_delete_repo_error_message(self):
194236
# test for #751
195237
# See https://github.com/huggingface/huggingface_hub/issues/751

0 commit comments

Comments
 (0)