Skip to content

Commit 9216784

Browse files
committed
Harden live profile processing fallback
1 parent 0ffc287 commit 9216784

2 files changed

Lines changed: 101 additions & 5 deletions

File tree

chronicle/activity_pipeline.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,8 @@ def _select_activity_profile(
20402040
settings: Settings,
20412041
detailed_activity: dict[str, Any],
20422042
training: dict[str, Any] | None = None,
2043+
*,
2044+
allow_working_profile_fallback: bool = False,
20432045
) -> dict[str, Any]:
20442046
profiles = [
20452047
profile
@@ -2087,7 +2089,7 @@ def _select_activity_profile(
20872089
"working_profile_id": working_profile_id or "default",
20882090
"selection_mode": "criteria_match",
20892091
}
2090-
if working_profile_id and working_profile_id != "default":
2092+
if allow_working_profile_fallback and working_profile_id and working_profile_id != "default":
20912093
return {
20922094
"profile_id": working_profile_id,
20932095
"profile_label": str(working_profile.get("label") or working_profile_id.title()),
@@ -2125,7 +2127,12 @@ def preview_profile_match(
21252127
if not isinstance(activity, dict) or not activity:
21262128
raise ValueError("Template context must include activity data for profile preview.")
21272129

2128-
selected = _select_activity_profile(settings, activity, training=training)
2130+
selected = _select_activity_profile(
2131+
settings,
2132+
activity,
2133+
training=training,
2134+
allow_working_profile_fallback=True,
2135+
)
21292136
profile_id = str(selected.get("profile_id") or "default")
21302137
profile = get_template_profile(settings, profile_id)
21312138
criteria = profile.get("criteria") if isinstance(profile, dict) else {}
@@ -2246,7 +2253,26 @@ def preview_specific_profile_against_activity(
22462253
}
22472254

22482255

2249-
def _profile_activity_update_payload(profile_id: str, detailed_activity: dict[str, Any], description: str) -> dict[str, Any]:
2256+
def _garmin_activity_type_to_strava_type(activity_type: Any) -> str | None:
2257+
normalized = _normalize_activity_type_key(activity_type)
2258+
if normalized in {"run", "running", "trailrun", "trailrunning"}:
2259+
return "Run"
2260+
if normalized in {"walk", "walking", "hike", "hiking"}:
2261+
return "Walk"
2262+
if normalized in {"ride", "cycling", "biking", "roadbiking", "mountainbiking"}:
2263+
return "Ride"
2264+
if normalized in {"strength", "strengthtraining", "weighttraining", "weightlifting", "strengthworkout"}:
2265+
return "Workout"
2266+
return None
2267+
2268+
2269+
def _profile_activity_update_payload(
2270+
profile_id: str,
2271+
detailed_activity: dict[str, Any],
2272+
description: str,
2273+
*,
2274+
training: dict[str, Any] | None = None,
2275+
) -> dict[str, Any]:
22502276
payload: dict[str, Any] = {"description": description}
22512277
if profile_id == "strength_training":
22522278
payload["private"] = True
@@ -2269,6 +2295,23 @@ def _profile_activity_update_payload(profile_id: str, detailed_activity: dict[st
22692295
payload["type"] = "Walk" if is_walk else "Run"
22702296
payload["name"] = "Treadmill Walk" if is_walk else "Treadmill Run"
22712297
payload["trainer"] = True
2298+
return payload
2299+
2300+
if profile_id == "default" and isinstance(training, dict) and bool(training.get("_garmin_activity_aligned")):
2301+
garmin_last = training.get("garmin_last_activity")
2302+
if isinstance(garmin_last, dict):
2303+
current_type_key = _normalize_activity_type_key(detailed_activity.get("sport_type") or detailed_activity.get("type"))
2304+
current_name = str(detailed_activity.get("name") or "").strip()
2305+
garmin_type_key = _normalize_activity_type_key(garmin_last.get("activity_type"))
2306+
garmin_type = _garmin_activity_type_to_strava_type(garmin_last.get("activity_type"))
2307+
garmin_name = str(garmin_last.get("activity_name") or "").strip()
2308+
stale_onewheel_title = current_name == "Onewheel Float 🛹" or "onewheel" in current_name.lower()
2309+
stale_onewheel_type = current_type_key == "ebikeride"
2310+
if (stale_onewheel_title or stale_onewheel_type) and garmin_type_key not in {"", "other"}:
2311+
if garmin_type:
2312+
payload["type"] = garmin_type
2313+
if garmin_name:
2314+
payload["name"] = garmin_name
22722315
return payload
22732316

22742317

@@ -3476,7 +3519,12 @@ def run_once(force_update: bool = False, activity_id: int | None = None) -> dict
34763519
logger.error("Template render failed for profile %s: %s", profile_id, error_text)
34773520
raise RuntimeError(f"Template render failed for profile '{profile_id}': {error_text}")
34783521

3479-
update_payload = _profile_activity_update_payload(profile_id, detailed_activity, description)
3522+
update_payload = _profile_activity_update_payload(
3523+
profile_id,
3524+
detailed_activity,
3525+
description,
3526+
training=training,
3527+
)
34803528
_run_required_call(
34813529
settings,
34823530
"strava.update_activity",

tests/test_activity_pipeline.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,23 @@ def test_onewheel_profile_update_payload_sets_ebike_title(self) -> None:
273273
self.assertEqual(payload.get("type"), "EBikeRide")
274274
self.assertEqual(payload.get("name"), "Onewheel Float 🛹")
275275

276+
def test_default_profile_update_payload_reverts_stale_onewheel_title_and_type_from_garmin(self) -> None:
277+
payload = _profile_activity_update_payload(
278+
"default",
279+
{"id": 123, "sport_type": "EBikeRide", "type": "EBikeRide", "name": "Onewheel Float 🛹"},
280+
"Miles 1.01",
281+
training={
282+
"_garmin_activity_aligned": True,
283+
"garmin_last_activity": {
284+
"activity_type": "running",
285+
"activity_name": "Forsyth County - Zone 2",
286+
},
287+
},
288+
)
289+
self.assertEqual(payload.get("description"), "Miles 1.01")
290+
self.assertEqual(payload.get("type"), "Run")
291+
self.assertEqual(payload.get("name"), "Forsyth County - Zone 2")
292+
276293
def test_strength_profile_matches_workout_sport_type(self) -> None:
277294
settings = SimpleNamespace(
278295
profile_trail_gain_per_mile_ft=220.0,
@@ -721,7 +738,7 @@ def test_profile_selection_skips_disabled_profiles(self) -> None:
721738
self.assertEqual(selected.get("profile_id"), "default")
722739
self.assertEqual(selected.get("reasons"), ["fallback"])
723740

724-
def test_profile_selection_falls_back_to_working_profile_before_default(self) -> None:
741+
def test_profile_selection_defaults_without_working_profile_fallback_for_processing(self) -> None:
725742
settings = SimpleNamespace(
726743
profile_trail_gain_per_mile_ft=220.0,
727744
profile_long_run_miles=10.0,
@@ -747,6 +764,37 @@ def test_profile_selection_falls_back_to_working_profile_before_default(self) ->
747764
):
748765
selected = _select_activity_profile(settings, activity)
749766

767+
self.assertEqual(selected.get("profile_id"), "default")
768+
self.assertEqual(selected.get("reasons"), ["fallback"])
769+
self.assertEqual(selected.get("working_profile_id"), "trail")
770+
self.assertEqual(selected.get("selection_mode"), "default_fallback")
771+
772+
def test_profile_selection_can_fall_back_to_working_profile_for_preview(self) -> None:
773+
settings = SimpleNamespace(
774+
profile_trail_gain_per_mile_ft=220.0,
775+
profile_long_run_miles=10.0,
776+
home_latitude=None,
777+
home_longitude=None,
778+
home_radius_miles=10.0,
779+
)
780+
profiles = [
781+
{"profile_id": "default", "label": "Default", "enabled": True, "priority": 0},
782+
{"profile_id": "trail", "label": "Trail", "enabled": True, "priority": 70},
783+
]
784+
activity = {
785+
"sport_type": "Run",
786+
"type": "Run",
787+
"name": "Neighborhood easy run",
788+
"distance": 4000.0,
789+
"elev_gain": 50.0,
790+
}
791+
792+
with patch("chronicle.activity_pipeline.list_template_profiles", return_value=profiles), patch(
793+
"chronicle.activity_pipeline.get_working_template_profile",
794+
return_value={"profile_id": "trail", "label": "Trail", "enabled": True},
795+
):
796+
selected = _select_activity_profile(settings, activity, allow_working_profile_fallback=True)
797+
750798
self.assertEqual(selected.get("profile_id"), "trail")
751799
self.assertEqual(selected.get("reasons"), ["working_profile_fallback"])
752800
self.assertEqual(selected.get("working_profile_id"), "trail")

0 commit comments

Comments
 (0)