CellTriage / src /utils /paths.py
Sarvarbek13's picture
Sync with repository cleanup: drop build-tooling docs, new root markers
d92710f verified
Raw
History Blame Contribute Delete
4.04 kB
"""Canonical filesystem locations for the CellTriage project.
WHAT: Resolves the project root once, at import time, and exposes every
directory the pipeline writes to as a module-level constant.
WHY: Results must be reproducible from a clean checkout on any machine. A
single hardcoded absolute path anywhere in the codebase silently breaks that
guarantee, and the failure mode is a run that *appears* to succeed while
writing artifacts somewhere nobody looks. Resolving the root by walking up
for a marker file means the pipeline behaves identically whether invoked from
the repository root, from ``notebooks/``, or by pytest.
"""
from __future__ import annotations
from pathlib import Path
# Files/directories that together identify the repository root unambiguously.
# No single marker is enough -- any one of these could plausibly exist in an
# unrelated ancestor directory -- so all three are required. They are also
# exactly the three that ship to the deployed Space, which resolves its root
# the same way.
_ROOT_MARKERS: tuple[str, ...] = ("configs", "src", "README.md")
def find_project_root(start: Path | None = None) -> Path:
"""Walk upward from ``start`` until a directory containing all root markers is found.
WHY a search rather than ``Path(__file__).parents[2]``: the relative depth
of this file is an implementation detail. If ``src/utils/`` is ever
reorganised, an index-based lookup breaks silently and points at the wrong
directory; a marker search fails loudly instead.
Args:
start: Directory to begin the search from. Defaults to this file's directory.
Returns:
Absolute path to the project root.
Raises:
RuntimeError: If no ancestor directory contains all root markers.
"""
current = (start or Path(__file__).resolve().parent).resolve()
for candidate in (current, *current.parents):
if all((candidate / marker).exists() for marker in _ROOT_MARKERS):
return candidate
raise RuntimeError(
f"Could not locate the CellTriage project root above {current}. "
f"Expected an ancestor directory containing all of: {', '.join(_ROOT_MARKERS)}."
)
PROJECT_ROOT: Path = find_project_root()
CONFIG_DIR: Path = PROJECT_ROOT / "configs"
DOCS_DIR: Path = PROJECT_ROOT / "docs"
NOTEBOOK_DIR: Path = PROJECT_ROOT / "notebooks"
APP_DIR: Path = PROJECT_ROOT / "app"
TEST_DIR: Path = PROJECT_ROOT / "tests"
DATA_DIR: Path = PROJECT_ROOT / "data"
RAW_DIR: Path = DATA_DIR / "raw"
INTERIM_DIR: Path = DATA_DIR / "interim"
PROCESSED_DIR: Path = DATA_DIR / "processed"
CELLS_DIR: Path = INTERIM_DIR / "cells"
OUTPUT_DIR: Path = PROJECT_ROOT / "outputs"
FIGURE_DIR: Path = OUTPUT_DIR / "figures"
MODEL_DIR: Path = OUTPUT_DIR / "models"
REPORT_DIR: Path = OUTPUT_DIR / "reports"
LOG_DIR: Path = OUTPUT_DIR / "logs"
#: Directories the pipeline is permitted to create on demand. ``data/raw`` is
#: deliberately absent: raw data is downloaded by an explicit Phase 2 step, and
#: auto-creating it would let a missing-download bug masquerade as an empty
#: dataset.
MANAGED_DIRS: tuple[Path, ...] = (
INTERIM_DIR,
PROCESSED_DIR,
CELLS_DIR,
FIGURE_DIR,
MODEL_DIR,
REPORT_DIR,
LOG_DIR,
)
def ensure_dirs(dirs: tuple[Path, ...] = MANAGED_DIRS) -> None:
"""Create the managed output directories if they do not already exist.
WHY: Logging and artifact writing must never fail on a fresh clone merely
because an output directory is absent (empty directories are not tracked
by git).
"""
for directory in dirs:
directory.mkdir(parents=True, exist_ok=True)
def relative_to_root(path: Path) -> Path:
"""Express ``path`` relative to the project root for stable, portable logging.
WHY: Absolute paths in logs and reports leak machine-specific detail and
make artifacts from two machines gratuitously diff-noisy.
"""
path = Path(path).resolve()
try:
return path.relative_to(PROJECT_ROOT)
except ValueError:
return path