from pathlib import Path
from typing import Dict, Any, Optional, Union
import json
import os
import logging
from .config_schema import PaymentConfig
logger = logging.getLogger(__name__)
[docs]
def load_config_from_file(file_path: Union[str, Path]) -> Dict[str, Any]:
"""
Load configuration from a JSON file.
Args:
file_path: Path to JSON configuration file
Returns:
Configuration dictionary
Raises:
FileNotFoundError: If the configuration file doesn't exist
json.JSONDecodeError: If the configuration file is not valid JSON
"""
file_path = Path(file_path) if isinstance(file_path, str) else file_path
if not file_path.exists():
raise FileNotFoundError(f"Configuration file not found: {file_path}")
with open(file_path, "r") as f:
config = json.load(f)
logger.info(f"Configuration loaded from {file_path}")
return config
[docs]
def load_config_from_env() -> Dict[str, Any]:
"""
Load configuration from environment variables.
Environment variables should be prefixed with PAYMENT_
For nested keys, use double underscore as separator
Examples:
- PAYMENT_DEFAULT_PROVIDER=stripe
- PAYMENT_DATABASE__URL=postgresql://user:pass@localhost/db
- PAYMENT_PROVIDERS__STRIPE__API_KEY=sk_test_123
Returns:
Configuration dictionary
"""
config = {}
# Find all environment variables with the PAYMENT_ prefix
payment_vars = {k: v for k, v in os.environ.items()
if k.startswith("PAYMENT_")}
for key, value in payment_vars.items():
# Remove prefix and split by double underscore
key = key[8:] # Remove "PAYMENT_"
parts = key.lower().split("__")
# Build nested configuration
current = config
for i, part in enumerate(parts):
if i == len(parts) - 1:
# Convert value types
if value.lower() == "true":
current[part] = True
elif value.lower() == "false":
current[part] = False
elif value.isdigit():
current[part] = int(value)
elif value.replace(".", "", 1).isdigit() and value.count(".") < 2:
current[part] = float(value)
else:
current[part] = value
else:
if part not in current:
current[part] = {}
current = current[part]
logger.info("Configuration loaded from environment variables")
return config
[docs]
def merge_configs(
base_config: Dict[str, Any], override_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Merge two configuration dictionaries with the override taking precedence.
Args:
base_config: Base configuration
override_config: Override configuration
Returns:
Merged configuration dictionary
"""
result = base_config.copy()
for key, value in override_config.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = merge_configs(result[key], value)
else:
result[key] = value
return result
[docs]
def load_config(
file_path: Optional[Union[str, Path]] = None,
env_override: bool = True,
validate: bool = True,
) -> Union[PaymentConfig, Dict[str, Any]]:
"""
Load and validate payment configuration.
Args:
file_path: Optional path to JSON configuration file
env_override: Whether to override with environment variables
validate: Whether to validate and return a PaymentConfig instance
Returns:
PaymentConfig if validate=True, otherwise a configuration dictionary
Raises:
FileNotFoundError: If the configuration file doesn't exist
json.JSONDecodeError: If the configuration file is not valid JSON
ValidationError: If validation is enabled and the configuration is invalid
"""
config = {}
# Load from file if provided
if file_path:
config = load_config_from_file(file_path)
# Override with environment variables if enabled
if env_override:
env_config = load_config_from_env()
config = merge_configs(config, env_config)
# Validate and return PaymentConfig instance if enabled
if validate:
return PaymentConfig(**config)
return config