feat: Phase 0c - nutrition_metrics.py module complete
Data Layer: - get_nutrition_average_data() - all macros in one call - get_nutrition_days_data() - coverage tracking - get_protein_targets_data() - 1.6g/kg and 2.2g/kg targets - get_energy_balance_data() - deficit/surplus/maintenance - get_protein_adequacy_data() - 0-100 score - get_macro_consistency_data() - 0-100 score Placeholder Layer: - get_nutrition_avg() - refactored to use data layer - get_nutrition_days() - refactored to use data layer - get_protein_ziel_low() - refactored to use data layer - get_protein_ziel_high() - refactored to use data layer All 6 nutrition data functions + 4 placeholder refactors complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c79cc9eafb
commit
e1d7670971
|
|
@ -30,9 +30,9 @@ from .utils import *
|
||||||
|
|
||||||
# Metric modules
|
# Metric modules
|
||||||
from .body_metrics import *
|
from .body_metrics import *
|
||||||
|
from .nutrition_metrics import *
|
||||||
|
|
||||||
# Future imports (will be added as modules are created):
|
# Future imports (will be added as modules are created):
|
||||||
# from .nutrition_metrics import *
|
|
||||||
# from .activity_metrics import *
|
# from .activity_metrics import *
|
||||||
# from .recovery_metrics import *
|
# from .recovery_metrics import *
|
||||||
# from .health_metrics import *
|
# from .health_metrics import *
|
||||||
|
|
@ -48,4 +48,12 @@ __all__ = [
|
||||||
'get_weight_trend_data',
|
'get_weight_trend_data',
|
||||||
'get_body_composition_data',
|
'get_body_composition_data',
|
||||||
'get_circumference_summary_data',
|
'get_circumference_summary_data',
|
||||||
|
|
||||||
|
# Nutrition Metrics
|
||||||
|
'get_nutrition_average_data',
|
||||||
|
'get_nutrition_days_data',
|
||||||
|
'get_protein_targets_data',
|
||||||
|
'get_energy_balance_data',
|
||||||
|
'get_protein_adequacy_data',
|
||||||
|
'get_macro_consistency_data',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
482
backend/data_layer/nutrition_metrics.py
Normal file
482
backend/data_layer/nutrition_metrics.py
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
"""
|
||||||
|
Nutrition Metrics Data Layer
|
||||||
|
|
||||||
|
Provides structured data for nutrition tracking and analysis.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- get_nutrition_average_data(): Average calor
|
||||||
|
|
||||||
|
ies, protein, carbs, fat
|
||||||
|
- get_nutrition_days_data(): Number of days with nutrition data
|
||||||
|
- get_protein_targets_data(): Protein targets based on weight
|
||||||
|
- get_energy_balance_data(): Energy balance calculation
|
||||||
|
- get_protein_adequacy_data(): Protein adequacy score
|
||||||
|
- get_macro_consistency_data(): Macro consistency analysis
|
||||||
|
|
||||||
|
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_nutrition_average_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 30
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Get average nutrition values for all macros.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 30)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"kcal_avg": float,
|
||||||
|
"protein_avg": float,
|
||||||
|
"carbs_avg": float,
|
||||||
|
"fat_avg": float,
|
||||||
|
"data_points": int,
|
||||||
|
"confidence": str,
|
||||||
|
"days_analyzed": int
|
||||||
|
}
|
||||||
|
|
||||||
|
Migration from Phase 0b:
|
||||||
|
OLD: get_nutrition_avg(pid, field, days) per field
|
||||||
|
NEW: All macros in one call
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT
|
||||||
|
AVG(kcal) as kcal_avg,
|
||||||
|
AVG(protein_g) as protein_avg,
|
||||||
|
AVG(carbs_g) as carbs_avg,
|
||||||
|
AVG(fat_g) as fat_avg,
|
||||||
|
COUNT(*) as data_points
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row or row['data_points'] == 0:
|
||||||
|
return {
|
||||||
|
"kcal_avg": 0.0,
|
||||||
|
"protein_avg": 0.0,
|
||||||
|
"carbs_avg": 0.0,
|
||||||
|
"fat_avg": 0.0,
|
||||||
|
"data_points": 0,
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
data_points = row['data_points']
|
||||||
|
confidence = calculate_confidence(data_points, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kcal_avg": safe_float(row['kcal_avg']),
|
||||||
|
"protein_avg": safe_float(row['protein_avg']),
|
||||||
|
"carbs_avg": safe_float(row['carbs_avg']),
|
||||||
|
"fat_avg": safe_float(row['fat_avg']),
|
||||||
|
"data_points": data_points,
|
||||||
|
"confidence": confidence,
|
||||||
|
"days_analyzed": days
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_nutrition_days_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 30
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Count days with nutrition data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 30)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"days_with_data": int,
|
||||||
|
"days_analyzed": int,
|
||||||
|
"coverage_pct": float,
|
||||||
|
"confidence": str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT COUNT(DISTINCT date) as days
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
days_with_data = row['days'] if row else 0
|
||||||
|
|
||||||
|
coverage_pct = (days_with_data / days * 100) if days > 0 else 0
|
||||||
|
confidence = calculate_confidence(days_with_data, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"days_with_data": days_with_data,
|
||||||
|
"days_analyzed": days,
|
||||||
|
"coverage_pct": coverage_pct,
|
||||||
|
"confidence": confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_protein_targets_data(
|
||||||
|
profile_id: str
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Calculate protein targets based on current weight.
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
- Low: 1.6 g/kg (maintenance)
|
||||||
|
- High: 2.2 g/kg (muscle building)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"current_weight": float,
|
||||||
|
"protein_target_low": float, # 1.6 g/kg
|
||||||
|
"protein_target_high": float, # 2.2 g/kg
|
||||||
|
"confidence": str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT weight FROM weight_log
|
||||||
|
WHERE profile_id=%s ORDER BY date DESC LIMIT 1""",
|
||||||
|
(profile_id,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return {
|
||||||
|
"current_weight": 0.0,
|
||||||
|
"protein_target_low": 0.0,
|
||||||
|
"protein_target_high": 0.0,
|
||||||
|
"confidence": "insufficient"
|
||||||
|
}
|
||||||
|
|
||||||
|
weight = safe_float(row['weight'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_weight": weight,
|
||||||
|
"protein_target_low": weight * 1.6,
|
||||||
|
"protein_target_high": weight * 2.2,
|
||||||
|
"confidence": "high"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_energy_balance_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 7
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Calculate energy balance (intake - estimated expenditure).
|
||||||
|
|
||||||
|
Note: This is a simplified calculation.
|
||||||
|
For accurate TDEE, use profile-based calculations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 7)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"energy_balance": float, # kcal/day (negative = deficit)
|
||||||
|
"avg_intake": float,
|
||||||
|
"estimated_tdee": float,
|
||||||
|
"status": str, # "deficit" | "surplus" | "maintenance"
|
||||||
|
"confidence": str,
|
||||||
|
"days_analyzed": int,
|
||||||
|
"data_points": int
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# Get average intake
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT AVG(kcal) as avg_kcal, COUNT(*) as cnt
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s AND kcal IS NOT NULL""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row or row['cnt'] == 0:
|
||||||
|
return {
|
||||||
|
"energy_balance": 0.0,
|
||||||
|
"avg_intake": 0.0,
|
||||||
|
"estimated_tdee": 0.0,
|
||||||
|
"status": "unknown",
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"days_analyzed": days,
|
||||||
|
"data_points": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_intake = safe_float(row['avg_kcal'])
|
||||||
|
data_points = row['cnt']
|
||||||
|
|
||||||
|
# Simple TDEE estimation (this should be improved with profile data)
|
||||||
|
# For now, use a rough estimate: 2500 kcal for average adult
|
||||||
|
estimated_tdee = 2500.0 # TODO: Calculate from profile (weight, height, age, activity)
|
||||||
|
|
||||||
|
energy_balance = avg_intake - estimated_tdee
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
if energy_balance < -200:
|
||||||
|
status = "deficit"
|
||||||
|
elif energy_balance > 200:
|
||||||
|
status = "surplus"
|
||||||
|
else:
|
||||||
|
status = "maintenance"
|
||||||
|
|
||||||
|
confidence = calculate_confidence(data_points, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"energy_balance": energy_balance,
|
||||||
|
"avg_intake": avg_intake,
|
||||||
|
"estimated_tdee": estimated_tdee,
|
||||||
|
"status": status,
|
||||||
|
"confidence": confidence,
|
||||||
|
"days_analyzed": days,
|
||||||
|
"data_points": data_points
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_protein_adequacy_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 28
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Calculate protein adequacy score (0-100).
|
||||||
|
|
||||||
|
Score based on:
|
||||||
|
- Daily protein intake vs. target (1.6-2.2 g/kg)
|
||||||
|
- Consistency across days
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 28)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"adequacy_score": int, # 0-100
|
||||||
|
"avg_protein_g": float,
|
||||||
|
"target_protein_low": float,
|
||||||
|
"target_protein_high": float,
|
||||||
|
"protein_g_per_kg": float,
|
||||||
|
"days_in_target": int,
|
||||||
|
"days_with_data": int,
|
||||||
|
"confidence": str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Get protein targets
|
||||||
|
targets = get_protein_targets_data(profile_id)
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT
|
||||||
|
AVG(protein_g) as avg_protein,
|
||||||
|
COUNT(*) as cnt,
|
||||||
|
SUM(CASE WHEN protein_g >= %s AND protein_g <= %s THEN 1 ELSE 0 END) as days_in_target
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s AND protein_g IS NOT NULL""",
|
||||||
|
(targets['protein_target_low'], targets['protein_target_high'], profile_id, cutoff)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row or row['cnt'] == 0:
|
||||||
|
return {
|
||||||
|
"adequacy_score": 0,
|
||||||
|
"avg_protein_g": 0.0,
|
||||||
|
"target_protein_low": targets['protein_target_low'],
|
||||||
|
"target_protein_high": targets['protein_target_high'],
|
||||||
|
"protein_g_per_kg": 0.0,
|
||||||
|
"days_in_target": 0,
|
||||||
|
"days_with_data": 0,
|
||||||
|
"confidence": "insufficient"
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_protein = safe_float(row['avg_protein'])
|
||||||
|
days_with_data = row['cnt']
|
||||||
|
days_in_target = row['days_in_target']
|
||||||
|
|
||||||
|
protein_g_per_kg = avg_protein / targets['current_weight'] if targets['current_weight'] > 0 else 0.0
|
||||||
|
|
||||||
|
# Calculate adequacy score
|
||||||
|
# 100 = always in target range
|
||||||
|
# Scale based on percentage of days in target + average relative to target
|
||||||
|
target_pct = (days_in_target / days_with_data * 100) if days_with_data > 0 else 0
|
||||||
|
|
||||||
|
# Bonus/penalty for average protein level
|
||||||
|
target_mid = (targets['protein_target_low'] + targets['protein_target_high']) / 2
|
||||||
|
avg_vs_target = (avg_protein / target_mid) if target_mid > 0 else 0
|
||||||
|
|
||||||
|
# Weighted score: 70% target days, 30% average level
|
||||||
|
adequacy_score = int(target_pct * 0.7 + min(avg_vs_target * 100, 100) * 0.3)
|
||||||
|
adequacy_score = max(0, min(100, adequacy_score)) # Clamp to 0-100
|
||||||
|
|
||||||
|
confidence = calculate_confidence(days_with_data, days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"adequacy_score": adequacy_score,
|
||||||
|
"avg_protein_g": avg_protein,
|
||||||
|
"target_protein_low": targets['protein_target_low'],
|
||||||
|
"target_protein_high": targets['protein_target_high'],
|
||||||
|
"protein_g_per_kg": protein_g_per_kg,
|
||||||
|
"days_in_target": days_in_target,
|
||||||
|
"days_with_data": days_with_data,
|
||||||
|
"confidence": confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_macro_consistency_data(
|
||||||
|
profile_id: str,
|
||||||
|
days: int = 28
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Calculate macro consistency score (0-100).
|
||||||
|
|
||||||
|
Measures how consistent macronutrient ratios are across days.
|
||||||
|
High consistency = predictable nutrition, easier to track progress.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: User profile ID
|
||||||
|
days: Analysis window (default 28)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"consistency_score": int, # 0-100 (100 = very consistent)
|
||||||
|
"avg_protein_pct": float,
|
||||||
|
"avg_carbs_pct": float,
|
||||||
|
"avg_fat_pct": float,
|
||||||
|
"std_dev_protein": float, # Standard deviation
|
||||||
|
"std_dev_carbs": float,
|
||||||
|
"std_dev_fat": float,
|
||||||
|
"confidence": str,
|
||||||
|
"data_points": int
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT
|
||||||
|
protein_g, carbs_g, fat_g, kcal
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s
|
||||||
|
AND date >= %s
|
||||||
|
AND protein_g IS NOT NULL
|
||||||
|
AND carbs_g IS NOT NULL
|
||||||
|
AND fat_g IS NOT NULL
|
||||||
|
AND kcal > 0
|
||||||
|
ORDER BY date""",
|
||||||
|
(profile_id, cutoff)
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
if len(rows) < 3:
|
||||||
|
return {
|
||||||
|
"consistency_score": 0,
|
||||||
|
"avg_protein_pct": 0.0,
|
||||||
|
"avg_carbs_pct": 0.0,
|
||||||
|
"avg_fat_pct": 0.0,
|
||||||
|
"std_dev_protein": 0.0,
|
||||||
|
"std_dev_carbs": 0.0,
|
||||||
|
"std_dev_fat": 0.0,
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"data_points": len(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate macro percentages for each day
|
||||||
|
import statistics
|
||||||
|
|
||||||
|
protein_pcts = []
|
||||||
|
carbs_pcts = []
|
||||||
|
fat_pcts = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
total_kcal = safe_float(row['kcal'])
|
||||||
|
if total_kcal == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Convert grams to kcal (protein=4, carbs=4, fat=9)
|
||||||
|
protein_kcal = safe_float(row['protein_g']) * 4
|
||||||
|
carbs_kcal = safe_float(row['carbs_g']) * 4
|
||||||
|
fat_kcal = safe_float(row['fat_g']) * 9
|
||||||
|
|
||||||
|
macro_kcal_total = protein_kcal + carbs_kcal + fat_kcal
|
||||||
|
|
||||||
|
if macro_kcal_total > 0:
|
||||||
|
protein_pcts.append(protein_kcal / macro_kcal_total * 100)
|
||||||
|
carbs_pcts.append(carbs_kcal / macro_kcal_total * 100)
|
||||||
|
fat_pcts.append(fat_kcal / macro_kcal_total * 100)
|
||||||
|
|
||||||
|
if len(protein_pcts) < 3:
|
||||||
|
return {
|
||||||
|
"consistency_score": 0,
|
||||||
|
"avg_protein_pct": 0.0,
|
||||||
|
"avg_carbs_pct": 0.0,
|
||||||
|
"avg_fat_pct": 0.0,
|
||||||
|
"std_dev_protein": 0.0,
|
||||||
|
"std_dev_carbs": 0.0,
|
||||||
|
"std_dev_fat": 0.0,
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"data_points": len(protein_pcts)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate averages and standard deviations
|
||||||
|
avg_protein_pct = statistics.mean(protein_pcts)
|
||||||
|
avg_carbs_pct = statistics.mean(carbs_pcts)
|
||||||
|
avg_fat_pct = statistics.mean(fat_pcts)
|
||||||
|
|
||||||
|
std_protein = statistics.stdev(protein_pcts) if len(protein_pcts) > 1 else 0.0
|
||||||
|
std_carbs = statistics.stdev(carbs_pcts) if len(carbs_pcts) > 1 else 0.0
|
||||||
|
std_fat = statistics.stdev(fat_pcts) if len(fat_pcts) > 1 else 0.0
|
||||||
|
|
||||||
|
# Consistency score: inverse of average standard deviation
|
||||||
|
# Lower std_dev = higher consistency
|
||||||
|
avg_std = (std_protein + std_carbs + std_fat) / 3
|
||||||
|
|
||||||
|
# Score: 100 - (avg_std * scale_factor)
|
||||||
|
# avg_std of 5% = score 75, avg_std of 10% = score 50, avg_std of 20% = score 0
|
||||||
|
consistency_score = max(0, min(100, int(100 - (avg_std * 5))))
|
||||||
|
|
||||||
|
confidence = calculate_confidence(len(protein_pcts), days, "general")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"consistency_score": consistency_score,
|
||||||
|
"avg_protein_pct": avg_protein_pct,
|
||||||
|
"avg_carbs_pct": avg_carbs_pct,
|
||||||
|
"avg_fat_pct": avg_fat_pct,
|
||||||
|
"std_dev_protein": std_protein,
|
||||||
|
"std_dev_carbs": std_carbs,
|
||||||
|
"std_dev_fat": std_fat,
|
||||||
|
"confidence": confidence,
|
||||||
|
"data_points": len(protein_pcts)
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,11 @@ from data_layer.body_metrics import (
|
||||||
get_body_composition_data,
|
get_body_composition_data,
|
||||||
get_circumference_summary_data
|
get_circumference_summary_data
|
||||||
)
|
)
|
||||||
|
from data_layer.nutrition_metrics import (
|
||||||
|
get_nutrition_average_data,
|
||||||
|
get_nutrition_days_data,
|
||||||
|
get_protein_targets_data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Helper Functions ──────────────────────────────────────────────────────────
|
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -81,33 +86,32 @@ def get_latest_bf(profile_id: str) -> Optional[str]:
|
||||||
|
|
||||||
|
|
||||||
def get_nutrition_avg(profile_id: str, field: str, days: int = 30) -> str:
|
def get_nutrition_avg(profile_id: str, field: str, days: int = 30) -> str:
|
||||||
"""Calculate average nutrition value."""
|
"""
|
||||||
with get_db() as conn:
|
Calculate average nutrition value.
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# Map field names to actual column names
|
Phase 0c: Refactored to use data_layer.nutrition_metrics.get_nutrition_average_data()
|
||||||
field_map = {
|
This function now only FORMATS the data for AI consumption.
|
||||||
'protein': 'protein_g',
|
"""
|
||||||
'fat': 'fat_g',
|
data = get_nutrition_average_data(profile_id, days)
|
||||||
'carb': 'carbs_g',
|
|
||||||
'kcal': 'kcal'
|
|
||||||
}
|
|
||||||
db_field = field_map.get(field, field)
|
|
||||||
|
|
||||||
cur.execute(
|
if data['confidence'] == 'insufficient':
|
||||||
f"""SELECT AVG({db_field}) as avg FROM nutrition_log
|
|
||||||
WHERE profile_id=%s AND date >= %s AND {db_field} IS NOT NULL""",
|
|
||||||
(profile_id, cutoff)
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row and row['avg']:
|
|
||||||
if field == 'kcal':
|
|
||||||
return f"{int(row['avg'])} kcal/Tag (Ø {days} Tage)"
|
|
||||||
else:
|
|
||||||
return f"{int(row['avg'])}g/Tag (Ø {days} Tage)"
|
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
# Map field names to data keys
|
||||||
|
field_map = {
|
||||||
|
'protein': 'protein_avg',
|
||||||
|
'fat': 'fat_avg',
|
||||||
|
'carb': 'carbs_avg',
|
||||||
|
'kcal': 'kcal_avg'
|
||||||
|
}
|
||||||
|
data_key = field_map.get(field, f'{field}_avg')
|
||||||
|
value = data.get(data_key, 0)
|
||||||
|
|
||||||
|
if field == 'kcal':
|
||||||
|
return f"{int(value)} kcal/Tag (Ø {days} Tage)"
|
||||||
|
else:
|
||||||
|
return f"{int(value)}g/Tag (Ø {days} Tage)"
|
||||||
|
|
||||||
|
|
||||||
def get_caliper_summary(profile_id: str) -> str:
|
def get_caliper_summary(profile_id: str) -> str:
|
||||||
"""Get latest caliper measurements summary."""
|
"""Get latest caliper measurements summary."""
|
||||||
|
|
@ -178,48 +182,45 @@ def get_goal_bf_pct(profile_id: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_nutrition_days(profile_id: str, days: int = 30) -> str:
|
def get_nutrition_days(profile_id: str, days: int = 30) -> str:
|
||||||
"""Get number of days with nutrition data."""
|
"""
|
||||||
with get_db() as conn:
|
Get number of days with nutrition data.
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
Phase 0c: Refactored to use data_layer.nutrition_metrics.get_nutrition_days_data()
|
||||||
cur.execute(
|
This function now only FORMATS the data for AI consumption.
|
||||||
"""SELECT COUNT(DISTINCT date) as days FROM nutrition_log
|
"""
|
||||||
WHERE profile_id=%s AND date >= %s""",
|
data = get_nutrition_days_data(profile_id, days)
|
||||||
(profile_id, cutoff)
|
return str(data['days_with_data'])
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
return str(row['days']) if row else "0"
|
|
||||||
|
|
||||||
|
|
||||||
def get_protein_ziel_low(profile_id: str) -> str:
|
def get_protein_ziel_low(profile_id: str) -> str:
|
||||||
"""Calculate lower protein target based on current weight (1.6g/kg)."""
|
"""
|
||||||
with get_db() as conn:
|
Calculate lower protein target based on current weight (1.6g/kg).
|
||||||
cur = get_cursor(conn)
|
|
||||||
cur.execute(
|
Phase 0c: Refactored to use data_layer.nutrition_metrics.get_protein_targets_data()
|
||||||
"""SELECT weight FROM weight_log
|
This function now only FORMATS the data for AI consumption.
|
||||||
WHERE profile_id=%s ORDER BY date DESC LIMIT 1""",
|
"""
|
||||||
(profile_id,)
|
data = get_protein_targets_data(profile_id)
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
if data['confidence'] == 'insufficient':
|
||||||
if row:
|
|
||||||
return f"{int(float(row['weight']) * 1.6)}"
|
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
return f"{int(data['protein_target_low'])}"
|
||||||
|
|
||||||
|
|
||||||
def get_protein_ziel_high(profile_id: str) -> str:
|
def get_protein_ziel_high(profile_id: str) -> str:
|
||||||
"""Calculate upper protein target based on current weight (2.2g/kg)."""
|
"""
|
||||||
with get_db() as conn:
|
Calculate upper protein target based on current weight (2.2g/kg).
|
||||||
cur = get_cursor(conn)
|
|
||||||
cur.execute(
|
Phase 0c: Refactored to use data_layer.nutrition_metrics.get_protein_targets_data()
|
||||||
"""SELECT weight FROM weight_log
|
This function now only FORMATS the data for AI consumption.
|
||||||
WHERE profile_id=%s ORDER BY date DESC LIMIT 1""",
|
"""
|
||||||
(profile_id,)
|
data = get_protein_targets_data(profile_id)
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
if data['confidence'] == 'insufficient':
|
||||||
if row:
|
|
||||||
return f"{int(float(row['weight']) * 2.2)}"
|
|
||||||
return "nicht verfügbar"
|
return "nicht verfügbar"
|
||||||
|
|
||||||
|
return f"{int(data['protein_target_high'])}"
|
||||||
|
|
||||||
|
|
||||||
def get_activity_summary(profile_id: str, days: int = 14) -> str:
|
def get_activity_summary(profile_id: str, days: int = 14) -> str:
|
||||||
"""Get activity summary for recent period."""
|
"""Get activity summary for recent period."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user