Coverage for src / moai_adk / core / template / config.py: 23.33%
60 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""Configuration Manager
3Manage .moai/config/config.json:
4- Read and write configuration files
5- Support deep merges
6- Preserve UTF-8 content
7- Create directories automatically
8"""
10import json
11from pathlib import Path
12from typing import Any
15class ConfigManager:
16 """Read and write .moai/config/config.json."""
18 DEFAULT_CONFIG = {"mode": "personal", "locale": "ko", "moai": {"version": "0.3.0"}}
20 def __init__(self, config_path: Path) -> None:
21 """Initialize the ConfigManager.
23 Args:
24 config_path: Path to config.json.
25 """
26 self.config_path = config_path
28 def load(self) -> dict[str, Any]:
29 """Load the configuration file.
31 Returns default values when the file is missing.
33 Returns:
34 Configuration dictionary.
35 """
36 if not self.config_path.exists():
37 return self.DEFAULT_CONFIG.copy()
39 with open(self.config_path, encoding="utf-8") as f:
40 data: dict[str, Any] = json.load(f)
41 return data
43 def save(self, config: dict[str, Any]) -> None:
44 """Persist the configuration file.
46 Creates directories when missing and preserves UTF-8 content.
48 Args:
49 config: Configuration dictionary to save.
50 """
51 # Ensure the directory exists
52 self.config_path.parent.mkdir(parents=True, exist_ok=True)
54 # Write while preserving UTF-8 characters
55 with open(self.config_path, "w", encoding="utf-8") as f:
56 json.dump(config, f, ensure_ascii=False, indent=2)
58 def update(self, updates: dict[str, Any]) -> None:
59 """Update the configuration using a deep merge.
61 Args:
62 updates: Dictionary of updates to apply.
63 """
64 current = self.load()
65 merged = self._deep_merge(current, updates)
66 self.save(merged)
68 def _deep_merge(
69 self, base: dict[str, Any], updates: dict[str, Any]
70 ) -> dict[str, Any]:
71 """Recursively deep-merge dictionaries.
73 Args:
74 base: Base dictionary.
75 updates: Dictionary with updates.
77 Returns:
78 Merged dictionary.
79 """
80 result = base.copy()
82 for key, value in updates.items():
83 if (
84 key in result
85 and isinstance(result[key], dict)
86 and isinstance(value, dict)
87 ):
88 # When both sides are dicts, merge recursively
89 result[key] = self._deep_merge(result[key], value)
90 else:
91 # Otherwise, overwrite the value
92 result[key] = value
94 return result
96 @staticmethod
97 def set_optimized(config_path: Path, value: bool) -> None:
98 """Set the optimized field in config.json.
100 Args:
101 config_path: Path to config.json.
102 value: Value to set (True or False).
103 """
104 if not config_path.exists():
105 return
107 try:
108 with open(config_path, encoding="utf-8") as f:
109 config = json.load(f)
111 config.setdefault("project", {})["optimized"] = value
113 with open(config_path, "w", encoding="utf-8") as f:
114 json.dump(config, f, ensure_ascii=False, indent=2)
115 f.write("\n") # Add trailing newline
116 except (json.JSONDecodeError, KeyError, OSError):
117 # Ignore errors if config.json is invalid or inaccessible
118 pass
120 @staticmethod
121 def set_optimized_with_timestamp(config_path: Path, value: bool) -> None:
122 """Set the optimized field with timestamp in config.json.
124 When value=True: Set optimized_at to current ISO timestamp
125 When value=False: Set optimized_at to null
127 Args:
128 config_path: Path to config.json.
129 value: Value to set (True or False).
130 """
131 if not config_path.exists():
132 return
134 try:
135 from datetime import datetime, timezone
137 with open(config_path, encoding="utf-8") as f:
138 config = json.load(f)
140 config.setdefault("project", {})["optimized"] = value
142 if value:
143 # Set timestamp when optimizing
144 now = datetime.now(timezone.utc)
145 timestamp_str = now.isoformat().replace("+00:00", "Z")
146 config["project"]["optimized_at"] = timestamp_str
147 else:
148 # Clear timestamp when not optimized
149 config["project"]["optimized_at"] = None
151 with open(config_path, "w", encoding="utf-8") as f:
152 json.dump(config, f, ensure_ascii=False, indent=2)
153 f.write("\n") # Add trailing newline
154 except (json.JSONDecodeError, KeyError, OSError):
155 # Ignore errors if config.json is invalid or inaccessible
156 pass