Source code for scitex_scholar.config.ScholarConfig

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-10-11 06:45:52 (ywatanabe)"
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/scholar/config/ScholarConfig.py
# ----------------------------------------
from __future__ import annotations

import os

__FILE__ = "./src/scitex/scholar/config/ScholarConfig.py"
__DIR__ = os.path.dirname(__FILE__)
# ----------------------------------------

__FILE__ = __file__

import re
from pathlib import Path
from typing import Optional, Union

import yaml
from scitex_logging import ScholarError, getLogger

from .core._CascadeConfig import CascadeConfig
from .core._PathManager import PathManager

logger = getLogger(__name__)


[docs] class ScholarConfig:
[docs] def __init__( self, config_path: Optional[Union[str, Path]] = None, scholar_dir: Optional[Union[str, Path]] = None, ): """Initialize ScholarConfig. Args: config_path: Path to custom config YAML file scholar_dir: Direct path to scholar directory (e.g., /data/users/alice/.scitex) This bypasses SCITEX_DIR env var for thread-safe multi-user usage. Use this in Django/multi-user environments to avoid race conditions. """ self.name = self.__class__.__name__ self._explicit_scholar_dir = scholar_dir # Store for thread-safe access if config_path and Path(config_path).exists(): config_data = self.load_yaml(config_path) else: default_path = Path(__file__).parent / "default.yaml" config_data = self.load_yaml(default_path) self.cascade = CascadeConfig(config_data, "SCITEX_SCHOLAR_") self._setup_path_manager()
[docs] def __getattr__(self, name): """Delegate all ``get_*`` methods to ``path_manager``.""" if name.startswith("get_") and hasattr(self.path_manager, name): return getattr(self.path_manager, name) raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'" )
[docs] def __dir__(self): """Include ``path_manager``'s ``get_*`` methods in ``dir()`` output.""" own_attrs = object.__dir__(self) path_manager_get_methods = [ attr for attr in dir(self.path_manager) if attr.startswith("get_") and callable(getattr(self.path_manager, attr)) ] return list(own_attrs) + path_manager_get_methods
# Delegate methods for cleaner API (composition over inheritance)
[docs] def resolve(self, key, direct_val=None, default=None, type=str, mask=None): """Resolve configuration value with precedence: direct → config → env → default""" return self.cascade.resolve(key, direct_val, default, type, mask)
[docs] def get(self, key): """Get value from config dict only""" return self.cascade.get(key)
[docs] def print(self): """Print how each config was resolved""" return self.cascade.print()
[docs] def clear_log(self): """Clear resolution log""" return self.cascade.clear_log()
[docs] def load_yaml(self, path: Path) -> dict: try: with open(path) as f: content = f.read() def env_replacer(match): env_expr = match.group(1) if ":-" in env_expr: var_name, default_value = env_expr.split(":-", 1) value = os.getenv(var_name, default_value.strip('"')) else: value = os.getenv(env_expr) if value in ["true", "false"]: return value elif value == "null": return "null" elif value and not (value.startswith('"') and value.endswith('"')): return f'"{value}"' else: return value or "null" content = re.sub(r"\$\{([^}]+)\}", env_replacer, content) # logger.info(f"ScholarConfig object configured with: {path}") return yaml.safe_load(content) except Exception: raise ScholarError( f"{path} not loaded and ScholarConfig object not created" )
[docs] @classmethod def load(cls, path: Optional[Union[str, Path]] = None): return cls(path)
# Path Management ---------------------------------------- def _setup_path_manager(self, scholar_dir=None): # Priority: explicit parameter > env var > config > default if self._explicit_scholar_dir: # Use explicitly provided path (thread-safe for multi-user) base_path = Path(self._explicit_scholar_dir).expanduser() / "scholar" else: # Fall back to cascade resolution (uses SCITEX_DIR env var) scholar_dir = self.cascade.resolve("scholar_dir", default="~/.scitex") base_path = Path(scholar_dir).expanduser() / "scholar" self.path_manager = PathManager(scholar_dir=base_path) @property def paths(self): """Access to path manager for organized directory structure""" return self.path_manager
# EOF