CoolFace
Apppublic

Tonic/g-android-control

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
commit_scheduler.py393 linesDownload Raw Back to root
1# Originally copied from https://github.com/huggingface/huggingface_hub/blob/d0a948fc2a32ed6e557042a95ef3e4af97ec4a7c/src/huggingface_hub/_commit_scheduler.py2 3import atexit4import logging5import os6import time7from concurrent.futures import Future8from dataclasses import dataclass9from io import SEEK_END, SEEK_SET, BytesIO10from pathlib import Path11from threading import Lock, Thread12from typing import Callable, Dict, List, Optional, Union13 14from huggingface_hub.hf_api import (15    DEFAULT_IGNORE_PATTERNS,16    CommitInfo,17    CommitOperationAdd,18    HfApi,19)20from huggingface_hub.utils import filter_repo_objects21 22logger = logging.getLogger(__name__)23 24 25@dataclass(frozen=True)26class _FileToUpload:27    """Temporary dataclass to store info about files to upload. Not meant to be used directly."""28 29    local_path: Path30    path_in_repo: str31    size_limit: int32    last_modified: float33 34 35class CommitScheduler:36    """37    Scheduler to upload a local folder to the Hub at regular intervals (e.g. push to hub every 5 minutes).38 39    The recommended way to use the scheduler is to use it as a context manager. This ensures that the scheduler is40    properly stopped and the last commit is triggered when the script ends. The scheduler can also be stopped manually41    with the `stop` method. Checkout the [upload guide](https://huggingface.co/docs/huggingface_hub/guides/upload#scheduled-uploads)42    to learn more about how to use it.43 44    Args:45        repo_id (`str`):46            The id of the repo to commit to.47        folder_path (`str` or `Path`):48            Path to the local folder to upload regularly.49        every (`int` or `float`, *optional*):50            The number of minutes between each commit. Defaults to 5 minutes.51        path_in_repo (`str`, *optional*):52            Relative path of the directory in the repo, for example: `"checkpoints/"`. Defaults to the root folder53            of the repository.54        repo_type (`str`, *optional*):55            The type of the repo to commit to. Defaults to `model`.56        revision (`str`, *optional*):57            The revision of the repo to commit to. Defaults to `main`.58        private (`bool`, *optional*):59            Whether to make the repo private. If `None` (default), the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.60        token (`str`, *optional*):61            The token to use to commit to the repo. Defaults to the token saved on the machine.62        allow_patterns (`List[str]` or `str`, *optional*):63            If provided, only files matching at least one pattern are uploaded.64        ignore_patterns (`List[str]` or `str`, *optional*):65            If provided, files matching any of the patterns are not uploaded.66        squash_history (`bool`, *optional*):67            Whether to squash the history of the repo after each commit. Defaults to `False`. Squashing commits is68            useful to avoid degraded performances on the repo when it grows too large.69        hf_api (`HfApi`, *optional*):70            The [`HfApi`] client to use to commit to the Hub. Can be set with custom settings (user agent, token,...).71        on_before_commit (`Callable[[], None]`, *optional*):72            If specified, a function that will be called before the CommitScheduler lists files to create a commit.73 74    Example:75    ```py76    >>> from pathlib import Path77    >>> from huggingface_hub import CommitScheduler78 79    # Scheduler uploads every 10 minutes80    >>> csv_path = Path("watched_folder/data.csv")81    >>> CommitScheduler(repo_id="test_scheduler", repo_type="dataset", folder_path=csv_path.parent, every=10)82 83    >>> with csv_path.open("a") as f:84    ...     f.write("first line")85 86    # Some time later (...)87    >>> with csv_path.open("a") as f:88    ...     f.write("second line")89    ```90 91    Example using a context manager:92    ```py93    >>> from pathlib import Path94    >>> from huggingface_hub import CommitScheduler95 96    >>> with CommitScheduler(repo_id="test_scheduler", repo_type="dataset", folder_path="watched_folder", every=10) as scheduler:97    ...     csv_path = Path("watched_folder/data.csv")98    ...     with csv_path.open("a") as f:99    ...         f.write("first line")100    ...     (...)101    ...     with csv_path.open("a") as f:102    ...         f.write("second line")103 104    # Scheduler is now stopped and last commit have been triggered105    ```106    """107 108    def __init__(109        self,110        *,111        repo_id: str,112        folder_path: Union[str, Path],113        every: Union[int, float] = 5,114        path_in_repo: Optional[str] = None,115        repo_type: Optional[str] = None,116        revision: Optional[str] = None,117        private: Optional[bool] = None,118        token: Optional[str] = None,119        allow_patterns: Optional[Union[List[str], str]] = None,120        ignore_patterns: Optional[Union[List[str], str]] = None,121        squash_history: bool = False,122        hf_api: Optional["HfApi"] = None,123        on_before_commit: Optional[Callable[[], None]] = None,124    ) -> None:125        self.api = hf_api or HfApi(token=token)126        self.on_before_commit = on_before_commit127 128        # Folder129        self.folder_path = Path(folder_path).expanduser().resolve()130        self.path_in_repo = path_in_repo or ""131        self.allow_patterns = allow_patterns132 133        if ignore_patterns is None:134            ignore_patterns = []135        elif isinstance(ignore_patterns, str):136            ignore_patterns = [ignore_patterns]137        self.ignore_patterns = ignore_patterns + DEFAULT_IGNORE_PATTERNS138 139        if self.folder_path.is_file():140            raise ValueError(141                f"'folder_path' must be a directory, not a file: '{self.folder_path}'."142            )143        self.folder_path.mkdir(parents=True, exist_ok=True)144 145        # Repository146        repo_url = self.api.create_repo(147            repo_id=repo_id, private=private, repo_type=repo_type, exist_ok=True148        )149        self.repo_id = repo_url.repo_id150        self.repo_type = repo_type151        self.revision = revision152        self.token = token153 154        # Keep track of already uploaded files155        self.last_uploaded: Dict[156            Path, float157        ] = {}  # key is local path, value is timestamp158 159        # Scheduler160        if not every > 0:161            raise ValueError(f"'every' must be a positive integer, not '{every}'.")162        self.lock = Lock()163        self.every = every164        self.squash_history = squash_history165 166        logger.info(167            f"Scheduled job to push '{self.folder_path}' to '{self.repo_id}' every {self.every} minutes."168        )169        self._scheduler_thread = Thread(target=self._run_scheduler, daemon=True)170        self._scheduler_thread.start()171        atexit.register(self._push_to_hub)172 173        self.__stopped = False174 175    def stop(self) -> None:176        """Stop the scheduler.177 178        A stopped scheduler cannot be restarted. Mostly for tests purposes.179        """180        self.__stopped = True181 182    def __enter__(self) -> "CommitScheduler":183        return self184 185    def __exit__(self, exc_type, exc_value, traceback) -> None:186        # Upload last changes before exiting187        self.trigger().result()188        self.stop()189        return190 191    def _run_scheduler(self) -> None:192        """Dumb thread waiting between each scheduled push to Hub."""193        while True:194            self.last_future = self.trigger()195            time.sleep(self.every * 60)196            if self.__stopped:197                break198 199    def trigger(self) -> Future:200        """Trigger a `push_to_hub` and return a future.201 202        This method is automatically called every `every` minutes. You can also call it manually to trigger a commit203        immediately, without waiting for the next scheduled commit.204        """205        return self.api.run_as_future(self._push_to_hub)206 207    def _push_to_hub(self) -> Optional[CommitInfo]:208        if self.__stopped:  # If stopped, already scheduled commits are ignored209            return None210 211        logger.info("(Background) scheduled commit triggered.")212        try:213            value = self.push_to_hub()214            if self.squash_history:215                logger.info("(Background) squashing repo history.")216                self.api.super_squash_history(217                    repo_id=self.repo_id, repo_type=self.repo_type, branch=self.revision218                )219            return value220        except Exception as e:221            logger.error(222                f"Error while pushing to Hub: {e}"223            )  # Depending on the setup, error might be silenced224            raise225 226    def push_to_hub(self) -> Optional[CommitInfo]:227        """228        Push folder to the Hub and return the commit info.229 230        <Tip warning={true}>231 232        This method is not meant to be called directly. It is run in the background by the scheduler, respecting a233        queue mechanism to avoid concurrent commits. Making a direct call to the method might lead to concurrency234        issues.235 236        </Tip>237 238        The default behavior of `push_to_hub` is to assume an append-only folder. It lists all files in the folder and239        uploads only changed files. If no changes are found, the method returns without committing anything. If you want240        to change this behavior, you can inherit from [`CommitScheduler`] and override this method. This can be useful241        for example to compress data together in a single file before committing. For more details and examples, check242        out our [integration guide](https://huggingface.co/docs/huggingface_hub/main/en/guides/upload#scheduled-uploads).243        """244        # Check files to upload (with lock)245        with self.lock:246            if self.on_before_commit is not None:247                self.on_before_commit()248 249            logger.debug("Listing files to upload for scheduled commit.")250 251            # List files from folder (taken from `_prepare_upload_folder_additions`)252            relpath_to_abspath = {253                path.relative_to(self.folder_path).as_posix(): path254                for path in sorted(255                    self.folder_path.glob("**/*")256                )  # sorted to be deterministic257                if path.is_file()258            }259            prefix = f"{self.path_in_repo.strip('/')}/" if self.path_in_repo else ""260 261            # Filter with pattern + filter out unchanged files + retrieve current file size262            files_to_upload: List[_FileToUpload] = []263            for relpath in filter_repo_objects(264                relpath_to_abspath.keys(),265                allow_patterns=self.allow_patterns,266                ignore_patterns=self.ignore_patterns,267            ):268                local_path = relpath_to_abspath[relpath]269                stat = local_path.stat()270                if (271                    self.last_uploaded.get(local_path) is None272                    or self.last_uploaded[local_path] != stat.st_mtime273                ):274                    files_to_upload.append(275                        _FileToUpload(276                            local_path=local_path,277                            path_in_repo=prefix + relpath,278                            size_limit=stat.st_size,279                            last_modified=stat.st_mtime,280                        )281                    )282 283        # Return if nothing to upload284        if len(files_to_upload) == 0:285            logger.debug("Dropping schedule commit: no changed file to upload.")286            return None287 288        # Convert `_FileToUpload` as `CommitOperationAdd` (=> compute file shas + limit to file size)289        logger.debug("Removing unchanged files since previous scheduled commit.")290        add_operations = [291            CommitOperationAdd(292                # TODO: Cap the file to its current size, even if the user append data to it while a scheduled commit is happening293                # (requires an upstream fix for XET-535: `hf_xet` should support `BinaryIO` for upload)294                path_or_fileobj=file_to_upload.local_path,295                path_in_repo=file_to_upload.path_in_repo,296            )297            for file_to_upload in files_to_upload298        ]299 300        # Upload files (append mode expected - no need for lock)301        logger.debug("Uploading files for scheduled commit.")302        commit_info = self.api.create_commit(303            repo_id=self.repo_id,304            repo_type=self.repo_type,305            operations=add_operations,306            commit_message="Scheduled Commit",307            revision=self.revision,308        )309 310        # Successful commit: keep track of the latest "last_modified" for each file311        for file in files_to_upload:312            self.last_uploaded[file.local_path] = file.last_modified313        return commit_info314 315 316class PartialFileIO(BytesIO):317    """A file-like object that reads only the first part of a file.318 319    Useful to upload a file to the Hub when the user might still be appending data to it. Only the first part of the320    file is uploaded (i.e. the part that was available when the filesystem was first scanned).321 322    In practice, only used internally by the CommitScheduler to regularly push a folder to the Hub with minimal323    disturbance for the user. The object is passed to `CommitOperationAdd`.324 325    Only supports `read`, `tell` and `seek` methods.326 327    Args:328        file_path (`str` or `Path`):329            Path to the file to read.330        size_limit (`int`):331            The maximum number of bytes to read from the file. If the file is larger than this, only the first part332            will be read (and uploaded).333    """334 335    def __init__(self, file_path: Union[str, Path], size_limit: int) -> None:336        self._file_path = Path(file_path)337        self._file = self._file_path.open("rb")338        self._size_limit = min(size_limit, os.fstat(self._file.fileno()).st_size)339 340    def __del__(self) -> None:341        self._file.close()342        return super().__del__()343 344    def __repr__(self) -> str:345        return (346            f"<PartialFileIO file_path={self._file_path} size_limit={self._size_limit}>"347        )348 349    def __len__(self) -> int:350        return self._size_limit351 352    def __getattribute__(self, name: str):353        if name.startswith("_") or name in (354            "read",355            "tell",356            "seek",357        ):  # only 3 public methods supported358            return super().__getattribute__(name)359        raise NotImplementedError(f"PartialFileIO does not support '{name}'.")360 361    def tell(self) -> int:362        """Return the current file position."""363        return self._file.tell()364 365    def seek(self, __offset: int, __whence: int = SEEK_SET) -> int:366        """Change the stream position to the given offset.367 368        Behavior is the same as a regular file, except that the position is capped to the size limit.369        """370        if __whence == SEEK_END:371            # SEEK_END => set from the truncated end372            __offset = len(self) + __offset373            __whence = SEEK_SET374 375        pos = self._file.seek(__offset, __whence)376        if pos > self._size_limit:377            return self._file.seek(self._size_limit)378        return pos379 380    def read(self, __size: Optional[int] = -1) -> bytes:381        """Read at most `__size` bytes from the file.382 383        Behavior is the same as a regular file, except that it is capped to the size limit.384        """385        current = self._file.tell()386        if __size is None or __size < 0:387            # Read until file limit388            truncated_size = self._size_limit - current389        else:390            # Read until file limit or __size391            truncated_size = min(__size, self._size_limit - current)392        return self._file.read(truncated_size)393