- 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.
112 lines
3.4 KiB
Python
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
|