From 76e1a829c83f2a5809dc11e1a76938dd506e428b Mon Sep 17 00:00:00 2001 From: Betina Benaduce Date: Wed, 28 Oct 2026 14:14:10 -0300 Subject: [PATCH 1/2] fix: add retry strategy --- src/sap_cloud_sdk/agentgateway/_lob.py | 78 +++++++++++++------ tests/agentgateway/unit/test_lob.py | 103 +++++++++++++++++++++++++ uv.lock | 48 ++++++------ 3 files changed, 181 insertions(+), 48 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 78e4e34c..2072bd83 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -8,6 +8,7 @@ import asyncio import logging import os +import time import uuid import httpx @@ -62,6 +63,9 @@ _DESTINATION_INSTANCE = "default" +_FETCH_AUTH_MAX_ATTEMPTS = 3 +_FETCH_AUTH_BACKOFF_BASE = 1.0 # seconds; actual delays: 1s, 2s + def _system_scope_key(tenant_subdomain: str) -> str: """Build the cache scope key for tenant-scoped system auth.""" @@ -101,6 +105,11 @@ def _fetch_auth_token( destination service (e.g. strips the "Bearer " prefix from "Bearer "), and the gateway URL from the destination's URL property. + Retries up to _FETCH_AUTH_MAX_ATTEMPTS times with exponential backoff on + transient failures (OAuth2 token endpoint blips, unparseable destination + responses). MCPServerNotFoundError is not retried — it indicates a genuine + missing resource. + Args: dest_name: Destination name. tenant_subdomain: Tenant subdomain for multi-tenant lookup. @@ -110,35 +119,56 @@ def _fetch_auth_token( Tuple of (raw_jwt, gateway_url). Raises: - MCPServerNotFoundError: If no auth token is returned. + MCPServerNotFoundError: If no auth token is returned (not retried). + Exception: Last exception raised after all retry attempts are exhausted. """ - client = create_destination_client( - instance=_DESTINATION_INSTANCE, - _telemetry_source=Module.AGENTGATEWAY, - ) - dest = client.get_destination( - dest_name, - level=ConsumptionLevel.PROVIDER_SUBACCOUNT, - options=options, - tenant=tenant_subdomain, - ) - - if not dest or not isinstance(dest, Destination) or not dest.auth_tokens: - raise MCPServerNotFoundError( - f"No auth token returned for destination '{dest_name}'" - ) + last_exc: Exception | None = None + for attempt in range(_FETCH_AUTH_MAX_ATTEMPTS): + try: + client = create_destination_client( + instance=_DESTINATION_INSTANCE, + _telemetry_source=Module.AGENTGATEWAY, + ) + dest = client.get_destination( + dest_name, + level=ConsumptionLevel.PROVIDER_SUBACCOUNT, + options=options, + tenant=tenant_subdomain, + ) - auth_token = dest.auth_tokens[0] - header_value = auth_token.http_header.get("value") or "" - if not header_value: - raise MCPServerNotFoundError(f"Empty auth header for destination '{dest_name}'") + if not dest or not isinstance(dest, Destination) or not dest.auth_tokens: + raise MCPServerNotFoundError( + f"No auth token returned for destination '{dest_name}'" + ) - # Strip "Bearer " prefix — AuthResult.access_token is always a raw JWT - raw_token = header_value.removeprefix("Bearer ").strip() + auth_token = dest.auth_tokens[0] + header_value = auth_token.http_header.get("value") or "" + if not header_value: + raise MCPServerNotFoundError( + f"Empty auth header for destination '{dest_name}'" + ) - gateway_url = (dest.url or "").rstrip("/") + # Strip "Bearer " prefix — AuthResult.access_token is always a raw JWT + raw_token = header_value.removeprefix("Bearer ").strip() + gateway_url = (dest.url or "").rstrip("/") + return raw_token, gateway_url + + except MCPServerNotFoundError: + raise + except Exception as exc: + last_exc = exc + if attempt < _FETCH_AUTH_MAX_ATTEMPTS - 1: + delay = _FETCH_AUTH_BACKOFF_BASE * (2**attempt) + logger.warning( + "Auth token fetch failed (attempt %d/%d): %s — retrying in %.1fs", + attempt + 1, + _FETCH_AUTH_MAX_ATTEMPTS, + exc, + delay, + ) + time.sleep(delay) - return raw_token, gateway_url + raise last_exc # type: ignore[misc] def get_ias_client_id_lob() -> str: diff --git a/tests/agentgateway/unit/test_lob.py b/tests/agentgateway/unit/test_lob.py index 7e6a2345..28ce61d3 100644 --- a/tests/agentgateway/unit/test_lob.py +++ b/tests/agentgateway/unit/test_lob.py @@ -195,6 +195,109 @@ def test_passes_options_to_destination(self): ) +# ============================================================ +# Test: _fetch_auth_token retry behaviour +# ============================================================ + + +class TestFetchAuthTokenRetry: + """Tests for retry logic in _fetch_auth_token.""" + + def _make_dest(self, token="Bearer jwt", url="https://agw.example.com"): + dest = MagicMock(spec=Destination) + dest.auth_tokens = [MagicMock()] + dest.auth_tokens[0].http_header = {"value": token} + dest.url = url + return dest + + def test_succeeds_on_first_attempt(self): + """Return result immediately when the first attempt succeeds.""" + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep") as mock_sleep: + mock_client.return_value.get_destination.return_value = self._make_dest() + + result = _fetch_auth_token("dest", "tenant") + + assert result == ("jwt", "https://agw.example.com") + mock_sleep.assert_not_called() + + def test_retries_on_transient_error_then_succeeds(self): + """Retry after a transient RuntimeError and return result on second attempt.""" + dest = self._make_dest() + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep") as mock_sleep: + mock_client.return_value.get_destination.side_effect = [ + RuntimeError("Failed to obtain OAuth2 token: (missing_token)"), + dest, + ] + + result = _fetch_auth_token("dest", "tenant") + + assert result == ("jwt", "https://agw.example.com") + mock_sleep.assert_called_once_with(1.0) + + def test_retries_on_destination_parse_error_then_succeeds(self): + """Retry after a destination parse error and return result on third attempt.""" + dest = self._make_dest() + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep") as mock_sleep: + mock_client.return_value.get_destination.side_effect = [ + Exception("failed to parse consume destination response: ..."), + Exception("invalid JSON in list fragments response: ..."), + dest, + ] + + result = _fetch_auth_token("dest", "tenant") + + assert result == ("jwt", "https://agw.example.com") + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(1.0) + mock_sleep.assert_any_call(2.0) + + def test_raises_after_all_attempts_exhausted(self): + """Re-raise the last exception after all retry attempts fail.""" + err = RuntimeError("Failed to obtain OAuth2 token: (missing_token)") + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep"): + mock_client.return_value.get_destination.side_effect = err + + with pytest.raises(RuntimeError, match="Failed to obtain OAuth2 token"): + _fetch_auth_token("dest", "tenant") + + assert mock_client.return_value.get_destination.call_count == 3 + + def test_mcp_server_not_found_not_retried(self): + """MCPServerNotFoundError is not retried — it is a genuine missing resource.""" + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep") as mock_sleep: + mock_client.return_value.get_destination.return_value = None + + with pytest.raises(MCPServerNotFoundError): + _fetch_auth_token("dest", "tenant") + + mock_client.return_value.get_destination.call_count == 1 + mock_sleep.assert_not_called() + + def test_backoff_delays_are_exponential(self): + """Delays follow base * 2^attempt: 1.0s then 2.0s.""" + err = Exception("transient") + dest = self._make_dest() + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client, patch("sap_cloud_sdk.agentgateway._lob.time.sleep") as mock_sleep: + mock_client.return_value.get_destination.side_effect = [err, err, dest] + + _fetch_auth_token("dest", "tenant") + + delays = [call.args[0] for call in mock_sleep.call_args_list] + assert delays == [1.0, 2.0] + + # ============================================================ # Test: list_mcp_fragments # ============================================================ diff --git a/uv.lock b/uv.lock index 3be64207..0d893200 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -4313,27 +4313,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, - { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, - { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, - { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, - { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, - { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, - { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, - { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, - { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, - { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, ] [[package]] @@ -4475,7 +4475,7 @@ requires-dist = [ { name = "python-odata", specifier = ">=0.7.0" }, { name = "requests", specifier = ">=2.33.0,<3" }, { name = "requests-oauthlib", specifier = ">=2.0.0,<3" }, - { name = "setuptools", specifier = ">=83.0,<84" }, + { name = "setuptools", specifier = ">=83.0,<85" }, { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=2.0.0,<3" }, { name = "starlette", marker = "extra == 'starlette'", specifier = ">=0.40.0,<1" }, { name = "traceloop-sdk", specifier = "~=0.61.0" }, @@ -4504,7 +4504,7 @@ dev = [ { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "responses", specifier = ">=0.25" }, - { name = "ruff", specifier = "==0.16.0" }, + { name = "ruff", specifier = "==0.17.1" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "starlette", specifier = ">=0.40.0" }, { name = "ty", specifier = "==0.0.64" }, From 738b2dfc35fc9aca62d69f9b348aa571e11d2293 Mon Sep 17 00:00:00 2001 From: Betina Benaduce Date: Wed, 28 Oct 2026 17:42:03 -0300 Subject: [PATCH 2/2] refactor: implement PR suggestions --- tests/agentgateway/unit/test_lob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/agentgateway/unit/test_lob.py b/tests/agentgateway/unit/test_lob.py index 28ce61d3..0c5a29f5 100644 --- a/tests/agentgateway/unit/test_lob.py +++ b/tests/agentgateway/unit/test_lob.py @@ -280,7 +280,7 @@ def test_mcp_server_not_found_not_retried(self): with pytest.raises(MCPServerNotFoundError): _fetch_auth_token("dest", "tenant") - mock_client.return_value.get_destination.call_count == 1 + assert mock_client.return_value.get_destination.call_count == 1 mock_sleep.assert_not_called() def test_backoff_delays_are_exponential(self):