"""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