Coverage for health / services / query.py: 0%
90 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"""
2Query service for health data analysis.
4Provides methods to query and analyze health data from storage.
5"""
7import json
8from datetime import date, timedelta
9from pathlib import Path
10from typing import Dict, List, Optional, Any, Union
12from health import config
13from health.db.repository import HealthRepository
14from health.services.storage import HealthStorage
15from health.utils.logging_config import setup_logger
17logger = setup_logger(__name__)
20class HealthDataQuery:
21 """Service for querying and analyzing health data."""
23 def __init__(
24 self, storage: Optional[HealthStorage] = None, repo: Optional[HealthRepository] = None
25 ) -> None:
26 """Initialize query service.
28 Args:
29 storage: Optional storage service instance
30 repo: Optional repository instance
31 """
32 self.storage = storage or HealthStorage()
33 self.repo = repo or HealthRepository()
35 def get_daily_summary(self, target_date: date) -> Dict[str, Any]:
36 """Get a summary of all health metrics for a specific day.
38 Args:
39 target_date: Date to query
41 Returns:
42 Dictionary containing all available metrics for the day
43 """
44 logger.info(f"Querying health data summary for {target_date}")
46 summary = {
47 "date": target_date.isoformat(),
48 "metrics": {},
49 "activities": [],
50 }
52 # Query all daily metrics
53 for metric_type in config.DATA_TYPE_CONFIG.keys():
54 # Skip activities (handled separately) and weight (body metric)
55 if metric_type in ["activities", "weight"]:
56 continue
58 data = self.storage.load_daily_metric(metric_type, target_date)
59 if data:
60 summary["metrics"][metric_type] = data
62 # Query activities for the day
63 activities = self.repo.get_activities_by_date_range(target_date, target_date)
64 for activity in activities:
65 activity_data = self.storage.load_activity(activity["activity_id"])
66 if activity_data:
67 summary["activities"].append(activity_data)
69 # Query weight (if available)
70 weight_data = self.storage.load_daily_metric("weight", target_date)
71 if weight_data:
72 summary["metrics"]["weight"] = weight_data
74 logger.info(
75 f"Found {len(summary['metrics'])} metrics and {len(summary['activities'])} activities"
76 )
78 return summary
80 def get_metric_range(
81 self, metric_type: str, start_date: date, end_date: date
82 ) -> List[Dict[str, Any]]:
83 """Get a specific metric's data over a date range.
85 Args:
86 metric_type: Type of metric (e.g., "steps", "sleep", "heart_rate")
87 start_date: Start date of range
88 end_date: End date of range
90 Returns:
91 List of metric data dictionaries, ordered by date
93 Raises:
94 ValueError: If metric_type is invalid
95 """
96 if metric_type not in config.DATA_TYPE_CONFIG:
97 valid_types = ", ".join(config.DATA_TYPE_CONFIG.keys())
98 raise ValueError(
99 f"Invalid metric type: {metric_type}. Valid types: {valid_types}"
100 )
102 logger.info(f"Querying {metric_type} from {start_date} to {end_date}")
104 results = []
105 current_date = start_date
107 while current_date <= end_date:
108 data = self.storage.load_daily_metric(metric_type, current_date)
109 if data:
110 results.append(data)
111 current_date += timedelta(days=1)
113 logger.info(f"Found {len(results)} records for {metric_type}")
115 return results
117 def get_activities_range(
118 self, start_date: date, end_date: date, activity_type: Optional[str] = None
119 ) -> List[Dict[str, Any]]:
120 """Get all activities within a date range.
122 Args:
123 start_date: Start date of range
124 end_date: End date of range
125 activity_type: Optional filter by activity type
127 Returns:
128 List of activity data dictionaries
129 """
130 logger.info(f"Querying activities from {start_date} to {end_date}")
132 # Get activity index entries
133 activity_indices = self.repo.get_activities_by_date_range(start_date, end_date)
135 # Load full activity data
136 activities = []
137 for idx in activity_indices:
138 # Filter by activity type if specified
139 if activity_type and idx.get("activity_type") != activity_type:
140 continue
142 activity_data = self.storage.load_activity(idx["activity_id"])
143 if activity_data:
144 activities.append(activity_data)
146 logger.info(f"Found {len(activities)} activities")
148 return activities
150 def get_metric_statistics(
151 self, metric_type: str, start_date: date, end_date: date, fields: List[str]
152 ) -> Dict[str, Dict[str, Any]]:
153 """Calculate statistics for specific metric fields over a date range.
155 Args:
156 metric_type: Type of metric
157 start_date: Start date of range
158 end_date: End date of range
159 fields: List of field names to calculate statistics for
161 Returns:
162 Dictionary mapping field names to their statistics (min, max, avg, count)
163 """
164 logger.info(
165 f"Calculating statistics for {metric_type} fields: {fields} from {start_date} to {end_date}"
166 )
168 # Get all data for the range
169 data_list = self.get_metric_range(metric_type, start_date, end_date)
171 # Calculate statistics for each field
172 stats = {}
173 for field in fields:
174 values = []
175 for data in data_list:
176 if field in data and data[field] is not None:
177 values.append(data[field])
179 if values:
180 stats[field] = {
181 "min": min(values),
182 "max": max(values),
183 "avg": sum(values) / len(values),
184 "count": len(values),
185 "sum": sum(values),
186 }
187 else:
188 stats[field] = {
189 "min": None,
190 "max": None,
191 "avg": None,
192 "count": 0,
193 "sum": None,
194 }
196 return stats
198 def get_available_dates(self, metric_type: str) -> List[str]:
199 """Get all available dates for a metric type.
201 Args:
202 metric_type: Type of metric
204 Returns:
205 List of date strings in ISO format
206 """
207 # This is a simplified implementation - in a real scenario,
208 # you might want to query the database index for better performance
209 storage_path = config.DATA_TYPE_CONFIG[metric_type]["storage_path"]
210 base_path = config.DATA_DIR / storage_path
212 dates = []
213 if base_path.exists():
214 # Walk through year/month directories
215 for year_dir in sorted(base_path.iterdir()):
216 if not year_dir.is_dir():
217 continue
218 for month_dir in sorted(year_dir.iterdir()):
219 if not month_dir.is_dir():
220 continue
221 for file_path in sorted(month_dir.glob("*.json")):
222 # Extract date from filename (YYYY-MM-DD.json)
223 date_str = file_path.stem
224 dates.append(date_str)
226 return sorted(dates)
228 def get_latest_data(self, metric_type: str, days: int = 7) -> List[Dict[str, Any]]:
229 """Get the most recent data for a metric.
231 Args:
232 metric_type: Type of metric
233 days: Number of days to look back (default: 7)
235 Returns:
236 List of metric data dictionaries for the last N days
237 """
238 end_date = date.today()
239 start_date = end_date - timedelta(days=days - 1)
241 return self.get_metric_range(metric_type, start_date, end_date)