Source code for paper_firehose.core.paths

"""Utilities for locating runtime data and built-in system assets."""

from __future__ import annotations

import os
import shutil
from pathlib import Path
from typing import Iterable

_ENV_VAR = "PAPER_FIREHOSE_DATA_DIR"
_DEFAULT_DIRNAME = ".paper_firehose"
_REPO_ROOT = Path(__file__).resolve().parents[3]
_PACKAGE_ROOT = Path(__file__).resolve().parents[1]
_SYSTEM_DIR = _PACKAGE_ROOT / "system"


def _normalize_relative(parts: Iterable[str]) -> Path:
    """Normalize relative path components, stripping legacy prefixes."""
    path = Path(*parts)
    if not path.parts:
        return Path()
    first = path.parts[0]
    if first in {"assets", _DEFAULT_DIRNAME, "system"}:
        path = Path(*path.parts[1:]) if len(path.parts) > 1 else Path()
    return path


[docs] def get_data_dir() -> Path: """Return the configured runtime data directory. Honors the PAPER_FIREHOSE_DATA_DIR environment variable; otherwise defaults to ~/.paper_firehose on the current platform. """ override = os.getenv(_ENV_VAR) if override is not None: cleaned = override.strip() if not cleaned: return (_REPO_ROOT / _DEFAULT_DIRNAME).resolve() candidate = Path(cleaned).expanduser() if not candidate.is_absolute(): candidate = (_REPO_ROOT / candidate).resolve() else: candidate = candidate.resolve() return candidate return (Path.home() / _DEFAULT_DIRNAME).resolve()
[docs] def ensure_data_dir() -> Path: """Ensure the data directory exists on disk and return it.""" data_dir = get_data_dir() data_dir.mkdir(parents=True, exist_ok=True) _seed_from_system(data_dir) return data_dir
[docs] def resolve_data_path(*relative: str, ensure_parent: bool = False) -> Path: """Resolve a path underneath the runtime data directory. Accepts legacy prefixes such as "assets/" to ease migration of existing configuration values. """ data_dir = ensure_data_dir() relative_path = _normalize_relative(relative) full_path = data_dir / relative_path if ensure_parent: full_path.parent.mkdir(parents=True, exist_ok=True) return full_path
[docs] def resolve_data_file(path: str, ensure_parent: bool = False) -> Path: """Resolve a configured file path against the data directory. Absolute paths (or explicit ones containing a drive letter on Windows) are used as-is. Relative paths are interpreted relative to the runtime data dir, with legacy "assets/" prefixes stripped for backward compatibility. """ candidate = Path(path).expanduser() if candidate.is_absolute(): if ensure_parent: candidate.parent.mkdir(parents=True, exist_ok=True) return candidate resolved = resolve_data_path(*candidate.parts, ensure_parent=ensure_parent) return resolved
[docs] def resolve_data_dir(*relative: str, ensure_exists: bool = False) -> Path: """Resolve a directory inside the runtime data directory.""" directory = resolve_data_path(*relative) if ensure_exists: directory.mkdir(parents=True, exist_ok=True) return directory
[docs] def get_system_dir() -> Path: """Return the repository's bundled system directory.""" return _SYSTEM_DIR
[docs] def get_system_path(*relative: str) -> Path: """Return a path inside the repository's system directory.""" return _SYSTEM_DIR.joinpath(*relative)
def _seed_from_system(target: Path) -> None: """Copy selected folders from system assets into *target* when missing.""" if target.resolve() == _SYSTEM_DIR.resolve(): return seeds = [ ("config", False), ("templates", False), ("models", True), ] for name, is_heavy in seeds: src = _SYSTEM_DIR / name if not src.exists(): continue dest = target / name if dest.exists(): continue try: shutil.copytree(src, dest) except FileExistsError: continue except Exception: if not is_heavy: raise __all__ = [ "get_data_dir", "ensure_data_dir", "resolve_data_path", "resolve_data_file", "resolve_data_dir", "get_system_dir", "get_system_path", ]