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