feat: add speed and pace formatting utility and include in activity summary table
CI/CD Pipeline / Lint & Check (push) Successful in 10s
CI/CD Pipeline / Publish to PyPI (push) Has been skipped
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m30s

This commit is contained in:
2026-05-30 11:52:35 +02:00
parent e7ec16abe5
commit b8c697fed3
3 changed files with 77 additions and 3 deletions
+8 -3
View File
@@ -8,6 +8,7 @@ from strava_mcp_server.utils import (
format_date_human,
format_duration_markdown,
get_sport_emoji,
format_speed_or_pace,
)
@@ -72,6 +73,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"moving_time": f"{a.get('moving_time', 0) / 60:.1f} min",
"total_elevation_gain": f"{a.get('total_elevation_gain', 0):.0f} m",
"average_heartrate": a.get("average_heartrate"),
"average_speed": a.get("average_speed"),
"gear_id": a.get("gear_id"),
}
)
@@ -82,8 +84,8 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
)
else:
markdown_summary = f"### 🚴 Aktivitäten (Seite {page})\n"
markdown_summary += "| 📅 Datum | 🏃 Icon | 🏷️ Name | 📐 Distanz | ⏱️ Zeit | 🏔️ Höhenmeter | ❤️ Ø HR |\n"
markdown_summary += "|----------|--------|--------|-----------|--------|--------------|--------|\n"
markdown_summary += "| 📅 Datum | 🏷️ Name | 📐 Distanz | ⏱️ Zeit | ⚡ Ø Pace/Geschw. | 🏔️ Höhenmeter | ❤️ Ø HR |\n"
markdown_summary += "|----------|--------|-----------|--------|------------------|--------------|--------|\n"
for a, raw_act in zip(essential_data, activities):
hr = (
f"{a['average_heartrate']:.0f} bpm"
@@ -92,7 +94,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
)
time_md = format_duration_markdown(raw_act.get("moving_time"))
sport_icon = get_sport_emoji(a["sport_type"])
markdown_summary += f"| {a['start_date_local']} | {sport_icon} | {a['name']} | {a['distance']} | {time_md} | {a['total_elevation_gain']} | {hr} |\n"
speed_or_pace = format_speed_or_pace(
raw_act.get("average_speed"), a["sport_type"]
)
markdown_summary += f"| {a['start_date_local']} | {sport_icon} {a['name']} | {a['distance']} | {time_md} | {speed_or_pace} | {a['total_elevation_gain']} | {hr} |\n"
return [
_user_text(markdown_summary.strip()),
+38
View File
@@ -161,3 +161,41 @@ def get_sport_emoji(sport_type: Optional[str]) -> str:
return "🛹"
else:
return "🏃"
def format_speed_or_pace(
average_speed_mps: Optional[float | int], sport_type: Optional[str]
) -> str:
"""
Formats speed/pace based on sport type.
- Runs, walks, hikes -> Pace (min/km, e.g. '5:30/km')
- Cycling and other sports -> Speed (km/h, e.g. '25.4 km/h')
"""
if not average_speed_mps or float(average_speed_mps) <= 0:
return "-"
try:
mps = float(average_speed_mps)
sport_lower = (sport_type or "").lower()
# Runs, walks, hikes get pace
if "run" in sport_lower or "walk" in sport_lower or "hike" in sport_lower:
pace_seconds_per_km = 1000 / mps
pace_minutes = int(pace_seconds_per_km // 60)
pace_seconds = int(round(pace_seconds_per_km % 60))
if pace_seconds == 60:
pace_minutes += 1
pace_seconds = 0
# Guard against crazy paces (e.g. standing still)
if pace_minutes > 59:
return "-"
return f"{pace_minutes}:{pace_seconds:02d}/km"
else:
# Cycling and other sports get speed in km/h
speed_kph = mps * 3.6
return f"{speed_kph:.1f} km/h"
except Exception:
return "-"
+31
View File
@@ -10,6 +10,7 @@ from strava_mcp_server.utils import (
format_duration_table,
format_duration_markdown,
get_sport_emoji,
format_speed_or_pace,
)
@@ -223,3 +224,33 @@ class TestGetSportEmoji:
def test_unknown_returns_default(self):
assert get_sport_emoji("SomeOddSport") == "🏃"
class TestFormatSpeedOrPace:
def test_invalid_input_returns_hyphen(self):
assert format_speed_or_pace(None, "Run") == "-"
assert format_speed_or_pace(0, "Run") == "-"
assert format_speed_or_pace(-1, "Run") == "-"
def test_running_pace_exact(self):
# 3.0303 m/s = ~5:30 min/km
# 1000 / 3.0303 = 330 seconds = 5m 30s
assert format_speed_or_pace(3.0303, "Run") == "5:30/km"
def test_running_pace_rounds_correctly(self):
# 3.0211 m/s = 330.99 seconds -> rounds to 331s -> 5:31/km
assert format_speed_or_pace(3.0211, "VirtualRun") == "5:31/km"
def test_cycling_speed(self):
# 10 m/s = 36 km/h
assert format_speed_or_pace(10, "Ride") == "36.0 km/h"
# 7.5 m/s = 27 km/h
assert format_speed_or_pace(7.5, "MountainBikeRide") == "27.0 km/h"
def test_hike_gets_pace(self):
# 2 m/s = 500s/km = 8:20/km
assert format_speed_or_pace(2, "Hike") == "8:20/km"
def test_other_sports_get_speed(self):
# 10 m/s = 36 km/h
assert format_speed_or_pace(10, "Swim") == "36.0 km/h"