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

@ -1,62 +1,83 @@
import os
import re
import tempfile
import threading
from collections import deque
from collections.abc import Iterable, Iterator
from contextlib import ExitStack
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
from itertools import chain
from pathlib import Path
from typing import Any, Iterator, NoReturn, Optional, Union
from pathlib import Path, PurePosixPath
from typing import Any, NoReturn, Union
from urllib.parse import quote, unquote
import fsspec
import httpx
from fsspec.callbacks import _DEFAULT_CALLBACK, NoOpCallback, TqdmCallback
from fsspec.config import apply_config
from fsspec.utils import isfilelike
from . import constants
from ._commit_api import CommitOperationCopy, CommitOperationDelete
from .errors import EntryNotFoundError, HfHubHTTPError, RepositoryNotFoundError, RevisionNotFoundError
from .file_download import hf_hub_url, http_get
from .hf_api import HfApi, LastCommitInfo, RepoFile
from .utils import HFValidationError, hf_raise_for_status, http_backoff, http_stream_backoff
from .utils.insecure_hashlib import md5
# Regex used to match special revisions with "/" in them (see #1710)
SPECIAL_REFS_REVISION_REGEX = re.compile(
r"""
(^refs\/convert\/\w+) # `refs/convert/parquet` revisions
|
(^refs\/pr\/\d+) # PR revisions
""",
re.VERBOSE,
from .errors import (
BucketNotFoundError,
EntryNotFoundError,
HfHubHTTPError,
RepositoryNotFoundError,
RevisionNotFoundError,
)
from .file_download import hf_hub_url, http_get
from .hf_api import SPECIAL_REFS_REVISION_REGEX, BucketFile, BucketFolder, HfApi, LastCommitInfo, RepoFile, RepoFolder
from .utils import HFValidationError, hf_raise_for_status, http_backoff, http_stream_backoff, parse_hf_uri
from .utils.insecure_hashlib import md5
@dataclass
class HfFileSystemResolvedPath:
"""Data structure containing information about a resolved Hugging Face file system path."""
"""Top level Data structure containing information about a resolved Hugging Face file system path."""
root: str
path: str
def unresolve(self) -> str:
return f"{self.root}/{self.path}".rstrip("/")
@dataclass
class HfFileSystemResolvedRepositoryPath(HfFileSystemResolvedPath):
"""Data structure containing information about a resolved path in a repository."""
repo_type: str
repo_id: str
revision: str
path_in_repo: str
root: str = field(init=False)
path: str = field(init=False)
# The part placed after '@' in the initial path. It can be a quoted or unquoted refs revision.
# Used to reconstruct the unresolved path to return to the user.
_raw_revision: Optional[str] = field(default=None, repr=False)
_raw_revision: str | None = field(default=None, repr=False)
def unresolve(self) -> str:
def __post_init__(self):
repo_path = constants.REPO_TYPES_URL_PREFIXES.get(self.repo_type, "") + self.repo_id
if self._raw_revision:
return f"{repo_path}@{self._raw_revision}/{self.path_in_repo}".rstrip("/")
self.root = f"{repo_path}@{self._raw_revision}"
elif self.revision != constants.DEFAULT_REVISION:
return f"{repo_path}@{safe_revision(self.revision)}/{self.path_in_repo}".rstrip("/")
self.root = f"{repo_path}@{safe_revision(self.revision)}"
else:
return f"{repo_path}/{self.path_in_repo}".rstrip("/")
self.root = repo_path
self.path = self.path_in_repo
@dataclass
class HfFileSystemResolvedBucketPath(HfFileSystemResolvedPath):
"""Data structure containing information about a resolved path in a bucket."""
bucket_id: str
root: str = field(init=False)
def __post_init__(self):
self.root = "buckets/" + self.bucket_id
# We need to improve fsspec.spec._Cached which is AbstractFileSystem's metaclass
@ -89,6 +110,9 @@ class _Cached(_cached_base):
def __call__(cls, *args, **kwargs):
# Hack: override https://github.com/fsspec/filesystem_spec/blob/dcb167e8f50e6273d4cfdfc4cab8fc5aa4c958bf/fsspec/spec.py#L65
# Apply fsspec config (env vars / config files) before tokenizing so that
# HfFileSystem picks up defaults the same way other fsspec filesystems do.
kwargs = apply_config(cls, kwargs)
skip = kwargs.pop("skip_instance_cache", False)
fs_token = cls._tokenize(cls, threading.get_ident(), *args, **kwargs)
fs_token_main_thread = cls._tokenize(cls, threading.main_thread().ident, *args, **kwargs)
@ -113,7 +137,7 @@ class _Cached(_cached_base):
return obj
class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached): # ty: ignore[conflicting-metaclass]
"""
Access a remote Hugging Face Hub repository as if were a local file system.
@ -122,6 +146,24 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
> Hugging Face datasets directly with `pandas`). However, it introduces additional overhead due to this compatibility
> layer. For better performance and reliability, it's recommended to use `HfApi` methods when possible.
The file system supports paths for the `hf://` protocol, which follows those URL schemes:
* Models, Datasets and Spaces repositories:
```
hf://<repo-id>[@<revision>]/<path/in/repo>
hf://datasets/<repo-id>[@<revision>]/<path/in/repo>
hf://spaces/<repo-id>[@<revision>]/<path/in/repo>
```
* Buckets (generic storage):
```
hf://buckets/<bucket-id>/<path/in/bucket>
```
Note: when using the [`HfFileSystem`] directly, passing the `hf://` protocol prefix is optional in paths.
Args:
endpoint (`str`, *optional*):
Endpoint of the Hub. Defaults to <https://huggingface.co>.
@ -168,10 +210,10 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
def __init__(
self,
*args,
endpoint: Optional[str] = None,
token: Union[bool, str, None] = None,
block_size: Optional[int] = None,
expand_info: Optional[bool] = None,
endpoint: str | None = None,
token: bool | str | None = None,
block_size: int | None = None,
expand_info: bool | None = None,
**storage_options,
):
super().__init__(*args, **storage_options)
@ -181,11 +223,12 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
self.block_size = block_size
self.expand_info = expand_info
# Maps (repo_type, repo_id, revision) to a 2-tuple with:
# * the 1st element indicating whether the repositoy and the revision exist
# * the 1st element indicating whether the repository and the revision exist
# * the 2nd element being the exception raised if the repository or revision doesn't exist
self._repo_and_revision_exists_cache: dict[
tuple[str, str, Optional[str]], tuple[bool, Optional[Exception]]
] = {}
self._repo_and_revision_exists_cache: dict[tuple[str, str, str | None], tuple[bool, Exception | None]] = {}
# Same for buckets
self._bucket_exists_cache: dict[str, tuple[bool, Exception | None]] = {}
# Note: special case for buckets: revision is always None
# Maps parent directory path to path infos
self.dircache: dict[str, list[dict[str, Any]]] = {}
@ -202,8 +245,8 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
return h.hexdigest()
def _repo_and_revision_exist(
self, repo_type: str, repo_id: str, revision: Optional[str]
) -> tuple[bool, Optional[Exception]]:
self, repo_type: str, repo_id: str, revision: str | None
) -> tuple[bool, Exception | None]:
if (repo_type, repo_id, revision) not in self._repo_and_revision_exists_cache:
try:
self._api.repo_info(
@ -220,7 +263,19 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
self._repo_and_revision_exists_cache[(repo_type, repo_id, None)] = True, None
return self._repo_and_revision_exists_cache[(repo_type, repo_id, revision)]
def resolve_path(self, path: str, revision: Optional[str] = None) -> HfFileSystemResolvedPath:
def _bucket_exists(self, bucket_id: str) -> tuple[bool, Exception | None]:
if bucket_id not in self._bucket_exists_cache:
try:
self._api.bucket_info(bucket_id)
except BucketNotFoundError as e:
self._bucket_exists_cache[bucket_id] = False, e
else:
self._bucket_exists_cache[bucket_id] = True, None
return self._bucket_exists_cache[bucket_id]
def resolve_path(
self, path: str, revision: str | None = None
) -> HfFileSystemResolvedRepositoryPath | HfFileSystemResolvedBucketPath:
"""
Resolve a Hugging Face file system path into its components.
@ -239,83 +294,65 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
`NotImplementedError`:
If trying to list repositories.
"""
def _align_revision_in_path_with_revision(
revision_in_path: Optional[str], revision: Optional[str]
) -> Optional[str]:
if revision is not None:
if revision_in_path is not None and revision_in_path != revision:
raise ValueError(
f'Revision specified in path ("{revision_in_path}") and in `revision` argument ("{revision}")'
" are not the same."
)
else:
revision = revision_in_path
return revision
path = self._strip_protocol(path)
if not path:
# can't list repositories at root
raise NotImplementedError("Access to repositories lists is not implemented.")
elif path.split("/")[0] + "/" in constants.REPO_TYPES_URL_PREFIXES.values():
if "/" not in path:
# can't list repositories at the repository type level
raise NotImplementedError("Access to repositories lists is not implemented.")
repo_type, path = path.split("/", 1)
repo_type = constants.REPO_TYPES_MAPPING[repo_type]
else:
repo_type = constants.REPO_TYPE_MODEL
if path.count("/") > 0:
if "@" in "/".join(path.split("/")[:2]):
repo_id, revision_in_path = path.split("@", 1)
if "/" in revision_in_path:
match = SPECIAL_REFS_REVISION_REGEX.search(revision_in_path)
if match is not None and revision in (None, match.group()):
# Handle `refs/convert/parquet` and PR revisions separately
path_in_repo = SPECIAL_REFS_REVISION_REGEX.sub("", revision_in_path).lstrip("/")
revision_in_path = match.group()
else:
revision_in_path, path_in_repo = revision_in_path.split("/", 1)
else:
path_in_repo = ""
revision = _align_revision_in_path_with_revision(unquote(revision_in_path), revision)
repo_and_revision_exist, err = self._repo_and_revision_exist(repo_type, repo_id, revision)
if not repo_and_revision_exist:
_raise_file_not_found(path, err)
else:
revision_in_path = None
repo_id_with_namespace = "/".join(path.split("/")[:2])
path_in_repo_with_namespace = "/".join(path.split("/")[2:])
repo_id_without_namespace = path.split("/")[0]
path_in_repo_without_namespace = "/".join(path.split("/")[1:])
repo_id = repo_id_with_namespace
path_in_repo = path_in_repo_with_namespace
repo_and_revision_exist, err = self._repo_and_revision_exist(repo_type, repo_id, revision)
if not repo_and_revision_exist:
if isinstance(err, (RepositoryNotFoundError, HFValidationError)):
repo_id = repo_id_without_namespace
path_in_repo = path_in_repo_without_namespace
repo_and_revision_exist, _ = self._repo_and_revision_exist(repo_type, repo_id, revision)
if not repo_and_revision_exist:
_raise_file_not_found(path, err)
else:
_raise_file_not_found(path, err)
else:
repo_id = path
path_in_repo = ""
if "@" in path:
repo_id, revision_in_path = path.split("@", 1)
revision = _align_revision_in_path_with_revision(unquote(revision_in_path), revision)
else:
revision_in_path = None
repo_and_revision_exist, _ = self._repo_and_revision_exist(repo_type, repo_id, revision)
raise NotImplementedError("Access to buckets and repositories lists is not implemented.")
if path.count("/") == 0:
raise ValueError(
f"Repository id must be 'namespace/name', got '{path}'. Single-segment ids (e.g. 'gpt2') are no longer supported."
)
parsed = parse_hf_uri(f"{constants.HF_PROTOCOL}{path}")
# --- Buckets ---
if parsed.is_bucket:
bucket_exists, err = self._bucket_exists(parsed.id)
if not bucket_exists:
_raise_file_not_found(path, err)
return HfFileSystemResolvedBucketPath(bucket_id=parsed.id, path=parsed.path_in_repo)
# --- Repositories ---
# Align revision from path with explicit revision argument
if revision is not None and parsed.revision is not None and parsed.revision != revision:
# The caller provided an explicit revision that conflicts with what parse_hf_uri
# parsed. This can happen when a user has a branch literally named "refs" and a
# file at "pr/10" — parse_hf_uri would greedily match "refs/pr/10" as a special
# ref. Fall back to simple '@' splitting so the caller's revision wins.
path_without_type = path.split("/", 1)[1] if path.split("/")[0] in constants.HF_URI_TYPE_PREFIXES else path
repo_id, after_at = path_without_type.split("@", 1)
revision_in_path, path_in_repo = after_at.split("/", 1) if "/" in after_at else (after_at, "")
revision_in_path_decoded = unquote(revision_in_path)
if revision_in_path_decoded != revision:
raise ValueError(
f'Revision specified in path ("{revision_in_path_decoded}") and in `revision` argument ("{revision}") are not the same.'
)
repo_and_revision_exist, err = self._repo_and_revision_exist(parsed.type, repo_id, revision)
if not repo_and_revision_exist:
raise NotImplementedError("Access to repositories lists is not implemented.")
_raise_file_not_found(path, err)
return HfFileSystemResolvedRepositoryPath(
parsed.type, repo_id, revision, path_in_repo, _raw_revision=revision_in_path
)
if parsed.revision is not None and revision is None:
revision = parsed.revision
repo_and_revision_exist, err = self._repo_and_revision_exist(parsed.type, parsed.id, revision)
if not repo_and_revision_exist:
_raise_file_not_found(path, err)
# Extract raw revision from original path for unresolve() fidelity
raw_revision: str | None = None
if "@" in path and parsed.revision is not None:
path_without_type = path.split("/", 1)[1] if path.split("/")[0] in constants.HF_URI_TYPE_PREFIXES else path
raw_after_at = path_without_type.split("@", 1)[1]
raw_revision = raw_after_at[: -(len(parsed.path_in_repo) + 1)] if parsed.path_in_repo else raw_after_at
revision = revision if revision is not None else constants.DEFAULT_REVISION
return HfFileSystemResolvedPath(repo_type, repo_id, revision, path_in_repo, _raw_revision=revision_in_path)
return HfFileSystemResolvedRepositoryPath(
parsed.type, parsed.id, revision, parsed.path_in_repo, _raw_revision=raw_revision
)
def invalidate_cache(self, path: Optional[str] = None) -> None:
def invalidate_cache(self, path: str | None = None) -> None:
"""
Clear the cache for a given path.
@ -337,18 +374,23 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
path = self._parent(path)
# Only clear repo cache if path is to repo root
if not resolved_path.path_in_repo:
self._repo_and_revision_exists_cache.pop((resolved_path.repo_type, resolved_path.repo_id, None), None)
self._repo_and_revision_exists_cache.pop(
(resolved_path.repo_type, resolved_path.repo_id, resolved_path.revision), None
)
if not resolved_path.path:
if isinstance(resolved_path, HfFileSystemResolvedRepositoryPath):
self._repo_and_revision_exists_cache.pop(
(resolved_path.repo_type, resolved_path.repo_id, None), None
)
self._repo_and_revision_exists_cache.pop(
(resolved_path.repo_type, resolved_path.repo_id, resolved_path.revision), None
)
else:
self._bucket_exists_cache.pop(resolved_path.bucket_id, None)
def _open( # type: ignore[override]
def _open( # type: ignore
self,
path: str,
mode: str = "rb",
block_size: Optional[int] = None,
revision: Optional[str] = None,
block_size: int | None = None,
revision: str | None = None,
**kwargs,
) -> Union["HfFileSystemFile", "HfFileSystemStreamFile"]:
block_size = block_size if block_size is not None else self.block_size
@ -361,25 +403,28 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
else:
return HfFileSystemFile(self, path, mode=mode, revision=revision, **kwargs)
def _rm(self, path: str, revision: Optional[str] = None, **kwargs) -> None:
def _rm(self, path: str, revision: str | None = None, **kwargs) -> None:
resolved_path = self.resolve_path(path, revision=revision)
self._api.delete_file(
path_in_repo=resolved_path.path_in_repo,
repo_id=resolved_path.repo_id,
token=self.token,
repo_type=resolved_path.repo_type,
revision=resolved_path.revision,
commit_message=kwargs.get("commit_message"),
commit_description=kwargs.get("commit_description"),
)
if isinstance(resolved_path, HfFileSystemResolvedBucketPath):
self._api.batch_bucket_files(resolved_path.bucket_id, delete=[resolved_path.path])
else:
self._api.delete_file(
path_in_repo=resolved_path.path_in_repo,
repo_id=resolved_path.repo_id,
token=self.token,
repo_type=resolved_path.repo_type,
revision=resolved_path.revision,
commit_message=kwargs.get("commit_message"),
commit_description=kwargs.get("commit_description"),
)
self.invalidate_cache(path=resolved_path.unresolve())
def rm(
self,
path: str,
recursive: bool = False,
maxdepth: Optional[int] = None,
revision: Optional[str] = None,
maxdepth: int | None = None,
revision: str | None = None,
**kwargs,
) -> None:
"""
@ -403,26 +448,30 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"""
resolved_path = self.resolve_path(path, revision=revision)
paths = self.expand_path(path, recursive=recursive, maxdepth=maxdepth, revision=revision)
paths_in_repo = [self.resolve_path(path).path_in_repo for path in paths if not self.isdir(path)]
operations = [CommitOperationDelete(path_in_repo=path_in_repo) for path_in_repo in paths_in_repo]
commit_message = f"Delete {path} "
commit_message += "recursively " if recursive else ""
commit_message += f"up to depth {maxdepth} " if maxdepth is not None else ""
# TODO: use `commit_description` to list all the deleted paths?
self._api.create_commit(
repo_id=resolved_path.repo_id,
repo_type=resolved_path.repo_type,
token=self.token,
operations=operations,
revision=resolved_path.revision,
commit_message=kwargs.get("commit_message", commit_message),
commit_description=kwargs.get("commit_description"),
)
if isinstance(resolved_path, HfFileSystemResolvedBucketPath):
delete = [self.resolve_path(path).path for path in paths if not self.isdir(path)]
self._api.batch_bucket_files(resolved_path.bucket_id, delete=delete)
else:
paths_in_repo = [self.resolve_path(path).path for path in paths if not self.isdir(path)]
operations = [CommitOperationDelete(path_in_repo=path_in_repo) for path_in_repo in paths_in_repo]
commit_message = f"Delete {path} "
commit_message += "recursively " if recursive else ""
commit_message += f"up to depth {maxdepth} " if maxdepth is not None else ""
# TODO: use `commit_description` to list all the deleted paths?
self._api.create_commit(
repo_id=resolved_path.repo_id,
repo_type=resolved_path.repo_type,
token=self.token,
operations=operations,
revision=resolved_path.revision,
commit_message=kwargs.get("commit_message", commit_message),
commit_description=kwargs.get("commit_description"),
)
self.invalidate_cache(path=resolved_path.unresolve())
def ls(
self, path: str, detail: bool = True, refresh: bool = False, revision: Optional[str] = None, **kwargs
) -> list[Union[str, dict[str, Any]]]:
self, path: str, detail: bool = True, refresh: bool = False, revision: str | None = None, **kwargs
) -> list[str | dict[str, Any]]:
"""
List the contents of a directory.
@ -452,9 +501,12 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
out = self._ls_tree(path, refresh=refresh, revision=revision, **kwargs)
except EntryNotFoundError:
# Path could be a file
if not resolved_path.path_in_repo:
if not resolved_path.path:
_raise_file_not_found(path, None)
out = self._ls_tree(self._parent(path), refresh=refresh, revision=revision, **kwargs)
try:
out = self._ls_tree(self._parent(path), refresh=refresh, revision=revision, **kwargs)
except EntryNotFoundError:
out = []
out = [o for o in out if o["name"] == path]
if len(out) == 0:
_raise_file_not_found(path, None)
@ -465,22 +517,17 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
path: str,
recursive: bool = False,
refresh: bool = False,
revision: Optional[str] = None,
expand_info: Optional[bool] = None,
maxdepth: Optional[int] = None,
revision: str | None = None,
expand_info: bool | None = None,
maxdepth: int | None = None,
):
expand_info = (
expand_info if expand_info is not None else (self.expand_info if self.expand_info is not None else False)
)
resolved_path = self.resolve_path(path, revision=revision)
path = resolved_path.unresolve()
root_path = HfFileSystemResolvedPath(
resolved_path.repo_type,
resolved_path.repo_id,
resolved_path.revision,
path_in_repo="",
_raw_revision=resolved_path._raw_revision,
).unresolve()
root_path = resolved_path.root
maxdepth = maxdepth if recursive else 1
out = []
if path in self.dircache and not refresh:
@ -511,8 +558,8 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
)
dirs_not_expanded = []
if expand_info:
# Check if there are directories with non-expanded entries
if expand_info and isinstance(resolved_path, HfFileSystemResolvedRepositoryPath):
# Check if there are directories in repos with non-expanded entries
dirs_not_expanded = [self._parent(o["name"]) for o in out if o["last_commit"] is None]
if (recursive and dirs_not_in_dircache) or (expand_info and dirs_not_expanded):
@ -546,14 +593,22 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
)
)
else:
tree = self._api.list_repo_tree(
resolved_path.repo_id,
resolved_path.path_in_repo,
recursive=recursive,
expand=expand_info,
revision=resolved_path.revision,
repo_type=resolved_path.repo_type,
)
tree: Iterable[RepoFile | RepoFolder | BucketFile | BucketFolder]
if isinstance(resolved_path, HfFileSystemResolvedBucketPath):
tree = self._list_bucket_tree_with_folders(
resolved_path.bucket_id,
prefix=resolved_path.path,
recursive=recursive,
)
else:
tree = self._api.list_repo_tree(
resolved_path.repo_id,
resolved_path.path,
recursive=recursive,
expand=expand_info,
revision=resolved_path.revision,
repo_type=resolved_path.repo_type,
)
for path_info in tree:
cache_path = root_path + "/" + path_info.path
if isinstance(path_info, RepoFile):
@ -563,10 +618,20 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"type": "file",
"blob_id": path_info.blob_id,
"lfs": path_info.lfs,
"xet_hash": path_info.xet_hash,
"last_commit": path_info.last_commit,
"security": path_info.security,
}
else:
elif isinstance(path_info, BucketFile):
cache_path_info = {
"name": cache_path,
"size": path_info.size,
"type": "file",
"xet_hash": path_info.xet_hash,
"mtime": path_info.mtime,
"uploaded_at": path_info.uploaded_at,
}
elif isinstance(path_info, RepoFolder):
cache_path_info = {
"name": cache_path,
"size": 0,
@ -574,6 +639,13 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"tree_id": path_info.tree_id,
"last_commit": path_info.last_commit,
}
else:
cache_path_info = {
"name": cache_path,
"size": 0,
"type": "directory",
"uploaded_at": path_info.uploaded_at,
}
parent_path = self._parent(cache_path_info["name"])
self.dircache.setdefault(parent_path, []).append(cache_path_info)
depth = cache_path[len(path) :].count("/")
@ -581,6 +653,46 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
out.append(cache_path_info)
return out
def _list_bucket_tree_with_folders(
self, bucket_id: str, prefix: str, recursive: bool
) -> Iterable[BucketFile | BucketFolder]:
"""Same as `HfApi.list_bucket_tree` but always includes folders"""
bucket_files = self._api.list_bucket_tree(bucket_id, prefix, recursive=recursive)
bucket_folders: dict[str, BucketFolder] = {}
min_depth = 1 + prefix.count("/") if prefix else 0
out: list[BucketFile | BucketFolder] = []
for bucket_entry in bucket_files:
out.append(bucket_entry)
# If recursive=False, both files and folders are returned by the server => nothing to do
if not recursive:
continue
# Otherwise, let's rebuild BucketFolders manually
for parent_bucket_folder_str in list(PurePosixPath(bucket_entry.path).parents)[: -min_depth - 1]:
parent_bucket_folder = BucketFolder(
type="directory", path=str(parent_bucket_folder_str), uploaded_at=bucket_entry.uploaded_at
)
# If folder not visited yet, add it
if parent_bucket_folder.path not in bucket_folders:
out.append(parent_bucket_folder)
bucket_folders[parent_bucket_folder.path] = parent_bucket_folder
continue
# Otherwise, get back BucketFolder object and update its 'uploaded_at'
if parent_bucket_folder.uploaded_at is not None:
bucket_folder = bucket_folders[parent_bucket_folder.path]
if bucket_folder.uploaded_at is None or (
bucket_folder.uploaded_at < parent_bucket_folder.uploaded_at
):
bucket_folder.uploaded_at = parent_bucket_folder.uploaded_at
if not out:
raise EntryNotFoundError(f"File not found in bucket '{bucket_id}': '{prefix}'")
return out
def walk(self, path: str, *args, **kwargs) -> Iterator[tuple[str, list[str], list[str]]]:
"""
Return all files below the given path.
@ -597,7 +709,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
path = self.resolve_path(path, revision=kwargs.get("revision")).unresolve()
yield from super().walk(path, *args, **kwargs)
def glob(self, path: str, maxdepth: Optional[int] = None, **kwargs) -> list[str]:
def glob(self, path: str, maxdepth: int | None = None, **kwargs) -> list[str]:
"""
Find files by glob-matching.
@ -606,6 +718,8 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
Args:
path (`str`):
Path pattern to match.
maxdepth (`int`, *optional*):
Maximum depth to descend into directories. By default, no limit.
Returns:
`list[str]`: List of paths matching the pattern.
@ -616,13 +730,13 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
def find(
self,
path: str,
maxdepth: Optional[int] = None,
maxdepth: int | None = None,
withdirs: bool = False,
detail: bool = False,
refresh: bool = False,
revision: Optional[str] = None,
revision: str | None = None,
**kwargs,
) -> Union[list[str], dict[str, dict[str, Any]]]:
) -> list[str] | dict[str, dict[str, Any]]:
"""
List all files below path.
@ -650,9 +764,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
resolved_path = self.resolve_path(path, revision=revision)
path = resolved_path.unresolve()
try:
out = self._ls_tree(
path, recursive=True, refresh=refresh, revision=resolved_path.revision, maxdepth=maxdepth, **kwargs
)
out = self._ls_tree(path, recursive=True, refresh=refresh, maxdepth=maxdepth, **kwargs)
except EntryNotFoundError:
# Path could be a file
try:
@ -667,7 +779,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
out = [o for o in out if o["type"] != "directory"]
else:
# If `withdirs=True`, include the directory itself to be consistent with the spec
path_info = self.info(path, revision=resolved_path.revision, **kwargs)
path_info = self.info(path, **kwargs)
out = [path_info] + out if path_info["type"] == "directory" else out
out = {o["name"]: o for o in out}
names = sorted(out)
@ -676,7 +788,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
else:
return {name: out[name] for name in names}
def cp_file(self, path1: str, path2: str, revision: Optional[str] = None, **kwargs) -> None:
def cp_file(self, path1: str, path2: str, revision: str | None = None, **kwargs) -> None:
"""
Copy a file within or between repositories.
@ -694,6 +806,10 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"""
resolved_path1 = self.resolve_path(path1, revision=revision)
resolved_path2 = self.resolve_path(path2, revision=revision)
if isinstance(resolved_path1, HfFileSystemResolvedBucketPath) or isinstance(
resolved_path2, HfFileSystemResolvedBucketPath
):
raise NotImplementedError("Copy from/to buckets is not available yet")
same_repo = (
resolved_path1.repo_type == resolved_path2.repo_type and resolved_path1.repo_id == resolved_path2.repo_id
@ -743,19 +859,29 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
Path to the file.
Returns:
`datetime`: Last commit date of the file.
`datetime`: Last modified time of the file.
"""
info = self.info(path, **{**kwargs, "expand_info": True}) # type: ignore
return info["last_commit"]["date"]
if "last_commit" in info:
if info["last_commit"] is None:
raise NotImplementedError(f"'modified' is not implemented for repository paths like '{path}'")
return info["last_commit"].date
elif "mtime" in info and info["mtime"]:
return info["mtime"]
elif "uploaded_at" in info and info["uploaded_at"]:
return info["uploaded_at"]
else:
raise NotImplementedError(f"Cannot determined 'modified' for path '{path}' (info: {info})")
def info(self, path: str, refresh: bool = False, revision: Optional[str] = None, **kwargs) -> dict[str, Any]:
def info(self, path: str, refresh: bool = False, revision: str | None = None, **kwargs) -> dict[str, Any]:
"""
Get information about a file or directory.
For more details, refer to [fsspec documentation](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem.info).
> [!WARNING]
> Note: When possible, use `HfApi.get_paths_info()` or `HfApi.repo_info()` for better performance.
> Note: When possible, use `HfApi.get_paths_info()` or `HfApi.repo_info()` for better performance
> (or `HfApi.get_bucket_paths_info()` or `HfApi.bucket_info()` for buckets)
Args:
path (`str`):
@ -774,15 +900,17 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
expand_info = kwargs.get(
"expand_info", self.expand_info if self.expand_info is not None else False
) # don't expose it as a parameter in the public API to follow the spec
if not resolved_path.path_in_repo:
out: dict[str, Any] | None
if not resolved_path.path:
# Path is the root directory
out = {
"name": path,
"size": 0,
"type": "directory",
"last_commit": None,
}
if expand_info:
if isinstance(resolved_path, HfFileSystemResolvedRepositoryPath):
out["last_commit"] = None
if isinstance(resolved_path, HfFileSystemResolvedRepositoryPath) and expand_info:
last_commit = self._api.list_repo_commits(
resolved_path.repo_id, repo_type=resolved_path.repo_type, revision=resolved_path.revision
)[-1]
@ -793,6 +921,14 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
oid=last_commit.commit_id, title=last_commit.title, date=last_commit.created_at
),
}
elif isinstance(resolved_path, HfFileSystemResolvedBucketPath):
parent_path = self._parent(path)
# Fill the cache with cheap call
self.ls(parent_path, refresh=refresh)
out1 = [o for o in self.dircache[parent_path] if o["name"] == path]
if not out1:
_raise_file_not_found(path, None)
out = out1[0]
else:
out = None
parent_path = self._parent(path)
@ -816,7 +952,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
if not paths_info:
_raise_file_not_found(path, None)
path_info = paths_info[0]
root_path = HfFileSystemResolvedPath(
root_path = HfFileSystemResolvedRepositoryPath(
resolved_path.repo_type,
resolved_path.repo_id,
resolved_path.revision,
@ -830,6 +966,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"type": "file",
"blob_id": path_info.blob_id,
"lfs": path_info.lfs,
"xet_hash": path_info.xet_hash,
"last_commit": path_info.last_commit,
"security": path_info.security,
}
@ -868,7 +1005,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
self.info(path, **kwargs)
return True
except: # noqa: E722
except OSError:
return False
def isdir(self, path):
@ -904,7 +1041,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
"""
try:
return self.info(path)["type"] == "file"
except: # noqa: E722
except OSError:
return False
def url(self, path: str) -> str:
@ -919,13 +1056,16 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
`str`: HTTP URL to access the file or directory on the Hub.
"""
resolved_path = self.resolve_path(path)
url = hf_hub_url(
resolved_path.repo_id,
resolved_path.path_in_repo,
repo_type=resolved_path.repo_type,
revision=resolved_path.revision,
endpoint=self.endpoint,
)
if isinstance(resolved_path, HfFileSystemResolvedBucketPath):
url = f"{self.endpoint}/buckets/{resolved_path.bucket_id}/resolve/{quote(resolved_path.path)}"
else:
url = hf_hub_url(
resolved_path.repo_id,
resolved_path.path_in_repo,
repo_type=resolved_path.repo_type,
revision=resolved_path.revision,
endpoint=self.endpoint,
)
if self.isdir(path):
url = url.replace("/resolve/", "/tree/", 1)
return url
@ -935,7 +1075,7 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
Copy single remote file to local.
> [!WARNING]
> Note: When possible, use `HfApi.hf_hub_download()` for better performance.
> Note: When possible, use `HfApi.hf_hub_download()` or `HfApi.download_bucket_files` for better performance.
Args:
rpath (`str`):
@ -978,14 +1118,8 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
callback.set_size(expected_size)
try:
http_get(
url=hf_hub_url(
repo_id=resolve_remote_path.repo_id,
revision=resolve_remote_path.revision,
filename=resolve_remote_path.path_in_repo,
repo_type=resolve_remote_path.repo_type,
endpoint=self.endpoint,
),
temp_file=outfile, # type: ignore[arg-type]
url=self.url(resolve_remote_path.unresolve()),
temp_file=outfile, # type: ignore
displayed_filename=rpath,
expected_size=expected_size,
resume_size=0,
@ -1028,11 +1162,12 @@ class HfFileSystem(fsspec.AbstractFileSystem, metaclass=_Cached):
return {
"dircache": deepcopy(self.dircache),
"_repo_and_revision_exists_cache": deepcopy(self._repo_and_revision_exists_cache),
"_bucket_exists_cache": deepcopy(self._bucket_exists_cache),
}
class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
def __init__(self, fs: HfFileSystem, path: str, revision: Optional[str] = None, **kwargs):
def __init__(self, fs: HfFileSystem, path: str, revision: str | None = None, **kwargs):
try:
self.resolved_path = fs.resolve_path(path, revision=revision)
except FileNotFoundError as e:
@ -1055,13 +1190,7 @@ class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
"range": f"bytes={start}-{end - 1}",
**self.fs._api._build_hf_headers(),
}
url = hf_hub_url(
repo_id=self.resolved_path.repo_id,
revision=self.resolved_path.revision,
filename=self.resolved_path.path_in_repo,
repo_type=self.resolved_path.repo_type,
endpoint=self.fs.endpoint,
)
url = self.url()
r = http_backoff("GET", url, headers=headers, timeout=constants.HF_HUB_DOWNLOAD_TIMEOUT)
hf_raise_for_status(r)
return r.content
@ -1075,16 +1204,21 @@ class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
self.temp_file.write(block)
if final:
self.temp_file.close()
self.fs._api.upload_file(
path_or_fileobj=self.temp_file.name,
path_in_repo=self.resolved_path.path_in_repo,
repo_id=self.resolved_path.repo_id,
token=self.fs.token,
repo_type=self.resolved_path.repo_type,
revision=self.resolved_path.revision,
commit_message=self.kwargs.get("commit_message"),
commit_description=self.kwargs.get("commit_description"),
)
if isinstance(self.resolved_path, HfFileSystemResolvedBucketPath):
self.fs._api.batch_bucket_files(
self.resolved_path.bucket_id, add=[(self.temp_file.name, self.resolved_path.path)]
)
else:
self.fs._api.upload_file(
path_or_fileobj=self.temp_file.name,
path_in_repo=self.resolved_path.path_in_repo,
repo_id=self.resolved_path.repo_id,
token=self.fs.token,
repo_type=self.resolved_path.repo_type,
revision=self.resolved_path.revision,
commit_message=self.kwargs.get("commit_message"),
commit_description=self.kwargs.get("commit_description"),
)
os.remove(self.temp_file.name)
self.fs.invalidate_cache(
path=self.resolved_path.unresolve(),
@ -1113,7 +1247,7 @@ class HfFileSystemStreamFile(fsspec.spec.AbstractBufferedFile):
fs: HfFileSystem,
path: str,
mode: str = "rb",
revision: Optional[str] = None,
revision: str | None = None,
block_size: int = 0,
cache_type: str = "none",
**kwargs,
@ -1136,11 +1270,11 @@ class HfFileSystemStreamFile(fsspec.spec.AbstractBufferedFile):
super().__init__(
fs, self.resolved_path.unresolve(), mode=mode, block_size=block_size, cache_type=cache_type, **kwargs
)
self.response: Optional[httpx.Response] = None
self.response: httpx.Response | None = None
self.fs: HfFileSystem
self._exit_stack = ExitStack()
# streaming state
self._stream_iterator: Optional[Iterator[bytes]] = None
self._stream_iterator: Iterator[bytes] | None = None
self._stream_buffer = bytearray()
def seek(self, loc: int, whence: int = 0):
@ -1231,13 +1365,7 @@ class HfFileSystemStreamFile(fsspec.spec.AbstractBufferedFile):
self._stream_buffer.clear()
self._stream_iterator = None
url = hf_hub_url(
repo_id=self.resolved_path.repo_id,
revision=self.resolved_path.revision,
filename=self.resolved_path.path_in_repo,
repo_type=self.resolved_path.repo_type,
endpoint=self.fs.endpoint,
)
url = self.url()
headers = self.fs._api._build_hf_headers()
if self.loc > 0:
headers["Range"] = f"bytes={self.loc}-"
@ -1270,7 +1398,7 @@ def safe_quote(s: str) -> str:
return quote(s, safe="")
def _raise_file_not_found(path: str, err: Optional[Exception]) -> NoReturn:
def _raise_file_not_found(path: str, err: Exception | None) -> NoReturn:
msg = path
if isinstance(err, RepositoryNotFoundError):
msg = f"{path} (repository not found)"