Source code for apsbits.utils.config_loaders
"""
Configuration management for the instrument.
This module serves as the single source of truth for instrument configuration.
It loads and validates the configuration from the iconfig.yml file and provides
access to the configuration throughout the application.
"""
import logging
import pathlib
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
import tomli # type: ignore
import yaml
logger = logging.getLogger(__name__)
logger.bsdev(__file__)
# Global configuration instance
_iconfig: Dict[str, Any] = {}
[docs]
def load_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
"""
Load configuration from a YAML or TOML file.
Args:
config_path: Path to the configuration file.
Returns:
The loaded configuration dictionary.
Raises:
ValueError: If config_path is None or if the file extension is not supported.
FileNotFoundError: If the configuration file does not exist.
"""
global _iconfig
if config_path is None:
raise ValueError("config_path must be provided")
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found at {config_path}")
try:
with open(config_path, "rb") as f:
if config_path.suffix.lower() == ".yml":
config = yaml.safe_load(f)
elif config_path.suffix.lower() == ".toml":
config = tomli.load(f)
else:
raise ValueError(
f"Unsupported configuration file format: {config_path.suffix}"
)
if config is None:
config = {}
_iconfig.update(config)
_iconfig["ICONFIG_PATH"] = str(config_path)
_iconfig["INSTRUMENT_PATH"] = str(config_path.parent)
_iconfig["INSTRUMENT_FOLDER"] = str(config_path.parent.name)
return _iconfig
except Exception as e:
logger.error("Error loading configuration: %s", e)
raise
[docs]
def get_config() -> Dict[str, Any]:
"""
Get the current configuration.
Returns:
The current configuration dictionary.
"""
return _iconfig
[docs]
def update_config(updates: Dict[str, Any]) -> None:
"""
Update the current configuration.
Args:
updates: Dictionary of configuration updates.
"""
_iconfig.update(updates)
[docs]
def load_config_yaml(config_obj) -> dict:
"""
Load configuration from a YAML file.
Args:
config_path: Path to the configuration file.
Returns:
The loaded configuration dictionary.
Raises:
FileNotFoundError: If the configuration file does not exist.
"""
if config_obj is None:
raise ValueError("config_path must be provided")
try:
# If it's a path, open it first
if isinstance(config_obj, (str, pathlib.Path)):
with open(config_obj, "r") as f:
content = f.read()
# Otherwise assume it's a file-like object
else:
content = config_obj.read()
iconfig = yaml.load(content, yaml.Loader)
return iconfig
except Exception as e:
logger.error("Error loading configuration: %s", e)
raise
[docs]
def validate_instrument_path(
instrument_path: Optional[Path] = None,
expected_files: Optional[List[str]] = None,
expected_dirs: Optional[List[str]] = None,
) -> Tuple[bool, str]:
"""
Validate if the provided instrument path is correct by checking for expected files
and directories.
Args:
instrument_path: Path to the instrument directory. If None, uses the path from
the current config.
expected_files: List of files that should exist in the instrument directory.
expected_dirs: List of directories that should exist in the instrument
directory.
Returns:
A tuple containing (is_valid, message) where is_valid is a boolean indicating if
the path is valid, and message is a description of the validation result.
"""
if instrument_path is None:
if "INSTRUMENT_PATH" not in _iconfig:
return False, "No instrument path found in configuration"
instrument_path = Path(_iconfig["INSTRUMENT_PATH"])
# Default expected files and directories if none provided
if expected_files is None:
expected_files = ["iconfig.yml", "iconfig.toml"]
if expected_dirs is None:
expected_dirs = ["src", "tests"]
# Check if the path exists and is a directory
if not instrument_path.exists():
return False, f"Instrument path does not exist: {instrument_path}"
if not instrument_path.is_dir():
return False, f"Instrument path is not a directory: {instrument_path}"
# Check for expected files
missing_files = []
for file in expected_files:
if not (instrument_path / file).exists():
missing_files.append(file)
if missing_files:
return (
False,
f"Missing expected files in instrument path: {', '.join(missing_files)}",
)
# Check for expected directories
missing_dirs = []
for directory in expected_dirs:
if (
not (instrument_path / directory).exists()
or not (instrument_path / directory).is_dir()
):
missing_dirs.append(directory)
if missing_dirs:
return (
False,
f"Missing expected directories in instrument path: "
f"{', '.join(missing_dirs)}",
)
return True, f"Instrument path is valid: {instrument_path}"