feat: Phase 0c - health_metrics.py module complete
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

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:
Lars 2026-03-28 19:15:31 +01:00
parent 432f7ba49f
commit b4558b0582
3 changed files with 238 additions and 37 deletions

View File

@ -33,9 +33,9 @@ from .body_metrics import *
from .nutrition_metrics import *
from .activity_metrics import *
from .recovery_metrics import *
from .health_metrics import *
# Future imports (will be added as modules are created):
# from .health_metrics import *
# from .goals import *
# from .correlations import *
@ -66,4 +66,9 @@ __all__ = [
'get_sleep_duration_data',
'get_sleep_quality_data',
'get_rest_days_data',
# Health Metrics
'get_resting_heart_rate_data',
'get_heart_rate_variability_data',
'get_vo2_max_data',
]

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

View File

@ -33,6 +33,11 @@ from data_layer.recovery_metrics import (
get_sleep_quality_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 ──────────────────────────────────────────────────────────
@ -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:
"""Calculate average resting heart rate."""
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 FROM vitals_baseline
WHERE profile_id=%s AND date >= %s AND resting_hr IS NOT NULL""",
(profile_id, cutoff)
)
row = cur.fetchone()
"""
Calculate average resting heart rate.
if row and row['avg']:
return f"{int(row['avg'])} bpm"
Phase 0c: Refactored to use data_layer.health_metrics.get_resting_heart_rate_data()
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 f"{data['avg_rhr']} bpm"
def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
"""Calculate average heart rate variability."""
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 FROM vitals_baseline
WHERE profile_id=%s AND date >= %s AND hrv IS NOT NULL""",
(profile_id, cutoff)
)
row = cur.fetchone()
"""
Calculate average heart rate variability.
if row and row['avg']:
return f"{int(row['avg'])} ms"
Phase 0c: Refactored to use data_layer.health_metrics.get_heart_rate_variability_data()
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 f"{data['avg_hrv']} ms"
def get_vitals_vo2_max(profile_id: str) -> str:
"""Get latest VO2 Max value."""
with get_db() as conn:
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()
"""
Get latest VO2 Max value.
if row and row['vo2_max']:
return f"{row['vo2_max']:.1f} ml/kg/min"
Phase 0c: Refactored to use data_layer.health_metrics.get_vo2_max_data()
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 f"{data['vo2_max']:.1f} ml/kg/min"
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────