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>
This commit is contained in:
487
examples/config_helper.py
Executable file
487
examples/config_helper.py
Executable file
@@ -0,0 +1,487 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user