Source code for eradiate.data._file_resolver

from __future__ import annotations

from pathlib import Path

import attrs
import xarray as xr

from ._asset_manager import asset_manager
from ..attrs import define
from ..config import SOURCE_DIR, settings
from ..typing import PathLike


def _validator_dir_exists(instance, attribute, value):
    if not value.is_dir():
        raise NotADirectoryError(value)


[docs] @define class FileResolver: """ This class resolves paths relative to a list of locations on disk. Locations are looked up in order upon calling the :meth:`.resolve` method. If a lookup is successful, the resolved absolute path is returned; otherwise, the input path is returned unchanged. Parameters ---------- paths : list of path-likes, optional A list of search directories. Each entry must exist. Notes ----- * By default, the unique file resolver instance :data:`eradiate.fresolver <.fresolver>` is initialized with the installation path of the asset manager (see :class:`.AssetManager`) and, when in dev mode (see :data:`SOURCE_DIR <eradiate.config.SOURCE_DIR>`), the path to the location of test files. * The current working directory is not appended to the file resolver by default. It can however optionally be added temporarily when calling :meth:`resolve` (and wrappers such as :meth:`load_dataset`). This is useful when using the ``strict`` mode, which raises if the requested path cannot be resolved to an existing file. Examples -------- A single instance of the file resolver is available as :data:`eradiate.fresolver <.fresolver>`: >>> from eradiate import fresolver To add a path to the file resolver, use the :meth:`~.FileResolver.append` or :meth:`~.FileResolver.prepend` methods, *e.g.*: >>> fresolver.append("some/path/on/the/drive") To resolve a relative path, use the :meth:`~.FileResolver.resolve` method: >>> fresolver.resolve("srf/sentinel_2a-msi-3.nc") If the path is expected to point to an existing dataset file, the :meth:`~.FileResolver.load_dataset` method can be used to load it immediately: >>> fresolver.load_dataset("srf/sentinel_2a-msi-3.nc") """ paths: list[Path] = attrs.field( factory=list, converter=lambda value: [Path(x).resolve() for x in value], validator=attrs.validators.deep_iterable(_validator_dir_exists), )
[docs] def append(self, path: PathLike, avoid_duplicates: bool = True) -> None: """ Append an entry to the end of the list of search paths. Parameters ---------- path : path-like Path to append. The location must exist on disk. avoid_duplicates : bool, optional If ``True``, do not append again a path that is already registered. See Also -------- :meth:`.prepend` """ path = Path(path).resolve() if not path.is_dir(): raise NotADirectoryError(f"{path}") if avoid_duplicates and path in self.paths: return self.paths.append(path)
[docs] def clear(self) -> None: """ Clear the list of search paths. """ self.paths.clear()
[docs] def prepend(self, path: PathLike, avoid_duplicates: bool = True) -> None: """ Prepend an entry at the beginning of the list of search paths. Parameters ---------- path : path-like Path to prepend. The location must exist on disk. avoid_duplicates : bool, optional If ``True``, do not prepend again a path that is already registered. See Also -------- :meth:`.append` """ path = Path(path).resolve() if not path.is_dir(): raise NotADirectoryError(f"{path}") if avoid_duplicates and path in self.paths: return self.paths.insert(0, path)
[docs] def resolve(self, path: PathLike, strict: bool = False, cwd: bool = False) -> Path: """ Resolve a path: search all registered locations in order. If no file is found in any of the registered location, ``path`` is returned unchanged. Parameters ---------- path : path-like Path to be resolved. strict : bool, default: False If ``True``, resolution failure will raise. cwd : bool, default: False If ``True``, check first if a file relative to the current working directory exists. Returns ------- Path Resolved path Raises ------ FileNotFoundError If ``strict`` is ``True`` and ``path`` was not found in one of the registered directories. """ path = Path(path) if not path.is_absolute(): for base in self.paths if not cwd else [Path.cwd()] + self.paths: combined = base / path if combined.exists(): return combined if strict: raise FileNotFoundError(path) return path
[docs] def load_dataset( self, path: PathLike, strict: bool = False, cwd: bool = False ) -> xr.Dataset: """ Chain :meth:`resolve` and :func:`xarray.load_dataset`. Parameters ---------- path : path-like Path to be resolved. strict : bool, default: False If ``True``, resolution failure will raise. cwd : bool, default: False If ``True``, check first if a file relative to the current working directory exists. Returns ------- Dataset """ fname = self.resolve(path, strict=strict, cwd=cwd) return xr.load_dataset(fname)
[docs] def info(self, show: bool = False) -> dict | None: """ Collect information about the file resolver. Parameters ---------- show : bool If ``True``, display information to the terminal. Otherwise, return it as a dictionary. Returns ------- dict or None """ if show: title = "File resolver" print(title) print("-" * len(title)) for path in self.paths: print(f"• {path}") return None else: return {"paths": self.paths}
#: Unique file resolver instance (exposed as :data:`eradiate.fresolver`) fresolver = FileResolver(settings["path"]) fresolver.append(asset_manager.install_dir) if SOURCE_DIR: fresolver.append(SOURCE_DIR / "resources/data")