Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
cache_metadata.py232 linesDownload Raw Back to implementations
1from __future__ import annotations2 3import os4import pickle5import time6from typing import TYPE_CHECKING7 8from fsspec.utils import atomic_write9 10try:11    import ujson as json12except ImportError:13    if not TYPE_CHECKING:14        import json15 16if TYPE_CHECKING:17    from collections.abc import Iterator18    from typing import Any, Literal, TypeAlias19 20    from .cached import CachingFileSystem21 22    Detail: TypeAlias = dict[str, Any]23 24 25class CacheMetadata:26    """Cache metadata.27 28    All reading and writing of cache metadata is performed by this class,29    accessing the cached files and blocks is not.30 31    Metadata is stored in a single file per storage directory in JSON format.32    For backward compatibility, also reads metadata stored in pickle format33    which is converted to JSON when next saved.34    """35 36    def __init__(self, storage: list[str]):37        """38 39        Parameters40        ----------41        storage: list[str]42            Directories containing cached files, must be at least one. Metadata43            is stored in the last of these directories by convention.44        """45        if not storage:46            raise ValueError("CacheMetadata expects at least one storage location")47 48        self._storage = storage49        self.cached_files: list[Detail] = [{}]50 51        # Private attribute to force saving of metadata in pickle format rather than52        # JSON for use in tests to confirm can read both pickle and JSON formats.53        self._force_save_pickle = False54 55    def _load(self, fn: str) -> Detail:56        """Low-level function to load metadata from specific file"""57        try:58            with open(fn, "r") as f:59                loaded = json.load(f)60        except ValueError:61            with open(fn, "rb") as f:62                loaded = pickle.load(f)63        for c in loaded.values():64            if isinstance(c.get("blocks"), list):65                c["blocks"] = set(c["blocks"])66        return loaded67 68    def _save(self, metadata_to_save: Detail, fn: str) -> None:69        """Low-level function to save metadata to specific file"""70        if self._force_save_pickle:71            with atomic_write(fn) as f:72                pickle.dump(metadata_to_save, f)73        else:74            with atomic_write(fn, mode="w") as f:75                json.dump(metadata_to_save, f)76 77    def _scan_locations(78        self, writable_only: bool = False79    ) -> Iterator[tuple[str, str, bool]]:80        """Yield locations (filenames) where metadata is stored, and whether81        writable or not.82 83        Parameters84        ----------85        writable: bool86            Set to True to only yield writable locations.87 88        Returns89        -------90        Yields (str, str, bool)91        """92        n = len(self._storage)93        for i, storage in enumerate(self._storage):94            writable = i == n - 195            if writable_only and not writable:96                continue97            yield os.path.join(storage, "cache"), storage, writable98 99    def check_file(100        self, path: str, cfs: CachingFileSystem | None101    ) -> Literal[False] | tuple[Detail, str]:102        """If path is in cache return its details, otherwise return ``False``.103 104        If the optional CachingFileSystem is specified then it is used to105        perform extra checks to reject possible matches, such as if they are106        too old.107        """108        for (fn, base, _), cache in zip(self._scan_locations(), self.cached_files):109            if path not in cache:110                continue111            detail = cache[path].copy()112 113            if cfs is not None:114                if cfs.check_files and detail["uid"] != cfs.fs.ukey(path):115                    # Wrong file as determined by hash of file properties116                    continue117                if cfs.expiry and time.time() - detail["time"] > cfs.expiry:118                    # Cached file has expired119                    continue120 121            fn = os.path.join(base, detail["fn"])122            if os.path.exists(fn):123                return detail, fn124        return False125 126    def clear_expired(self, expiry_time: int) -> tuple[list[str], bool]:127        """Remove expired metadata from the cache.128 129        Returns names of files corresponding to expired metadata and a boolean130        flag indicating whether the writable cache is empty. Caller is131        responsible for deleting the expired files.132        """133        expired_files = []134        for path, detail in self.cached_files[-1].copy().items():135            if time.time() - detail["time"] > expiry_time:136                fn = detail.get("fn", "")137                if not fn:138                    raise RuntimeError(139                        f"Cache metadata does not contain 'fn' for {path}"140                    )141                fn = os.path.join(self._storage[-1], fn)142                expired_files.append(fn)143                self.cached_files[-1].pop(path)144 145        if self.cached_files[-1]:146            cache_path = os.path.join(self._storage[-1], "cache")147            self._save(self.cached_files[-1], cache_path)148 149        writable_cache_empty = not self.cached_files[-1]150        return expired_files, writable_cache_empty151 152    def load(self) -> None:153        """Load all metadata from disk and store in ``self.cached_files``"""154        cached_files = []155        for fn, _, _ in self._scan_locations():156            if os.path.exists(fn):157                # TODO: consolidate blocks here158                cached_files.append(self._load(fn))159            else:160                cached_files.append({})161        self.cached_files = cached_files or [{}]162 163    def on_close_cached_file(self, f: Any, path: str) -> None:164        """Perform side-effect actions on closing a cached file.165 166        The actual closing of the file is the responsibility of the caller.167        """168        # File must be writable, so in self.cached_files[-1]169        c = self.cached_files[-1][path]170        if c["blocks"] is not True and len(c["blocks"]) * f.blocksize >= f.size:171            c["blocks"] = True172 173    def pop_file(self, path: str) -> str | None:174        """Remove metadata of cached file.175 176        If path is in the cache, return the filename of the cached file,177        otherwise return ``None``.  Caller is responsible for deleting the178        cached file.179        """180        details = self.check_file(path, None)181        if not details:182            return None183        _, fn = details184        if fn.startswith(self._storage[-1]):185            self.cached_files[-1].pop(path)186            self.save()187        else:188            raise PermissionError(189                "Can only delete cached file in last, writable cache location"190            )191        return fn192 193    def save(self) -> None:194        """Save metadata to disk"""195        for (fn, _, writable), cache in zip(self._scan_locations(), self.cached_files):196            if not writable:197                continue198 199            if os.path.exists(fn):200                cached_files = self._load(fn)201                for k, c in cached_files.items():202                    if k in cache:203                        if c["blocks"] is True or cache[k]["blocks"] is True:204                            c["blocks"] = True205                        else:206                            # self.cached_files[*][*]["blocks"] must continue to207                            # point to the same set object so that updates208                            # performed by MMapCache are propagated back to209                            # self.cached_files.210                            blocks = cache[k]["blocks"]211                            blocks.update(c["blocks"])212                            c["blocks"] = blocks213                        c["time"] = max(c["time"], cache[k]["time"])214                        c["uid"] = cache[k]["uid"]215 216                # Files can be added to cache after it was written once217                for k, c in cache.items():218                    if k not in cached_files:219                        cached_files[k] = c220            else:221                cached_files = cache222            cache = {k: v.copy() for k, v in cached_files.items()}223            for c in cache.values():224                if isinstance(c["blocks"], set):225                    c["blocks"] = list(c["blocks"])226            self._save(cache, fn)227            self.cached_files[-1] = cached_files228 229    def update_file(self, path: str, detail: Detail) -> None:230        """Update metadata for specific file in memory, do not save"""231        self.cached_files[-1][path] = detail232 
codekingpro/portable-devtools · Team Ai