Files
manual_slop/src/paths.py
2026-03-12 19:08:51 -04:00

165 lines
5.5 KiB
Python

"""
Paths - Centralized path resolution for configuration and environment variables.
This module provides centralized path resolution for all configurable paths in the application.
All paths can be overridden via environment variables or config.toml.
Environment Variables:
SLOP_CONFIG: Path to config.toml
SLOP_LOGS_DIR: Path to logs directory
SLOP_SCRIPTS_DIR: Path to generated scripts directory
Configuration (config.toml):
[paths]
logs_dir = "logs/sessions"
scripts_dir = "scripts/generated"
Path Functions:
get_config_path() -> Path to config.toml
get_conductor_dir(project_path=None) -> Path to conductor directory
get_logs_dir() -> Path to logs/sessions
get_scripts_dir() -> Path to scripts/generated
get_tracks_dir(project_path=None) -> Path to conductor/tracks
get_track_state_dir(track_id, project_path=None) -> Path to conductor/tracks/<track_id>
get_archive_dir(project_path=None) -> Path to conductor/archive
Resolution Order:
1. Check project-specific manual_slop.toml (for conductor paths)
2. Check environment variable (for logs/scripts)
3. Check config.toml [paths] section (for logs/scripts)
4. Fall back to default
Usage:
from src.paths import get_logs_dir, get_scripts_dir
logs_dir = get_logs_dir()
scripts_dir = get_scripts_dir()
See Also:
- docs/guide_tools.md for configuration documentation
- src/session_logger.py for logging paths
- src/project_manager.py for project paths
"""
from pathlib import Path
import os
import tomllib
from typing import Optional, Any
_RESOLVED: dict[str, Path] = {}
def get_config_path() -> Path:
root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
def get_global_presets_path() -> Path:
root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml"))
def get_project_presets_path(project_root: Path) -> Path:
return project_root / "project_presets.toml"
def get_global_tool_presets_path() -> Path:
root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml"))
def get_project_tool_presets_path(project_root: Path) -> Path:
return project_root / "project_tool_presets.toml"
def get_global_personas_path() -> Path:
root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_GLOBAL_PERSONAS", root_dir / "personas.toml"))
def get_project_personas_path(project_root: Path) -> Path:
return project_root / "project_personas.toml"
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
root_dir = Path(__file__).resolve().parent.parent
p = None
if env_var in os.environ:
p = Path(os.environ[env_var])
else:
try:
with open(get_config_path(), "rb") as f:
cfg = tomllib.load(f)
if "paths" in cfg and config_key in cfg["paths"]:
p = Path(cfg["paths"][config_key])
except (FileNotFoundError, tomllib.TOMLDecodeError):
pass
if p is None:
p = Path(default)
if not p.is_absolute():
return root_dir / p
return p
def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
# Look for manual_slop.toml in project_root
toml_path = project_root / 'manual_slop.toml'
if not toml_path.exists(): return None
try:
with open(toml_path, 'rb') as f:
data = tomllib.load(f)
# Check [conductor] dir = '...'
c_dir = data.get('conductor', {}).get('dir')
if c_dir:
p = Path(c_dir)
if not p.is_absolute(): p = project_root / p
return p.resolve()
except: pass
return None
def get_conductor_dir(project_path: Optional[str] = None) -> Path:
if not project_path:
# Fallback for legacy/tests, but we should avoid this
return Path('conductor').resolve()
project_root = Path(project_path).resolve()
p = _get_project_conductor_dir_from_toml(project_root)
if p: return p
return (project_root / "conductor").resolve()
def get_logs_dir() -> Path:
if "logs_dir" not in _RESOLVED:
_RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions")
return _RESOLVED["logs_dir"]
def get_scripts_dir() -> Path:
if "scripts_dir" not in _RESOLVED:
_RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated")
return _RESOLVED["scripts_dir"]
def get_tracks_dir(project_path: Optional[str] = None) -> Path:
return get_conductor_dir(project_path) / "tracks"
def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Path:
return get_tracks_dir(project_path) / track_id
def get_archive_dir(project_path: Optional[str] = None) -> Path:
return get_conductor_dir(project_path) / "archive"
def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, Any]:
if env_var in os.environ:
return {'path': Path(os.environ[env_var]).resolve(), 'source': f'env:{env_var}'}
try:
with open(get_config_path(), 'rb') as f:
cfg = tomllib.load(f)
if 'paths' in cfg and config_key in cfg['paths']:
p = Path(cfg['paths'][config_key])
if not p.is_absolute():
p = (Path(__file__).resolve().parent.parent / p).resolve()
return {'path': p, 'source': 'config.toml'}
except: pass
root_dir = Path(__file__).resolve().parent.parent
p = (root_dir / default).resolve()
return {'path': p, 'source': 'default'}
def get_full_path_info() -> dict[str, dict[str, Any]]:
return {
'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'),
'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated')
}
def reset_resolved() -> None:
"""For testing only - clear cached resolutions."""
_RESOLVED.clear()