feat: implement configuration and environment management CLI (issue #18)
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled

Complete implementation of configuration management capabilities for MarkiTect CLI:

New CLI Commands:
- markitect config-show: Display current configuration with multiple output formats
- markitect config-set: Set configuration values with validation and persistence
- markitect config-init: Initialize configuration for new project with interactive setup
- markitect config-validate: Validate current configuration and show issues
- markitect config-help: Get help information for configuration keys

Core Features:
- Comprehensive configuration management with multiple sources (files, env vars, defaults)
- Support for YAML, JSON, and simple output formats
- Sensitive data masking for secure configuration display
- Interactive project initialization with intelligent defaults
- Configuration validation with path creation and URL validation
- Environment variable integration with MARKITECT_ prefix
- Nested configuration support with dot notation (e.g., gitea.url)
- Type conversion for boolean, numeric, and string values
- Project-specific configuration files (.markitect.yml/yaml/json)

Technical Implementation:
- ConfigurationManager class with robust error handling
- Integration with existing configuration system
- File-based configuration with automatic format detection
- Configuration validation and help system
- Support for custom configuration file locations
- Graceful fallback when advanced config system unavailable

Configuration Features:
- Multiple file format support (YAML, JSON)
- Environment variable precedence
- Sensitive data protection
- Directory structure validation and creation
- URL and path validation
- Interactive and non-interactive modes

Testing:
- 58 comprehensive tests covering all functionality
- CLI integration tests with isolated environments
- Edge cases: permissions, invalid paths, complex structures
- Configuration file parsing and saving tests
- Environment variable handling tests
- Validation and error handling scenarios

All acceptance criteria fulfilled:
 Configuration display and management
 Project initialization functionality
 Configuration validation
 Integration with existing config system
 Comprehensive test coverage

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 10:53:44 +02:00
parent 0982e771e4
commit f6c285b774
3 changed files with 1683 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ from .database import DatabaseManager
from .legacy_compat import LegacyMode, emit_deprecation_warning, legacy_switch_option
from .__version__ import get_version_info, get_release_info
from .batch_processor import BatchProcessor, ProcessingMode, ErrorHandling, create_file_processor
from .config_manager import ConfigurationManager
# Import legacy system components for advanced management
try:
@@ -4744,6 +4745,301 @@ def recursive(config, directory, depth, operation, pattern, error_handling, quie
sys.exit(1)
# Configuration Management Commands - Issue #18
@cli.command(name='config-show')
@click.option('--format', 'output_format', type=click.Choice(['yaml', 'json', 'simple']),
default='yaml', help='Output format for configuration display')
@click.option('--show-sensitive', is_flag=True, help='Show sensitive values (tokens, passwords)')
@pass_config
def config_show(config, output_format, show_sensitive):
"""Display current configuration.
Shows comprehensive configuration information including current settings,
file sources, environment variables, and workspace information.
Examples:
markitect config-show
markitect config-show --format json
markitect config-show --format simple --show-sensitive
"""
try:
config_manager = ConfigurationManager()
output = config_manager.display_config(
show_sensitive=show_sensitive,
output_format=output_format
)
click.echo(output)
except Exception as e:
click.echo(f"Failed to display configuration: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='config-set')
@click.argument('key', type=str)
@click.argument('value', type=str)
@click.option('--config-file', type=click.Path(), help='Target configuration file')
@click.option('--validate/--no-validate', default=True, help='Validate configuration after setting')
@pass_config
def config_set(config, key, value, config_file, validate):
"""Set configuration values.
Sets a configuration value and persists it to a configuration file.
Supports nested keys using dot notation (e.g., 'gitea.url').
Examples:
markitect config-set gitea_url http://localhost:3000
markitect config-set repo_owner myorganization
markitect config-set api_token abc123def456
markitect config-set workspace.dir ./my_workspace
"""
try:
config_manager = ConfigurationManager()
# Set the configuration value
success = config_manager.set_config_value(key, value, config_file)
if success:
click.echo(f"✅ Configuration updated: {key} = {value}")
# Show which file was updated
target_file = config_manager._get_target_config_file(config_file)
click.echo(f"📁 Updated file: {target_file}")
# Validate configuration if requested
if validate:
validation_results = config_manager.validate_configuration()
errors = [r for r in validation_results if r['status'] == 'error']
if errors:
click.echo("⚠️ Configuration validation warnings:")
for error in errors:
click.echo(f"{error['key']}: {error['message']}")
else:
click.echo(f"❌ Failed to set configuration: {key}", err=True)
sys.exit(1)
except ValueError as e:
click.echo(f"❌ Configuration error: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"❌ Failed to set configuration: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='config-init')
@click.option('--project-dir', type=click.Path(path_type=Path), help='Target project directory')
@click.option('--interactive/--no-interactive', default=True, help='Interactive configuration setup')
@click.option('--force', is_flag=True, help='Overwrite existing configuration')
@pass_config
def config_init(config, project_dir, interactive, force):
"""Initialize configuration for new project.
Creates a new configuration file with sensible defaults and sets up
the necessary directory structure for a MarkiTect project.
Examples:
markitect config-init
markitect config-init --project-dir ./my-project
markitect config-init --no-interactive --force
"""
try:
target_dir = project_dir or Path.cwd()
config_file = target_dir / '.markitect.yml'
# Check if configuration already exists
if config_file.exists() and not force:
click.echo(f"❌ Configuration file already exists: {config_file}")
click.echo(" Use --force to overwrite or choose a different directory")
sys.exit(1)
config_manager = ConfigurationManager()
# Interactive setup if requested
initial_config = {
'gitea_url': 'http://localhost:3000',
'repo_owner': '',
'repo_name': target_dir.name,
'workspace_dir': '.markitect_workspace',
'cache_dir': '.ast_cache',
'tests_dir': 'tests',
'test_file_pattern': 'test_issue_{issue_num}_{scenario}.py',
'claude_code_command': 'claude'
}
if interactive:
click.echo("🔧 Interactive MarkiTect configuration setup")
click.echo(f"📁 Target directory: {target_dir}")
click.echo()
# Prompt for each configuration value
initial_config['gitea_url'] = click.prompt(
'Gitea server URL',
default=initial_config['gitea_url']
)
initial_config['repo_owner'] = click.prompt(
'Repository owner/organization',
default=initial_config['repo_owner']
)
initial_config['repo_name'] = click.prompt(
'Repository name',
default=initial_config['repo_name']
)
if click.confirm('Configure API token now?', default=False):
initial_config['api_token'] = click.prompt(
'API token',
default='',
hide_input=True
)
initial_config['workspace_dir'] = click.prompt(
'Workspace directory',
default=initial_config['workspace_dir']
)
initial_config['tests_dir'] = click.prompt(
'Tests directory',
default=initial_config['tests_dir']
)
# Initialize the project
result = config_manager.initialize_project_config(target_dir, interactive=False)
# Update with interactive values if provided
if interactive:
config_manager._save_config_file(initial_config, config_file)
result['config'] = initial_config
click.echo("✅ MarkiTect project initialized successfully!")
click.echo(f"📄 Configuration file: {result['config_file']}")
click.echo("📁 Created directories:")
for directory in result['created_directories']:
click.echo(f"{directory}")
# Show validation results
validation_results = config_manager.validate_configuration(result['config'])
warnings = [r for r in validation_results if r['status'] == 'warning']
errors = [r for r in validation_results if r['status'] == 'error']
if warnings:
click.echo("⚠️ Configuration warnings:")
for warning in warnings:
click.echo(f"{warning['message']}")
if errors:
click.echo("❌ Configuration errors:")
for error in errors:
click.echo(f"{error['message']}")
else:
click.echo("🎉 Configuration validation passed!")
except Exception as e:
click.echo(f"❌ Failed to initialize configuration: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='config-validate')
@click.option('--verbose', '-v', is_flag=True, help='Show detailed validation information')
@pass_config
def config_validate(config, verbose):
"""Validate current configuration.
Checks the current configuration for common issues, missing required fields,
and validates paths and URLs. Provides suggestions for fixing any problems.
Examples:
markitect config-validate
markitect config-validate --verbose
"""
try:
config_manager = ConfigurationManager()
validation_results = config_manager.validate_configuration()
# Categorize results
errors = [r for r in validation_results if r['status'] == 'error']
warnings = [r for r in validation_results if r['status'] == 'warning']
ok_results = [r for r in validation_results if r['status'] == 'ok']
# Display summary
click.echo(f"📊 Configuration Validation Summary:")
click.echo(f" ✅ OK: {len(ok_results)}")
click.echo(f" ⚠️ Warnings: {len(warnings)}")
click.echo(f" ❌ Errors: {len(errors)}")
click.echo()
# Show errors
if errors:
click.echo("❌ Configuration Errors:")
for error in errors:
click.echo(f"{error['key']}: {error['message']}")
click.echo()
# Show warnings
if warnings:
click.echo("⚠️ Configuration Warnings:")
for warning in warnings:
click.echo(f"{warning['key']}: {warning['message']}")
click.echo()
# Show OK results in verbose mode
if verbose and ok_results:
click.echo("✅ Valid Configuration:")
for ok_result in ok_results:
click.echo(f"{ok_result['key']}: {ok_result['message']}")
click.echo()
# Overall status
if errors:
click.echo("❌ Configuration validation failed")
sys.exit(1)
elif warnings:
click.echo("⚠️ Configuration validation passed with warnings")
else:
click.echo("✅ Configuration validation passed")
except Exception as e:
click.echo(f"❌ Configuration validation failed: {e}", err=True)
if config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command(name='config-help')
@click.argument('key', required=False)
@pass_config
def config_help(config, key):
"""Get help information for configuration keys.
Provides detailed information about available configuration options,
their purposes, and example values.
Examples:
markitect config-help
markitect config-help gitea_url
markitect config-help api_token
"""
try:
config_manager = ConfigurationManager()
help_text = config_manager.get_config_help(key)
click.echo(help_text)
except Exception as e:
click.echo(f"❌ Failed to get configuration help: {e}", err=True)
sys.exit(1)
# Register issue management commands
cli.add_command(issues_group)

490
markitect/config_manager.py Normal file
View File

@@ -0,0 +1,490 @@
"""
Configuration and Environment Management - Issue #18
This module provides comprehensive configuration management capabilities for MarkiTect,
allowing users to manage configuration and environment settings through CLI commands.
Features:
- Display current configuration with optional sensitive data masking
- Set configuration values with validation
- Initialize configuration for new projects
- Configuration validation and help
- Integration with existing configuration system
- Environment variable management
- Project-specific configuration support
Commands implemented:
- config-show: Display current configuration
- config-set: Set configuration values
- config-init: Initialize configuration for new project
"""
import os
import json
import yaml
from pathlib import Path
from typing import Dict, Any, Optional, List, Union, Tuple
from dataclasses import asdict, fields
from copy import deepcopy
import click
class ConfigurationManager:
"""Core configuration management functionality."""
def __init__(self):
self.config_file_names = [
'.markitect.yml',
'.markitect.yaml',
'.markitect.json',
'markitect.config.yml',
'markitect.config.yaml',
'markitect.config.json'
]
self.sensitive_keys = {
'api_token', 'token', 'password', 'secret', 'key',
'gitea_token', 'github_token', 'auth_token'
}
def get_current_config(self) -> Dict[str, Any]:
"""Get current configuration from all sources."""
config = {}
# Load from existing configuration system
try:
from config.manager import MarkitectConfig
markitect_config = MarkitectConfig()
config = asdict(markitect_config)
# Convert Path objects to strings for JSON serialization
for key, value in config.items():
if isinstance(value, Path):
config[key] = str(value)
except (ImportError, Exception):
# Fallback to basic configuration
config = {
'gitea_url': os.getenv('MARKITECT_GITEA_URL', 'http://localhost:3000'),
'repo_owner': os.getenv('MARKITECT_REPO_OWNER', ''),
'repo_name': os.getenv('MARKITECT_REPO_NAME', ''),
'api_token': os.getenv('MARKITECT_API_TOKEN', ''),
'workspace_dir': os.getenv('MARKITECT_WORKSPACE_DIR', '.markitect_workspace'),
'database_path': os.getenv('MARKITECT_DATABASE_PATH', str(Path.home() / '.markitect' / 'markitect.db')),
'cache_dir': os.getenv('MARKITECT_CACHE_DIR', '.ast_cache'),
'tests_dir': os.getenv('MARKITECT_TESTS_DIR', 'tests'),
'test_file_pattern': os.getenv('MARKITECT_TEST_FILE_PATTERN', 'test_issue_{issue_num}_{scenario}.py'),
'claude_code_command': os.getenv('MARKITECT_CLAUDE_CODE_COMMAND', 'claude')
}
# Add metadata about configuration sources
config['_meta'] = {
'config_sources': self._get_config_sources(),
'env_variables': self._get_relevant_env_vars(),
'working_directory': str(Path.cwd()),
'config_file_locations': self._get_config_file_locations()
}
return config
def _get_config_sources(self) -> List[str]:
"""Get list of configuration sources in order of precedence."""
sources = []
# Check for config files
for config_name in self.config_file_names:
if Path(config_name).exists():
sources.append(f"File: {config_name}")
# Check for environment variables
env_vars = [key for key in os.environ.keys() if key.startswith('MARKITECT_')]
if env_vars:
sources.append(f"Environment variables: {len(env_vars)} found")
# Always include defaults
sources.append("Built-in defaults")
return sources
def _get_relevant_env_vars(self) -> Dict[str, str]:
"""Get MARKITECT-related environment variables."""
env_vars = {}
for key, value in os.environ.items():
if key.startswith('MARKITECT_'):
env_vars[key] = value
return env_vars
def _get_config_file_locations(self) -> Dict[str, bool]:
"""Get status of potential config file locations."""
locations = {}
# Current directory
for config_name in self.config_file_names:
current_dir_path = Path.cwd() / config_name
locations[str(current_dir_path)] = current_dir_path.exists()
# Home directory
home_config = Path.home() / '.markitect' / 'config.yml'
locations[str(home_config)] = home_config.exists()
return locations
def display_config(self, show_sensitive: bool = False, output_format: str = 'yaml') -> str:
"""Display current configuration in specified format."""
config = self.get_current_config()
if not show_sensitive:
config = self._mask_sensitive_data(config)
if output_format == 'json':
return json.dumps(config, indent=2, default=str)
elif output_format == 'yaml':
return yaml.dump(config, default_flow_style=False, sort_keys=True)
else:
# Simple format
return self._format_simple(config)
def _mask_sensitive_data(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Mask sensitive configuration values."""
masked_config = deepcopy(config)
def mask_recursive(obj):
if isinstance(obj, dict):
for key, value in obj.items():
if any(sensitive in key.lower() for sensitive in self.sensitive_keys):
if value and str(value).strip():
obj[key] = '***MASKED***'
else:
mask_recursive(value)
elif isinstance(obj, list):
for item in obj:
mask_recursive(item)
mask_recursive(masked_config)
return masked_config
def _format_simple(self, config: Dict[str, Any]) -> str:
"""Format configuration in simple key=value style."""
lines = []
def format_recursive(obj, prefix=''):
if isinstance(obj, dict):
for key, value in sorted(obj.items()):
full_key = f"{prefix}{key}" if prefix else key
if isinstance(value, (dict, list)):
if key != '_meta': # Skip meta in simple format
format_recursive(value, f"{full_key}.")
else:
lines.append(f"{full_key} = {value}")
elif isinstance(obj, list):
for i, item in enumerate(obj):
format_recursive(item, f"{prefix}{i}.")
format_recursive(config)
return '\n'.join(lines)
def set_config_value(self, key: str, value: str, config_file: Optional[str] = None) -> bool:
"""Set a configuration value and persist it."""
# Determine target config file
target_file = self._get_target_config_file(config_file)
# Load existing config from file
existing_config = self._load_config_file(target_file) if target_file.exists() else {}
# Set the value (support nested keys with dot notation)
self._set_nested_value(existing_config, key, value)
# Validate the new configuration
validation_errors = self._validate_config_value(key, value)
if validation_errors:
raise ValueError(f"Configuration validation failed: {'; '.join(validation_errors)}")
# Save back to file
self._save_config_file(existing_config, target_file)
return True
def _get_target_config_file(self, config_file: Optional[str] = None) -> Path:
"""Determine target configuration file."""
if config_file:
return Path(config_file)
# Look for existing config file
for config_name in self.config_file_names:
config_path = Path(config_name)
if config_path.exists():
return config_path
# Default to .markitect.yml in current directory
return Path('.markitect.yml')
def _load_config_file(self, config_file: Path) -> Dict[str, Any]:
"""Load configuration from file."""
try:
content = config_file.read_text()
if config_file.suffix in ['.yml', '.yaml']:
return yaml.safe_load(content) or {}
elif config_file.suffix == '.json':
return json.loads(content)
else:
# Try YAML first, then JSON
try:
return yaml.safe_load(content) or {}
except yaml.YAMLError:
return json.loads(content)
except Exception as e:
raise ValueError(f"Failed to load config file {config_file}: {e}")
def _save_config_file(self, config: Dict[str, Any], config_file: Path) -> None:
"""Save configuration to file."""
try:
# Ensure directory exists
config_file.parent.mkdir(parents=True, exist_ok=True)
if config_file.suffix in ['.yml', '.yaml']:
content = yaml.dump(config, default_flow_style=False, sort_keys=True)
elif config_file.suffix == '.json':
content = json.dumps(config, indent=2, sort_keys=True)
else:
# Default to YAML
content = yaml.dump(config, default_flow_style=False, sort_keys=True)
config_file.write_text(content)
except Exception as e:
raise ValueError(f"Failed to save config file {config_file}: {e}")
def _set_nested_value(self, config: Dict[str, Any], key: str, value: str) -> None:
"""Set a nested configuration value using dot notation."""
keys = key.split('.')
current = config
# Navigate to the parent of the target key
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
# Set the final value
final_key = keys[-1]
# Try to convert value to appropriate type
converted_value = self._convert_value(value)
current[final_key] = converted_value
def _convert_value(self, value: str) -> Any:
"""Convert string value to appropriate type."""
# Handle boolean values
if value.lower() in ('true', 'yes', 'on', '1'):
return True
elif value.lower() in ('false', 'no', 'off', '0'):
return False
# Try to convert to number
try:
if '.' in value:
return float(value)
else:
return int(value)
except ValueError:
pass
# Return as string
return value
def _validate_config_value(self, key: str, value: str) -> List[str]:
"""Validate a configuration value."""
errors = []
# Path validation
if any(path_key in key.lower() for path_key in ['dir', 'path', 'file']):
if value and not self._is_valid_path(value):
errors.append(f"'{value}' is not a valid path format")
# URL validation
if 'url' in key.lower():
if value and not self._is_valid_url(value):
errors.append(f"'{value}' is not a valid URL format")
# Required field validation for setting values (not for display)
# We only enforce required fields when explicitly setting them
if key == 'gitea_url' and not value.strip():
errors.append(f"'{key}' is required and cannot be empty")
elif key == 'database_path' and not value.strip():
errors.append(f"'{key}' is required and cannot be empty")
return errors
def _is_valid_path(self, path_str: str) -> bool:
"""Check if a string represents a valid path."""
try:
Path(path_str)
return True
except (ValueError, OSError):
return False
def _is_valid_url(self, url_str: str) -> bool:
"""Check if a string represents a valid URL."""
import re
url_pattern = re.compile(
r'^https?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain...
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return url_pattern.match(url_str) is not None
def initialize_project_config(self, project_dir: Optional[Path] = None, interactive: bool = True) -> Dict[str, Any]:
"""Initialize configuration for a new project."""
target_dir = project_dir or Path.cwd()
config_file = target_dir / '.markitect.yml'
# Default configuration template
default_config = {
'gitea_url': 'http://localhost:3000',
'repo_owner': '',
'repo_name': target_dir.name,
'workspace_dir': '.markitect_workspace',
'cache_dir': '.ast_cache',
'tests_dir': 'tests',
'test_file_pattern': 'test_issue_{issue_num}_{scenario}.py',
'claude_code_command': 'claude'
}
if interactive:
default_config = self._interactive_config_setup(default_config)
# Create necessary directories
workspace_dir = target_dir / default_config['workspace_dir']
cache_dir = target_dir / default_config['cache_dir']
tests_dir = target_dir / default_config['tests_dir']
for directory in [workspace_dir, cache_dir, tests_dir]:
directory.mkdir(parents=True, exist_ok=True)
# Save configuration
self._save_config_file(default_config, config_file)
return {
'config_file': str(config_file),
'config': default_config,
'created_directories': [str(workspace_dir), str(cache_dir), str(tests_dir)]
}
def _interactive_config_setup(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Interactive configuration setup (would be used with CLI)."""
# This method would be called by CLI with click.prompt()
# For now, return the default config
# The CLI integration will handle the interactive prompts
return config
def list_config_keys(self) -> List[Tuple[str, str, Any]]:
"""List all available configuration keys with descriptions."""
config_schema = [
('gitea_url', 'Gitea server URL', 'http://localhost:3000'),
('repo_owner', 'Repository owner/organization', ''),
('repo_name', 'Repository name', ''),
('api_token', 'API token for authentication', ''),
('workspace_dir', 'Workspace directory path', '.markitect_workspace'),
('database_path', 'Database file path', '~/.markitect/markitect.db'),
('cache_dir', 'AST cache directory', '.ast_cache'),
('tests_dir', 'Tests directory', 'tests'),
('test_file_pattern', 'Test file naming pattern', 'test_issue_{issue_num}_{scenario}.py'),
('claude_code_command', 'Claude Code command', 'claude'),
]
return config_schema
def get_config_help(self, key: Optional[str] = None) -> str:
"""Get help information for configuration."""
if key:
# Get help for specific key
for config_key, description, default in self.list_config_keys():
if config_key == key:
return f"{config_key}: {description} (default: {default})"
return f"Unknown configuration key: {key}"
else:
# Get general help
lines = ["Available configuration keys:"]
for config_key, description, default in self.list_config_keys():
lines.append(f" {config_key:<20} {description}")
return '\n'.join(lines)
def validate_configuration(self, config: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""Validate current or provided configuration."""
if config is None:
config = self.get_current_config()
validation_results = []
# Check required fields
required_fields = ['gitea_url', 'database_path']
for field in required_fields:
if field not in config or not config[field]:
validation_results.append({
'key': field,
'status': 'error',
'message': f'Required field {field} is missing or empty'
})
else:
validation_results.append({
'key': field,
'status': 'ok',
'message': f'{field} is configured'
})
# Check paths exist or are creatable
path_fields = ['workspace_dir', 'cache_dir', 'tests_dir']
for field in path_fields:
if field in config:
path = Path(config[field])
if path.exists():
validation_results.append({
'key': field,
'status': 'ok',
'message': f'{field} exists at {path}'
})
else:
try:
path.mkdir(parents=True, exist_ok=True)
validation_results.append({
'key': field,
'status': 'warning',
'message': f'{field} created at {path}'
})
except OSError as e:
validation_results.append({
'key': field,
'status': 'error',
'message': f'{field} cannot be created: {e}'
})
# Check database path parent directory
if 'database_path' in config:
db_path = Path(config['database_path'])
db_parent = db_path.parent
if not db_parent.exists():
try:
db_parent.mkdir(parents=True, exist_ok=True)
validation_results.append({
'key': 'database_path',
'status': 'warning',
'message': f'Database directory created at {db_parent}'
})
except OSError as e:
validation_results.append({
'key': 'database_path',
'status': 'error',
'message': f'Cannot create database directory: {e}'
})
else:
validation_results.append({
'key': 'database_path',
'status': 'ok',
'message': f'Database directory exists at {db_parent}'
})
return validation_results