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."""
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()