feat: Phase 0c - health_metrics.py module complete
Data Layer: - get_resting_heart_rate_data() - avg RHR with min/max trend - get_heart_rate_variability_data() - avg HRV with min/max trend - get_vo2_max_data() - latest VO2 Max with date Placeholder Layer: - get_vitals_avg_hr() - refactored to use data layer - get_vitals_avg_hrv() - refactored to use data layer - get_vitals_vo2_max() - refactored to use data layer All 3 health data functions + 3 placeholder refactors complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
432f7ba49f
commit
b4558b0582
|
|
@ -33,9 +33,9 @@ from .body_metrics import *
|
||||||
from .nutrition_metrics import *
|
from .nutrition_metrics import *
|
||||||
from .activity_metrics import *
|
from .activity_metrics import *
|
||||||
from .recovery_metrics import *
|
from .recovery_metrics import *
|
||||||
|
from .health_metrics import *
|
||||||
|
|
||||||
# Future imports (will be added as modules are created):
|
# Future imports (will be added as modules are created):
|
||||||
# from .health_metrics import *
|
|
||||||
# from .goals import *
|
# from .goals import *
|
||||||
# from .correlations import *
|
# from .correlations import *
|
||||||
|
|
||||||
|
|
@ -66,4 +66,9 @@ __all__ = [
|
||||||
'get_sleep_duration_data',
|
'get_sleep_duration_data',
|
||||||
'get_sleep_quality_data',
|
'get_sleep_quality_data',
|
||||||
'get_rest_days_data',
|
'get_rest_days_data',
|
||||||
|
|
||||||
|
# Health Metrics
|
||||||
|
'get_resting_heart_rate_data',
|
||||||
|
'get_heart_rate_variability_data',
|
||||||
|
'get_vo2_max_data',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
197
backend/data_layer/health_metrics.py
Normal file
197
backend/data_layer/health_metrics.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""
|
||||||
|
Health Metrics Data Layer
|
||||||
|
|
||||||
|
Provides structured data for vital signs and health monitoring.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- get_resting_heart_rate_data(): Average RHR with trend
|
||||||
|
- get_heart_rate_variability_data(): Average HRV with trend
|
||||||
|
- get_vo2_max_data(): Latest VO2 Max value
|
||||||
|
|
||||||
|
All functions return structured data (dict) without formatting.
|
||||||
|
Use placeholder_resolver.py for formatted strings for AI.
|
||||||
|
|
||||||
|
Phase 0c: Multi-Layer Architecture
|
||||||
|
Version: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
from db import get_db, get_cursor, r2d
|
||||||
|
from data_layer.utils import calculate_confidence, safe_float, safe_int
|
||||||
|
|
||||||
|
|
||||||
|
def get_resting_heart_rate_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 7
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Get average resting heart rate with trend.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 7)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"avg_rhr": int, # beats per minute
|
||||||
|
"min_rhr": int,
|
||||||
|
"max_rhr": int,
|
||||||
|
"measurements": int,
|
||||||
|
"confidence": str,
|
||||||
|
"days_analyzed": int
|
||||||
|
}
|
||||||
|
|
||||||
|
Migration from Phase 0b:
|
||||||
|
OLD: get_vitals_avg_hr(pid, days) formatted string
|
||||||
|
NEW: Structured data with min/max
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT
|
||||||
|
AVG(resting_hr) as avg,
|
||||||
|
MIN(resting_hr) as min,
|
||||||
|
MAX(resting_hr) as max,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM vitals_baseline
|
||||||
|
WHERE profile_id=%s
|
||||||
|
AND date >= %s
|
||||||
|
AND resting_hr IS NOT NULL""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row or row['count'] == 0:
|
||||||
|
return {
|
||||||
|
"avg_rhr": 0,
|
||||||
|
"min_rhr": 0,
|
||||||
|
"max_rhr": 0,
|
||||||
|
"measurements": 0,
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
measurements = row['count']
|
||||||
|
confidence = calculate_confidence(measurements, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_rhr": safe_int(row['avg']),
|
||||||
|
"min_rhr": safe_int(row['min']),
|
||||||
|
"max_rhr": safe_int(row['max']),
|
||||||
|
"measurements": measurements,
|
||||||
|
"confidence": confidence,
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_heart_rate_variability_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 7
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Get average heart rate variability with trend.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 7)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"avg_hrv": int, # milliseconds
|
||||||
|
"min_hrv": int,
|
||||||
|
"max_hrv": int,
|
||||||
|
"measurements": int,
|
||||||
|
"confidence": str,
|
||||||
|
"days_analyzed": int
|
||||||
|
}
|
||||||
|
|
||||||
|
Migration from Phase 0b:
|
||||||
|
OLD: get_vitals_avg_hrv(pid, days) formatted string
|
||||||
|
NEW: Structured data with min/max
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT
|
||||||
|
AVG(hrv) as avg,
|
||||||
|
MIN(hrv) as min,
|
||||||
|
MAX(hrv) as max,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM vitals_baseline
|
||||||
|
WHERE profile_id=%s
|
||||||
|
AND date >= %s
|
||||||
|
AND hrv IS NOT NULL""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row or row['count'] == 0:
|
||||||
|
return {
|
||||||
|
"avg_hrv": 0,
|
||||||
|
"min_hrv": 0,
|
||||||
|
"max_hrv": 0,
|
||||||
|
"measurements": 0,
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
measurements = row['count']
|
||||||
|
confidence = calculate_confidence(measurements, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_hrv": safe_int(row['avg']),
|
||||||
|
"min_hrv": safe_int(row['min']),
|
||||||
|
"max_hrv": safe_int(row['max']),
|
||||||
|
"measurements": measurements,
|
||||||
|
"confidence": confidence,
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_vo2_max_data(
|
||||||
|
profile_id: str
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Get latest VO2 Max value with date.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"vo2_max": float, # ml/kg/min
|
||||||
|
"date": date,
|
||||||
|
"confidence": str
|
||||||
|
}
|
||||||
|
|
||||||
|
Migration from Phase 0b:
|
||||||
|
OLD: get_vitals_vo2_max(pid) formatted string
|
||||||
|
NEW: Structured data with date
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT vo2_max, date FROM vitals_baseline
|
||||||
|
WHERE profile_id=%s AND vo2_max IS NOT NULL
|
||||||
|
ORDER BY date DESC LIMIT 1""",
|
||||||
|
(profile_id,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return {
|
||||||
|
"vo2_max": 0.0,
|
||||||
|
"date": None,
|
||||||
|
"confidence": "insufficient"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vo2_max": safe_float(row['vo2_max']),
|
||||||
|
"date": row['date'],
|
||||||
|
"confidence": "high"
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,11 @@ from data_layer.recovery_metrics import (
|
||||||
get_sleep_quality_data,
|
get_sleep_quality_data,
|
||||||
get_rest_days_data
|
get_rest_days_data
|
||||||
)
|
)
|
||||||
|
from data_layer.health_metrics import (
|
||||||
|
get_resting_heart_rate_data,
|
||||||
|
get_heart_rate_variability_data,
|
||||||
|
get_vo2_max_data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Helper Functions ──────────────────────────────────────────────────────────
|
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -351,55 +356,49 @@ def get_rest_days_count(profile_id: str, days: int = 30) -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
|
def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
|
||||||
"""Calculate average resting heart rate."""
|
"""
|
||||||
with get_db() as conn:
|
Calculate average resting heart rate.
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT AVG(resting_hr) as avg FROM vitals_baseline
|
|
||||||
WHERE profile_id=%s AND date >= %s AND resting_hr IS NOT NULL""",
|
|
||||||
(profile_id, cutoff)
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
|
|
||||||
if row and row['avg']:
|
Phase 0c: Refactored to use data_layer.health_metrics.get_resting_heart_rate_data()
|
||||||
return f"{int(row['avg'])} bpm"
|
This function now only FORMATS the data for AI consumption.
|
||||||
|
"""
|
||||||
|
data = get_resting_heart_rate_data(profile_id, days)
|
||||||
|
|
||||||
|
if data['confidence'] == 'insufficient':
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
return f"{data['avg_rhr']} bpm"
|
||||||
|
|
||||||
|
|
||||||
def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
|
def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
|
||||||
"""Calculate average heart rate variability."""
|
"""
|
||||||
with get_db() as conn:
|
Calculate average heart rate variability.
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT AVG(hrv) as avg FROM vitals_baseline
|
|
||||||
WHERE profile_id=%s AND date >= %s AND hrv IS NOT NULL""",
|
|
||||||
(profile_id, cutoff)
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
|
|
||||||
if row and row['avg']:
|
Phase 0c: Refactored to use data_layer.health_metrics.get_heart_rate_variability_data()
|
||||||
return f"{int(row['avg'])} ms"
|
This function now only FORMATS the data for AI consumption.
|
||||||
|
"""
|
||||||
|
data = get_heart_rate_variability_data(profile_id, days)
|
||||||
|
|
||||||
|
if data['confidence'] == 'insufficient':
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
return f"{data['avg_hrv']} ms"
|
||||||
|
|
||||||
|
|
||||||
def get_vitals_vo2_max(profile_id: str) -> str:
|
def get_vitals_vo2_max(profile_id: str) -> str:
|
||||||
"""Get latest VO2 Max value."""
|
"""
|
||||||
with get_db() as conn:
|
Get latest VO2 Max value.
|
||||||
cur = get_cursor(conn)
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT vo2_max FROM vitals_baseline
|
|
||||||
WHERE profile_id=%s AND vo2_max IS NOT NULL
|
|
||||||
ORDER BY date DESC LIMIT 1""",
|
|
||||||
(profile_id,)
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
|
|
||||||
if row and row['vo2_max']:
|
Phase 0c: Refactored to use data_layer.health_metrics.get_vo2_max_data()
|
||||||
return f"{row['vo2_max']:.1f} ml/kg/min"
|
This function now only FORMATS the data for AI consumption.
|
||||||
|
"""
|
||||||
|
data = get_vo2_max_data(profile_id)
|
||||||
|
|
||||||
|
if data['confidence'] == 'insufficient':
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
return f"{data['vo2_max']:.1f} ml/kg/min"
|
||||||
|
|
||||||
|
|
||||||
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────
|
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user