Source code for nstat.datasets

"""Lightweight manifest-driven dataset locator.

This module exposes a small public API for discovering and verifying the
example datasets distributed with nSTAT (and downloaded on demand via
:mod:`nstat.data_manager`).  It reads ``nstat/data/manifest.json`` to
resolve dataset names to filesystem paths.

Exported symbols
----------------
- :func:`list_datasets` — list every dataset name in the manifest.
- :func:`get_dataset_path` — resolve a dataset name to a
  :class:`pathlib.Path` (raises :class:`~nstat.errors.DataNotFoundError`).
- :func:`verify_checksums` — recompute SHA-256 digests for every dataset
  and compare against the manifest.

No MATLAB counterpart — the MATLAB toolbox ships datasets as ``.mat``
files in the repository.  See :mod:`nstat.data_manager` for the
download / cache layer.
"""
from __future__ import annotations

import hashlib
import json
from pathlib import Path

from .data_manager import ensure_example_data
from .errors import DataNotFoundError

MANIFEST_PATH = Path(__file__).resolve().parent / "data" / "manifest.json"


def _repo_root() -> Path:
    cur = Path(__file__).resolve()
    for candidate in [cur, *cur.parents]:
        if (candidate / "data").exists() and (candidate / "nstat" / "data" / "manifest.json").exists():
            return candidate
    # Standalone Python checkouts may not include MATLAB-side data/helpfiles trees.
    return MANIFEST_PATH.parents[2]


def _load_manifest() -> dict[str, dict[str, str]]:
    if not MANIFEST_PATH.exists():
        raise DataNotFoundError(f"Dataset manifest not found: {MANIFEST_PATH}")
    payload = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
    entries = payload.get("datasets", {})
    if not isinstance(entries, dict):
        raise ValueError("Invalid dataset manifest format; 'datasets' must be a mapping")
    return entries


def _sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


[docs] def list_datasets() -> list[str]: """List the names of all datasets registered in the manifest. Returns ------- list of str Dataset names from ``nstat/data/manifest.json``, sorted alphabetically. Raises ------ nstat.errors.DataNotFoundError If the manifest file is missing. ValueError If the manifest's ``datasets`` entry is not a mapping. """ return sorted(_load_manifest().keys())
def _resolve_dataset_target(rel_path: str) -> Path: repo_root = _repo_root() rel = Path(rel_path) if not rel.parts: return repo_root / rel if rel.parts[0] == "data": try: data_dir = ensure_example_data(download=False) except FileNotFoundError as exc: raise DataNotFoundError(str(exc)) from exc return data_dir.joinpath(*rel.parts[1:]) return repo_root / rel
[docs] def get_dataset_path(name: str) -> Path: """Resolve a registered dataset name to its path on disk. Parameters ---------- name : str Dataset name, one of :func:`list_datasets`. Returns ------- pathlib.Path Existing filesystem path of the dataset. Entries under ``data/`` are looked up in the example-data cache (see :func:`nstat.data_manager.ensure_example_data`); no download is attempted. Raises ------ nstat.errors.DataNotFoundError If ``name`` is not in the manifest, the example data is not installed, or the resolved path does not exist. """ entries = _load_manifest() if name not in entries: raise DataNotFoundError(f"Unknown dataset '{name}'. Available: {', '.join(sorted(entries))}") path = _resolve_dataset_target(entries[name]["path"]) if not path.exists(): raise DataNotFoundError(f"Dataset '{name}' not found at expected path: {path}") return path
[docs] def verify_checksums() -> dict[str, bool]: """Verify every manifest dataset against its recorded SHA-256 digest. Returns ------- dict of str to bool Maps each dataset name to ``True`` when the file exists and its SHA-256 digest equals the manifest value. ``False`` is reported for a missing or uninstalled file and for a manifest entry that has no digest. Raises ------ nstat.errors.DataNotFoundError If the manifest file itself is missing. """ entries = _load_manifest() result: dict[str, bool] = {} for name, item in entries.items(): try: path = _resolve_dataset_target(item["path"]) except DataNotFoundError: result[name] = False continue expected = item.get("sha256", "") if not path.exists() or not expected: result[name] = False continue result[name] = _sha256(path) == expected return result