Spaces:
Running on Zero
Running on Zero
| """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 | |