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:
2026-04-23 12:45:51 +03:00
parent ae61fa6382
commit 28cdc90056

View File

@@ -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."""
path = ConfigManager.get_config_path()
if not os.path.exists(path):
log.warning(f" {path} not found. Returning empty dict.")
return {}
try:
with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f) or {}
except Exception as e:
log.error(f"❌ Failed to read {path}: {e}")
return {}
@staticmethod
def update_config(updates: dict) -> dict:
""" """
Updates specific keys in inventory.env. Update backend.yaml with new values and return updated config.
Preserves comments and order where possible. Only updates sections in backend.yaml.
Appends new keys at the end if not found.
""" """
env_path = ConfigManager.get_root_env_path() path = ConfigManager.get_config_path()
current_yaml = ConfigManager.read_config()
if not os.path.exists(env_path): # Merge updates (deep merge for sections)
# Create a basic file if it doesn't exist (unlikely in this project) for section, values in updates.items():
with open(env_path, 'w', encoding='utf-8') as f: if isinstance(values, dict) and section in current_yaml and isinstance(current_yaml[section], dict):
f.write("# TFM aInventory — Generated Configuration\n") current_yaml[section].update(values)
else:
current_yaml[section] = values
with open(env_path, 'r', encoding='utf-8') as f: try:
lines = f.readlines() with open(path, 'w', encoding='utf-8') as f:
yaml.safe_dump(current_yaml, f, default_flow_style=False, sort_keys=False)
log.info(f"✅ Updated {path} with new values.")
new_lines = [] # Reload the global config
keys_to_process = set(updates.keys()) load_config()
processed_keys = set() return get_config()
except Exception as e:
log.error(f"❌ Failed to write {path}: {e}")
raise
for line in lines: @staticmethod
trimmed = line.strip() def validate_config_file() -> bool:
# Skip empty lines or comments when matching """Validate backend.yaml syntax and required fields."""
if not trimmed or trimmed.startswith('#'): path = ConfigManager.get_config_path()
new_lines.append(line) if not os.path.exists(path):
continue return False
# Check if this line is a key assignment we want to update try:
found_match = False with open(path, 'r', encoding='utf-8') as f:
for key in keys_to_process: yaml.safe_load(f)
if trimmed.startswith(f"{key}="): return True
new_lines.append(f"{key}={updates[key]}\n") except Exception:
processed_keys.add(key) return False
found_match = True
break
if not found_match:
new_lines.append(line)
# Append keys that weren't found in the file
missing_keys = keys_to_process - processed_keys
if missing_keys:
if new_lines and not new_lines[-1].endswith('\n'):
new_lines.append('\n')
if new_lines and not new_lines[-1].strip() == '':
new_lines.append('\n')
new_lines.append("# --- Automatically Added Keys ---\n")
for key in missing_keys:
new_lines.append(f"{key}={updates[key]}\n")
with open(env_path, 'w', encoding='utf-8') as f:
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()