Created
April 28, 2026 18:12
-
-
Save Bastian-Kuhn/2e668fd4af3c5986847a4e90b8adebb1 to your computer and use it in GitHub Desktop.
Show last 7 Days of Workout from Hevy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Read Hevy workouts from the last 7 days with full details.""" | |
| import sys | |
| from datetime import datetime, timedelta, timezone | |
| import requests | |
| API_KEY = "xxxx" | |
| BASE_URL = "https://api.hevyapp.com/v1" | |
| HEADERS = {"api-key": API_KEY, "accept": "application/json"} | |
| def fetch_workouts_since(since: datetime) -> list[dict]: | |
| workouts: list[dict] = [] | |
| page = 1 | |
| while True: | |
| resp = requests.get( | |
| f"{BASE_URL}/workouts", | |
| headers=HEADERS, | |
| params={"page": page, "pageSize": 10}, | |
| timeout=30, | |
| ) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| batch = data.get("workouts", []) | |
| if not batch: | |
| break | |
| stop = False | |
| for w in batch: | |
| start = datetime.fromisoformat(w["start_time"].replace("Z", "+00:00")) | |
| if start < since: | |
| stop = True | |
| break | |
| workouts.append(w) | |
| if stop or page >= data.get("page_count", page): | |
| break | |
| page += 1 | |
| return workouts | |
| def format_duration(start_iso: str, end_iso: str) -> str: | |
| start = datetime.fromisoformat(start_iso.replace("Z", "+00:00")) | |
| end = datetime.fromisoformat(end_iso.replace("Z", "+00:00")) | |
| minutes = int((end - start).total_seconds() // 60) | |
| h, m = divmod(minutes, 60) | |
| return f"{h}h {m}m" if h else f"{m}m" | |
| def print_workout(w: dict) -> None: | |
| start = datetime.fromisoformat(w["start_time"].replace("Z", "+00:00")) | |
| print("=" * 70) | |
| print(f"{w['title']}") | |
| print(f" Date: {start.astimezone().strftime('%Y-%m-%d %H:%M')}") | |
| print(f" Duration: {format_duration(w['start_time'], w['end_time'])}") | |
| if w.get("description"): | |
| print(f" Note: {w['description']}") | |
| print(f" Exercises: {len(w.get('exercises', []))}") | |
| print() | |
| for ex in w.get("exercises", []): | |
| print(f" • {ex.get('title', 'Unknown')}") | |
| if ex.get("notes"): | |
| print(f" Note: {ex['notes']}") | |
| for i, s in enumerate(ex.get("sets", []), 1): | |
| parts = [f"Set {i}"] | |
| if s.get("weight_kg") is not None: | |
| parts.append(f"{s['weight_kg']} kg") | |
| if s.get("reps") is not None: | |
| parts.append(f"{s['reps']} reps") | |
| if s.get("duration_seconds"): | |
| parts.append(f"{s['duration_seconds']}s") | |
| if s.get("distance_meters"): | |
| parts.append(f"{s['distance_meters']}m") | |
| if s.get("rpe") is not None: | |
| parts.append(f"RPE {s['rpe']}") | |
| if s.get("type") and s["type"] != "normal": | |
| parts.append(f"[{s['type']}]") | |
| print(" " + " · ".join(parts)) | |
| print() | |
| def main() -> int: | |
| since = datetime.now(timezone.utc) - timedelta(days=7) | |
| try: | |
| workouts = fetch_workouts_since(since) | |
| except requests.HTTPError as e: | |
| print(f"API error: {e}", file=sys.stderr) | |
| return 1 | |
| if not workouts: | |
| print("No workouts in the last 7 days.") | |
| return 0 | |
| print(f"{len(workouts)} workout(s) in the last 7 days since {since.astimezone().strftime('%Y-%m-%d %H:%M')}:\n") | |
| for w in sorted(workouts, key=lambda x: x["start_time"]): | |
| print_workout(w) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment