Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_commit_api.py977 linesDownload Raw Back to huggingface_hub
1"""2Type definitions and utilities for the `create_commit` API3"""4 5import base646import io7import os8import warnings9from collections import defaultdict10from collections.abc import Iterable, Iterator11from contextlib import contextmanager12from dataclasses import dataclass, field13from itertools import groupby14from pathlib import Path, PurePosixPath15from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Union16 17from tqdm.contrib.concurrent import thread_map18 19from . import constants20from .errors import EntryNotFoundError, HfHubHTTPError, XetAuthorizationError, XetRefreshTokenError21from .file_download import hf_hub_url22from .lfs import UploadInfo, lfs_upload, post_lfs_batch_info23from .utils import (24    FORBIDDEN_FOLDERS,25    XetTokenType,26    are_progress_bars_disabled,27    chunk_iterable,28    fetch_xet_connection_info_from_repo_info,29    get_session,30    hf_raise_for_status,31    http_backoff,32    logging,33    sha,34    tqdm_stream_file,35    validate_hf_hub_args,36)37from .utils import tqdm as hf_tqdm38from .utils._runtime import is_xet_available39 40 41if TYPE_CHECKING:42    from .hf_api import RepoFile43 44 45logger = logging.get_logger(__name__)46 47 48UploadMode = Literal["lfs", "regular"]49 50# Max is 1,000 per request on the Hub for HfApi.get_paths_info51# Otherwise we get:52# HfHubHTTPError: 413 Client Error: Payload Too Large for url: https://huggingface.co/api/datasets/xxx (Request ID: xxx)\n\ntoo many parameters53# See https://github.com/huggingface/huggingface_hub/issues/150354FETCH_LFS_BATCH_SIZE = 50055 56UPLOAD_BATCH_MAX_NUM_FILES = 25657 58 59@dataclass60class CommitOperationDelete:61    """62    Data structure holding necessary info to delete a file or a folder from a repository63    on the Hub.64 65    Args:66        path_in_repo (`str`):67            Relative filepath in the repo, for example: `"checkpoints/1fec34a/weights.bin"`68            for a file or `"checkpoints/1fec34a/"` for a folder.69        is_folder (`bool` or `Literal["auto"]`, *optional*)70            Whether the Delete Operation applies to a folder or not. If "auto", the path71            type (file or folder) is guessed automatically by looking if path ends with72            a "/" (folder) or not (file). To explicitly set the path type, you can set73            `is_folder=True` or `is_folder=False`.74    """75 76    path_in_repo: str77    is_folder: bool | Literal["auto"] = "auto"78 79    def __post_init__(self):80        self.path_in_repo = _validate_path_in_repo(self.path_in_repo)81 82        if self.is_folder == "auto":83            self.is_folder = self.path_in_repo.endswith("/")84        if not isinstance(self.is_folder, bool):85            raise ValueError(86                f"Wrong value for `is_folder`. Must be one of [`True`, `False`, `'auto'`]. Got '{self.is_folder}'."87            )88 89 90@dataclass91class CommitOperationCopy:92    """93    Data structure holding necessary info to copy a file in a repository on the Hub.94 95    Limitations:96      - Only LFS files can be copied. To copy a regular file, you need to download it locally and re-upload it97      - Cross-repository copies are not supported.98 99    Note: you can combine a [`CommitOperationCopy`] and a [`CommitOperationDelete`] to rename an LFS file on the Hub.100 101    Args:102        src_path_in_repo (`str`):103            Relative filepath in the repo of the file to be copied, e.g. `"checkpoints/1fec34a/weights.bin"`.104        path_in_repo (`str`):105            Relative filepath in the repo where to copy the file, e.g. `"checkpoints/1fec34a/weights_copy.bin"`.106        src_revision (`str`, *optional*):107            The git revision of the file to be copied. Can be any valid git revision.108            Default to the target commit revision.109    """110 111    src_path_in_repo: str112    path_in_repo: str113    src_revision: str | None = None114    # set to the OID of the file to be copied if it has already been uploaded115    # useful to determine if a commit will be empty or not.116    _src_oid: str | None = None117    # set to the OID of the file to copy to if it has already been uploaded118    # useful to determine if a commit will be empty or not.119    _dest_oid: str | None = None120 121    def __post_init__(self):122        self.src_path_in_repo = _validate_path_in_repo(self.src_path_in_repo)123        self.path_in_repo = _validate_path_in_repo(self.path_in_repo)124 125 126@dataclass127class CommitOperationAdd:128    """129    Data structure holding necessary info to upload a file to a repository on the Hub.130 131    Args:132        path_in_repo (`str`):133            Relative filepath in the repo, for example: `"checkpoints/1fec34a/weights.bin"`134        path_or_fileobj (`str`, `Path`, `bytes`, or `BinaryIO`):135            Either:136            - a path to a local file (as `str` or `pathlib.Path`) to upload137            - a buffer of bytes (`bytes`) holding the content of the file to upload138            - a "file object" (subclass of `io.BufferedIOBase`), typically obtained139                with `open(path, "rb")`. It must support `seek()` and `tell()` methods.140 141    Raises:142        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)143            If `path_or_fileobj` is not one of `str`, `Path`, `bytes` or `io.BufferedIOBase`.144        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)145            If `path_or_fileobj` is a `str` or `Path` but not a path to an existing file.146        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)147            If `path_or_fileobj` is a `io.BufferedIOBase` but it doesn't support both148            `seek()` and `tell()`.149    """150 151    path_in_repo: str152    path_or_fileobj: str | Path | bytes | BinaryIO153    upload_info: UploadInfo = field(init=False, repr=False)154 155    # Internal attributes156 157    # set to "lfs" or "regular" once known158    _upload_mode: UploadMode | None = field(init=False, repr=False, default=None)159 160    # set to True if .gitignore rules prevent the file from being uploaded as LFS161    # (server-side check)162    _should_ignore: bool | None = field(init=False, repr=False, default=None)163 164    # set to the remote OID of the file if it has already been uploaded165    # useful to determine if a commit will be empty or not166    _remote_oid: str | None = field(init=False, repr=False, default=None)167 168    # set to True once the file has been uploaded as LFS169    _is_uploaded: bool = field(init=False, repr=False, default=False)170 171    # set to True once the file has been committed172    _is_committed: bool = field(init=False, repr=False, default=False)173 174    def __post_init__(self) -> None:175        """Validates `path_or_fileobj` and compute `upload_info`."""176        self.path_in_repo = _validate_path_in_repo(self.path_in_repo)177 178        # Validate `path_or_fileobj` value179        if isinstance(self.path_or_fileobj, Path):180            self.path_or_fileobj = str(self.path_or_fileobj)181        if isinstance(self.path_or_fileobj, str):182            path_or_fileobj = os.path.normpath(os.path.expanduser(self.path_or_fileobj))183            if not os.path.isfile(path_or_fileobj):184                raise ValueError(f"Provided path: '{path_or_fileobj}' is not a file on the local file system")185        elif not isinstance(self.path_or_fileobj, (io.BufferedIOBase, bytes)):186            # ^^ Inspired from: https://stackoverflow.com/questions/44584829/how-to-determine-if-file-is-opened-in-binary-or-text-mode187            raise ValueError(188                "path_or_fileobj must be either an instance of str, bytes or"189                " io.BufferedIOBase. If you passed a file-like object, make sure it is"190                " in binary mode."191            )192        if isinstance(self.path_or_fileobj, io.BufferedIOBase):193            try:194                self.path_or_fileobj.tell()195                self.path_or_fileobj.seek(0, os.SEEK_CUR)196            except (OSError, AttributeError) as exc:197                raise ValueError(198                    "path_or_fileobj is a file-like object but does not implement seek() and tell()"199                ) from exc200 201        # Compute "upload_info" attribute202        if isinstance(self.path_or_fileobj, str):203            self.upload_info = UploadInfo.from_path(self.path_or_fileobj)204        elif isinstance(self.path_or_fileobj, bytes):205            self.upload_info = UploadInfo.from_bytes(self.path_or_fileobj)206        else:207            self.upload_info = UploadInfo.from_fileobj(self.path_or_fileobj)208 209    @contextmanager210    def as_file(self, with_tqdm: bool = False) -> Iterator[BinaryIO]:211        """212        A context manager that yields a file-like object allowing to read the underlying213        data behind `path_or_fileobj`.214 215        Args:216            with_tqdm (`bool`, *optional*, defaults to `False`):217                If True, iterating over the file object will display a progress bar. Only218                works if the file-like object is a path to a file. Pure bytes and buffers219                are not supported.220 221        Example:222 223        ```python224        >>> operation = CommitOperationAdd(225        ...        path_in_repo="remote/dir/weights.h5",226        ...        path_or_fileobj="./local/weights.h5",227        ... )228        CommitOperationAdd(path_in_repo='remote/dir/weights.h5', path_or_fileobj='./local/weights.h5')229 230        >>> with operation.as_file() as file:231        ...     content = file.read()232 233        >>> with operation.as_file(with_tqdm=True) as file:234        ...     while True:235        ...         data = file.read(1024)236        ...         if not data:237        ...              break238        config.json: 100%|█████████████████████████| 8.19k/8.19k [00:02<00:00, 3.72kB/s]239 240        >>> with operation.as_file(with_tqdm=True) as file:241        ...     httpx.put(..., data=file)242        config.json: 100%|█████████████████████████| 8.19k/8.19k [00:02<00:00, 3.72kB/s]243        ```244        """245        if isinstance(self.path_or_fileobj, str) or isinstance(self.path_or_fileobj, Path):246            if with_tqdm:247                with tqdm_stream_file(self.path_or_fileobj) as file:248                    yield file249            else:250                with open(self.path_or_fileobj, "rb") as file:251                    yield file252        elif isinstance(self.path_or_fileobj, bytes):253            yield io.BytesIO(self.path_or_fileobj)254        elif isinstance(self.path_or_fileobj, io.BufferedIOBase):255            prev_pos = self.path_or_fileobj.tell()256            yield self.path_or_fileobj257            self.path_or_fileobj.seek(prev_pos, io.SEEK_SET)258 259    def b64content(self) -> bytes:260        """261        The base64-encoded content of `path_or_fileobj`262 263        Returns: `bytes`264        """265        with self.as_file() as file:266            return base64.b64encode(file.read())267 268    @property269    def _local_oid(self) -> str | None:270        """Return the OID of the local file.271 272        This OID is then compared to `self._remote_oid` to check if the file has changed compared to the remote one.273        If the file did not change, we won't upload it again to prevent empty commits.274 275        For LFS files, the OID corresponds to the SHA256 of the file content (used a LFS ref).276        For regular files, the OID corresponds to the SHA1 of the file content.277        Note: this is slightly different to git OID computation since the oid of an LFS file is usually the git-SHA1 of the278              pointer file content (not the actual file content). However, using the SHA256 is enough to detect changes279              and more convenient client-side.280        """281        if self._upload_mode is None:282            return None283        elif self._upload_mode == "lfs":284            return self.upload_info.sha256.hex()285        else:286            # Regular file => compute sha1287            # => no need to read by chunk since the file is guaranteed to be <=5MB.288            with self.as_file() as file:289                return sha.git_hash(file.read())290 291 292def _validate_path_in_repo(path_in_repo: str) -> str:293    # Validate `path_in_repo` value to prevent a server-side issue294    if path_in_repo.startswith("/"):295        path_in_repo = path_in_repo[1:]296    if path_in_repo == "." or path_in_repo == ".." or path_in_repo.startswith("../"):297        raise ValueError(f"Invalid `path_in_repo` in CommitOperation: '{path_in_repo}'")298    if path_in_repo.startswith("./"):299        path_in_repo = path_in_repo[2:]300    for forbidden in FORBIDDEN_FOLDERS:301        if any(part == forbidden for part in path_in_repo.split("/")):302            raise ValueError(303                f"Invalid `path_in_repo` in CommitOperation: cannot update files under a '{forbidden}/' folder (path:"304                f" '{path_in_repo}')."305            )306    return path_in_repo307 308 309CommitOperation = Union[CommitOperationAdd, CommitOperationCopy, CommitOperationDelete]310 311 312def _warn_on_overwriting_operations(operations: list[CommitOperation]) -> None:313    """314    Warn user when a list of operations is expected to overwrite itself in a single315    commit.316 317    Rules:318    - If a filepath is updated by multiple `CommitOperationAdd` operations, a warning319      message is triggered.320    - If a filepath is updated at least once by a `CommitOperationAdd` and then deleted321      by a `CommitOperationDelete`, a warning is triggered.322    - If a `CommitOperationDelete` deletes a filepath that is then updated by a323      `CommitOperationAdd`, no warning is triggered. This is usually useless (no need to324      delete before upload) but can happen if a user deletes an entire folder and then325      add new files to it.326    """327    nb_additions_per_path: dict[str, int] = defaultdict(int)328    for operation in operations:329        path_in_repo = operation.path_in_repo330        if isinstance(operation, CommitOperationAdd):331            if nb_additions_per_path[path_in_repo] > 0:332                warnings.warn(333                    "About to update multiple times the same file in the same commit:"334                    f" '{path_in_repo}'. This can cause undesired inconsistencies in"335                    " your repo."336                )337            nb_additions_per_path[path_in_repo] += 1338            for parent in PurePosixPath(path_in_repo).parents:339                # Also keep track of number of updated files per folder340                # => warns if deleting a folder overwrite some contained files341                nb_additions_per_path[str(parent)] += 1342        if isinstance(operation, CommitOperationDelete):343            if nb_additions_per_path[str(PurePosixPath(path_in_repo))] > 0:344                if operation.is_folder:345                    warnings.warn(346                        "About to delete a folder containing files that have just been"347                        f" updated within the same commit: '{path_in_repo}'. This can"348                        " cause undesired inconsistencies in your repo."349                    )350                else:351                    warnings.warn(352                        "About to delete a file that have just been updated within the"353                        f" same commit: '{path_in_repo}'. This can cause undesired"354                        " inconsistencies in your repo."355                    )356 357 358@validate_hf_hub_args359def _upload_files(360    *,361    additions: list[CommitOperationAdd],362    repo_type: str,363    repo_id: str,364    headers: dict[str, str],365    endpoint: str | None = None,366    num_threads: int = 5,367    revision: str | None = None,368    create_pr: bool | None = None,369):370    """371    Negotiates per-file transfer (LFS vs Xet) and uploads in batches.372    """373    xet_additions: list[CommitOperationAdd] = []374    lfs_actions: list[dict[str, Any]] = []375    lfs_oid2addop: dict[str, CommitOperationAdd] = {}376 377    for chunk in chunk_iterable(additions, chunk_size=UPLOAD_BATCH_MAX_NUM_FILES):378        chunk_list = [op for op in chunk]379 380        transfers: list[str] = ["basic", "multipart"]381        has_buffered_io_data = any(isinstance(op.path_or_fileobj, io.BufferedIOBase) for op in chunk_list)382        if is_xet_available():383            if not has_buffered_io_data:384                transfers.append("xet")385            else:386                logger.warning(387                    "Uploading files as a binary IO buffer is not supported by Xet Storage. "388                    "Falling back to HTTP upload."389                )390 391        actions_chunk, errors_chunk, chosen_transfer = post_lfs_batch_info(392            upload_infos=[op.upload_info for op in chunk_list],393            repo_id=repo_id,394            repo_type=repo_type,395            revision=revision,396            endpoint=endpoint,397            headers=headers,398            token=None,  # already passed in 'headers'399            transfers=transfers,400        )401        if errors_chunk:402            message = "\n".join(403                [404                    f"Encountered error for file with OID {err.get('oid')}: `{err.get('error', {}).get('message')}"405                    for err in errors_chunk406                ]407            )408            raise ValueError(f"LFS batch API returned errors:\n{message}")409 410        # If server returns a transfer we didn't offer (e.g "xet" while uploading from BytesIO),411        # fall back to LFS for this chunk.412        if chosen_transfer == "xet" and ("xet" in transfers):413            xet_additions.extend(chunk_list)414        else:415            lfs_actions.extend(actions_chunk)416            for op in chunk_list:417                lfs_oid2addop[op.upload_info.sha256.hex()] = op418 419    if len(lfs_actions) > 0:420        _upload_lfs_files(421            actions=lfs_actions,422            oid2addop=lfs_oid2addop,423            headers=headers,424            endpoint=endpoint,425            num_threads=num_threads,426        )427 428    if len(xet_additions) > 0:429        _upload_xet_files(430            additions=xet_additions,431            repo_type=repo_type,432            repo_id=repo_id,433            headers=headers,434            endpoint=endpoint,435            revision=revision,436            create_pr=create_pr,437        )438 439 440@validate_hf_hub_args441def _upload_lfs_files(442    *,443    actions: list[dict[str, Any]],444    oid2addop: dict[str, CommitOperationAdd],445    headers: dict[str, str],446    endpoint: str | None = None,447    num_threads: int = 5,448):449    """450    Uploads the content of `additions` to the Hub using the large file storage protocol.451 452    Relevant external documentation:453        - LFS Batch API: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md454 455    Args:456        actions (`list[dict[str, Any]]`):457            LFS batch actions returned by the server.458        oid2addop (`dict[str, CommitOperationAdd]`):459            A dictionary mapping the OID of the file to the corresponding `CommitOperationAdd` object.460        headers (`dict[str, str]`):461            Headers to use for the request, including authorization headers and user agent.462        endpoint (`str`, *optional*):463            The endpoint to use for the request. Defaults to `constants.ENDPOINT`.464        num_threads (`int`, *optional*):465            The number of concurrent threads to use when uploading. Defaults to 5.466 467    Raises:468        [`EnvironmentError`](https://docs.python.org/3/library/exceptions.html#EnvironmentError)469            If an upload failed for any reason470        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)471            Type of the repo to upload to: `"model"`, `"dataset"` or `"space"`.472        repo_id (`str`):473            A namespace (user or an organization) and a repo name separated474            by a `/`.475        headers (`dict[str, str]`):476            Headers to use for the request, including authorization headers and user agent.477        num_threads (`int`, *optional*):478            The number of concurrent threads to use when uploading. Defaults to 5.479        revision (`str`, *optional*):480            The git revision to upload to.481 482    Raises:483        [`EnvironmentError`](https://docs.python.org/3/library/exceptions.html#EnvironmentError)484            If an upload failed for any reason485        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)486            If the server returns malformed responses487        [`HfHubHTTPError`]488            If the LFS batch endpoint returned an HTTP error.489    """490    # Filter out files already present upstream491    filtered_actions = []492    for action in actions:493        if action.get("actions") is None:494            logger.debug(495                f"Content of file {oid2addop[action['oid']].path_in_repo} is already present upstream - skipping upload."496            )497        else:498            filtered_actions.append(action)499 500    # Upload according to server-provided actions501    def _wrapped_lfs_upload(batch_action) -> None:502        try:503            operation = oid2addop[batch_action["oid"]]504            lfs_upload(operation=operation, lfs_batch_action=batch_action, headers=headers, endpoint=endpoint)505        except Exception as exc:506            raise RuntimeError(f"Error while uploading '{operation.path_in_repo}' to the Hub.") from exc507 508    if len(filtered_actions) == 1:509        logger.debug("Uploading 1 LFS file to the Hub")510        _wrapped_lfs_upload(filtered_actions[0])511    else:512        logger.debug(513            f"Uploading {len(filtered_actions)} LFS files to the Hub using up to {num_threads} threads concurrently"514        )515        thread_map(516            _wrapped_lfs_upload,517            filtered_actions,518            desc=f"Upload {len(filtered_actions)} LFS files",519            max_workers=num_threads,520            tqdm_class=hf_tqdm,521        )522 523 524@validate_hf_hub_args525def _upload_xet_files(526    *,527    additions: list[CommitOperationAdd],528    repo_type: str,529    repo_id: str,530    headers: dict[str, str],531    endpoint: str | None = None,532    revision: str | None = None,533    create_pr: bool | None = None,534):535    """536    Uploads the content of `additions` to the Hub using the xet storage protocol.537    This chunks the files and deduplicates the chunks before uploading them to xetcas storage.538 539    Args:540        additions (`` of `CommitOperationAdd`):541            The files to be uploaded.542        repo_type (`str`):543            Type of the repo to upload to: `"model"`, `"dataset"` or `"space"`.544        repo_id (`str`):545            A namespace (user or an organization) and a repo name separated546            by a `/`.547        headers (`dict[str, str]`):548            Headers to use for the request, including authorization headers and user agent.549        endpoint: (`str`, *optional*):550            The endpoint to use for the xetcas service. Defaults to `constants.ENDPOINT`.551        revision (`str`, *optional*):552            The git revision to upload to.553        create_pr (`bool`, *optional*):554            Whether or not to create a Pull Request with that commit.555 556    Raises:557        [`EnvironmentError`](https://docs.python.org/3/library/exceptions.html#EnvironmentError)558            If an upload failed for any reason.559        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)560            If the server returns malformed responses or if the user is unauthorized to upload to xet storage.561        [`HfHubHTTPError`]562            If the LFS batch endpoint returned an HTTP error.563 564    **How it works:**565        The file download system uses Xet storage, which is a content-addressable storage system that breaks files into chunks566            for efficient storage and transfer.567 568        `hf_xet.upload_files` manages uploading files by:569            - Taking a list of file paths to upload570            - Breaking files into smaller chunks for efficient storage571            - Avoiding duplicate storage by recognizing identical chunks across files572            - Connecting to a storage server (CAS server) that manages these chunks573 574        The upload process works like this:575        1. Create a local folder at ~/.cache/huggingface/xet/chunk-cache to store file chunks for reuse.576        2. Process files in parallel (up to 8 files at once):577            2.1. Read the file content.578            2.2. Split the file content into smaller chunks based on content patterns: each chunk gets a unique ID based on what's in it.579            2.3. For each chunk:580                - Check if it already exists in storage.581                - Skip uploading chunks that already exist.582            2.4. Group chunks into larger blocks for efficient transfer.583            2.5. Upload these blocks to the storage server.584            2.6. Create and upload information about how the file is structured.585        3. Return reference files that contain information about the uploaded files, which can be used later to download them.586    """587    if len(additions) == 0:588        return589 590    # at this point, we know that hf_xet is installed591    from hf_xet import upload_bytes, upload_files592 593    from .utils._xet_progress_reporting import XetProgressReporter594 595    try:596        xet_connection_info = fetch_xet_connection_info_from_repo_info(597            token_type=XetTokenType.WRITE,598            repo_id=repo_id,599            repo_type=repo_type,600            revision=revision,601            headers=headers,602            endpoint=endpoint,603            params={"create_pr": "1"} if create_pr else None,604        )605    except HfHubHTTPError as e:606        if e.response.status_code == 401:607            raise XetAuthorizationError(608                f"You are unauthorized to upload to xet storage for {repo_type}/{repo_id}. "609                f"Please check that you have configured your access token with write access to the repo."610            ) from e611        raise612 613    xet_endpoint = xet_connection_info.endpoint614    access_token_info = (xet_connection_info.access_token, xet_connection_info.expiration_unix_epoch)615 616    def token_refresher() -> tuple[str, int]:617        new_xet_connection = fetch_xet_connection_info_from_repo_info(618            token_type=XetTokenType.WRITE,619            repo_id=repo_id,620            repo_type=repo_type,621            revision=revision,622            headers=headers,623            endpoint=endpoint,624            params={"create_pr": "1"} if create_pr else None,625        )626        if new_xet_connection is None:627            raise XetRefreshTokenError("Failed to refresh xet token")628        return new_xet_connection.access_token, new_xet_connection.expiration_unix_epoch629 630    if not are_progress_bars_disabled():631        progress = XetProgressReporter()632        progress_callback = progress.update_progress633    else:634        progress, progress_callback = None, None635 636    try:637        all_bytes_ops = [op for op in additions if isinstance(op.path_or_fileobj, bytes)]638        all_paths_ops = [op for op in additions if isinstance(op.path_or_fileobj, (str, Path))]639 640        xet_headers = headers.copy()641        xet_headers.pop("authorization", None)642 643        if len(all_paths_ops) > 0:644            all_paths = [str(op.path_or_fileobj) for op in all_paths_ops]645            all_sha256s = [op.upload_info.sha256.hex() for op in all_paths_ops]646            upload_files(647                all_paths,648                xet_endpoint,649                access_token_info,650                token_refresher,651                progress_callback,652                repo_type,653                request_headers=xet_headers,654                sha256s=all_sha256s,655            )656 657        if len(all_bytes_ops) > 0:658            all_bytes = [op.path_or_fileobj for op in all_bytes_ops]659            all_sha256s = [op.upload_info.sha256.hex() for op in all_bytes_ops]660            upload_bytes(661                all_bytes,662                xet_endpoint,663                access_token_info,664                token_refresher,665                progress_callback,666                repo_type,667                request_headers=xet_headers,668                sha256s=all_sha256s,669            )670 671    finally:672        if progress is not None:673            progress.close(False)674 675    return676 677 678def _validate_preupload_info(preupload_info: dict):679    files = preupload_info.get("files")680    if not isinstance(files, list):681        raise ValueError("preupload_info is improperly formatted")682    for file_info in files:683        if not (684            isinstance(file_info, dict)685            and isinstance(file_info.get("path"), str)686            and isinstance(file_info.get("uploadMode"), str)687            and (file_info["uploadMode"] in ("lfs", "regular"))688        ):689            raise ValueError("preupload_info is improperly formatted:")690    return preupload_info691 692 693@validate_hf_hub_args694def _fetch_upload_modes(695    additions: Iterable[CommitOperationAdd],696    repo_type: str,697    repo_id: str,698    headers: dict[str, str],699    revision: str,700    endpoint: str | None = None,701    create_pr: bool = False,702    gitignore_content: str | None = None,703) -> None:704    """705    Requests the Hub "preupload" endpoint to determine whether each input file should be uploaded as a regular git blob,706    as a git LFS blob, or as a XET file. Input `additions` are mutated in-place with the upload mode.707 708    Args:709        additions (`Iterable` of :class:`CommitOperationAdd`):710            Iterable of :class:`CommitOperationAdd` describing the files to711            upload to the Hub.712        repo_type (`str`):713            Type of the repo to upload to: `"model"`, `"dataset"` or `"space"`.714        repo_id (`str`):715            A namespace (user or an organization) and a repo name separated716            by a `/`.717        headers (`dict[str, str]`):718            Headers to use for the request, including authorization headers and user agent.719        revision (`str`):720            The git revision to upload the files to. Can be any valid git revision.721        gitignore_content (`str`, *optional*):722            The content of the `.gitignore` file to know which files should be ignored. The order of priority723            is to first check if `gitignore_content` is passed, then check if the `.gitignore` file is present724            in the list of files to commit and finally default to the `.gitignore` file already hosted on the Hub725            (if any).726    Raises:727        [`~utils.HfHubHTTPError`]728            If the Hub API returned an error.729        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)730            If the Hub API response is improperly formatted.731    """732    endpoint = endpoint if endpoint is not None else constants.ENDPOINT733 734    # Fetch upload mode (LFS or regular) chunk by chunk.735    upload_modes: dict[str, UploadMode] = {}736    should_ignore_info: dict[str, bool] = {}737    oid_info: dict[str, str | None] = {}738 739    for chunk in chunk_iterable(additions, 256):740        payload: dict = {741            "files": [742                {743                    "path": op.path_in_repo,744                    "sample": base64.b64encode(op.upload_info.sample).decode("ascii"),745                    "size": op.upload_info.size,746                }747                for op in chunk748            ]749        }750        if gitignore_content is not None:751            payload["gitIgnore"] = gitignore_content752 753        resp = http_backoff(754            "POST",755            f"{endpoint}/api/{repo_type}s/{repo_id}/preupload/{revision}",756            json=payload,757            headers=headers,758            params={"create_pr": "1"} if create_pr else None,759        )760        hf_raise_for_status(resp)761        preupload_info = _validate_preupload_info(resp.json())762        upload_modes.update(**{file["path"]: file["uploadMode"] for file in preupload_info["files"]})763        should_ignore_info.update(**{file["path"]: file["shouldIgnore"] for file in preupload_info["files"]})764        oid_info.update(**{file["path"]: file.get("oid") for file in preupload_info["files"]})765 766    # Set upload mode for each addition operation767    for addition in additions:768        addition._upload_mode = upload_modes[addition.path_in_repo]769        addition._should_ignore = should_ignore_info[addition.path_in_repo]770        addition._remote_oid = oid_info[addition.path_in_repo]771 772    # Empty files cannot be uploaded as LFS (S3 would fail with a 501 Not Implemented)773    # => empty files are uploaded as "regular" to still allow users to commit them.774    for addition in additions:775        if addition.upload_info.size == 0:776            addition._upload_mode = "regular"777 778 779@validate_hf_hub_args780def _fetch_files_to_copy(781    copies: Iterable[CommitOperationCopy],782    repo_type: str,783    repo_id: str,784    headers: dict[str, str],785    revision: str,786    endpoint: str | None = None,787) -> dict[tuple[str, str | None], Union["RepoFile", bytes]]:788    """789    Fetch information about the files to copy.790 791    For LFS files, we only need their metadata (file size and sha256) while for regular files792    we need to download the raw content from the Hub.793 794    Args:795        copies (`Iterable` of :class:`CommitOperationCopy`):796            Iterable of :class:`CommitOperationCopy` describing the files to797            copy on the Hub.798        repo_type (`str`):799            Type of the repo to upload to: `"model"`, `"dataset"` or `"space"`.800        repo_id (`str`):801            A namespace (user or an organization) and a repo name separated802            by a `/`.803        headers (`dict[str, str]`):804            Headers to use for the request, including authorization headers and user agent.805        revision (`str`):806            The git revision to upload the files to. Can be any valid git revision.807 808    Returns: `dict[tuple[str, Optional[str]], Union[RepoFile, bytes]]]`809        Key is the file path and revision of the file to copy.810        Value is the raw content as bytes (for regular files) or the file information as a RepoFile (for LFS files).811 812    Raises:813        [`~utils.HfHubHTTPError`]814            If the Hub API returned an error.815        [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)816            If the Hub API response is improperly formatted.817    """818    from .hf_api import HfApi, RepoFolder819 820    hf_api = HfApi(endpoint=endpoint, headers=headers)821    files_to_copy: dict[tuple[str, str | None], Union["RepoFile", bytes]] = {}822    # Store (path, revision) -> oid mapping823    oid_info: dict[tuple[str, str | None], str | None] = {}824    # 1. Fetch OIDs for destination paths in batches.825    dest_paths = [op.path_in_repo for op in copies]826    for offset in range(0, len(dest_paths), FETCH_LFS_BATCH_SIZE):827        dest_repo_files = hf_api.get_paths_info(828            repo_id=repo_id,829            paths=dest_paths[offset : offset + FETCH_LFS_BATCH_SIZE],830            revision=revision,831            repo_type=repo_type,832        )833        for file in dest_repo_files:834            if not isinstance(file, RepoFolder):835                oid_info[(file.path, revision)] = file.blob_id836 837    # 2. Group by source revision and fetch source file info in batches.838    for src_revision, operations in groupby(copies, key=lambda op: op.src_revision):839        operations = list(operations)  # type: ignore840        src_paths = [op.src_path_in_repo for op in operations]841        for offset in range(0, len(src_paths), FETCH_LFS_BATCH_SIZE):842            src_repo_files = hf_api.get_paths_info(843                repo_id=repo_id,844                paths=src_paths[offset : offset + FETCH_LFS_BATCH_SIZE],845                revision=src_revision or revision,846                repo_type=repo_type,847            )848 849            for src_repo_file in src_repo_files:850                if isinstance(src_repo_file, RepoFolder):851                    raise NotImplementedError("Copying a folder is not implemented.")852                oid_info[(src_repo_file.path, src_revision)] = src_repo_file.blob_id853                # If it's an LFS file, store the RepoFile object. Otherwise, download raw bytes.854                if src_repo_file.lfs:855                    files_to_copy[(src_repo_file.path, src_revision)] = src_repo_file856                else:857                    # TODO: (optimization) download regular files to copy concurrently858                    url = hf_hub_url(859                        endpoint=endpoint,860                        repo_type=repo_type,861                        repo_id=repo_id,862                        revision=src_revision or revision,863                        filename=src_repo_file.path,864                    )865                    response = get_session().get(url, headers=headers)866                    hf_raise_for_status(response)867                    files_to_copy[(src_repo_file.path, src_revision)] = response.content868        # 3. Ensure all operations found a corresponding file in the Hub869        #  and track src/dest OIDs for each operation.870        for operation in operations:871            if (operation.src_path_in_repo, src_revision) not in files_to_copy:872                raise EntryNotFoundError(873                    f"Cannot copy {operation.src_path_in_repo} at revision "874                    f"{src_revision or revision}: file is missing on repo."875                )876            operation._src_oid = oid_info.get((operation.src_path_in_repo, operation.src_revision))877            operation._dest_oid = oid_info.get((operation.path_in_repo, revision))878    return files_to_copy879 880 881def _prepare_commit_payload(882    operations: Iterable[CommitOperation],883    files_to_copy: dict[tuple[str, str | None], Union["RepoFile", bytes]],884    commit_message: str,885    commit_description: str | None = None,886    parent_commit: str | None = None,887) -> Iterable[dict[str, Any]]:888    """889    Builds the payload to POST to the `/commit` API of the Hub.890 891    Payload is returned as an iterator so that it can be streamed as a ndjson in the892    POST request.893 894    For more information, see:895        - https://github.com/huggingface/huggingface_hub/issues/1085#issuecomment-1265208073896        - http://ndjson.org/897    """898    commit_description = commit_description if commit_description is not None else ""899 900    # 1. Send a header item with the commit metadata901    header_value = {"summary": commit_message, "description": commit_description}902    if parent_commit is not None:903        header_value["parentCommit"] = parent_commit904    yield {"key": "header", "value": header_value}905 906    nb_ignored_files = 0907 908    # 2. Send operations, one per line909    for operation in operations:910        # Skip ignored files911        if isinstance(operation, CommitOperationAdd) and operation._should_ignore:912            logger.debug(f"Skipping file '{operation.path_in_repo}' in commit (ignored by gitignore file).")913            nb_ignored_files += 1914            continue915 916        # 2.a. Case adding a regular file917        if isinstance(operation, CommitOperationAdd) and operation._upload_mode == "regular":918            yield {919                "key": "file",920                "value": {921                    "content": operation.b64content().decode(),922                    "path": operation.path_in_repo,923                    "encoding": "base64",924                },925            }926        # 2.b. Case adding an LFS file927        elif isinstance(operation, CommitOperationAdd) and operation._upload_mode == "lfs":928            yield {929                "key": "lfsFile",930                "value": {931                    "path": operation.path_in_repo,932                    "algo": "sha256",933                    "oid": operation.upload_info.sha256.hex(),934                    "size": operation.upload_info.size,935                },936            }937        # 2.c. Case deleting a file or folder938        elif isinstance(operation, CommitOperationDelete):939            yield {940                "key": "deletedFolder" if operation.is_folder else "deletedFile",941                "value": {"path": operation.path_in_repo},942            }943        # 2.d. Case copying a file or folder944        elif isinstance(operation, CommitOperationCopy):945            file_to_copy = files_to_copy[(operation.src_path_in_repo, operation.src_revision)]946            if isinstance(file_to_copy, bytes):947                yield {948                    "key": "file",949                    "value": {950                        "content": base64.b64encode(file_to_copy).decode(),951                        "path": operation.path_in_repo,952                        "encoding": "base64",953                    },954                }955            elif file_to_copy.lfs:956                yield {957                    "key": "lfsFile",958                    "value": {959                        "path": operation.path_in_repo,960                        "algo": "sha256",961                        "oid": file_to_copy.lfs.sha256,962                    },963                }964            else:965                raise ValueError(966                    "Malformed files_to_copy (should be raw file content as bytes or RepoFile objects with LFS info."967                )968        # 2.e. Never expected to happen969        else:970            raise ValueError(971                f"Unknown operation to commit. Operation: {operation}. Upload mode:"972                f" {getattr(operation, '_upload_mode', None)}"973            )974 975    if nb_ignored_files > 0:976        logger.info(f"Skipped {nb_ignored_files} file(s) in commit (ignored by gitignore file).")977 
codekingpro/portable-devtools · Team Ai