#!/usr/bin/env python3 """Configuration helper for py-wikijs. This module provides utilities to load configuration from various sources: - Environment variables (.env files) - YAML files - JSON files - INI files - Python dictionaries Usage: from config_helper import load_config, create_client_from_config # Load config from file config = load_config('config.yaml') # Create client from config client = create_client_from_config(config) # Or use auto-detection client = create_client_from_config() # Searches for config files """ import json import logging import os from configparser import ConfigParser from pathlib import Path from typing import Any, Dict, Optional, Union # Optional dependencies try: import yaml YAML_AVAILABLE = True except ImportError: YAML_AVAILABLE = False try: from dotenv import load_dotenv DOTENV_AVAILABLE = True except ImportError: DOTENV_AVAILABLE = False class ConfigLoader: """Load configuration from various sources.""" def __init__(self, config_dir: Optional[str] = None): """Initialize config loader. Args: config_dir: Directory to search for config files (default: current dir) """ self.config_dir = Path(config_dir) if config_dir else Path.cwd() def load_env(self, env_file: Optional[str] = None) -> Dict[str, Any]: """Load configuration from environment variables. Args: env_file: Path to .env file (optional) Returns: Configuration dictionary """ # Load .env file if available if env_file and DOTENV_AVAILABLE: load_dotenv(env_file) elif DOTENV_AVAILABLE: # Try to find .env in config directory env_path = self.config_dir / ".env" if env_path.exists(): load_dotenv(env_path) config = { "wikijs": { "url": os.getenv("WIKIJS_URL"), "auth": { "method": os.getenv("WIKIJS_AUTH_METHOD", "api_key"), "api_key": os.getenv("WIKIJS_API_KEY"), "jwt_token": os.getenv("WIKIJS_JWT_TOKEN"), }, }, "client": { "timeout": float(os.getenv("WIKIJS_TIMEOUT", "30.0")), "rate_limit": ( float(os.getenv("WIKIJS_RATE_LIMIT")) if os.getenv("WIKIJS_RATE_LIMIT") else None ), "max_retries": int(os.getenv("WIKIJS_MAX_RETRIES", "3")), "verify_ssl": os.getenv("WIKIJS_VERIFY_SSL", "true").lower() == "true", "pool_size": int(os.getenv("WIKIJS_POOL_SIZE", "10")), "user_agent": os.getenv("WIKIJS_USER_AGENT"), }, "logging": { "debug": os.getenv("WIKIJS_DEBUG", "false").lower() == "true", "level": os.getenv("WIKIJS_LOG_LEVEL", "INFO"), }, "metrics": { "enabled": os.getenv("WIKIJS_ENABLE_METRICS", "true").lower() == "true", }, } # Remove None values return self._clean_dict(config) def load_yaml(self, yaml_file: Union[str, Path]) -> Dict[str, Any]: """Load configuration from YAML file. Args: yaml_file: Path to YAML file Returns: Configuration dictionary Raises: ImportError: If PyYAML is not installed """ if not YAML_AVAILABLE: raise ImportError( "PyYAML is required for YAML config files. " "Install with: pip install pyyaml" ) yaml_path = Path(yaml_file) if not yaml_path.is_absolute(): yaml_path = self.config_dir / yaml_path with open(yaml_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def load_json(self, json_file: Union[str, Path]) -> Dict[str, Any]: """Load configuration from JSON file. Args: json_file: Path to JSON file Returns: Configuration dictionary """ json_path = Path(json_file) if not json_path.is_absolute(): json_path = self.config_dir / json_path with open(json_path, "r", encoding="utf-8") as f: return json.load(f) def load_ini(self, ini_file: Union[str, Path]) -> Dict[str, Any]: """Load configuration from INI file. Args: ini_file: Path to INI file Returns: Configuration dictionary """ ini_path = Path(ini_file) if not ini_path.is_absolute(): ini_path = self.config_dir / ini_path parser = ConfigParser() parser.read(ini_path) config: Dict[str, Any] = {} # Convert INI sections to nested dict for section in parser.sections(): section_dict: Dict[str, Any] = {} for key, value in parser.items(section): # Convert string values to appropriate types section_dict[key] = self._parse_value(value) # Handle nested sections (e.g., cache.redis) if "." in section: parent, child = section.split(".", 1) if parent not in config: config[parent] = {} config[parent][child] = section_dict else: config[section] = section_dict # Restructure for consistency with other formats if "wikijs" in config: config["wikijs"]["auth"] = { "method": config["wikijs"].pop("auth_method", "api_key"), "api_key": config["wikijs"].pop("api_key", None), "jwt_token": config["wikijs"].pop("jwt_token", None), } if "logging" in config: if "file_enabled" in config["logging"]: config["logging"]["file"] = { "enabled": config["logging"].pop("file_enabled"), "path": config["logging"].pop("file_path", None), "max_bytes": config["logging"].pop("file_max_bytes", None), "backup_count": config["logging"].pop("file_backup_count", None), } return self._clean_dict(config) def auto_load(self) -> Dict[str, Any]: """Auto-detect and load configuration from available sources. Searches for config files in this order: 1. .env file 2. config.yaml 3. config.yml 4. config.json 5. config.ini Returns: Configuration dictionary Raises: FileNotFoundError: If no config file is found """ # Try .env env_path = self.config_dir / ".env" if env_path.exists(): return self.load_env(env_path) # Try YAML for yaml_name in ["config.yaml", "config.yml"]: yaml_path = self.config_dir / yaml_name if yaml_path.exists(): return self.load_yaml(yaml_path) # Try JSON json_path = self.config_dir / "config.json" if json_path.exists(): return self.load_json(json_path) # Try INI ini_path = self.config_dir / "config.ini" if ini_path.exists(): return self.load_ini(ini_path) raise FileNotFoundError( f"No configuration file found in {self.config_dir}. " "Tried: .env, config.yaml, config.yml, config.json, config.ini" ) def _parse_value(self, value: str) -> Any: """Parse string value to appropriate type.""" # Boolean if value.lower() in ("true", "yes", "1", "on"): return True if value.lower() in ("false", "no", "0", "off"): return False # None/null if value.lower() in ("none", "null", ""): return None # Number try: if "." in value: return float(value) return int(value) except ValueError: pass # String return value def _clean_dict(self, d: Dict[str, Any]) -> Dict[str, Any]: """Remove None values from nested dictionary.""" cleaned = {} for key, value in d.items(): if isinstance(value, dict): nested = self._clean_dict(value) if nested: # Only add if not empty cleaned[key] = nested elif value is not None: cleaned[key] = value return cleaned def load_config( source: Optional[Union[str, Path, Dict[str, Any]]] = None, config_dir: Optional[str] = None, ) -> Dict[str, Any]: """Load configuration from a source. Args: source: Config source (file path, dict, or None for auto-detection) config_dir: Directory to search for config files Returns: Configuration dictionary Examples: >>> config = load_config('config.yaml') >>> config = load_config('.env') >>> config = load_config() # Auto-detect >>> config = load_config({'wikijs': {'url': '...'}}) """ loader = ConfigLoader(config_dir) # If source is a dict, return it directly if isinstance(source, dict): return source # If source is provided, load it if source: source_path = Path(source) # Determine file type by extension suffix = source_path.suffix.lower() if suffix in [".yaml", ".yml"]: return loader.load_yaml(source) elif suffix == ".json": return loader.load_json(source) elif suffix == ".ini": return loader.load_ini(source) elif suffix == ".env" or source_path.name == ".env": return loader.load_env(source) else: raise ValueError(f"Unknown config file type: {suffix}") # Auto-detect return loader.auto_load() def create_client_from_config( config: Optional[Union[str, Path, Dict[str, Any]]] = None, **kwargs: Any, ) -> Any: """Create WikiJSClient from configuration. Args: config: Config source (file path, dict, or None for auto-detection) **kwargs: Additional arguments to override config values Returns: Configured WikiJSClient instance Examples: >>> client = create_client_from_config('config.yaml') >>> client = create_client_from_config() # Auto-detect >>> client = create_client_from_config(timeout=60.0) # Override """ from wikijs import WikiJSClient # Load configuration if config is None: # Try to auto-load, fall back to env vars try: cfg = load_config() except FileNotFoundError: cfg = ConfigLoader().load_env() else: cfg = load_config(config) # Extract Wiki.js settings wikijs_cfg = cfg.get("wikijs", {}) url = kwargs.pop("url", wikijs_cfg.get("url")) if not url: raise ValueError("Wiki.js URL is required (set WIKIJS_URL or in config file)") # Determine auth auth_cfg = wikijs_cfg.get("auth", {}) auth_method = auth_cfg.get("method", "api_key") if "auth" not in kwargs: if auth_method == "api_key": auth = auth_cfg.get("api_key") if not auth: raise ValueError( "API key is required (set WIKIJS_API_KEY or in config file)" ) elif auth_method == "jwt": auth = auth_cfg.get("jwt_token") if not auth: raise ValueError( "JWT token is required (set WIKIJS_JWT_TOKEN or in config file)" ) else: auth = None else: auth = kwargs.pop("auth") # Extract client settings client_cfg = cfg.get("client", {}) timeout = kwargs.pop("timeout", client_cfg.get("timeout", 30.0)) rate_limit = kwargs.pop("rate_limit", client_cfg.get("rate_limit")) max_retries = kwargs.pop("max_retries", client_cfg.get("max_retries", 3)) verify_ssl = kwargs.pop("verify_ssl", client_cfg.get("verify_ssl", True)) # Extract logging settings logging_cfg = cfg.get("logging", {}) log_level_str = kwargs.pop("log_level", logging_cfg.get("level", "INFO")) # Convert log level string to logging constant log_level = getattr(logging, log_level_str.upper(), logging.INFO) # Create client client = WikiJSClient( url=url, auth=auth, timeout=timeout, rate_limit=rate_limit, max_retries=max_retries, verify_ssl=verify_ssl, log_level=log_level, **kwargs, ) return client def validate_config(config: Dict[str, Any]) -> bool: """Validate configuration dictionary. Args: config: Configuration dictionary Returns: True if valid Raises: ValueError: If configuration is invalid """ # Check required fields if "wikijs" not in config: raise ValueError("Missing 'wikijs' section in config") wikijs = config["wikijs"] if "url" not in wikijs: raise ValueError("Missing 'wikijs.url' in config") # Validate URL format url = wikijs["url"] if not url.startswith(("http://", "https://")): raise ValueError(f"Invalid URL format: {url}") # Check auth if "auth" in wikijs: auth = wikijs["auth"] method = auth.get("method", "api_key") if method == "api_key" and not auth.get("api_key"): raise ValueError("Missing API key for api_key auth method") if method == "jwt" and not auth.get("jwt_token"): raise ValueError("Missing JWT token for jwt auth method") return True if __name__ == "__main__": # Example usage print("šŸ”§ py-wikijs Configuration Helper\n") try: # Try to load config config = load_config() print(f"āœ… Loaded configuration from auto-detected source") print(f"\nConfiguration:") print(json.dumps(config, indent=2)) # Validate validate_config(config) print("\nāœ… Configuration is valid") # Create client client = create_client_from_config(config) print(f"\nāœ… Created WikiJSClient") print(f" URL: {client.base_url}") except FileNotFoundError as e: print(f"āŒ {e}") print("\nā„¹ļø Create a config file from one of the examples:") print(" - config.env.example → .env") print(" - config.yaml.example → config.yaml") print(" - config.json.example → config.json") print(" - config.ini.example → config.ini") except Exception as e: print(f"āŒ Error: {e}")