72 lines
3.0 KiB
Python
72 lines
3.0 KiB
Python
"""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.")}
|