mitai-jinkendo/backend/caliper_composition.py
Lars 49e9c9c214
All checks were successful
Deploy Development / deploy (push) Successful in 1m0s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat: Integrate caliper data enrichment and weight loading in API responses
- Enhanced the caliper listing and export functionalities to include enriched data from weight logs.
- Updated the upsert and update operations to utilize new composition functions for body composition calculations.
- Refactored the CaliperScreen component to streamline payload construction by removing unnecessary parameters.
2026-04-06 06:08:37 +02:00

112 lines
3.4 KiB
Python

"""Derive lean_mass and fat_mass from weight_log + body_fat_pct for caliper entries."""
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from db import get_cursor, r2d
def as_date(d: Any) -> Optional[date]:
if d is None:
return None
if isinstance(d, datetime):
return d.date()
if isinstance(d, date):
return d
if isinstance(d, str) and len(d) >= 10:
return datetime.strptime(d[:10], "%Y-%m-%d").date()
return None
def _floaty(x: Any) -> float:
if isinstance(x, Decimal):
return float(x)
return float(x)
def load_weight_rows(conn: Any, profile_id: str) -> List[Dict[str, Any]]:
cur = get_cursor(conn)
cur.execute(
"SELECT date, weight FROM weight_log WHERE profile_id=%s ORDER BY date",
(profile_id,),
)
return [r2d(r) for r in cur.fetchall()]
def nearest_weight_kg(weight_rows: List[Dict[str, Any]], caliper_date: Any) -> Optional[float]:
td = as_date(caliper_date)
if td is None or not weight_rows:
return None
best: Optional[float] = None
best_delta: Optional[int] = None
for row in weight_rows:
wd = as_date(row.get("date"))
if wd is None:
continue
wraw = row.get("weight")
if wraw is None:
continue
delta = abs((wd - td).days)
if best_delta is None or delta < best_delta:
best_delta = delta
best = _floaty(wraw)
return best
def nearest_weight_kg_from_map(wlog: Dict[Any, Any], target_date: Any) -> Optional[float]:
if not wlog:
return None
td = as_date(target_date)
if td is None:
return None
best_w: Optional[float] = None
best_delta: Optional[int] = None
for wd, wval in wlog.items():
if wval is None:
continue
wdate = as_date(wd)
if wdate is None:
continue
delta = abs((wdate - td).days)
if best_delta is None or delta < best_delta:
best_delta = delta
best_w = _floaty(wval)
return best_w
def compute_lean_fat_kg(weight_kg: float, body_fat_pct: float) -> Tuple[float, float]:
lean = round(weight_kg - (weight_kg * body_fat_pct / 100.0), 2)
fat = round(weight_kg * body_fat_pct / 100.0, 2)
return lean, fat
def fill_caliper_body_comp(d: Dict[str, Any], conn: Any, profile_id: str) -> None:
"""Mutate caliper API payload (model_dump) before INSERT/UPDATE."""
bf = d.get("body_fat_pct")
if bf is None:
return
if d.get("lean_mass") is not None and d.get("fat_mass") is not None:
return
rows = load_weight_rows(conn, profile_id)
w = nearest_weight_kg(rows, d.get("date"))
if w is None:
return
lean, fat = compute_lean_fat_kg(w, _floaty(bf))
d["lean_mass"] = lean
d["fat_mass"] = fat
def enrich_caliper_row_for_response(row: Dict[str, Any], weight_rows: List[Dict[str, Any]]) -> None:
"""Fill missing lean_mass / fat_mass for API responses (read path)."""
bf = row.get("body_fat_pct")
if bf is None:
return
if row.get("lean_mass") is not None and row.get("fat_mass") is not None:
return
w = nearest_weight_kg(weight_rows, row.get("date"))
if w is None:
return
lean, fat = compute_lean_fat_kg(w, _floaty(bf))
row["lean_mass"] = lean
row["fat_mass"] = fat