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:
@@ -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"}
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user