feat(07-02): update config_manager.py for YAML support
- Replace dotenv with PyYAML - Implement read_config, update_config, validate_config_file for backend.yaml - Update get_masked_key to use new config structure - Remove inventory.env references
This commit is contained in:
@@ -1,90 +1,118 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
import yaml
|
||||||
|
import logging
|
||||||
|
from .config_loader import load_config, get_config
|
||||||
|
|
||||||
|
log = logging.getLogger("ainventory")
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
"""Safely manages multi-line .env files without corrupting other content."""
|
"""Manages backend.yaml configuration file updates."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_root_env_path():
|
def get_config_path():
|
||||||
# backend/config_manager.py -> backend/ -> /
|
"""Returns the absolute path to backend.yaml."""
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
project_root = os.path.dirname(base_dir)
|
project_root = os.path.dirname(base_dir)
|
||||||
return os.path.join(project_root, "inventory.env")
|
return os.path.join(project_root, "config", "backend.yaml")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_keys(updates: dict):
|
def read_config() -> dict:
|
||||||
"""
|
"""Read backend.yaml and return current config."""
|
||||||
Updates specific keys in inventory.env.
|
path = ConfigManager.get_config_path()
|
||||||
Preserves comments and order where possible.
|
if not os.path.exists(path):
|
||||||
Appends new keys at the end if not found.
|
log.warning(f"ℹ️ {path} not found. Returning empty dict.")
|
||||||
"""
|
return {}
|
||||||
env_path = ConfigManager.get_root_env_path()
|
|
||||||
|
|
||||||
if not os.path.exists(env_path):
|
try:
|
||||||
# Create a basic file if it doesn't exist (unlikely in this project)
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
with open(env_path, 'w', encoding='utf-8') as f:
|
return yaml.safe_load(f) or {}
|
||||||
f.write("# TFM aInventory — Generated Configuration\n")
|
except Exception as e:
|
||||||
|
log.error(f"❌ Failed to read {path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
with open(env_path, 'r', encoding='utf-8') as f:
|
@staticmethod
|
||||||
lines = f.readlines()
|
def update_config(updates: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Update backend.yaml with new values and return updated config.
|
||||||
|
Only updates sections in backend.yaml.
|
||||||
|
"""
|
||||||
|
path = ConfigManager.get_config_path()
|
||||||
|
current_yaml = ConfigManager.read_config()
|
||||||
|
|
||||||
|
# Merge updates (deep merge for sections)
|
||||||
|
for section, values in updates.items():
|
||||||
|
if isinstance(values, dict) and section in current_yaml and isinstance(current_yaml[section], dict):
|
||||||
|
current_yaml[section].update(values)
|
||||||
|
else:
|
||||||
|
current_yaml[section] = values
|
||||||
|
|
||||||
new_lines = []
|
try:
|
||||||
keys_to_process = set(updates.keys())
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
processed_keys = set()
|
yaml.safe_dump(current_yaml, f, default_flow_style=False, sort_keys=False)
|
||||||
|
log.info(f"✅ Updated {path} with new values.")
|
||||||
for line in lines:
|
|
||||||
trimmed = line.strip()
|
|
||||||
# Skip empty lines or comments when matching
|
|
||||||
if not trimmed or trimmed.startswith('#'):
|
|
||||||
new_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if this line is a key assignment we want to update
|
|
||||||
found_match = False
|
|
||||||
for key in keys_to_process:
|
|
||||||
if trimmed.startswith(f"{key}="):
|
|
||||||
new_lines.append(f"{key}={updates[key]}\n")
|
|
||||||
processed_keys.add(key)
|
|
||||||
found_match = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found_match:
|
# Reload the global config
|
||||||
new_lines.append(line)
|
load_config()
|
||||||
|
return get_config()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"❌ Failed to write {path}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
# Append keys that weren't found in the file
|
@staticmethod
|
||||||
missing_keys = keys_to_process - processed_keys
|
def validate_config_file() -> bool:
|
||||||
if missing_keys:
|
"""Validate backend.yaml syntax and required fields."""
|
||||||
if new_lines and not new_lines[-1].endswith('\n'):
|
path = ConfigManager.get_config_path()
|
||||||
new_lines.append('\n')
|
if not os.path.exists(path):
|
||||||
if new_lines and not new_lines[-1].strip() == '':
|
return False
|
||||||
new_lines.append('\n')
|
|
||||||
|
try:
|
||||||
new_lines.append("# --- Automatically Added Keys ---\n")
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
for key in missing_keys:
|
yaml.safe_load(f)
|
||||||
new_lines.append(f"{key}={updates[key]}\n")
|
return True
|
||||||
|
except Exception:
|
||||||
with open(env_path, 'w', encoding='utf-8') as f:
|
return False
|
||||||
f.writelines(new_lines)
|
|
||||||
|
|
||||||
# Force reload environment variables for the current process
|
|
||||||
load_dotenv(env_path, override=True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_masked_key(key_name: str):
|
def get_masked_key(key_name: str):
|
||||||
"""Returns a masked version of the environment variable."""
|
"""Returns a masked version of a configuration value or env var."""
|
||||||
val = os.environ.get(key_name)
|
config = get_config()
|
||||||
|
|
||||||
|
# Try to find in config first
|
||||||
|
val = None
|
||||||
|
if key_name == "JWT_SECRET_KEY":
|
||||||
|
val = config.get("auth", {}).get("jwt_secret_key")
|
||||||
|
elif key_name == "GEMINI_API_KEY":
|
||||||
|
val = config.get("ai", {}).get("gemini_api_key")
|
||||||
|
elif key_name == "CLAUDE_API_KEY":
|
||||||
|
val = config.get("ai", {}).get("claude_api_key")
|
||||||
|
|
||||||
|
# Fallback to env
|
||||||
|
if not val:
|
||||||
|
val = os.environ.get(key_name)
|
||||||
|
|
||||||
if not val:
|
if not val:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Determine prefix based on key type
|
|
||||||
prefix = ""
|
prefix = ""
|
||||||
if key_name == "GEMINI_API_KEY":
|
if "GEMINI" in key_name:
|
||||||
prefix = "G-"
|
prefix = "G-"
|
||||||
elif key_name == "CLAUDE_API_KEY":
|
elif "CLAUDE" in key_name:
|
||||||
prefix = "sk-"
|
prefix = "sk-"
|
||||||
|
|
||||||
if len(val) <= 8:
|
if len(val) <= 8:
|
||||||
return f"{prefix}****"
|
return f"{prefix}****"
|
||||||
|
|
||||||
return f"{prefix}****{val[-4:]}"
|
return f"{prefix}****{val[-4:]}"
|
||||||
|
|
||||||
|
# Module-level functions for backward compatibility and direct import
|
||||||
|
def read_config() -> dict:
|
||||||
|
"""Read backend.yaml and return current config."""
|
||||||
|
return ConfigManager.read_config()
|
||||||
|
|
||||||
|
def update_config(updates: dict) -> dict:
|
||||||
|
"""Update backend.yaml with new values and return updated config."""
|
||||||
|
return ConfigManager.update_config(updates)
|
||||||
|
|
||||||
|
def validate_config_file() -> bool:
|
||||||
|
"""Validate backend.yaml syntax and required fields."""
|
||||||
|
return ConfigManager.validate_config_file()
|
||||||
|
|||||||
Reference in New Issue
Block a user