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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-02 17:44 +0800
1"""
2Storage service for manual health logs.
4Handles reading/writing user-entered diet, alcohol, supplement, and feeling data.
5"""
7import json
8from datetime import date, datetime
9from pathlib import Path
10from typing import Optional, List
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
22logger = setup_logger(__name__)
25class ManualLogStorage:
26 """Service for storing and retrieving manual health logs."""
28 def __init__(self, data_dir: Optional[Path] = None) -> None:
29 """Initialize manual log storage service.
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)
38 def _get_file_path(self, target_date: date, create_dirs: bool = True) -> Path:
39 """Get file path for a manual log.
41 Args:
42 target_date: Date of the log
43 create_dirs: Whether to create directories if they don't exist
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"
52 file_path = self.manual_log_dir / year / month / filename
54 if create_dirs:
55 file_path.parent.mkdir(parents=True, exist_ok=True)
57 return file_path
59 def load_log(self, target_date: date) -> DailyManualLog:
60 """Load manual log for a specific date.
62 Args:
63 target_date: Date to load
65 Returns:
66 DailyManualLog instance (creates new if doesn't exist)
67 """
68 file_path = self._get_file_path(target_date, create_dirs=False)
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)
75 try:
76 with open(file_path, "r", encoding="utf-8") as f:
77 data = json.load(f)
79 log = DailyManualLog(**data)
80 logger.debug(f"Loaded manual log for {target_date}")
81 return log
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)
88 def save_log(self, log: DailyManualLog) -> Path:
89 """Save manual log to JSON file.
91 Args:
92 log: DailyManualLog instance to save
94 Returns:
95 Path to saved file
96 """
97 try:
98 # Update timestamp
99 log.updated_at = datetime.now()
101 file_path = self._get_file_path(log.log_date)
103 # Convert to dict and save as JSON
104 data_dict = log.model_dump(mode="json")
106 with open(file_path, "w", encoding="utf-8") as f:
107 json.dump(data_dict, f, indent=2, ensure_ascii=False)
109 logger.info(f"Saved manual log for {log.log_date} to {file_path}")
110 return file_path
112 except Exception as e:
113 logger.error(f"Failed to save manual log for {log.log_date}: {e}")
114 raise
116 def add_diet_entry(
117 self, target_date: date, entry: DietEntry
118 ) -> DailyManualLog:
119 """Add a diet entry to the log.
121 Args:
122 target_date: Date of the entry
123 entry: DietEntry to add
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
134 def add_alcohol_entry(
135 self, target_date: date, entry: AlcoholEntry
136 ) -> DailyManualLog:
137 """Add an alcohol entry to the log.
139 Args:
140 target_date: Date of the entry
141 entry: AlcoholEntry to add
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
152 def add_supplement_entry(
153 self, target_date: date, entry: SupplementEntry
154 ) -> DailyManualLog:
155 """Add a supplement entry to the log.
157 Args:
158 target_date: Date of the entry
159 entry: SupplementEntry to add
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
170 def add_feeling_entry(
171 self, target_date: date, entry: BodyFeelingEntry
172 ) -> DailyManualLog:
173 """Add a body feeling entry to the log.
175 Args:
176 target_date: Date of the entry
177 entry: BodyFeelingEntry to add
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
188 def set_fasting_mode(self, target_date: date, mode: str) -> DailyManualLog:
189 """Set the fasting mode for a day.
191 Args:
192 target_date: Date to set
193 mode: Fasting mode (PSMF/OMAD/Water Fast/Normal)
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
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.
209 Args:
210 start_date: Start date (inclusive)
211 end_date: End date (inclusive)
213 Returns:
214 List of DailyManualLog instances
215 """
216 logs = []
217 current_date = start_date
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)
231 # Move to next day
232 from datetime import timedelta
233 current_date += timedelta(days=1)
235 return logs