Files
madomeda/core/config.py
T
2025-10-23 20:14:21 +02:00

290 lines
11 KiB
Python

"""
Configuration management module
"""
import os
import sys
import shutil
from pathlib import Path
from typing import Optional
class ConfigManager:
def __init__(self):
self.config_dir = self._get_config_dir()
self.env_file = self.config_dir / '.env'
self.templates_dir = self.config_dir / 'templates'
self.rules_dir = self.config_dir / 'rules'
self.prompts_dir = self.config_dir / 'prompts'
def _get_config_dir(self) -> Path:
"""Get platform-specific config directory"""
if sys.platform == 'win32':
# Windows: %USERPROFILE%\.config\madomeda
base = Path(os.environ.get('USERPROFILE', os.path.expanduser('~')))
config_dir = base / '.config' / 'madomeda'
elif sys.platform == 'darwin':
# macOS: ~/.config/madomeda
config_dir = Path.home() / '.config' / 'madomeda'
else:
# Linux: ~/.config/madomeda
config_dir = Path.home() / '.config' / 'madomeda'
return config_dir
def ensure_config_exists(self):
"""Ensure configuration directory exists with default files"""
if not self.config_dir.exists():
self.config_dir.mkdir(parents=True, exist_ok=True)
self._create_default_config()
# Ensure subdirectories exist
self.templates_dir.mkdir(exist_ok=True)
self.rules_dir.mkdir(exist_ok=True)
self.prompts_dir.mkdir(exist_ok=True)
# Copy defaults if they don't exist
if not (self.templates_dir / 'default.json').exists():
self._create_default_template()
if not list(self.rules_dir.glob('*.json')):
self._create_default_rules()
if not (self.prompts_dir / 'default.txt').exists():
self._create_default_prompt()
def _create_default_config(self):
"""Create default .env file"""
default_env = """# Madomeda Configuration
# OpenAI-compatible API settings
# API endpoint URL
OPENAI_API_URL=https://api.openai.com/v1
# Model to use
OPENAI_MODEL=gpt-4
# API Key options:
# 1. Empty string "" = No API key needed (LLM features disabled)
# 2. $ENV_VAR_NAME = Read from environment variable
# 3. Direct key = Use the key (WARNING: security risk in plain text)
OPENAI_API_KEY=""
"""
with open(self.env_file, 'w', encoding='utf-8') as f:
f.write(default_env)
def _create_default_template(self):
"""Create default template"""
import json
default_template = {
"title": "heur|orig:text",
"created": "heur|orig:datetime",
"changed": "heur|orig:datetime",
"authors": "heur|orig:list",
"version": "heur|orig:text",
"tags": "heur|orig|ai default:list"
}
with open(self.templates_dir / 'default.json', 'w', encoding='utf-8') as f:
json.dump(default_template, f, indent=2)
def _create_default_rules(self):
"""Create default rules"""
import json
rules = {
'01_lowercase_keys.json': {
"name": "lowercase_keys",
"description": "All frontmatter keys must be lowercase",
"field": "*",
"priority": 1,
"action": "normalize_keys",
"transform": "lower",
"llm_prompt": "Ensure all keys are lowercase"
},
'02_tag_to_tags.json': {
"name": "tag_to_tags",
"description": "Rename 'tag' field to 'tags'",
"field": "tag",
"priority": 2,
"action": "rename_field",
"from": "tag",
"to": "tags",
"llm_prompt": "Convert tag field to tags array"
},
'03_summary_to_description.json': {
"name": "summary_to_description",
"description": "Rename 'summary' field to 'description'",
"field": "summary",
"priority": 2,
"action": "rename_field",
"from": "summary",
"to": "description",
"llm_prompt": "Convert summary to description"
},
'10_tags_format_list.json': {
"name": "tags_format_list",
"description": "Convert comma-separated tags to list",
"field": "tags",
"priority": 10,
"action": "normalize_value",
"split_on": ",",
"pattern": ".*",
"replacement": "\\g<0>",
"llm_prompt": "Ensure tags is a list, not a comma-separated string"
},
'20_tags_replace_spaces.json': {
"name": "tags_replace_spaces",
"description": "Replace spaces with underscores in tags",
"field": "tags",
"priority": 20,
"action": "normalize_value",
"pattern": " ",
"replacement": "_",
"llm_prompt": "Replace spaces with underscores in tags"
},
'21_tags_replace_hyphens.json': {
"name": "tags_replace_hyphens",
"description": "Replace hyphens with underscores in tags",
"field": "tags",
"priority": 21,
"action": "normalize_value",
"pattern": "-",
"replacement": "_",
"llm_prompt": "Replace hyphens with underscores in tags"
},
'30_tags_lowercase.json': {
"name": "tags_lowercase",
"description": "Convert tags to lowercase",
"field": "tags",
"priority": 30,
"action": "normalize_value",
"transform": "lower",
"llm_prompt": "Convert all tags to lowercase"
},
'40_tags_remove_invalid_chars.json': {
"name": "tags_remove_invalid_chars",
"description": "Remove characters that are not alphanumeric or underscore",
"field": "tags",
"priority": 40,
"action": "normalize_value",
"pattern": "[^a-z0-9_]",
"replacement": "",
"llm_prompt": "Remove any characters that are not lowercase letters, numbers, or underscores from tags"
},
'50_tags_validate_format.json': {
"name": "tags_validate_format",
"description": "Validate that tags only contain lowercase alphanumeric and underscores",
"field": "tags",
"priority": 50,
"action": "validate",
"pattern": "^[a-z0-9_]+$",
"multiline": False,
"llm_prompt": "Tags must contain only lowercase letters, numbers, and underscores"
}
}
for filename, rule_data in rules.items():
with open(self.rules_dir / filename, 'w', encoding='utf-8') as f:
json.dump(rule_data, f, indent=2)
def _create_default_prompt(self):
"""Create default prompt"""
default_prompt = """You are a metadata extraction assistant for markdown documents.
Your task is to analyze the document content and suggest appropriate frontmatter metadata.
Guidelines:
- For tags: prefer selecting from the provided existing tags list when appropriate
- You may create new tags if they better fit the content
- New tags must follow the rules: lowercase letters, numbers, and underscores only
- Be concise and accurate
- Preserve important existing metadata when present"""
with open(self.prompts_dir / 'default.txt', 'w', encoding='utf-8') as f:
f.write(default_prompt)
def get_api_config(self) -> dict:
"""Get API configuration with environment variable resolution"""
config = {
'url': 'https://api.openai.com/v1',
'model': 'gpt-4',
'api_key': '',
'api_key_source': 'none'
}
if not self.env_file.exists():
return config
# Parse .env file
env_vars = {}
with open(self.env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip()
# Get URL
if 'OPENAI_API_URL' in env_vars:
config['url'] = env_vars['OPENAI_API_URL']
# Get model
if 'OPENAI_MODEL' in env_vars:
config['model'] = env_vars['OPENAI_MODEL']
# Get API key with special handling
if 'OPENAI_API_KEY' in env_vars:
api_key_value = env_vars['OPENAI_API_KEY']
# Remove quotes if present
api_key_value = api_key_value.strip('"').strip("'")
if not api_key_value:
# Empty string = no key needed
config['api_key'] = ''
config['api_key_source'] = 'none'
elif api_key_value.startswith('$'):
# Environment variable reference
env_var_name = api_key_value[1:]
env_value = os.environ.get(env_var_name)
if env_value is None:
raise ValueError(
f"Environment variable '{env_var_name}' not found. "
f"Referenced in .env as OPENAI_API_KEY={api_key_value}"
)
config['api_key'] = env_value
config['api_key_source'] = f'env:{env_var_name}'
else:
# Plain text key (security warning will be issued)
config['api_key'] = api_key_value
config['api_key_source'] = 'plaintext'
return config
def print_config_info(self):
"""Print configuration location and status"""
print(f"Configuration directory: {self.config_dir}")
print(f" .env file: {self.env_file}")
print(f" Templates: {self.templates_dir}")
print(f" Rules: {self.rules_dir}")
print(f" Prompts: {self.prompts_dir}")
# Get API config to show status
try:
api_config = self.get_api_config()
if api_config['api_key_source'] == 'none':
print(f" API Key: Not configured (LLM features disabled)")
elif api_config['api_key_source'] == 'plaintext':
print(f" API Key: Configured (plain text)")
print(f" WARNING: Storing API keys in plain text is a security risk!")
print(f" Consider using environment variable: OPENAI_API_KEY=$YOUR_ENV_VAR")
elif api_config['api_key_source'].startswith('env:'):
env_var = api_config['api_key_source'].split(':', 1)[1]
print(f" API Key: From environment variable ${env_var}")
except ValueError as e:
print(f" API Key: ERROR - {e}")
print()