fixed dynamic TDEE
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
@@ -17,10 +18,13 @@ from app.db.models import (
|
||||
from app.fitness.activity_budget import estimate_workout_active_kcal
|
||||
from app.fitness.calculators import (
|
||||
compute_daily_targets,
|
||||
compute_expected_targets,
|
||||
compute_targets,
|
||||
one_rep_max,
|
||||
targets_to_api,
|
||||
tdee_breakdown_to_api,
|
||||
tdee_expected_to_api,
|
||||
EXPECTED_LOOKBACK_DAYS,
|
||||
)
|
||||
from app.fitness.body_composition import compute_body_composition
|
||||
|
||||
@@ -54,6 +58,10 @@ class FitnessService:
|
||||
"weight_kg": row.weight_kg,
|
||||
"goal": row.goal,
|
||||
"neat_base_kcal": row.neat_base_kcal,
|
||||
"activity_level": row.activity_level,
|
||||
"weekly_workouts": row.weekly_workouts,
|
||||
"baseline_steps": row.baseline_steps,
|
||||
"baseline_workout_kcal": row.baseline_workout_kcal,
|
||||
}
|
||||
|
||||
def _profile_to_dict(self, row: FitnessProfile) -> dict[str, Any]:
|
||||
@@ -66,6 +74,10 @@ class FitnessService:
|
||||
"goal": row.goal,
|
||||
"target_weight_kg": row.target_weight_kg,
|
||||
"neat_base_kcal": row.neat_base_kcal,
|
||||
"activity_level": row.activity_level,
|
||||
"weekly_workouts": row.weekly_workouts,
|
||||
"baseline_steps": row.baseline_steps,
|
||||
"baseline_workout_kcal": row.baseline_workout_kcal,
|
||||
"calorie_target": row.calorie_target,
|
||||
"protein_g": row.protein_g,
|
||||
"fat_g": row.fat_g,
|
||||
@@ -86,6 +98,8 @@ class FitnessService:
|
||||
for key in (
|
||||
"sex", "age", "height_cm", "weight_kg",
|
||||
"goal", "target_weight_kg", "neat_base_kcal",
|
||||
"activity_level", "weekly_workouts",
|
||||
"baseline_steps", "baseline_workout_kcal",
|
||||
):
|
||||
if key in updates and updates[key] is not None:
|
||||
setattr(row, key, updates[key])
|
||||
@@ -188,8 +202,95 @@ class FitnessService:
|
||||
"sex": "male",
|
||||
"goal": "maintain",
|
||||
"neat_base_kcal": 200,
|
||||
"activity_level": "moderate",
|
||||
"weekly_workouts": 3,
|
||||
"baseline_steps": None,
|
||||
"baseline_workout_kcal": None,
|
||||
}
|
||||
|
||||
def _activity_history(
|
||||
self,
|
||||
end_day: date,
|
||||
*,
|
||||
days: int = EXPECTED_LOOKBACK_DAYS,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Daily steps and workout kcal for `days` calendar days before end_day (exclusive)."""
|
||||
if days <= 0:
|
||||
return []
|
||||
start = end_day - timedelta(days=days)
|
||||
range_start = datetime.combine(start, time.min, tzinfo=timezone.utc)
|
||||
range_end = datetime.combine(end_day - timedelta(days=1), time.max, tzinfo=timezone.utc)
|
||||
|
||||
profile_row = self._get_profile_row()
|
||||
weight_kg = float(profile_row.weight_kg) if profile_row else 70.0
|
||||
|
||||
steps_by_day: dict[date, int] = defaultdict(int)
|
||||
workout_kcal_by_day: dict[date, float] = defaultdict(float)
|
||||
|
||||
steps_rows = self.db.scalars(
|
||||
select(StepLog).where(
|
||||
StepLog.user_id == self.user_id,
|
||||
StepLog.logged_at >= range_start,
|
||||
StepLog.logged_at <= range_end,
|
||||
)
|
||||
).all()
|
||||
for row in steps_rows:
|
||||
steps_by_day[row.logged_at.date()] += row.steps
|
||||
|
||||
workouts_rows = self.db.scalars(
|
||||
select(WorkoutLog).where(
|
||||
WorkoutLog.user_id == self.user_id,
|
||||
WorkoutLog.logged_at >= range_start,
|
||||
WorkoutLog.logged_at <= range_end,
|
||||
)
|
||||
).all()
|
||||
for row in workouts_rows:
|
||||
d = row.logged_at.date()
|
||||
workout_kcal_by_day[d] += estimate_workout_active_kcal(
|
||||
self._workout_to_dict(row),
|
||||
weight_kg=weight_kg,
|
||||
)
|
||||
|
||||
history: list[dict[str, Any]] = []
|
||||
cursor = start
|
||||
while cursor < end_day:
|
||||
history.append(
|
||||
{
|
||||
"date": cursor.isoformat(),
|
||||
"steps": steps_by_day.get(cursor, 0),
|
||||
"workout_kcal": round(workout_kcal_by_day.get(cursor, 0.0), 1),
|
||||
}
|
||||
)
|
||||
cursor += timedelta(days=1)
|
||||
return history
|
||||
|
||||
def _maybe_update_baseline(self, profile_row: FitnessProfile | None, expected: dict[str, Any]) -> None:
|
||||
if profile_row is None:
|
||||
return
|
||||
if expected.get("source") != "weekly_avg":
|
||||
return
|
||||
if int(expected.get("days_with_data") or 0) < 5:
|
||||
return
|
||||
profile_row.baseline_steps = int(expected.get("expected_steps") or 0)
|
||||
profile_row.baseline_workout_kcal = round(
|
||||
float(expected.get("expected_workout_kcal") or 0) * 7,
|
||||
1,
|
||||
)
|
||||
|
||||
def _expected_payload(
|
||||
self,
|
||||
profile: dict[str, Any],
|
||||
day: date,
|
||||
*,
|
||||
profile_row: FitnessProfile | None = None,
|
||||
update_baseline: bool = False,
|
||||
) -> tuple[dict[str, Any], dict[str, float]]:
|
||||
history = self._activity_history(day, days=EXPECTED_LOOKBACK_DAYS)
|
||||
expected_daily = compute_expected_targets(profile, history=history)
|
||||
if update_baseline:
|
||||
self._maybe_update_baseline(profile_row, expected_daily)
|
||||
return tdee_expected_to_api(expected_daily), targets_to_api(expected_daily)
|
||||
|
||||
|
||||
def _day_bounds(self, day: date | None = None) -> tuple[datetime, datetime]:
|
||||
d = day or datetime.now(timezone.utc).date()
|
||||
@@ -199,8 +300,9 @@ class FitnessService:
|
||||
|
||||
def get_daily_summary(self, day: date | None = None) -> dict[str, Any]:
|
||||
start, end = self._day_bounds(day)
|
||||
profile_row = self.get_profile()
|
||||
profile = self._profile_for_budget(profile_row)
|
||||
profile_row = self._get_profile_row()
|
||||
profile_dict = self.get_profile()
|
||||
profile = self._profile_for_budget(profile_dict)
|
||||
|
||||
foods = self.db.scalars(
|
||||
select(FoodLog)
|
||||
@@ -241,13 +343,24 @@ class FitnessService:
|
||||
workouts=workouts,
|
||||
)
|
||||
targets = targets_to_api(daily)
|
||||
target_day = day or datetime.now(timezone.utc).date()
|
||||
tdee_expected, targets_expected = self._expected_payload(
|
||||
profile,
|
||||
target_day,
|
||||
profile_row=profile_row,
|
||||
update_baseline=target_day == datetime.now(timezone.utc).date(),
|
||||
)
|
||||
if profile_row is not None and target_day == datetime.now(timezone.utc).date():
|
||||
self.db.commit()
|
||||
|
||||
return {
|
||||
"date": (day or datetime.now(timezone.utc).date()).isoformat(),
|
||||
"date": target_day.isoformat(),
|
||||
"profile_configured": profile_row is not None,
|
||||
"totals": totals,
|
||||
"targets": targets,
|
||||
"targets_expected": targets_expected,
|
||||
"tdee_breakdown": tdee_breakdown_to_api(daily),
|
||||
"tdee_expected": tdee_expected,
|
||||
"meals": [self._food_to_dict(f) for f in foods],
|
||||
"water": [self._water_to_dict(w) for w in waters],
|
||||
"workouts": workouts,
|
||||
@@ -603,11 +716,13 @@ class FitnessService:
|
||||
"has_data": has_data,
|
||||
"totals": totals,
|
||||
"targets": full["targets"],
|
||||
"targets_expected": full.get("targets_expected"),
|
||||
"meal_count": len(full["meals"]),
|
||||
"workout_count": len(full["workouts"]),
|
||||
}
|
||||
if include_tdee_breakdown:
|
||||
item["tdee_breakdown"] = full.get("tdee_breakdown")
|
||||
item["tdee_expected"] = full.get("tdee_expected")
|
||||
summaries.append(item)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user