Coverage for health / services / manual_log_storage.py: 0%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-02 17:44 +0800

1""" 

2Storage service for manual health logs. 

3 

4Handles reading/writing user-entered diet, alcohol, supplement, and feeling data. 

5""" 

6 

7import json 

8from datetime import date, datetime 

9from pathlib import Path 

10from typing import Optional, List 

11 

12from health import config 

13from health.models.manual_log import ( 

14 DailyManualLog, 

15 DietEntry, 

16 AlcoholEntry, 

17 SupplementEntry, 

18 BodyFeelingEntry, 

19) 

20from health.utils.logging_config import setup_logger 

21 

22logger = setup_logger(__name__) 

23 

24 

25class ManualLogStorage: 

26 """Service for storing and retrieving manual health logs.""" 

27 

28 def __init__(self, data_dir: Optional[Path] = None) -> None: 

29 """Initialize manual log storage service. 

30 

31 Args: 

32 data_dir: Optional data directory path (defaults to config.DATA_DIR) 

33 """ 

34 self.data_dir = data_dir or config.DATA_DIR 

35 self.manual_log_dir = self.data_dir / "manual_logs" 

36 self.manual_log_dir.mkdir(parents=True, exist_ok=True) 

37 

38 def _get_file_path(self, target_date: date, create_dirs: bool = True) -> Path: 

39 """Get file path for a manual log. 

40 

41 Args: 

42 target_date: Date of the log 

43 create_dirs: Whether to create directories if they don't exist 

44 

45 Returns: 

46 Full path to JSON file (data/health/manual_logs/YYYY/MM/YYYY-MM-DD.json) 

47 """ 

48 year = str(target_date.year) 

49 month = f"{target_date.month:02d}" 

50 filename = f"{target_date.isoformat()}.json" 

51 

52 file_path = self.manual_log_dir / year / month / filename 

53 

54 if create_dirs: 

55 file_path.parent.mkdir(parents=True, exist_ok=True) 

56 

57 return file_path 

58 

59 def load_log(self, target_date: date) -> DailyManualLog: 

60 """Load manual log for a specific date. 

61 

62 Args: 

63 target_date: Date to load 

64 

65 Returns: 

66 DailyManualLog instance (creates new if doesn't exist) 

67 """ 

68 file_path = self._get_file_path(target_date, create_dirs=False) 

69 

70 if not file_path.exists(): 

71 # Return empty log for this date 

72 logger.debug(f"No manual log found for {target_date}, creating new") 

73 return DailyManualLog(log_date=target_date) 

74 

75 try: 

76 with open(file_path, "r", encoding="utf-8") as f: 

77 data = json.load(f) 

78 

79 log = DailyManualLog(**data) 

80 logger.debug(f"Loaded manual log for {target_date}") 

81 return log 

82 

83 except Exception as e: 

84 logger.error(f"Failed to load manual log for {target_date}: {e}") 

85 # Return empty log on error 

86 return DailyManualLog(log_date=target_date) 

87 

88 def save_log(self, log: DailyManualLog) -> Path: 

89 """Save manual log to JSON file. 

90 

91 Args: 

92 log: DailyManualLog instance to save 

93 

94 Returns: 

95 Path to saved file 

96 """ 

97 try: 

98 # Update timestamp 

99 log.updated_at = datetime.now() 

100 

101 file_path = self._get_file_path(log.log_date) 

102 

103 # Convert to dict and save as JSON 

104 data_dict = log.model_dump(mode="json") 

105 

106 with open(file_path, "w", encoding="utf-8") as f: 

107 json.dump(data_dict, f, indent=2, ensure_ascii=False) 

108 

109 logger.info(f"Saved manual log for {log.log_date} to {file_path}") 

110 return file_path 

111 

112 except Exception as e: 

113 logger.error(f"Failed to save manual log for {log.log_date}: {e}") 

114 raise 

115 

116 def add_diet_entry( 

117 self, target_date: date, entry: DietEntry 

118 ) -> DailyManualLog: 

119 """Add a diet entry to the log. 

120 

121 Args: 

122 target_date: Date of the entry 

123 entry: DietEntry to add 

124 

125 Returns: 

126 Updated DailyManualLog 

127 """ 

128 log = self.load_log(target_date) 

129 log.diet_entries.append(entry) 

130 self.save_log(log) 

131 logger.info(f"Added diet entry for {target_date} at {entry.time}") 

132 return log 

133 

134 def add_alcohol_entry( 

135 self, target_date: date, entry: AlcoholEntry 

136 ) -> DailyManualLog: 

137 """Add an alcohol entry to the log. 

138 

139 Args: 

140 target_date: Date of the entry 

141 entry: AlcoholEntry to add 

142 

143 Returns: 

144 Updated DailyManualLog 

145 """ 

146 log = self.load_log(target_date) 

147 log.alcohol_entries.append(entry) 

148 self.save_log(log) 

149 logger.info(f"Added alcohol entry for {target_date} at {entry.time}") 

150 return log 

151 

152 def add_supplement_entry( 

153 self, target_date: date, entry: SupplementEntry 

154 ) -> DailyManualLog: 

155 """Add a supplement entry to the log. 

156 

157 Args: 

158 target_date: Date of the entry 

159 entry: SupplementEntry to add 

160 

161 Returns: 

162 Updated DailyManualLog 

163 """ 

164 log = self.load_log(target_date) 

165 log.supplement_entries.append(entry) 

166 self.save_log(log) 

167 logger.info(f"Added supplement entry for {target_date}: {entry.supplement_name}") 

168 return log 

169 

170 def add_feeling_entry( 

171 self, target_date: date, entry: BodyFeelingEntry 

172 ) -> DailyManualLog: 

173 """Add a body feeling entry to the log. 

174 

175 Args: 

176 target_date: Date of the entry 

177 entry: BodyFeelingEntry to add 

178 

179 Returns: 

180 Updated DailyManualLog 

181 """ 

182 log = self.load_log(target_date) 

183 log.feeling_entries.append(entry) 

184 self.save_log(log) 

185 logger.info(f"Added feeling entry for {target_date}: {entry.feeling_type}") 

186 return log 

187 

188 def set_fasting_mode(self, target_date: date, mode: str) -> DailyManualLog: 

189 """Set the fasting mode for a day. 

190 

191 Args: 

192 target_date: Date to set 

193 mode: Fasting mode (PSMF/OMAD/Water Fast/Normal) 

194 

195 Returns: 

196 Updated DailyManualLog 

197 """ 

198 log = self.load_log(target_date) 

199 log.fasting_mode = mode 

200 self.save_log(log) 

201 logger.info(f"Set fasting mode for {target_date}: {mode}") 

202 return log 

203 

204 def get_logs_in_range( 

205 self, start_date: date, end_date: date 

206 ) -> List[DailyManualLog]: 

207 """Get all manual logs in a date range. 

208 

209 Args: 

210 start_date: Start date (inclusive) 

211 end_date: End date (inclusive) 

212 

213 Returns: 

214 List of DailyManualLog instances 

215 """ 

216 logs = [] 

217 current_date = start_date 

218 

219 while current_date <= end_date: 

220 log = self.load_log(current_date) 

221 # Only include logs that have actual data 

222 if ( 

223 log.diet_entries 

224 or log.alcohol_entries 

225 or log.supplement_entries 

226 or log.feeling_entries 

227 or log.fasting_mode 

228 ): 

229 logs.append(log) 

230 

231 # Move to next day 

232 from datetime import timedelta 

233 current_date += timedelta(days=1) 

234 

235 return logs