111 lines
3.6 KiB
Python
Executable File
111 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from garmin_coach_clone.coach import (
|
|
best_workout_for_date,
|
|
find_date_matches,
|
|
find_workout_like_objects,
|
|
)
|
|
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,
|
|
summarize_workout,
|
|
validate_workout_payload,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Analyze saved Garmin JSON without logging in.")
|
|
parser.add_argument("json_file", type=Path, help="Path to a saved JSON dump.")
|
|
parser.add_argument("--date", default="today", help="today, tomorrow, or YYYY-MM-DD")
|
|
parser.add_argument(
|
|
"--clone-dry-run",
|
|
action="store_true",
|
|
help="Build a local clone payload from the matched workout.",
|
|
)
|
|
parser.add_argument("--prefix", default="GCClone", help="Clone workout name prefix.")
|
|
parser.add_argument(
|
|
"--dump-json", action="store_true", help="Write analysis JSON under debug/."
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
target_date = parse_date(args.date)
|
|
data = _load_json(args.json_file)
|
|
date_matches = find_date_matches(data, target_date)
|
|
workout_like = find_workout_like_objects(data)
|
|
matched = best_workout_for_date(data, target_date)
|
|
|
|
analysis: dict[str, Any] = {
|
|
"source": str(args.json_file),
|
|
"target_date": target_date.isoformat(),
|
|
"date_match_count": len(date_matches),
|
|
"workout_like_count": len(workout_like),
|
|
"matched_workout": matched is not None,
|
|
}
|
|
|
|
print(f"Source: {args.json_file}")
|
|
print(f"Target date: {target_date.isoformat()}")
|
|
print(f"Date-containing objects: {len(date_matches)}")
|
|
print(f"Workout-like objects with workoutSegments/workoutSteps: {len(workout_like)}")
|
|
|
|
if matched is None:
|
|
print("No dated workout-like object was found.")
|
|
if args.dump_json:
|
|
out = _analysis_path(args.json_file)
|
|
dump_redacted_json(out, analysis)
|
|
print(f"Analysis JSON written to {out}")
|
|
return 1
|
|
|
|
print("\nMatched workout summary:")
|
|
print(summarize_workout(matched))
|
|
|
|
if args.clone_dry_run:
|
|
payload = clone_workout_payload(matched, target_date, args.prefix)
|
|
errors = validate_workout_payload(payload)
|
|
analysis["clone_validation_errors"] = errors
|
|
analysis["clone_payload"] = payload
|
|
if errors:
|
|
print("\nLocal clone payload failed validation:")
|
|
for error in errors:
|
|
print(f" - {error}")
|
|
return_code = 1
|
|
else:
|
|
print("\nLocal clone payload passed validation.")
|
|
print_json(payload)
|
|
return_code = 0
|
|
else:
|
|
return_code = 0
|
|
|
|
if args.dump_json:
|
|
out = _analysis_path(args.json_file)
|
|
dump_redacted_json(out, analysis)
|
|
print(f"Analysis JSON written to {out}")
|
|
return return_code
|
|
|
|
|
|
def _load_json(path: Path) -> Any:
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
raise SystemExit(f"File not found: {path}") from None
|
|
except json.JSONDecodeError as exc:
|
|
raise SystemExit(f"Invalid JSON in {path}: {exc}") from None
|
|
|
|
|
|
def _analysis_path(source: Path) -> Path:
|
|
stem = source.stem.replace(".", "_")
|
|
return Path("debug") / f"analysis_{stem}.json"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|