Add matched training signals, daily recovery and performance analysis
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Bounded optional daily Garmin imports; provider failures never disclose sessions."""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from .analytics import numeric
|
||||
from .extensions import db
|
||||
from .models import DailyHealth
|
||||
|
||||
|
||||
def normalize(stats, hrv, sleep):
|
||||
summary = (hrv or {}).get("hrvSummary") or {}
|
||||
dto = (sleep or {}).get("dailySleepDTO") or {}
|
||||
candidates = {"resting_hr": (stats or {}).get("restingHeartRate"),
|
||||
"hrv": summary.get("lastNightAvg"),
|
||||
"sleep_hours": dto.get("sleepTimeSeconds"),
|
||||
"stress": (stats or {}).get("averageStressLevel")}
|
||||
values = {}
|
||||
for key, number in candidates.items():
|
||||
if not numeric(number):
|
||||
continue
|
||||
if key == "sleep_hours":
|
||||
number /= 3600
|
||||
low, high = {"resting_hr": (25, 220), "hrv": (1, 500),
|
||||
"sleep_hours": (0, 24), "stress": (0, 100)}[key]
|
||||
if low <= number <= high:
|
||||
values[key] = number
|
||||
return values
|
||||
|
||||
|
||||
def sync_health(api, user, days=90, limit=5):
|
||||
today = datetime.now(UTC).astimezone(ZoneInfo(user.timezone)).date()
|
||||
first = today - timedelta(days=days - 1)
|
||||
known = {row.day: row for row in db.session.scalars(db.select(DailyHealth).where(
|
||||
DailyHealth.owner_id == user.id, DailyHealth.day >= first))}
|
||||
# Refresh today/yesterday; then walk backwards through missing history.
|
||||
dates = [today, today - timedelta(days=1)]
|
||||
dates += [today - timedelta(days=i) for i in range(2, days)
|
||||
if today - timedelta(days=i) not in known or
|
||||
not known[today - timedelta(days=i)].values.get("_complete")]
|
||||
dates = [day for day in dates if day >= first][:limit]
|
||||
imported = failures = 0
|
||||
for day in dates:
|
||||
documents = []
|
||||
day_failures = 0
|
||||
for method in ("get_stats", "get_hrv_data", "get_sleep_data"):
|
||||
try:
|
||||
document = getattr(api, method)(day.isoformat())
|
||||
documents.append(document if isinstance(document, dict) else {})
|
||||
except Exception: # noqa: BLE001 -- never log provider responses or credentials
|
||||
documents.append({})
|
||||
failures += 1
|
||||
day_failures += 1
|
||||
values = normalize(*documents)
|
||||
if day_failures and not values:
|
||||
# Retry later; stop the batch instead of hammering a failing provider.
|
||||
break
|
||||
row = known.get(day)
|
||||
if not row:
|
||||
row = DailyHealth(owner_id=user.id, day=day, values={})
|
||||
db.session.add(row)
|
||||
row.values = {**row.values, **values, "_complete": day_failures == 0}
|
||||
row.fetched_at = datetime.now(UTC)
|
||||
db.session.commit()
|
||||
imported += 1
|
||||
if day_failures:
|
||||
break
|
||||
return {"days": imported, "failures": failures,
|
||||
"message": f"Daily recovery: {imported} dates checked." +
|
||||
(" Some observations unavailable; will retry." if failures else
|
||||
" Missing watch observations remain missing.")}
|
||||
Reference in New Issue
Block a user