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

1"""Configuration Manager 

2 

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""" 

9 

10import json 

11from pathlib import Path 

12from typing import Any 

13 

14 

15class ConfigManager: 

16 """Read and write .moai/config/config.json.""" 

17 

18 DEFAULT_CONFIG = {"mode": "personal", "locale": "ko", "moai": {"version": "0.3.0"}} 

19 

20 def __init__(self, config_path: Path) -> None: 

21 """Initialize the ConfigManager. 

22 

23 Args: 

24 config_path: Path to config.json. 

25 """ 

26 self.config_path = config_path 

27 

28 def load(self) -> dict[str, Any]: 

29 """Load the configuration file. 

30 

31 Returns default values when the file is missing. 

32 

33 Returns: 

34 Configuration dictionary. 

35 """ 

36 if not self.config_path.exists(): 

37 return self.DEFAULT_CONFIG.copy() 

38 

39 with open(self.config_path, encoding="utf-8") as f: 

40 data: dict[str, Any] = json.load(f) 

41 return data 

42 

43 def save(self, config: dict[str, Any]) -> None: 

44 """Persist the configuration file. 

45 

46 Creates directories when missing and preserves UTF-8 content. 

47 

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) 

53 

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) 

57 

58 def update(self, updates: dict[str, Any]) -> None: 

59 """Update the configuration using a deep merge. 

60 

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) 

67 

68 def _deep_merge( 

69 self, base: dict[str, Any], updates: dict[str, Any] 

70 ) -> dict[str, Any]: 

71 """Recursively deep-merge dictionaries. 

72 

73 Args: 

74 base: Base dictionary. 

75 updates: Dictionary with updates. 

76 

77 Returns: 

78 Merged dictionary. 

79 """ 

80 result = base.copy() 

81 

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 

93 

94 return result 

95 

96 @staticmethod 

97 def set_optimized(config_path: Path, value: bool) -> None: 

98 """Set the optimized field in config.json. 

99 

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 

106 

107 try: 

108 with open(config_path, encoding="utf-8") as f: 

109 config = json.load(f) 

110 

111 config.setdefault("project", {})["optimized"] = value 

112 

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 

119 

120 @staticmethod 

121 def set_optimized_with_timestamp(config_path: Path, value: bool) -> None: 

122 """Set the optimized field with timestamp in config.json. 

123 

124 When value=True: Set optimized_at to current ISO timestamp 

125 When value=False: Set optimized_at to null 

126 

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 

133 

134 try: 

135 from datetime import datetime, timezone 

136 

137 with open(config_path, encoding="utf-8") as f: 

138 config = json.load(f) 

139 

140 config.setdefault("project", {})["optimized"] = value 

141 

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 

150 

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