feat: consolidate datastore into sleep, nutrition, mila databases

This commit is contained in:
Moritz Graf
2026-10-13 09:28:07 +02:00
parent fe4edcc217
commit 7b5d039323
5 changed files with 896 additions and 161 deletions
+82 -4
View File
@@ -173,12 +173,14 @@ class GarminMirrorWriter:
"""Manages month-partitioned JSONL files with idempotent upserts."""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.garmin_dir = data_dir / "garmin"
self.daily_dir = self.garmin_dir / "daily"
self.sleep_dir = self.garmin_dir / "sleep"
self.unified_sleep_dir = data_dir / "sleep"
self.activities_dir = self.garmin_dir / "activities"
for d in [self.daily_dir, self.sleep_dir, self.activities_dir]:
for d in [self.daily_dir, self.sleep_dir, self.unified_sleep_dir, self.activities_dir]:
d.mkdir(parents=True, exist_ok=True)
def _read_existing(self, file_path: Path) -> List[Dict[str, Any]]:
@@ -230,8 +232,9 @@ class GarminMirrorWriter:
def upsert_sleep_data(self, sleep_data: Dict[str, Any]):
cal_date = None
if "dailySleepDTO" in sleep_data and isinstance(sleep_data["dailySleepDTO"], dict):
cal_date = sleep_data["dailySleepDTO"].get("calendarDate")
dto = sleep_data.get("dailySleepDTO") if isinstance(sleep_data.get("dailySleepDTO"), dict) else {}
if dto:
cal_date = dto.get("calendarDate")
if not cal_date:
cal_date = sleep_data.get("calendarDate")
@@ -267,7 +270,82 @@ class GarminMirrorWriter:
new_items.sort(key=_sort_key)
self._write_atomic(target_file, new_items)
logger.info(f"Upserted sleep data for {cal_date} into {target_file.name}")
# Also upsert into unified data/sleep/<YYYY-MM>.jsonl
start_gmt = dto.get("sleepStartTimestampGMT")
end_gmt = dto.get("sleepEndTimestampGMT")
if start_gmt:
start_dt = datetime.datetime.fromtimestamp(start_gmt / 1000, datetime.timezone.utc)
start_iso = start_dt.isoformat()
else:
start_iso = f"{cal_date}T22:30:00+02:00"
if end_gmt:
end_dt = datetime.datetime.fromtimestamp(end_gmt / 1000, datetime.timezone.utc)
end_iso = end_dt.isoformat()
else:
end_iso = f"{cal_date}T06:30:00+02:00"
nap_sec = dto.get("napTimeSeconds") or 0
sleep_sec = dto.get("sleepTimeSeconds") or 0
sleep_type = "nap" if (nap_sec > 0 and sleep_sec == 0) else "night"
unified_rec = {
"id": str(dto.get("id") or (int(start_gmt) if start_gmt else cal_date)),
"subject": "moritz",
"calendarDate": cal_date,
"sleepType": sleep_type,
"location": "bett",
"isOpen": False,
"startIso": start_iso,
"endIso": end_iso,
"sleepStartTimestampGMT": dto.get("sleepStartTimestampGMT"),
"sleepEndTimestampGMT": dto.get("sleepEndTimestampGMT"),
"sleepStartTimestampLocal": dto.get("sleepStartTimestampLocal"),
"sleepEndTimestampLocal": dto.get("sleepEndTimestampLocal"),
"sleepTimeSeconds": dto.get("sleepTimeSeconds"),
"napTimeSeconds": nap_sec,
"deepSleepSeconds": dto.get("deepSleepSeconds"),
"lightSleepSeconds": dto.get("lightSleepSeconds"),
"remSleepSeconds": dto.get("remSleepSeconds"),
"awakeSleepSeconds": dto.get("awakeSleepSeconds"),
"awakeCount": dto.get("awakeCount"),
"avgSleepStress": dto.get("avgSleepStress"),
"avgHeartRate": dto.get("avgHeartRate"),
"averageSpO2Value": dto.get("averageSpO2Value"),
"lowestSpO2Value": dto.get("lowestSpO2Value"),
"highestSpO2Value": dto.get("highestSpO2Value"),
"averageRespirationValue": dto.get("averageRespirationValue"),
"sleepScores": dto.get("sleepScores"),
"avgOvernightHrv": sleep_data.get("avgOvernightHrv"),
"hrvStatus": sleep_data.get("hrvStatus"),
"bodyBatteryChange": sleep_data.get("bodyBatteryChange"),
"restingHeartRate": sleep_data.get("restingHeartRate"),
"restlessMomentsCount": sleep_data.get("restlessMomentsCount"),
"note": None,
"actor": "garmin_sync",
"created_at": start_iso,
"updated_at": None,
"deleted_at": None,
}
unified_file = self.unified_sleep_dir / f"{month}.jsonl"
existing_unified = self._read_existing(unified_file)
updated_u = False
new_unified = []
for item in existing_unified:
if item.get("subject") == "moritz" and item.get("calendarDate") == cal_date:
new_unified.append(unified_rec)
updated_u = True
else:
new_unified.append(item)
if not updated_u:
new_unified.append(unified_rec)
new_unified.sort(key=lambda x: (str(x.get("calendarDate", "")), str(x.get("startIso", "")), str(x.get("id", ""))))
self._write_atomic(unified_file, new_unified)
logger.info(f"Upserted sleep data for {cal_date} into {target_file.name} and {unified_file.name}")
def upsert_activities(self, activities: List[Dict[str, Any]]):
if not activities: