Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -9,14 +9,19 @@ import uuid
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import Any, BinaryIO, Literal, NoReturn, Optional, Union, overload
from typing import Any, BinaryIO, Literal, NoReturn, overload
from urllib.parse import quote, urlparse
import httpx
from tqdm.auto import tqdm as base_tqdm
from . import constants
from ._local_folder import get_local_download_paths, read_download_metadata, write_download_metadata
from ._local_folder import (
_create_cachedir_tag,
get_local_download_paths,
read_download_metadata,
write_download_metadata,
)
from .errors import (
FileMetadataError,
GatedRepoError,
@ -35,7 +40,6 @@ from .utils import (
hf_raise_for_status,
logging,
parse_xet_file_data_from_response,
refresh_xet_connection_info,
tqdm,
validate_hf_hub_args,
)
@ -43,11 +47,10 @@ from .utils._http import (
_DEFAULT_RETRY_ON_EXCEPTIONS,
_DEFAULT_RETRY_ON_STATUS_CODES,
_adjust_range_header,
http_backoff,
_httpx_follow_relative_redirects_with_backoff,
http_stream_backoff,
)
from .utils._runtime import is_xet_available
from .utils._typing import HTTP_METHOD_T
from .utils.sha import sha_fileobj
from .utils.tqdm import _get_progress_bar_context
@ -73,7 +76,7 @@ _are_symlinks_supported_in_dir: dict[str, bool] = {}
_ETAG_RETRY_TIMEOUT = 60
def are_symlinks_supported(cache_dir: Union[str, Path, None] = None) -> bool:
def are_symlinks_supported(cache_dir: str | Path | None = None) -> bool:
"""Return whether the symlinks are supported on the machine.
Since symlinks support can change depending on the mounted disk, we need to check
@ -90,6 +93,10 @@ def are_symlinks_supported(cache_dir: Union[str, Path, None] = None) -> bool:
cache_dir = constants.HF_HUB_CACHE
cache_dir = str(Path(cache_dir).expanduser().resolve()) # make it unique
# If symlinks are explicitly disabled by the user, always return False
if constants.HF_HUB_DISABLE_SYMLINKS:
return False
# Check symlink compatibility only once (per cache directory) at first time use
if cache_dir not in _are_symlinks_supported_in_dir:
_are_symlinks_supported_in_dir[cache_dir] = True
@ -152,11 +159,11 @@ class HfFileMetadata:
Xet information for the file. This is only set if the file is stored using Xet storage.
"""
commit_hash: Optional[str]
etag: Optional[str]
commit_hash: str | None
etag: str | None
location: str
size: Optional[int]
xet_file_data: Optional[XetFileData]
size: int | None
xet_file_data: XetFileData | None
@dataclass
@ -192,10 +199,10 @@ def hf_hub_url(
repo_id: str,
filename: str,
*,
subfolder: Optional[str] = None,
repo_type: Optional[str] = None,
revision: Optional[str] = None,
endpoint: Optional[str] = None,
subfolder: str | None = None,
repo_type: str | None = None,
revision: str | None = None,
endpoint: str | None = None,
) -> str:
"""Construct the URL of a file from the given information.
@ -212,11 +219,13 @@ def hf_hub_url(
subfolder (`str`, *optional*):
An optional value corresponding to a folder inside the repo.
repo_type (`str`, *optional*):
Set to `"dataset"` or `"space"` if downloading from a dataset or space,
Set to `"dataset"`, `"space"` or `"kernel"` if downloading from a dataset, space or kernel repo,
`None` or `"model"` if downloading from a model. Default is `None`.
revision (`str`, *optional*):
An optional Git revision id which can be a branch name, a tag, or a
commit hash.
endpoint (`str`, *optional*):
The Hub endpoint to send the request to. Defaults to the value of `HF_ENDPOINT`.
Example:
@ -256,7 +265,7 @@ def hf_hub_url(
if subfolder is not None:
filename = f"{subfolder}/{filename}"
if repo_type not in constants.REPO_TYPES:
if repo_type not in constants.REPO_TYPES_WITH_KERNEL:
raise ValueError("Invalid repo type")
if repo_type in constants.REPO_TYPES_URL_PREFIXES:
@ -273,56 +282,7 @@ def hf_hub_url(
return url
def _httpx_follow_relative_redirects(
method: HTTP_METHOD_T, url: str, *, retry_on_errors: bool = False, **httpx_kwargs
) -> httpx.Response:
"""Perform an HTTP request with backoff and follow relative redirects only.
This is useful to follow a redirection to a renamed repository without following redirection to a CDN.
A backoff mechanism retries the HTTP call on errors (429, 5xx, timeout, network errors).
Args:
method (`str`):
HTTP method, such as 'GET' or 'HEAD'.
url (`str`):
The URL of the resource to fetch.
retry_on_errors (`bool`, *optional*, defaults to `False`):
Whether to retry on errors. If False, no retry is performed (fast fallback to local cache).
If True, uses default retry behavior (429, 5xx, timeout, network errors).
**httpx_kwargs (`dict`, *optional*):
Params to pass to `httpx.request`.
"""
# if `retry_on_errors=False`, disable all retries for fast fallback to cache
no_retry_kwargs: dict[str, Any] = (
{} if retry_on_errors else {"retry_on_exceptions": (), "retry_on_status_codes": ()}
)
while True:
response = http_backoff(
method=method,
url=url,
**httpx_kwargs,
follow_redirects=False,
**no_retry_kwargs,
)
hf_raise_for_status(response)
# Check if response is a relative redirect
if 300 <= response.status_code <= 399:
parsed_target = urlparse(response.headers["Location"])
if parsed_target.netloc == "":
# Relative redirect -> update URL and retry
url = urlparse(url)._replace(path=parsed_target.path).geturl()
continue
# Break if no relative redirect
break
return response
def _get_file_length_from_http_response(response: httpx.Response) -> Optional[int]:
def _get_file_length_from_http_response(response: httpx.Response) -> int | None:
"""
Get the length of the file from the HTTP response headers.
@ -363,12 +323,12 @@ def http_get(
temp_file: BinaryIO,
*,
resume_size: int = 0,
headers: Optional[dict[str, Any]] = None,
expected_size: Optional[int] = None,
displayed_filename: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
headers: dict[str, Any] | None = None,
expected_size: int | None = None,
displayed_filename: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
_nb_retries: int = 5,
_tqdm_bar: Optional[tqdm] = None,
_tqdm_bar: tqdm | None = None,
) -> None:
"""
Download a remote file. Do not gobble up errors, and will return errors tailored to the Hugging Face Hub.
@ -418,7 +378,26 @@ def http_get(
retry_on_status_codes=(429,),
) as response:
hf_raise_for_status(response)
total: Optional[int] = _get_file_length_from_http_response(response)
# If we requested a Range but got 200 back, the server ignored our Range header
# (e.g. CloudFront with Accept-Encoding: gzip). Reset file to avoid corruption.
if resume_size > 0 and response.status_code == 200:
temp_file.seek(0)
temp_file.truncate()
if _tqdm_bar is not None:
# When the progress bar is reused across retries, its counter has already been advanced by `resume_size`
# worth of chunks from earlier attempts. Those bytes are gone from disk now, so roll the counter back
# to keep the upcoming full re-download from double-counting (e.g. ending at 130/100 on a 100-byte file).
_tqdm_bar.update(-resume_size)
resume_size = 0
total: int | None = _get_file_length_from_http_response(response)
if total is None:
# Hub serves compressible text files (e.g. vocab.json) with `Content-Encoding: gzip` and
# `Transfer-Encoding: chunked`, so the response carries no `Content-Length`. Fall back to the caller's
# `expected_size` (always known from the metadata HEAD on the hf_hub path) so the progress bar, and any
# aggregating wrapper such as snapshot_download's `_AggregatedTqdm` — still sees the file size.
total = expected_size
if displayed_filename is None:
displayed_filename = url
@ -461,7 +440,7 @@ def http_get(
except (httpx.ConnectError, httpx.TimeoutException) as e:
# If ConnectionError (SSLError) or ReadTimeout happen while streaming data from the server, it is most likely
# a transient error (network outage?). We log a warning message and try to resume the download a few times
# before giving up. Tre retry mechanism is basic but should be enough in most cases.
# before giving up. The retry mechanism is basic but should be enough in most cases.
if _nb_retries <= 0:
logger.warning("Error while downloading from %s: %s\nMax retries exceeded.", url, str(e))
raise
@ -475,11 +454,13 @@ def http_get(
expected_size=expected_size,
tqdm_class=tqdm_class,
_nb_retries=_nb_retries - 1,
_tqdm_bar=_tqdm_bar,
# Reuse the existing progress bar across retries so a custom `tqdm_class` (e.g. snapshot_download's `_AggregatedTqdm`,
# which mutates a shared parent bar in `__init__`) is not re-instantiated and does not double-count `total`/`initial`.
_tqdm_bar=progress,
)
if expected_size is not None and expected_size != temp_file.tell():
raise EnvironmentError(
raise OSError(
consistency_error_message.format(
actual_size=temp_file.tell(),
)
@ -491,10 +472,10 @@ def xet_get(
incomplete_path: Path,
xet_file_data: XetFileData,
headers: dict[str, str],
expected_size: Optional[int] = None,
displayed_filename: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
_tqdm_bar: Optional[tqdm] = None,
expected_size: int | None = None,
displayed_filename: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
_tqdm_bar: tqdm | None = None,
) -> None:
"""
Download a file using Xet storage service.
@ -517,24 +498,23 @@ def xet_get(
The file download system uses Xet storage, which is a content-addressable storage system that breaks files into chunks
for efficient storage and transfer.
`hf_xet.download_files` manages downloading files by:
- Taking a list of files to download (each with its unique content hash)
``session.new_file_download_group()`` manages downloading files by:
- Registering download tasks (each with its unique content hash) and starting download immediately in the background
- Connecting to a storage server (CAS server) that knows how files are chunked
- Using authentication to ensure secure access
- Providing progress updates during download
Authentication works by regularly refreshing access tokens through `refresh_xet_connection_info` to maintain a valid
connection to the storage server.
Authentication works transparently: the download group accepts a ``token_refresh_url``
that is used to refresh the short-lived xet access token as needed.
The download process works like this:
1. Create a local cache folder at `~/.cache/huggingface/xet/chunk-cache` to store reusable file chunks
2. Download files in parallel:
2.1. Prepare to write the file to disk
2.2. Ask the server "how is this file split into chunks?" using the file's unique hash
1. Download tasks run in parallel:
1.1. Prepare to write the file to disk or to a stream (e.g. truncate file, set up cache)
1.2. Ask the server "how is this file split into chunks?" using the file's unique hash
The server responds with:
- Which chunks make up the complete file
- Where each chunk can be downloaded from
2.3. For each needed chunk:
1.3. For each needed chunk:
- Checks if we already have it in our local cache
- If not, download it from cloud storage (S3)
- Save it to cache for future use
@ -542,27 +522,13 @@ def xet_get(
"""
try:
from hf_xet import PyXetDownloadInfo, download_files # type: ignore[no-redef]
from hf_xet import XetFileInfo # type: ignore[no-redef]
except ImportError:
raise ValueError(
"To use optimized download using Xet storage, you need to install the hf_xet package. "
'Try `pip install "huggingface_hub[hf_xet]"` or `pip install hf_xet`.'
)
connection_info = refresh_xet_connection_info(file_data=xet_file_data, headers=headers)
def token_refresher() -> tuple[str, int]:
connection_info = refresh_xet_connection_info(file_data=xet_file_data, headers=headers)
if connection_info is None:
raise ValueError("Failed to refresh token using xet metadata.")
return connection_info.access_token, connection_info.expiration_unix_epoch
xet_download_info = [
PyXetDownloadInfo(
destination_path=str(incomplete_path.absolute()), hash=xet_file_data.file_hash, file_size=expected_size
)
]
if not displayed_filename:
displayed_filename = incomplete_path.name
@ -580,21 +546,37 @@ def xet_get(
_tqdm_bar=_tqdm_bar,
)
from .utils._xet import abort_xet_session, get_xet_session, xet_headers_without_auth
xet_headers = xet_headers_without_auth(headers)
session = get_xet_session()
with progress_cm as progress:
_prev = 0
def progress_updater(progress_bytes: float):
progress.update(progress_bytes)
def _on_progress(group_report, _):
nonlocal _prev
current = group_report.total_bytes_completed
progress.update(max(0, current - _prev))
_prev = current
download_files(
xet_download_info,
endpoint=connection_info.endpoint,
token_info=(connection_info.access_token, connection_info.expiration_unix_epoch),
token_refresher=token_refresher,
progress_updater=[progress_updater],
)
try:
with session.new_file_download_group(
token_refresh_url=xet_file_data.refresh_route,
token_refresh_headers=headers,
custom_headers=xet_headers,
progress_callback=_on_progress,
) as group:
group.start_download_file(
XetFileInfo(xet_file_data.file_hash, expected_size), str(incomplete_path.absolute())
)
except KeyboardInterrupt:
abort_xet_session()
raise
def _normalize_etag(etag: Optional[str]) -> Optional[str]:
def _normalize_etag(etag: str | None) -> str | None:
"""Normalize ETag HTTP header, so it can be used to create nice filepaths.
The HTTP spec allows two forms of ETag:
@ -673,7 +655,7 @@ def _create_symlink(src: str, dst: str, new_blob: bool = False) -> None:
except ValueError:
# Raised if src and dst are not on the same volume. Symlinks will still work on Linux/Macos.
# See https://docs.python.org/3/library/os.path.html#os.path.commonpath
_support_symlinks = os.name != "nt"
_support_symlinks = os.name != "nt" and not constants.HF_HUB_DISABLE_SYMLINKS
except PermissionError:
# Permission error means src and dst are not in the same volume (e.g. destination path has been provided
# by the user via `local_dir`. Let's test symlink support there)
@ -727,7 +709,11 @@ def _cache_commit_hash_for_specific_revision(storage_folder: str, revision: str,
# Update ref only if has been updated. Could cause useless error in case
# repo is already cached and user doesn't have write access to cache folder.
# See https://github.com/huggingface/huggingface_hub/issues/1216.
ref_path.write_text(commit_hash)
# Write atomically (tmp file + rename) so that concurrent readers never see
# a partially written ref.
tmp_path = ref_path.with_name(f"{ref_path.name}.{uuid.uuid4().hex[:8]}.tmp")
tmp_path.write_text(commit_hash)
os.replace(tmp_path, ref_path)
@validate_hf_hub_args
@ -742,7 +728,7 @@ def repo_folder_name(*, repo_id: str, repo_type: str) -> str:
return constants.REPO_ID_SEPARATOR.join(parts)
def _check_disk_space(expected_size: int, target_dir: Union[str, Path]) -> None:
def _check_disk_space(expected_size: int, target_dir: str | Path) -> None:
"""Check disk usage and log a warning if there is not enough disk space to download the file.
Args:
@ -772,21 +758,21 @@ def hf_hub_download(
repo_id: str,
filename: str,
*,
subfolder: Optional[str] = None,
repo_type: Optional[str] = None,
revision: Optional[str] = None,
library_name: Optional[str] = None,
library_version: Optional[str] = None,
cache_dir: Union[str, Path, None] = None,
local_dir: Union[str, Path, None] = None,
user_agent: Union[dict, str, None] = None,
subfolder: str | None = None,
repo_type: str | None = None,
revision: str | None = None,
library_name: str | None = None,
library_version: str | None = None,
cache_dir: str | Path | None = None,
local_dir: str | Path | None = None,
user_agent: dict | str | None = None,
force_download: bool = False,
etag_timeout: float = constants.DEFAULT_ETAG_TIMEOUT,
token: Union[bool, str, None] = None,
token: bool | str | None = None,
local_files_only: bool = False,
headers: Optional[dict[str, str]] = None,
endpoint: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
headers: dict[str, str] | None = None,
endpoint: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
dry_run: Literal[False] = False,
) -> str: ...
@ -796,21 +782,21 @@ def hf_hub_download(
repo_id: str,
filename: str,
*,
subfolder: Optional[str] = None,
repo_type: Optional[str] = None,
revision: Optional[str] = None,
library_name: Optional[str] = None,
library_version: Optional[str] = None,
cache_dir: Union[str, Path, None] = None,
local_dir: Union[str, Path, None] = None,
user_agent: Union[dict, str, None] = None,
subfolder: str | None = None,
repo_type: str | None = None,
revision: str | None = None,
library_name: str | None = None,
library_version: str | None = None,
cache_dir: str | Path | None = None,
local_dir: str | Path | None = None,
user_agent: dict | str | None = None,
force_download: bool = False,
etag_timeout: float = constants.DEFAULT_ETAG_TIMEOUT,
token: Union[bool, str, None] = None,
token: bool | str | None = None,
local_files_only: bool = False,
headers: Optional[dict[str, str]] = None,
endpoint: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
headers: dict[str, str] | None = None,
endpoint: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
dry_run: Literal[True] = True,
) -> DryRunFileInfo: ...
@ -820,23 +806,23 @@ def hf_hub_download(
repo_id: str,
filename: str,
*,
subfolder: Optional[str] = None,
repo_type: Optional[str] = None,
revision: Optional[str] = None,
library_name: Optional[str] = None,
library_version: Optional[str] = None,
cache_dir: Union[str, Path, None] = None,
local_dir: Union[str, Path, None] = None,
user_agent: Union[dict, str, None] = None,
subfolder: str | None = None,
repo_type: str | None = None,
revision: str | None = None,
library_name: str | None = None,
library_version: str | None = None,
cache_dir: str | Path | None = None,
local_dir: str | Path | None = None,
user_agent: dict | str | None = None,
force_download: bool = False,
etag_timeout: float = constants.DEFAULT_ETAG_TIMEOUT,
token: Union[bool, str, None] = None,
token: bool | str | None = None,
local_files_only: bool = False,
headers: Optional[dict[str, str]] = None,
endpoint: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
headers: dict[str, str] | None = None,
endpoint: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
dry_run: bool = False,
) -> Union[str, DryRunFileInfo]: ...
) -> str | DryRunFileInfo: ...
@validate_hf_hub_args
@ -844,23 +830,23 @@ def hf_hub_download(
repo_id: str,
filename: str,
*,
subfolder: Optional[str] = None,
repo_type: Optional[str] = None,
revision: Optional[str] = None,
library_name: Optional[str] = None,
library_version: Optional[str] = None,
cache_dir: Union[str, Path, None] = None,
local_dir: Union[str, Path, None] = None,
user_agent: Union[dict, str, None] = None,
subfolder: str | None = None,
repo_type: str | None = None,
revision: str | None = None,
library_name: str | None = None,
library_version: str | None = None,
cache_dir: str | Path | None = None,
local_dir: str | Path | None = None,
user_agent: dict | str | None = None,
force_download: bool = False,
etag_timeout: float = constants.DEFAULT_ETAG_TIMEOUT,
token: Union[bool, str, None] = None,
token: bool | str | None = None,
local_files_only: bool = False,
headers: Optional[dict[str, str]] = None,
endpoint: Optional[str] = None,
tqdm_class: Optional[type[base_tqdm]] = None,
headers: dict[str, str] | None = None,
endpoint: str | None = None,
tqdm_class: type[base_tqdm] | None = None,
dry_run: bool = False,
) -> Union[str, DryRunFileInfo]:
) -> str | DryRunFileInfo:
"""Download a given file if it's not already present in the local cache.
The new cache file layout looks like this:
@ -904,7 +890,7 @@ def hf_hub_download(
subfolder (`str`, *optional*):
An optional value corresponding to a folder inside the model repo.
repo_type (`str`, *optional*):
Set to `"dataset"` or `"space"` if downloading from a dataset or space,
Set to `"dataset"`, `"space"` or `"kernel"` if downloading from a dataset, space or kernel repo,
`None` or `"model"` if downloading from a model. Default is `None`.
revision (`str`, *optional*):
An optional Git revision id which can be a branch name, a tag, or a
@ -935,6 +921,8 @@ def hf_hub_download(
local cached file if it exists.
headers (`dict`, *optional*):
Additional headers to be sent with the request.
endpoint (`str`, *optional*):
The Hub endpoint to send the request to. Defaults to the value of `HF_ENDPOINT`.
tqdm_class (`tqdm`, *optional*):
If provided, overwrites the default behavior for the progress bar. Passed
argument must inherit from `tqdm.auto.tqdm` or at least mimic its behavior.
@ -971,14 +959,15 @@ def hf_hub_download(
# Respect environment variable above user value
etag_timeout = constants.HF_HUB_ETAG_TIMEOUT
if cache_dir is None:
cache_dir = constants.HF_HUB_CACHE
if revision is None:
revision = constants.DEFAULT_REVISION
if isinstance(cache_dir, Path):
cache_dir = str(cache_dir)
if isinstance(local_dir, Path):
local_dir = str(local_dir)
if cache_dir is None:
cache_dir = constants.HF_HUB_CACHE
cache_dir = str(Path(cache_dir).expanduser().resolve())
if local_dir is not None:
local_dir = str(Path(local_dir).expanduser().resolve())
if subfolder == "":
subfolder = None
@ -988,8 +977,10 @@ def hf_hub_download(
if repo_type is None:
repo_type = "model"
if repo_type not in constants.REPO_TYPES:
raise ValueError(f"Invalid repo type: {repo_type}. Accepted repo types are: {str(constants.REPO_TYPES)}")
if repo_type not in constants.REPO_TYPES_WITH_KERNEL:
raise ValueError(
f"Invalid repo type: {repo_type}. Accepted repo types are: {str(constants.REPO_TYPES_WITH_KERNEL)}"
)
hf_headers = build_hf_headers(
token=token,
@ -1052,16 +1043,16 @@ def _hf_hub_download_to_cache_dir(
repo_type: str,
revision: str,
# HTTP info
endpoint: Optional[str],
endpoint: str | None,
etag_timeout: float,
headers: dict[str, str],
token: Optional[Union[bool, str]],
token: bool | str | None,
# Additional options
local_files_only: bool,
force_download: bool,
tqdm_class: Optional[type[base_tqdm]],
tqdm_class: type[base_tqdm] | None,
dry_run: bool,
) -> Union[str, DryRunFileInfo]:
) -> str | DryRunFileInfo:
"""Download a given file to a cache folder, if not already present.
Method should not be called directly. Please use `hf_hub_download` instead.
@ -1196,13 +1187,19 @@ def _hf_hub_download_to_cache_dir(
os.makedirs(os.path.dirname(blob_path), exist_ok=True)
os.makedirs(os.path.dirname(pointer_path), exist_ok=True)
# Tag cache_dir so backup tools can skip it (CACHEDIR.TAG standard).
_create_cachedir_tag(Path(cache_dir))
# if passed revision is not identical to commit_hash
# then revision has to be a branch name or tag name.
# In that case store a ref.
_cache_commit_hash_for_specific_revision(storage_folder, revision, commit_hash)
# Prevent parallel downloads of the same file with a lock.
# etag could be duplicated across repos,
# etag could be duplicated across repos.
# Note: the lock is best-effort to avoid downloading the same file twice. Cache correctness
# does not depend on it: each download writes to a process-unique temporary file that is
# atomically renamed into place (see `_download_to_tmp_and_move`).
lock_path = os.path.join(locks_dir, repo_folder_name(repo_id=repo_id, repo_type=repo_type), f"{etag}.lock")
# Some Windows versions do not allow for paths longer than 255 characters.
@ -1258,24 +1255,24 @@ def _hf_hub_download_to_cache_dir(
def _hf_hub_download_to_local_dir(
*,
# Destination
local_dir: Union[str, Path],
local_dir: str | Path,
# File info
repo_id: str,
repo_type: str,
filename: str,
revision: str,
# HTTP info
endpoint: Optional[str],
endpoint: str | None,
etag_timeout: float,
headers: dict[str, str],
token: Union[bool, str, None],
token: bool | str | None,
# Additional options
cache_dir: str,
force_download: bool,
local_files_only: bool,
tqdm_class: Optional[type[base_tqdm]],
tqdm_class: type[base_tqdm] | None,
dry_run: bool,
) -> Union[str, DryRunFileInfo]:
) -> str | DryRunFileInfo:
"""Download a given file to a local folder, if not already present.
Method should not be called directly. Please use `hf_hub_download` instead.
@ -1469,10 +1466,10 @@ def _hf_hub_download_to_local_dir(
def try_to_load_from_cache(
repo_id: str,
filename: str,
cache_dir: Union[str, Path, None] = None,
revision: Optional[str] = None,
repo_type: Optional[str] = None,
) -> Union[str, _CACHED_NO_EXIST_T, None]:
cache_dir: str | Path | None = None,
revision: str | None = None,
repo_type: str | None = None,
) -> str | _CACHED_NO_EXIST_T | None:
"""
Explores the cache to return the latest cached file for a given revision if found.
@ -1519,13 +1516,14 @@ def try_to_load_from_cache(
revision = "main"
if repo_type is None:
repo_type = "model"
if repo_type not in constants.REPO_TYPES:
raise ValueError(f"Invalid repo type: {repo_type}. Accepted repo types are: {str(constants.REPO_TYPES)}")
if repo_type not in constants.REPO_TYPES_WITH_KERNEL:
raise ValueError(
f"Invalid repo type: {repo_type}. Accepted repo types are: {str(constants.REPO_TYPES_WITH_KERNEL)}"
)
if cache_dir is None:
cache_dir = constants.HF_HUB_CACHE
object_id = repo_id.replace("/", "--")
repo_cache = os.path.join(cache_dir, f"{repo_type}s--{object_id}")
repo_cache = os.path.join(cache_dir, repo_folder_name(repo_id=repo_id, repo_type=repo_type))
if not os.path.isdir(repo_cache):
# No cache for this model
return None
@ -1561,13 +1559,13 @@ def try_to_load_from_cache(
@validate_hf_hub_args
def get_hf_file_metadata(
url: str,
token: Union[bool, str, None] = None,
timeout: Optional[float] = constants.HF_HUB_ETAG_TIMEOUT,
library_name: Optional[str] = None,
library_version: Optional[str] = None,
user_agent: Union[dict, str, None] = None,
headers: Optional[dict[str, str]] = None,
endpoint: Optional[str] = None,
token: bool | str | None = None,
timeout: float | None = constants.HF_HUB_ETAG_TIMEOUT,
library_name: str | None = None,
library_version: str | None = None,
user_agent: dict | str | None = None,
headers: dict[str, str] | None = None,
endpoint: str | None = None,
retry_on_errors: bool = False,
) -> HfFileMetadata:
"""Fetch metadata of a file versioned on the Hub for a given url.
@ -1611,7 +1609,7 @@ def get_hf_file_metadata(
hf_headers["Accept-Encoding"] = "identity" # prevent any compression => we want to know the real size of the file
# Retrieve metadata
response = _httpx_follow_relative_redirects(
response = _httpx_follow_relative_redirects_with_backoff(
method="HEAD", url=url, headers=hf_headers, timeout=timeout, retry_on_errors=retry_on_errors
)
hf_raise_for_status(response)
@ -1639,21 +1637,22 @@ def _get_metadata_or_catch_error(
filename: str,
repo_type: str,
revision: str,
endpoint: Optional[str],
etag_timeout: Optional[float],
endpoint: str | None,
etag_timeout: float | None,
headers: dict[str, str], # mutated inplace!
token: Union[bool, str, None],
token: bool | str | None,
local_files_only: bool,
relative_filename: Optional[str] = None, # only used to store `.no_exists` in cache
storage_folder: Optional[str] = None, # only used to store `.no_exists` in cache
relative_filename: str | None = None, # only used to store `.no_exists` in cache
storage_folder: str | None = None, # only used to store `.no_exists` in cache
retry_on_errors: bool = False,
) -> Union[
) -> (
# Either an exception is caught and returned
tuple[None, None, None, None, None, Exception],
tuple[None, None, None, None, None, Exception]
|
# Or the metadata is returned as
# `(url_to_download, etag, commit_hash, expected_size, xet_file_data, None)`
tuple[str, str, str, int, Optional[XetFileData], None],
]:
tuple[str, str, str, int, XetFileData | None, None]
):
"""Get metadata for a file on the Hub, safely handling network issues.
Returns either the etag, commit_hash and expected size of the file, or the error
@ -1677,11 +1676,11 @@ def _get_metadata_or_catch_error(
url = hf_hub_url(repo_id, filename, repo_type=repo_type, revision=revision, endpoint=endpoint)
url_to_download: str = url
etag: Optional[str] = None
commit_hash: Optional[str] = None
expected_size: Optional[int] = None
head_error_call: Optional[Exception] = None
xet_file_data: Optional[XetFileData] = None
etag: str | None = None
commit_hash: str | None = None
expected_size: int | None = None
head_error_call: Exception | None = None
xet_file_data: XetFileData | None = None
# Try to get metadata from the server.
# Do not raise yet if the file is not found or not accessible.
@ -1702,13 +1701,14 @@ def _get_metadata_or_catch_error(
commit_hash = http_error.response.headers.get(constants.HUGGINGFACE_HEADER_X_REPO_COMMIT)
if commit_hash is not None:
no_exist_file_path = Path(storage_folder) / ".no_exist" / commit_hash / relative_filename
try:
no_exist_file_path.parent.mkdir(parents=True, exist_ok=True)
no_exist_file_path.touch()
except OSError as e:
logger.error(
f"Could not cache non-existence of file. Will ignore error and continue. Error: {e}"
)
if not no_exist_file_path.exists():
try:
no_exist_file_path.parent.mkdir(parents=True, exist_ok=True)
no_exist_file_path.touch()
except OSError as e:
logger.error(
f"Could not cache non-existence of file. Will ignore error and continue. Error: {e}"
)
_cache_commit_hash_for_specific_revision(storage_folder, revision, commit_hash)
raise
@ -1776,7 +1776,7 @@ def _get_metadata_or_catch_error(
if not (local_files_only or etag is not None or head_error_call is not None):
raise RuntimeError("etag is empty due to uncovered problems")
return (url_to_download, etag, commit_hash, expected_size, xet_file_data, head_error_call) # type: ignore [return-value]
return (url_to_download, etag, commit_hash, expected_size, xet_file_data, head_error_call) # type: ignore
def _raise_on_head_call_error(head_call_error: Exception, force_download: bool, local_files_only: bool) -> NoReturn:
@ -1816,21 +1816,19 @@ def _download_to_tmp_and_move(
destination_path: Path,
url_to_download: str,
headers: dict[str, str],
expected_size: Optional[int],
expected_size: int | None,
filename: str,
force_download: bool,
etag: Optional[str],
xet_file_data: Optional[XetFileData],
tqdm_class: Optional[type[base_tqdm]] = None,
etag: str | None,
xet_file_data: XetFileData | None,
tqdm_class: type[base_tqdm] | None = None,
) -> None:
"""Download content from a URL to a destination path.
Internal logic:
- return early if file is already downloaded
- resume download if possible (from incomplete file)
- do not resume download if `force_download=True`
- check disk space before downloading
- download content to a temporary file
- download content to a process-unique temporary file
- set correct permissions on temporary file
- move the temporary file to the destination path
@ -1840,57 +1838,58 @@ def _download_to_tmp_and_move(
# Do nothing if already exists (except if force_download=True)
return
if incomplete_path.exists() and force_download:
# By default, we will try to resume the download if possible.
# However, if the user has set `force_download=True`, then we should
# not resume the download => delete the incomplete file.
logger.debug(f"Removing incomplete file '{incomplete_path}' (force_download=True)")
incomplete_path.unlink(missing_ok=True)
# Download to a process-unique temporary file before moving it in place. A shared
# `<etag>.incomplete` file corrupts the cache whenever the surrounding lock is not honored:
# on some filesystems (Lustre, GPFS, some NFS mounts) `flock(2)` silently succeeds for every
# caller and concurrent processes end up appending to the same file. With a unique file per
# process, a broken lock costs only duplicated bandwidth: each process downloads the full
# file and atomically renames it to the final destination.
# See https://github.com/huggingface/huggingface_hub/pull/4228.
tmp_path = incomplete_path.with_name(f"{incomplete_path.stem}.{uuid.uuid4().hex[:8]}.incomplete")
try:
with tmp_path.open("wb") as f:
logger.debug(f"Downloading '{filename}' to '{tmp_path}'")
with incomplete_path.open("ab") as f:
resume_size = f.tell()
message = f"Downloading '{filename}' to '{incomplete_path}'"
if resume_size > 0 and expected_size is not None:
message += f" (resume from {resume_size}/{expected_size})"
logger.debug(message)
if expected_size is not None: # might be None if HTTP header not set correctly
# Check disk space in both tmp and destination path
_check_disk_space(expected_size, tmp_path.parent)
_check_disk_space(expected_size, destination_path.parent)
if expected_size is not None: # might be None if HTTP header not set correctly
# Check disk space in both tmp and destination path
_check_disk_space(expected_size, incomplete_path.parent)
_check_disk_space(expected_size, destination_path.parent)
if xet_file_data is not None and is_xet_available():
logger.debug("Xet Storage is enabled for this repo. Downloading file from Xet Storage..")
xet_get(
incomplete_path=tmp_path,
xet_file_data=xet_file_data,
headers=headers,
expected_size=expected_size,
displayed_filename=filename,
tqdm_class=tqdm_class,
)
else:
if xet_file_data is not None and not constants.HF_HUB_DISABLE_XET:
logger.warning(
"Xet Storage is enabled for this repo, but the 'hf_xet' package is not installed. "
"Falling back to regular HTTP download. "
"For better performance, install the package with: `pip install huggingface_hub[hf_xet]` or `pip install hf_xet`"
)
if xet_file_data is not None and is_xet_available():
logger.debug("Xet Storage is enabled for this repo. Downloading file from Xet Storage..")
xet_get(
incomplete_path=incomplete_path,
xet_file_data=xet_file_data,
headers=headers,
expected_size=expected_size,
displayed_filename=filename,
tqdm_class=tqdm_class,
)
else:
if xet_file_data is not None and not constants.HF_HUB_DISABLE_XET:
logger.warning(
"Xet Storage is enabled for this repo, but the 'hf_xet' package is not installed. "
"Falling back to regular HTTP download. "
"For better performance, install the package with: `pip install huggingface_hub[hf_xet]` or `pip install hf_xet`"
http_get(
url_to_download,
f,
headers=headers,
expected_size=expected_size,
tqdm_class=tqdm_class,
)
http_get(
url_to_download,
f,
resume_size=resume_size,
headers=headers,
expected_size=expected_size,
tqdm_class=tqdm_class,
)
logger.debug(f"Download complete. Moving file to {destination_path}")
_chmod_and_move(incomplete_path, destination_path)
logger.debug(f"Download complete. Moving file to {destination_path}")
_chmod_and_move(tmp_path, destination_path)
finally:
# No-op on success (file has been moved). On failure, do not keep a partial file around:
# it could not be reused anyway since the temporary name is unique to this download.
tmp_path.unlink(missing_ok=True)
def _int_or_none(value: Optional[str]) -> Optional[int]:
def _int_or_none(value: str | None) -> int | None:
try:
return int(value) # type: ignore
except (TypeError, ValueError):
@ -1910,8 +1909,12 @@ def _chmod_and_move(src: Path, dst: Path) -> None:
- Fix issue: https://github.com/huggingface/huggingface_hub/issues/1141
- Fix issue: https://github.com/huggingface/huggingface_hub/issues/1215
"""
# Get umask by creating a temporary file in the cached repo folder.
tmp_file = dst.parent.parent / f"tmp_{uuid.uuid4()}"
# Get umask by creating a temporary file in the folder containing the incomplete file.
# We know this folder is writable since the incomplete file has just been written there.
# Probing next to `dst` is not always possible: when downloading to a local dir, `dst` is the
# final file location and its parents might not be writable (e.g. read-only root filesystem).
# See https://github.com/huggingface/huggingface_hub/issues/4304.
tmp_file = src.parent / f"tmp_{uuid.uuid4()}"
try:
tmp_file.touch()
cache_dir_mode = Path(tmp_file).stat().st_mode