Skip to content

Instantly share code, notes, and snippets.

@JamesGardiner
Created March 16, 2026 15:26
Show Gist options
  • Select an option

  • Save JamesGardiner/bcf6d6b626d911f1f0a86d19fc8f097c to your computer and use it in GitHub Desktop.

Select an option

Save JamesGardiner/bcf6d6b626d911f1f0a86d19fc8f097c to your computer and use it in GitHub Desktop.
Extract stats from a GPX file for Mountain Training QMD logging (uv script)
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["gpxpy"]
# ///
"""Extract stats from a GPX file for Mountain Training QMD logging."""
import argparse
import sys
from pathlib import Path
import gpxpy
def main():
parser = argparse.ArgumentParser(
description="Extract stats from a GPX file for QMD logging"
)
parser.add_argument("gpx_file", type=Path, help="Path to GPX file")
args = parser.parse_args()
if not args.gpx_file.exists():
print(f"Error: {args.gpx_file} not found", file=sys.stderr)
sys.exit(1)
with open(args.gpx_file) as f:
gpx = gpxpy.parse(f)
bounds = gpx.get_bounds()
time_bounds = gpx.get_time_bounds()
moving = gpx.get_moving_data()
uphill, downhill = gpx.get_uphill_downhill()
ele_extremes = gpx.get_elevation_extremes()
# Activity name
name = gpx.tracks[0].name if gpx.tracks and gpx.tracks[0].name else "Unknown"
# Duration
duration = gpx.get_duration()
if duration:
hours = int(duration // 3600)
mins = int((duration % 3600) // 60)
duration_str = f"{hours}h {mins}m"
else:
duration_str = "Unknown"
# Moving time
if moving and moving.moving_time:
mh = int(moving.moving_time // 3600)
mm = int((moving.moving_time % 3600) // 60)
moving_str = f"{mh}h {mm}m"
else:
moving_str = "Unknown"
# Distance
length_2d = gpx.length_2d() / 1000
length_3d = gpx.length_3d() / 1000
# Start point
start_lat = bounds.min_latitude if bounds else None
start_lon = bounds.min_longitude if bounds else None
first_point = None
for track in gpx.tracks:
for seg in track.segments:
if seg.points:
first_point = seg.points[0]
break
if first_point:
break
print(f"Activity: {name}")
if time_bounds.start_time:
print(f"Date: {time_bounds.start_time.strftime('%Y-%m-%d')}")
print(f"Start time: {time_bounds.start_time.strftime('%H:%M:%S %Z')}")
if time_bounds.end_time:
print(f"End time: {time_bounds.end_time.strftime('%H:%M:%S %Z')}")
print(f"Duration: {duration_str}")
print(f"Moving time: {moving_str}")
print(f"Distance (2D): {length_2d:.1f} km")
print(f"Distance (3D): {length_3d:.1f} km")
if ele_extremes:
print(f"Max elevation: {ele_extremes.maximum:.0f} m")
print(f"Min elevation: {ele_extremes.minimum:.0f} m")
print(f"Elevation gain: {uphill:.0f} m (GPX - noisy, use Garmin corrected value)")
print(f"Elevation loss: {downhill:.0f} m (GPX - noisy, use Garmin corrected value)")
if first_point:
print(f"Start coords: {first_point.latitude:.5f}, {first_point.longitude:.5f}")
print()
# QMD criteria checks
print("--- QMD Criteria Pre-fill ---")
if ele_extremes and ele_extremes.maximum >= 600:
print(f"Mountain ascended: YES (max elevation {ele_extremes.maximum:.0f} m)")
else:
max_e = ele_extremes.maximum if ele_extremes else 0
print(f"Mountain ascended: NO (max elevation {max_e:.0f} m, need >= 600 m)")
if duration and duration >= 5 * 3600:
print(f"Five hours+: YES ({duration_str})")
elif duration:
print(f"Five hours+: NO ({duration_str}, need >= 5h)")
else:
print("Five hours+: UNKNOWN")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment