INITIAL COMMIT

This commit is contained in:
2026-06-16 15:14:37 +02:00
commit 1477ec36fd
49 changed files with 6835 additions and 0 deletions

144
scripts/clone_today_workout.py Executable file
View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from dotenv import load_dotenv
from garmin_coach_clone.auth import login
from garmin_coach_clone.coach import (
best_workout_for_date,
extract_training_plans,
get_fbt_adaptive_workout,
is_adaptive_plan,
plan_id,
task_workout_for_date,
)
from garmin_coach_clone.dates import parse_date
from garmin_coach_clone.io import dump_redacted_json, print_json
from garmin_coach_clone.workouts import (
clone_workout_payload,
existing_clone_names,
summarize_workout,
validate_workout_payload,
)
load_dotenv()
def main() -> int:
parser = argparse.ArgumentParser(
description="Clone a Garmin Coach workout if step data is exposed."
)
parser.add_argument("--date", default="today", help="today, tomorrow, or YYYY-MM-DD")
parser.add_argument("--dry-run", action="store_true", help="Convert and print JSON only.")
parser.add_argument("--schedule", action="store_true", help="Upload and schedule the clone.")
parser.add_argument(
"--force", action="store_true", help="Create even if a clone already exists."
)
parser.add_argument(
"--dump-json", action="store_true", help="Dump redacted Coach/adaptive JSON."
)
args = parser.parse_args()
target_date = parse_date(args.date)
prefix = os.getenv("CLONE_PREFIX", "GCClone")
api = login()
plans = extract_training_plans(api.get_training_plans())
candidates = [plan for plan in plans if is_adaptive_plan(plan)]
if not candidates:
print("No Garmin Coach/adaptive plan was identifiable. Cannot clone.")
return 1
source = None
source_plan_name = None
for plan in candidates:
pid = plan_id(plan)
if pid is None:
continue
detail = (
api.get_adaptive_training_plan_by_id(pid)
if str(plan.get("trainingPlanCategory", "")).upper() == "FBT_ADAPTIVE"
else api.get_training_plan_by_id(pid)
)
if args.dump_json:
out = Path("debug") / f"coach_clone_source_{pid}_{target_date.isoformat()}.json"
dump_redacted_json(out, detail)
print(f"Redacted source JSON written to {out}")
source = best_workout_for_date(detail, target_date)
if source is None:
task_workout = task_workout_for_date(detail, target_date)
workout_uuid = task_workout.get("workoutUuid") if task_workout else None
if workout_uuid:
source = get_fbt_adaptive_workout(api, str(workout_uuid))
if source is not None:
source_plan_name = plan.get("name")
break
if source is None:
print(
"Coach/adaptive plan data was fetched, but no object with workoutSegments "
f"could be matched to {target_date.isoformat()}."
)
print(
"This means the missing piece is Coach step extraction, "
"not normal workout scheduling."
)
return 1
payload = clone_workout_payload(source, target_date, prefix)
print(f"Source plan: {source_plan_name}")
print(summarize_workout(payload))
validation_errors = validate_workout_payload(payload)
if validation_errors:
print("\nConverted clone payload failed local validation. Nothing was uploaded.")
for error in validation_errors:
print(f" - {error}")
return 1
print("\nConverted clone payload passed local validation.")
existing = existing_clone_names(api.get_workouts(limit=100), target_date, prefix)
if existing and not args.force:
print("A clone already exists for this date; skipping upload:")
for name in existing:
print(f" - {name}")
return 0
if args.dry_run or not args.schedule:
print("\nDry run: converted clone JSON follows. Nothing was uploaded.")
print_json(payload)
if not args.schedule:
print("\nPass --schedule to upload and schedule this clone.")
return 0
result = api.upload_workout(payload)
workout_id = result.get("workoutId") or result.get("id")
if workout_id is None:
print("Upload returned no workoutId. Raw response:")
print_json(result)
return 1
schedule_result = api.schedule_workout(workout_id, target_date.isoformat())
print(f"Uploaded clone workout ID: {workout_id}")
print("Schedule response:")
print_json(schedule_result)
print(
f"""
Next checks on the Edge 1030 after Garmin Connect sync:
1. Check Training > Workouts for "{payload["workoutName"]}".
2. Check Training > Training Plan > calendar icon > {target_date.isoformat()}.
3. Start it with a normal route/course active and verify navigation still works.
4. After completing, check whether Garmin Coach marks anything complete; this is not expected yet.
"""
)
return 0
if __name__ == "__main__":
raise SystemExit(main())