Edit on GitHub

starplast.paths

Where data lives — resolved in one place, so no module ever opens a relative path again.

Before this module existed, build_graph.py computed its dataset root as the repository's parent directory. That is true on exactly one machine. Measured from a clean checkout, 1 of 16 registry paths resolved; the other 15 were two directories away, and nothing said so. Any reader who cloned the repository and followed the instructions got fifteen missing files and no explanation, which for a paper whose whole claim is reproducibility is the wrong first impression.

There are two separate roots and confusing them is the mistake this module exists to prevent:

the CACHE     `starplast/data/` — built artefacts, committed, 17 MB, ships inside the package.
              The application needs this and nothing else. It must resolve offline, from an
              installed wheel, with no configuration, forever.

the DATASETS  `datasets/` — raw published inputs, tens of gigabytes, NOT committed. Only
              `build_graph.py` needs it. It is allowed to be missing, and when it is missing the
              answer is to fetch it, not to fail.

So data_dir() must never fail and dataset_root() is allowed to return a directory that is empty.

Resolution order

Both roots take an explicit override first, because a user who sets one has a reason:

$STARPLAST_DATA      the raw datasets tree
$STARPLAST_CACHE     the built cache

then the layouts that actually occur, in order, and finally the platform cache directory, which is where anything downloaded lands. describe() prints which candidate won, since a resolver that silently picks the wrong directory is harder to debug than one that fails.

ENV_DATASETS = 'STARPLAST_DATA'
ENV_CACHE = 'STARPLAST_CACHE'
ENV_STATE = 'STARPLAST_STATE'
def user_cache_dir() -> str:

Platform cache directory. Downloads land here, so it must be writable and per-user.

def data_dir() -> str:

The built cache. Returns the first candidate that holds a real cache, else the best guess.

Never raises. A missing cache is reported by check() with an instruction, because an exception from an import is the least useful way to tell someone a file is absent.

def cache_file(name: str) -> str:

Absolute path to one file in the built cache, resolved through the search chain.

def space_file(space, name: str) -> str:

Resolve a wheel file or the active downloaded pack, never falling back across species.

REQUIRED = ('nodes.parquet', 'graph.npz', 'toxodb_identity.tsv')
def check() -> tuple:

(ok, message). What the application needs, and what to do when it is not there.

def dataset_roots() -> list:

Every dataset root that exists, in priority order.

All of them, not just the first: after the move to datasets/<level>/<type>/<PMID>/ some files live under the new taxonomy and some under the old, and a resolver that stopped at the first existing root silently lost the screens once already.

def dataset_root(create: bool = False) -> str:

The primary dataset root. Falls back to the cache directory, which is always writable.

def find(*relpath) -> str | None:

Search every dataset root for a file. None when it is genuinely absent.

Returning None rather than raising is deliberate: a missing raw dataset is a normal state that the caller decides about -- fetch it, skip it, or report it -- and a resolver is the wrong layer to make that decision.

def describe() -> str:

What resolved to where. A resolver that picks silently is harder to debug than one that fails.