huggingface-hub 1.0.0rc6__py3-none-any.whl → 1.0.0rc7__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.

Potentially problematic release.


This version of huggingface-hub might be problematic. Click here for more details.

Files changed (36) hide show
  1. huggingface_hub/__init__.py +7 -1
  2. huggingface_hub/_commit_api.py +1 -5
  3. huggingface_hub/_jobs_api.py +1 -1
  4. huggingface_hub/_login.py +3 -3
  5. huggingface_hub/_snapshot_download.py +4 -3
  6. huggingface_hub/_upload_large_folder.py +2 -15
  7. huggingface_hub/_webhooks_server.py +1 -1
  8. huggingface_hub/cli/_cli_utils.py +1 -1
  9. huggingface_hub/cli/auth.py +0 -20
  10. huggingface_hub/cli/cache.py +561 -304
  11. huggingface_hub/cli/download.py +2 -2
  12. huggingface_hub/cli/repo.py +0 -7
  13. huggingface_hub/cli/upload.py +0 -8
  14. huggingface_hub/community.py +16 -8
  15. huggingface_hub/constants.py +10 -11
  16. huggingface_hub/file_download.py +9 -61
  17. huggingface_hub/hf_api.py +170 -126
  18. huggingface_hub/hf_file_system.py +31 -6
  19. huggingface_hub/inference/_client.py +1 -1
  20. huggingface_hub/inference/_generated/_async_client.py +1 -1
  21. huggingface_hub/inference/_providers/__init__.py +15 -2
  22. huggingface_hub/inference/_providers/_common.py +39 -0
  23. huggingface_hub/inference/_providers/clarifai.py +13 -0
  24. huggingface_hub/lfs.py +3 -65
  25. huggingface_hub/serialization/_torch.py +1 -1
  26. huggingface_hub/utils/__init__.py +0 -2
  27. huggingface_hub/utils/_cache_manager.py +17 -42
  28. huggingface_hub/utils/_http.py +25 -3
  29. huggingface_hub/utils/_parsing.py +98 -0
  30. huggingface_hub/utils/_runtime.py +1 -14
  31. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/METADATA +4 -14
  32. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/RECORD +36 -34
  33. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/LICENSE +0 -0
  34. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/WHEEL +0 -0
  35. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/entry_points.txt +0 -0
  36. {huggingface_hub-1.0.0rc6.dist-info → huggingface_hub-1.0.0rc7.dist-info}/top_level.txt +0 -0
@@ -102,18 +102,22 @@ class HfFileSystem(fsspec.AbstractFileSystem):
102
102
  *args,
103
103
  endpoint: Optional[str] = None,
104
104
  token: Union[bool, str, None] = None,
105
+ block_size: Optional[int] = None,
105
106
  **storage_options,
106
107
  ):
107
108
  super().__init__(*args, **storage_options)
108
109
  self.endpoint = endpoint or constants.ENDPOINT
109
110
  self.token = token
110
111
  self._api = HfApi(endpoint=endpoint, token=token)
112
+ self.block_size = block_size
111
113
  # Maps (repo_type, repo_id, revision) to a 2-tuple with:
112
114
  # * the 1st element indicating whether the repositoy and the revision exist
113
115
  # * the 2nd element being the exception raised if the repository or revision doesn't exist
114
116
  self._repo_and_revision_exists_cache: dict[
115
117
  tuple[str, str, Optional[str]], tuple[bool, Optional[Exception]]
116
118
  ] = {}
119
+ # Maps parent directory path to path infos
120
+ self.dircache: dict[str, list[dict[str, Any]]] = {}
117
121
 
118
122
  def _repo_and_revision_exist(
119
123
  self, repo_type: str, repo_id: str, revision: Optional[str]
@@ -265,12 +269,15 @@ class HfFileSystem(fsspec.AbstractFileSystem):
265
269
  block_size: Optional[int] = None,
266
270
  **kwargs,
267
271
  ) -> "HfFileSystemFile":
272
+ block_size = block_size if block_size is not None else self.block_size
273
+ if block_size is not None:
274
+ kwargs["block_size"] = block_size
268
275
  if "a" in mode:
269
276
  raise NotImplementedError("Appending to remote files is not yet supported.")
270
277
  if block_size == 0:
271
- return HfFileSystemStreamFile(self, path, mode=mode, revision=revision, block_size=block_size, **kwargs)
278
+ return HfFileSystemStreamFile(self, path, mode=mode, revision=revision, **kwargs)
272
279
  else:
273
- return HfFileSystemFile(self, path, mode=mode, revision=revision, block_size=block_size, **kwargs)
280
+ return HfFileSystemFile(self, path, mode=mode, revision=revision, **kwargs)
274
281
 
275
282
  def _rm(self, path: str, revision: Optional[str] = None, **kwargs) -> None:
276
283
  resolved_path = self.resolve_path(path, revision=revision)
@@ -439,7 +446,7 @@ class HfFileSystem(fsspec.AbstractFileSystem):
439
446
  common_path_depth = common_path[len(path) :].count("/")
440
447
  maxdepth -= common_path_depth
441
448
  out = [o for o in out if not o["name"].startswith(common_path + "/")]
442
- for cached_path in self.dircache:
449
+ for cached_path in list(self.dircache):
443
450
  if cached_path.startswith(common_path + "/"):
444
451
  self.dircache.pop(cached_path, None)
445
452
  self.dircache.pop(common_path, None)
@@ -923,6 +930,18 @@ class HfFileSystem(fsspec.AbstractFileSystem):
923
930
  # See https://github.com/huggingface/huggingface_hub/issues/1733
924
931
  raise NotImplementedError("Transactional commits are not supported.")
925
932
 
933
+ def __reduce__(self):
934
+ # re-populate the instance cache at HfFileSystem._cache and re-populate the cache attributes of every instance
935
+ return make_instance, (
936
+ type(self),
937
+ self.storage_args,
938
+ self.storage_options,
939
+ {
940
+ "dircache": self.dircache,
941
+ "_repo_and_revision_exists_cache": self._repo_and_revision_exists_cache,
942
+ },
943
+ )
944
+
926
945
 
927
946
  class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
928
947
  def __init__(self, fs: HfFileSystem, path: str, revision: Optional[str] = None, **kwargs):
@@ -986,9 +1005,8 @@ class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
986
1005
  def read(self, length=-1):
987
1006
  """Read remote file.
988
1007
 
989
- If `length` is not provided or is -1, the entire file is downloaded and read. On POSIX systems and if
990
- `hf_transfer` is not enabled, the file is loaded in memory directly. Otherwise, the file is downloaded to a
991
- temporary file and read from there.
1008
+ If `length` is not provided or is -1, the entire file is downloaded and read. On POSIX systems the file is
1009
+ loaded in memory directly. Otherwise, the file is downloaded to a temporary file and read from there.
992
1010
  """
993
1011
  if self.mode == "rb" and (length is None or length == -1) and self.loc == 0:
994
1012
  with self.fs.open(self.path, "rb", block_size=0) as f: # block_size=0 enables fast streaming
@@ -1158,3 +1176,10 @@ def _partial_read(response: httpx.Response, length: int = -1) -> bytes:
1158
1176
  return bytes(buf[:length])
1159
1177
 
1160
1178
  return bytes(buf) # may be < length if response ended
1179
+
1180
+
1181
+ def make_instance(cls, args, kwargs, instance_cache_attributes_dict):
1182
+ fs = cls(*args, **kwargs)
1183
+ for attr, cached_value in instance_cache_attributes_dict.items():
1184
+ setattr(fs, attr, cached_value)
1185
+ return fs
@@ -135,7 +135,7 @@ class InferenceClient:
135
135
  Note: for better compatibility with OpenAI's client, `model` has been aliased as `base_url`. Those 2
136
136
  arguments are mutually exclusive. If a URL is passed as `model` or `base_url` for chat completion, the `(/v1)/chat/completions` suffix path will be appended to the URL.
137
137
  provider (`str`, *optional*):
138
- Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `publicai`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"` or `"zai-org"`.
138
+ Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `publicai`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"` or `"zai-org"`.
139
139
  Defaults to "auto" i.e. the first of the providers available for the model, sorted by the user's order in https://hf.co/settings/inference-providers.
140
140
  If model is a URL or `base_url` is passed, then `provider` is not used.
141
141
  token (`str`, *optional*):
@@ -126,7 +126,7 @@ class AsyncInferenceClient:
126
126
  Note: for better compatibility with OpenAI's client, `model` has been aliased as `base_url`. Those 2
127
127
  arguments are mutually exclusive. If a URL is passed as `model` or `base_url` for chat completion, the `(/v1)/chat/completions` suffix path will be appended to the URL.
128
128
  provider (`str`, *optional*):
129
- Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `publicai`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"` or `"zai-org"`.
129
+ Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `publicai`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"` or `"zai-org"`.
130
130
  Defaults to "auto" i.e. the first of the providers available for the model, sorted by the user's order in https://hf.co/settings/inference-providers.
131
131
  If model is a URL or `base_url` is passed, then `provider` is not used.
132
132
  token (`str`, *optional*):
@@ -6,9 +6,10 @@ from huggingface_hub.inference._providers.featherless_ai import (
6
6
  )
7
7
  from huggingface_hub.utils import logging
8
8
 
9
- from ._common import TaskProviderHelper, _fetch_inference_provider_mapping
9
+ from ._common import AutoRouterConversationalTask, TaskProviderHelper, _fetch_inference_provider_mapping
10
10
  from .black_forest_labs import BlackForestLabsTextToImageTask
11
11
  from .cerebras import CerebrasConversationalTask
12
+ from .clarifai import ClarifaiConversationalTask
12
13
  from .cohere import CohereConversationalTask
13
14
  from .fal_ai import (
14
15
  FalAIAutomaticSpeechRecognitionTask,
@@ -50,6 +51,7 @@ logger = logging.get_logger(__name__)
50
51
  PROVIDER_T = Literal[
51
52
  "black-forest-labs",
52
53
  "cerebras",
54
+ "clarifai",
53
55
  "cohere",
54
56
  "fal-ai",
55
57
  "featherless-ai",
@@ -71,6 +73,8 @@ PROVIDER_T = Literal[
71
73
 
72
74
  PROVIDER_OR_POLICY_T = Union[PROVIDER_T, Literal["auto"]]
73
75
 
76
+ CONVERSATIONAL_AUTO_ROUTER = AutoRouterConversationalTask()
77
+
74
78
  PROVIDERS: dict[PROVIDER_T, dict[str, TaskProviderHelper]] = {
75
79
  "black-forest-labs": {
76
80
  "text-to-image": BlackForestLabsTextToImageTask(),
@@ -78,6 +82,9 @@ PROVIDERS: dict[PROVIDER_T, dict[str, TaskProviderHelper]] = {
78
82
  "cerebras": {
79
83
  "conversational": CerebrasConversationalTask(),
80
84
  },
85
+ "clarifai": {
86
+ "conversational": ClarifaiConversationalTask(),
87
+ },
81
88
  "cohere": {
82
89
  "conversational": CohereConversationalTask(),
83
90
  },
@@ -201,13 +208,19 @@ def get_provider_helper(
201
208
 
202
209
  if provider is None:
203
210
  logger.info(
204
- "Defaulting to 'auto' which will select the first provider available for the model, sorted by the user's order in https://hf.co/settings/inference-providers."
211
+ "No provider specified for task `conversational`. Defaulting to server-side auto routing."
212
+ if task == "conversational"
213
+ else "Defaulting to 'auto' which will select the first provider available for the model, sorted by the user's order in https://hf.co/settings/inference-providers."
205
214
  )
206
215
  provider = "auto"
207
216
 
208
217
  if provider == "auto":
209
218
  if model is None:
210
219
  raise ValueError("Specifying a model is required when provider is 'auto'")
220
+ if task == "conversational":
221
+ # Special case: we have a dedicated auto-router for conversational models. No need to fetch provider mapping.
222
+ return CONVERSATIONAL_AUTO_ROUTER
223
+
211
224
  provider_mapping = _fetch_inference_provider_mapping(model)
212
225
  provider = next(iter(provider_mapping)).provider
213
226
 
@@ -24,6 +24,7 @@ HARDCODED_MODEL_INFERENCE_MAPPING: dict[str, dict[str, InferenceProviderMapping]
24
24
  # status="live")
25
25
  "cerebras": {},
26
26
  "cohere": {},
27
+ "clarifai": {},
27
28
  "fal-ai": {},
28
29
  "fireworks-ai": {},
29
30
  "groq": {},
@@ -278,6 +279,44 @@ class BaseConversationalTask(TaskProviderHelper):
278
279
  return filter_none({"messages": inputs, **parameters, "model": provider_mapping_info.provider_id})
279
280
 
280
281
 
282
+ class AutoRouterConversationalTask(BaseConversationalTask):
283
+ """
284
+ Auto-router for conversational tasks.
285
+
286
+ We let the Hugging Face router select the best provider for the model, based on availability and user preferences.
287
+ This is a special case since the selection is done server-side (avoid 1 API call to fetch provider mapping).
288
+ """
289
+
290
+ def __init__(self):
291
+ super().__init__(provider="auto", base_url="https://router.huggingface.co")
292
+
293
+ def _prepare_base_url(self, api_key: str) -> str:
294
+ """Return the base URL to use for the request.
295
+
296
+ Usually not overwritten in subclasses."""
297
+ # Route to the proxy if the api_key is a HF TOKEN
298
+ if not api_key.startswith("hf_"):
299
+ raise ValueError("Cannot select auto-router when using non-Hugging Face API key.")
300
+ else:
301
+ return self.base_url # No `/auto` suffix in the URL
302
+
303
+ def _prepare_mapping_info(self, model: Optional[str]) -> InferenceProviderMapping:
304
+ """
305
+ In auto-router, we don't need to fetch provider mapping info.
306
+ We just return a dummy mapping info with provider_id set to the HF model ID.
307
+ """
308
+ if model is None:
309
+ raise ValueError("Please provide an HF model ID.")
310
+
311
+ return InferenceProviderMapping(
312
+ provider="auto",
313
+ hf_model_id=model,
314
+ providerId=model,
315
+ status="live",
316
+ task="conversational",
317
+ )
318
+
319
+
281
320
  class BaseTextGenerationTask(TaskProviderHelper):
282
321
  """
283
322
  Base class for text-generation (completion) tasks.
@@ -0,0 +1,13 @@
1
+ from ._common import BaseConversationalTask
2
+
3
+
4
+ _PROVIDER = "clarifai"
5
+ _BASE_URL = "https://api.clarifai.com"
6
+
7
+
8
+ class ClarifaiConversationalTask(BaseConversationalTask):
9
+ def __init__(self):
10
+ super().__init__(provider=_PROVIDER, base_url=_BASE_URL)
11
+
12
+ def _prepare_route(self, mapped_model: str, api_key: str) -> str:
13
+ return "/v2/ext/openai/v1/chat/completions"
huggingface_hub/lfs.py CHANGED
@@ -16,11 +16,9 @@
16
16
 
17
17
  import io
18
18
  import re
19
- import warnings
20
19
  from dataclasses import dataclass
21
20
  from math import ceil
22
21
  from os.path import getsize
23
- from pathlib import Path
24
22
  from typing import TYPE_CHECKING, BinaryIO, Iterable, Optional, TypedDict
25
23
  from urllib.parse import unquote
26
24
 
@@ -33,12 +31,10 @@ from .utils import (
33
31
  hf_raise_for_status,
34
32
  http_backoff,
35
33
  logging,
36
- tqdm,
37
34
  validate_hf_hub_args,
38
35
  )
39
36
  from .utils._lfs import SliceFileObj
40
37
  from .utils.sha import sha256, sha_fileobj
41
- from .utils.tqdm import is_tqdm_disabled
42
38
 
43
39
 
44
40
  if TYPE_CHECKING:
@@ -332,23 +328,9 @@ def _upload_multi_part(operation: "CommitOperationAdd", header: dict, chunk_size
332
328
  # 1. Get upload URLs for each part
333
329
  sorted_parts_urls = _get_sorted_parts_urls(header=header, upload_info=operation.upload_info, chunk_size=chunk_size)
334
330
 
335
- # 2. Upload parts (either with hf_transfer or in pure Python)
336
- use_hf_transfer = constants.HF_HUB_ENABLE_HF_TRANSFER
337
- if (
338
- constants.HF_HUB_ENABLE_HF_TRANSFER
339
- and not isinstance(operation.path_or_fileobj, str)
340
- and not isinstance(operation.path_or_fileobj, Path)
341
- ):
342
- warnings.warn(
343
- "hf_transfer is enabled but does not support uploading from bytes or BinaryIO, falling back to regular"
344
- " upload"
345
- )
346
- use_hf_transfer = False
347
-
348
- response_headers = (
349
- _upload_parts_hf_transfer(operation=operation, sorted_parts_urls=sorted_parts_urls, chunk_size=chunk_size)
350
- if use_hf_transfer
351
- else _upload_parts_iteratively(operation=operation, sorted_parts_urls=sorted_parts_urls, chunk_size=chunk_size)
331
+ # 2. Upload parts (pure Python)
332
+ response_headers = _upload_parts_iteratively(
333
+ operation=operation, sorted_parts_urls=sorted_parts_urls, chunk_size=chunk_size
352
334
  )
353
335
 
354
336
  # 3. Send completion request
@@ -409,47 +391,3 @@ def _upload_parts_iteratively(
409
391
  hf_raise_for_status(part_upload_res)
410
392
  headers.append(part_upload_res.headers)
411
393
  return headers # type: ignore
412
-
413
-
414
- def _upload_parts_hf_transfer(
415
- operation: "CommitOperationAdd", sorted_parts_urls: list[str], chunk_size: int
416
- ) -> list[dict]:
417
- # Upload file using an external Rust-based package. Upload is faster but support less features (no progress bars).
418
- try:
419
- from hf_transfer import multipart_upload
420
- except ImportError:
421
- raise ValueError(
422
- "Fast uploading using 'hf_transfer' is enabled (HF_HUB_ENABLE_HF_TRANSFER=1) but 'hf_transfer' package is"
423
- " not available in your environment. Try `pip install hf_transfer`."
424
- )
425
-
426
- total = operation.upload_info.size
427
- desc = operation.path_in_repo
428
- if len(desc) > 40:
429
- desc = f"(…){desc[-40:]}"
430
-
431
- with tqdm(
432
- unit="B",
433
- unit_scale=True,
434
- total=total,
435
- initial=0,
436
- desc=desc,
437
- disable=is_tqdm_disabled(logger.getEffectiveLevel()),
438
- name="huggingface_hub.lfs_upload",
439
- ) as progress:
440
- try:
441
- output = multipart_upload(
442
- file_path=operation.path_or_fileobj,
443
- parts_urls=sorted_parts_urls,
444
- chunk_size=chunk_size,
445
- max_files=128,
446
- parallel_failures=127, # could be removed
447
- max_retries=5,
448
- callback=progress.update,
449
- )
450
- except Exception as e:
451
- raise RuntimeError(
452
- "An error occurred while uploading using `hf_transfer`. Consider disabling HF_HUB_ENABLE_HF_TRANSFER for"
453
- " better error handling."
454
- ) from e
455
- return output
@@ -266,7 +266,7 @@ def save_torch_state_dict(
266
266
  safe_file_kwargs = {"metadata": per_file_metadata} if safe_serialization else {}
267
267
  for filename, tensors in state_dict_split.filename_to_tensors.items():
268
268
  shard = {tensor: state_dict[tensor] for tensor in tensors}
269
- save_file_fn(shard, os.path.join(save_directory, filename), **safe_file_kwargs)
269
+ save_file_fn(shard, os.path.join(save_directory, filename), **safe_file_kwargs) # ty: ignore[invalid-argument-type]
270
270
  logger.debug(f"Shard saved to {filename}")
271
271
 
272
272
  # Save the index (if any)
@@ -75,7 +75,6 @@ from ._runtime import (
75
75
  get_gradio_version,
76
76
  get_graphviz_version,
77
77
  get_hf_hub_version,
78
- get_hf_transfer_version,
79
78
  get_jinja_version,
80
79
  get_numpy_version,
81
80
  get_pillow_version,
@@ -94,7 +93,6 @@ from ._runtime import (
94
93
  is_google_colab,
95
94
  is_gradio_available,
96
95
  is_graphviz_available,
97
- is_hf_transfer_available,
98
96
  is_jinja_available,
99
97
  is_notebook,
100
98
  is_numpy_available,
@@ -16,7 +16,6 @@
16
16
 
17
17
  import os
18
18
  import shutil
19
- import time
20
19
  from collections import defaultdict
21
20
  from dataclasses import dataclass
22
21
  from pathlib import Path
@@ -26,6 +25,7 @@ from huggingface_hub.errors import CacheNotFound, CorruptedCacheException
26
25
 
27
26
  from ..constants import HF_HUB_CACHE
28
27
  from . import logging
28
+ from ._parsing import format_timesince
29
29
  from ._terminal import tabulate
30
30
 
31
31
 
@@ -79,7 +79,7 @@ class CachedFileInfo:
79
79
 
80
80
  Example: "2 weeks ago".
81
81
  """
82
- return _format_timesince(self.blob_last_accessed)
82
+ return format_timesince(self.blob_last_accessed)
83
83
 
84
84
  @property
85
85
  def blob_last_modified_str(self) -> str:
@@ -89,7 +89,7 @@ class CachedFileInfo:
89
89
 
90
90
  Example: "2 weeks ago".
91
91
  """
92
- return _format_timesince(self.blob_last_modified)
92
+ return format_timesince(self.blob_last_modified)
93
93
 
94
94
  @property
95
95
  def size_on_disk_str(self) -> str:
@@ -153,7 +153,7 @@ class CachedRevisionInfo:
153
153
 
154
154
  Example: "2 weeks ago".
155
155
  """
156
- return _format_timesince(self.last_modified)
156
+ return format_timesince(self.last_modified)
157
157
 
158
158
  @property
159
159
  def size_on_disk_str(self) -> str:
@@ -223,7 +223,7 @@ class CachedRepoInfo:
223
223
 
224
224
  Example: "2 weeks ago".
225
225
  """
226
- return _format_timesince(self.last_accessed)
226
+ return format_timesince(self.last_accessed)
227
227
 
228
228
  @property
229
229
  def last_modified_str(self) -> str:
@@ -233,7 +233,7 @@ class CachedRepoInfo:
233
233
 
234
234
  Example: "2 weeks ago".
235
235
  """
236
- return _format_timesince(self.last_modified)
236
+ return format_timesince(self.last_modified)
237
237
 
238
238
  @property
239
239
  def size_on_disk_str(self) -> str:
@@ -244,6 +244,11 @@ class CachedRepoInfo:
244
244
  """
245
245
  return _format_size(self.size_on_disk)
246
246
 
247
+ @property
248
+ def cache_id(self) -> str:
249
+ """Canonical `type/id` identifier used across cache tooling."""
250
+ return f"{self.repo_type}/{self.repo_id}"
251
+
247
252
  @property
248
253
  def refs(self) -> dict[str, CachedRevisionInfo]:
249
254
  """
@@ -607,15 +612,12 @@ def scan_cache_dir(cache_dir: Optional[Union[str, Path]] = None) -> HFCacheInfo:
607
612
 
608
613
  You can also print a detailed report directly from the `hf` command line using:
609
614
  ```text
610
- > hf cache scan
611
- REPO ID REPO TYPE SIZE ON DISK NB FILES REFS LOCAL PATH
612
- --------------------------- --------- ------------ -------- ------------------- -------------------------------------------------------------------------
613
- glue dataset 116.3K 15 1.17.0, main, 2.4.0 /Users/lucain/.cache/huggingface/hub/datasets--glue
614
- google/fleurs dataset 64.9M 6 main, refs/pr/1 /Users/lucain/.cache/huggingface/hub/datasets--google--fleurs
615
- Jean-Baptiste/camembert-ner model 441.0M 7 main /Users/lucain/.cache/huggingface/hub/models--Jean-Baptiste--camembert-ner
616
- bert-base-cased model 1.9G 13 main /Users/lucain/.cache/huggingface/hub/models--bert-base-cased
617
- t5-base model 10.1K 3 main /Users/lucain/.cache/huggingface/hub/models--t5-base
618
- t5-small model 970.7M 11 refs/pr/1, main /Users/lucain/.cache/huggingface/hub/models--t5-small
615
+ > hf cache ls
616
+ ID SIZE LAST_ACCESSED LAST_MODIFIED REFS
617
+ --------------------------- -------- ------------- ------------- -----------
618
+ dataset/nyu-mll/glue 157.4M 2 days ago 2 days ago main script
619
+ model/LiquidAI/LFM2-VL-1.6B 3.2G 4 days ago 4 days ago main
620
+ model/microsoft/UserLM-8b 32.1G 4 days ago 4 days ago main
619
621
 
620
622
  Done in 0.0s. Scanned 6 repo(s) for a total of 3.4G.
621
623
  Got 1 warning(s) while scanning. Use -vvv to print details.
@@ -816,33 +818,6 @@ def _format_size(num: int) -> str:
816
818
  return f"{num_f:.1f}Y"
817
819
 
818
820
 
819
- _TIMESINCE_CHUNKS = (
820
- # Label, divider, max value
821
- ("second", 1, 60),
822
- ("minute", 60, 60),
823
- ("hour", 60 * 60, 24),
824
- ("day", 60 * 60 * 24, 6),
825
- ("week", 60 * 60 * 24 * 7, 6),
826
- ("month", 60 * 60 * 24 * 30, 11),
827
- ("year", 60 * 60 * 24 * 365, None),
828
- )
829
-
830
-
831
- def _format_timesince(ts: float) -> str:
832
- """Format timestamp in seconds into a human-readable string, relative to now.
833
-
834
- Vaguely inspired by Django's `timesince` formatter.
835
- """
836
- delta = time.time() - ts
837
- if delta < 20:
838
- return "a few seconds ago"
839
- for label, divider, max_value in _TIMESINCE_CHUNKS: # noqa: B007
840
- value = round(delta / divider)
841
- if max_value is not None and value <= max_value:
842
- break
843
- return f"{value} {label}{'s' if value > 1 else ''} ago"
844
-
845
-
846
821
  def _try_delete_path(path: Path, path_type: str) -> None:
847
822
  """Try to delete a local file or folder.
848
823
 
@@ -109,6 +109,20 @@ async def async_hf_request_event_hook(request: httpx.Request) -> None:
109
109
  return hf_request_event_hook(request)
110
110
 
111
111
 
112
+ async def async_hf_response_event_hook(response: httpx.Response) -> None:
113
+ if response.status_code >= 400:
114
+ # If response will raise, read content from stream to have it available when raising the exception
115
+ # If content-length is not set or is too large, skip reading the content to avoid OOM
116
+ if "Content-length" in response.headers:
117
+ try:
118
+ length = int(response.headers["Content-length"])
119
+ except ValueError:
120
+ return
121
+
122
+ if length < 1_000_000:
123
+ await response.aread()
124
+
125
+
112
126
  def default_client_factory() -> httpx.Client:
113
127
  """
114
128
  Factory function to create a `httpx.Client` with the default transport.
@@ -125,7 +139,7 @@ def default_async_client_factory() -> httpx.AsyncClient:
125
139
  Factory function to create a `httpx.AsyncClient` with the default transport.
126
140
  """
127
141
  return httpx.AsyncClient(
128
- event_hooks={"request": [async_hf_request_event_hook]},
142
+ event_hooks={"request": [async_hf_request_event_hook], "response": [async_hf_response_event_hook]},
129
143
  follow_redirects=True,
130
144
  timeout=httpx.Timeout(constants.DEFAULT_REQUEST_TIMEOUT, write=60.0),
131
145
  )
@@ -626,8 +640,16 @@ def _format(error_type: type[HfHubHTTPError], custom_message: str, response: htt
626
640
  try:
627
641
  data = response.json()
628
642
  except httpx.ResponseNotRead:
629
- response.read() # In case of streaming response, we need to read the response first
630
- data = response.json()
643
+ try:
644
+ response.read() # In case of streaming response, we need to read the response first
645
+ data = response.json()
646
+ except RuntimeError:
647
+ # In case of async streaming response, we can't read the stream here.
648
+ # In practice if user is using the default async client from `get_async_client`, the stream will have
649
+ # already been read in the async event hook `async_hf_response_event_hook`.
650
+ #
651
+ # Here, we are skipping reading the response to avoid RuntimeError but it happens only if async + stream + used httpx.AsyncClient directly.
652
+ data = {}
631
653
 
632
654
  error = data.get("error")
633
655
  if error is not None:
@@ -0,0 +1,98 @@
1
+ # coding=utf-8
2
+ # Copyright 2025-present, the HuggingFace Inc. team.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Parsing helpers shared across modules."""
16
+
17
+ import re
18
+ import time
19
+ from typing import Dict
20
+
21
+
22
+ RE_NUMBER_WITH_UNIT = re.compile(r"(\d+)([a-z]+)", re.IGNORECASE)
23
+
24
+ BYTE_UNITS: Dict[str, int] = {
25
+ "k": 1_000,
26
+ "m": 1_000_000,
27
+ "g": 1_000_000_000,
28
+ "t": 1_000_000_000_000,
29
+ "p": 1_000_000_000_000_000,
30
+ }
31
+
32
+ TIME_UNITS: Dict[str, int] = {
33
+ "s": 1,
34
+ "m": 60,
35
+ "h": 60 * 60,
36
+ "d": 24 * 60 * 60,
37
+ "w": 7 * 24 * 60 * 60,
38
+ "mo": 30 * 24 * 60 * 60,
39
+ "y": 365 * 24 * 60 * 60,
40
+ }
41
+
42
+
43
+ def parse_size(value: str) -> int:
44
+ """Parse a size expressed as a string with digits and unit (like `"10MB"`) to an integer (in bytes)."""
45
+ return _parse_with_unit(value, BYTE_UNITS)
46
+
47
+
48
+ def parse_duration(value: str) -> int:
49
+ """Parse a duration expressed as a string with digits and unit (like `"10s"`) to an integer (in seconds)."""
50
+ return _parse_with_unit(value, TIME_UNITS)
51
+
52
+
53
+ def _parse_with_unit(value: str, units: Dict[str, int]) -> int:
54
+ """Parse a numeric value with optional unit."""
55
+ stripped = value.strip()
56
+ if not stripped:
57
+ raise ValueError("Value cannot be empty.")
58
+ try:
59
+ return int(value)
60
+ except ValueError:
61
+ pass
62
+
63
+ match = RE_NUMBER_WITH_UNIT.fullmatch(stripped)
64
+ if not match:
65
+ raise ValueError(f"Invalid value '{value}'. Must match pattern '\\d+[a-z]+' or be a plain number.")
66
+
67
+ number = int(match.group(1))
68
+ unit = match.group(2).lower()
69
+
70
+ if unit not in units:
71
+ raise ValueError(f"Unknown unit '{unit}'. Must be one of {list(units.keys())}.")
72
+
73
+ return number * units[unit]
74
+
75
+
76
+ def format_timesince(ts: float) -> str:
77
+ """Format timestamp in seconds into a human-readable string, relative to now.
78
+
79
+ Vaguely inspired by Django's `timesince` formatter.
80
+ """
81
+ _TIMESINCE_CHUNKS = (
82
+ # Label, divider, max value
83
+ ("second", 1, 60),
84
+ ("minute", 60, 60),
85
+ ("hour", 60 * 60, 24),
86
+ ("day", 60 * 60 * 24, 6),
87
+ ("week", 60 * 60 * 24 * 7, 6),
88
+ ("month", 60 * 60 * 24 * 30, 11),
89
+ ("year", 60 * 60 * 24 * 365, None),
90
+ )
91
+ delta = time.time() - ts
92
+ if delta < 20:
93
+ return "a few seconds ago"
94
+ for label, divider, max_value in _TIMESINCE_CHUNKS: # noqa: B007
95
+ value = round(delta / divider)
96
+ if max_value is not None and value <= max_value:
97
+ break
98
+ return f"{value} {label}{'s' if value > 1 else ''} ago"