Ctrl K
ComunidadGuías…

rex/runtime_paths.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Canonical writable runtime paths for AskRex.

Runtime state must never depend on the process working directory. Electron
sets ``ASKREX_RUNTIME_DIR`` to its per-user data directory. Source checkouts
fall back to the repository root, while installed CLI use falls back to the
platform user-data directory.

Within ``data/``, household-shared state and private per-Rex-user state are
separated explicitly::

    data/household/...          shared configuration and service state
    data/users/<user_id>/...    private user-owned state
"""

from __future__ import annotations

import os
import sys
from pathlib import Path

_RUNTIME_DIR_ENV = "ASKREX_RUNTIME_DIR"
_CONFIG_PATH_ENV = "ASKREX_CONFIG_PATH"
_ENV_PATH_ENV = "ASKREX_ENV_PATH"
_PROFILES_DIR_ENV = "ASKREX_PROFILES_DIR"
_DATA_DIR_ENV = "REX_DATA_DIR"
_HOUSEHOLD_DATA_DIR_ENV = "ASKREX_HOUSEHOLD_DATA_DIR"
_USERS_DATA_DIR_ENV = "ASKREX_USERS_DATA_DIR"
_MEMORY_DIR_ENV = "ASKREX_MEMORY_DIR"


def _expanded_path(raw: str | os.PathLike[str]) -> Path:
    return Path(raw).expanduser().resolve(strict=False)


def source_checkout_root(start: Path | None = None) -> Path | None:
    """Return the source checkout root when ``pyproject.toml`` is discoverable."""
    current = (start or Path(__file__)).resolve(strict=False)
    if current.is_file():
        current = current.parent
    for candidate in (current, *current.parents):
        if (candidate / "pyproject.toml").is_file():
            return candidate
    return None


def _platform_user_data_root() -> Path:
    home = Path.home()
    if sys.platform == "win32":
        # LOCALAPPDATA is the correct boundary for machine-local application
        # state. APPDATA is only a compatibility fallback.
        base = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA")
        return _expanded_path(base) / "AskRex" if base else home / "AppData" / "Local" / "AskRex"
    if sys.platform == "darwin":
        return home / "Library" / "Application Support" / "AskRex"
    base = os.getenv("XDG_DATA_HOME")
    return (_expanded_path(base) if base else home / ".local" / "share") / "askrex"


def runtime_root() -> Path:
    """Return the canonical writable runtime root."""
    override = os.getenv(_RUNTIME_DIR_ENV)
    if override:
        return _expanded_path(override)
    checkout = source_checkout_root()
    return checkout if checkout is not None else _platform_user_data_root()


def _resolve(value: str | os.PathLike[str] | None, default: str) -> Path:
    if value is None:
        return (runtime_root() / default).resolve(strict=False)
    path = Path(value).expanduser()
    return (
        path.resolve(strict=False)
        if path.is_absolute()
        else (runtime_root() / path).resolve(strict=False)
    )


def config_path(value: str | os.PathLike[str] | None = None) -> Path:
    if value is None and os.getenv(_CONFIG_PATH_ENV):
        value = os.environ[_CONFIG_PATH_ENV]
    return _resolve(value, "config/rex_config.json")


def env_path(value: str | os.PathLike[str] | None = None) -> Path:
    if value is None and os.getenv(_ENV_PATH_ENV):
        value = os.environ[_ENV_PATH_ENV]
    return _resolve(value, ".env")


def profiles_dir(value: str | os.PathLike[str] | None = None) -> Path:
    if value is None and os.getenv(_PROFILES_DIR_ENV):
        value = os.environ[_PROFILES_DIR_ENV]
    return _resolve(value, "profiles")


def data_dir(value: str | os.PathLike[str] | None = None) -> Path:
    if value is None and os.getenv(_DATA_DIR_ENV):
        value = os.environ[_DATA_DIR_ENV]
    return _resolve(value, "data")


def household_data_dir(value: str | os.PathLike[str] | None = None) -> Path:
    """Return shared state, preserving the legacy ``REX_DATA_DIR`` contract."""
    if value is None and os.getenv(_HOUSEHOLD_DATA_DIR_ENV):
        value = os.environ[_HOUSEHOLD_DATA_DIR_ENV]
    if value is not None:
        return _resolve(value, "data/household")
    if os.getenv(_DATA_DIR_ENV):
        return data_dir()
    return _resolve(None, "data/household")


def users_data_dir(value: str | os.PathLike[str] | None = None) -> Path:
    """Return the parent directory for private Rex-user state."""
    if value is None and os.getenv(_USERS_DATA_DIR_ENV):
        value = os.environ[_USERS_DATA_DIR_ENV]
    if value is not None:
        return _resolve(value, "data/users")
    return (data_dir() / "users").resolve(strict=False)


def user_data_dir(user_id: str) -> Path:
    """Return private storage for a validated Rex user."""
    # Lazy import avoids a config/runtime_paths import cycle.
    from rex.identity import validate_user_id

    return users_data_dir() / validate_user_id(user_id)


def household_data_path(*parts: str | os.PathLike[str]) -> Path:
    """Resolve a path beneath household-shared storage."""
    return household_data_dir().joinpath(*map(Path, parts)).resolve(strict=False)


def user_data_path(user_id: str, *parts: str | os.PathLike[str]) -> Path:
    """Resolve a path beneath one validated user's private storage."""
    return user_data_dir(user_id).joinpath(*map(Path, parts)).resolve(strict=False)


def memory_dir(value: str | os.PathLike[str] | None = None) -> Path:
    if value is None and os.getenv(_MEMORY_DIR_ENV):
        value = os.environ[_MEMORY_DIR_ENV]
    return _resolve(value, "Memory")


__all__ = [
    "config_path",
    "data_dir",
    "env_path",
    "household_data_dir",
    "household_data_path",
    "memory_dir",
    "profiles_dir",
    "runtime_root",
    "source_checkout_root",
    "user_data_dir",
    "user_data_path",
    "users_data_dir",
]