Source code for atloop.config.loader

"""Configuration loader using varlord (lib/api)."""

import logging
from pathlib import Path
from typing import Optional

from varlord import Config, sources
from varlord.global_config import get_global_config, set_global_config

from atloop.config.models import AtloopConfig

logger = logging.getLogger(__name__)


[docs] class ConfigLoader: """Configuration loader - uses varlord for lib/api.""" _atloop_dir: Optional[Path] = None
[docs] @staticmethod def setup(atloop_dir: Optional[str] = None) -> Config: """ Setup configuration - call once at application startup. Args: atloop_dir: Custom atloop directory (for testing) Returns: Config instance (also registered globally) """ logger.debug(f"[ConfigLoader] Setting up config with atloop_dir: {atloop_dir}") # Step 1: Find .atloop directory (independent of varlord) if atloop_dir: atloop_path = Path(atloop_dir).resolve() if not atloop_path.exists(): raise ValueError(f"Specified atloop directory does not exist: {atloop_path}") logger.debug(f"[ConfigLoader] Using specified atloop_dir: {atloop_path}") else: # Check current directory first project_atloop = Path.cwd() / ".atloop" if project_atloop.exists() and project_atloop.is_dir(): atloop_path = project_atloop logger.debug(f"[ConfigLoader] Using project .atloop: {atloop_path}") else: # Fallback to user home directory atloop_path = Path.home() / ".atloop" logger.debug(f"[ConfigLoader] Using user .atloop: {atloop_path}") # Ensure .atloop directory exists (create if needed) atloop_path.mkdir(parents=True, exist_ok=True) # Store for later access ConfigLoader._atloop_dir = atloop_path # Build sources list (lowest to highest priority) config_sources = [] logger.debug("[ConfigLoader] Building config sources") # User config (lowest priority) user_config = Path.home() / ".atloop" / "config" / "atloop.yaml" if user_config.exists(): config_sources.append(sources.YAML(str(user_config))) logger.debug(f"[ConfigLoader] Added user config: {user_config}") # Project config (higher priority) project_config = Path.cwd() / ".atloop" / "config" / "atloop.yaml" if project_config.exists() and project_config != atloop_path / "config" / "atloop.yaml": config_sources.append(sources.YAML(str(project_config))) logger.debug(f"[ConfigLoader] Added project config: {project_config}") # Custom atloop_dir config (highest priority for files) if atloop_dir: custom_config = atloop_path / "config" / "atloop.yaml" if custom_config.exists(): config_sources.append(sources.YAML(str(custom_config))) logger.debug(f"[ConfigLoader] Added custom config: {custom_config}") # Environment variables config_sources.append(sources.Env(prefix="ATLOOP__")) logger.debug("[ConfigLoader] Added environment variables source") # .env file env_file = Path.cwd() / ".env" if env_file.exists(): config_sources.append(sources.DotEnv(str(env_file))) logger.debug(f"[ConfigLoader] Added .env file: {env_file}") config_sources.append(sources.CLI()) # Create configuration logger.debug(f"[ConfigLoader] Creating Config with {len(config_sources)} sources") cfg = Config( model=AtloopConfig, sources=config_sources, ) # Register globally set_global_config(cfg, name="atloop") logger.info("[ConfigLoader] Configuration setup complete, registered globally") return cfg
[docs] @staticmethod def get() -> AtloopConfig: """ Get configuration - access from anywhere in lib/api. Returns: Loaded AtloopConfig instance (type-safe, validated against AtloopConfig model) Raises: KeyError: If config not initialized (call setup() first) RequiredFieldError: If required fields missing (varlord validation) TypeError: If types don't match model (varlord validation) """ logger.debug("[ConfigLoader] Getting config from global registry") config = get_global_config(name="atloop") loaded_config = config.load() # Validated against AtloopConfig model logger.debug(f"[ConfigLoader] Config loaded: ai={loaded_config.ai.completion.model}") return loaded_config # Type: AtloopConfig (guaranteed by varlord)
[docs] @staticmethod def get_atloop_dir() -> Path: """Get the found .atloop directory path.""" if ConfigLoader._atloop_dir is None: raise RuntimeError("ConfigLoader.setup() must be called first") return ConfigLoader._atloop_dir