feat(garmin): mark a wrapper 404 with not_found in the error response

garminconnect's own GarminConnectNotFoundError already exists
specifically for this (its docstring: "so callers can now catch a
missing resource specifically, e.g. deleting an already-deleted
workout"), raised by connectapi() for any real HTTP 404. dispatch()
now surfaces that distinction as an extra not_found: true field
alongside the existing error string, so internal/garmin/client.go
(next commit) can tell a definitive 404 apart from a transient
failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 18:50:43 +02:00
parent b5cd47c219
commit 1d878e0f46
2 changed files with 32 additions and 2 deletions

View File

@@ -269,3 +269,25 @@ def test_authenticate_prevents_later_lazy_startup_login():
wrapper.dispatch({"id": 34, "cmd": "call", "params": {"method": "x", "args": {}}}) wrapper.dispatch({"id": 34, "cmd": "call", "params": {"method": "x", "args": {}}})
mock_startup.assert_not_called() mock_startup.assert_not_called()
def test_call_marks_not_found_error_specifically():
from garminconnect import GarminConnectNotFoundError
wrapper._auth_state = "authenticated"
wrapper._client = MagicMock()
wrapper._client.get_workout_by_id.side_effect = GarminConnectNotFoundError("API Error 404")
resp = wrapper.dispatch({
"id": 11, "cmd": "call", "params": {"method": "get_workout_by_id", "args": {"workout_id": "999"}},
})
assert resp == {"id": 11, "error": "API Error 404", "not_found": True}
def test_call_does_not_mark_other_errors_as_not_found():
wrapper._auth_state = "authenticated"
wrapper._client = MagicMock()
wrapper._client.get_workout_by_id.side_effect = Exception("rate limited")
resp = wrapper.dispatch({
"id": 12, "cmd": "call", "params": {"method": "get_workout_by_id", "args": {"workout_id": "999"}},
})
assert resp == {"id": 12, "error": "rate limited"}

View File

@@ -9,7 +9,7 @@ import sys
import threading import threading
import traceback import traceback
from garminconnect import Garmin from garminconnect import Garmin, GarminConnectNotFoundError
TOKENSTORE = os.path.expanduser(os.environ.get("GARMIN_TOKENSTORE", "~/.garmin")) TOKENSTORE = os.path.expanduser(os.environ.get("GARMIN_TOKENSTORE", "~/.garmin"))
@@ -208,7 +208,15 @@ def dispatch(req):
except Exception as exc: except Exception as exc:
_debug(f"{req.get('cmd')} raised {type(exc).__name__}: {exc}") _debug(f"{req.get('cmd')} raised {type(exc).__name__}: {exc}")
_debug(traceback.format_exc()) _debug(traceback.format_exc())
return {"id": req.get("id"), "error": str(exc)} resp = {"id": req.get("id"), "error": str(exc)}
# A 404 (e.g. get_workout_by_id for a workout deleted on Garmin's
# side after being linked to an activity) is definitive, not a
# transient failure worth retrying forever -- marked specifically so
# internal/garmin/client.go can tell the two apart (see
# docs/superpowers/specs/2026-07-27-workout-not-found-design.md).
if isinstance(exc, GarminConnectNotFoundError):
resp["not_found"] = True
return resp
def main(): def main():