Files
py-wikijs/examples/config_helper.py
Claude 2ace16f5f0 feat: Add comprehensive configuration system and examples
Added complete configuration support with multiple formats and examples
to help users easily configure py-wikijs for their projects.

New Features:
- Configuration file templates for ENV, YAML, JSON, and INI formats
- config_helper.py: Universal configuration loader and client factory
  * Auto-detects and loads configs from multiple formats
  * Supports environment variables, YAML, JSON, and INI files
  * Provides create_client_from_config() for easy client creation
  * Validates configuration and provides helpful error messages

Configuration Templates:
- config.env.example: Environment variables (Docker, 12-factor apps)
- config.yaml.example: YAML with multi-environment support
- config.json.example: JSON for programmatic configuration
- config.ini.example: INI for traditional setups

Usage Examples:
- using_env_config.py: Complete example using .env files
- using_yaml_config.py: Complete example using YAML configuration
- using_json_config.py: Complete example using JSON configuration

Documentation:
- docs/CONFIGURATION_GUIDE.md: Comprehensive configuration guide
  * All configuration methods explained
  * Security best practices
  * Environment-specific configurations
  * Troubleshooting guide

Benefits:
 Flexible configuration (choose your preferred format)
 Keep credentials secure (no hardcoding)
 Environment-specific configs (dev/staging/prod)
 Docker/container-ready
 Full validation and error handling
 Comprehensive documentation and examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 19:41:39 +00:00

488 lines
14 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}")