diff --git a/libs/sdk-py/langgraph_sdk/_async/cron.py b/libs/sdk-py/langgraph_sdk/_async/cron.py index 8ffd73a32..db64a44af 100644 --- a/libs/sdk-py/langgraph_sdk/_async/cron.py +++ b/libs/sdk-py/langgraph_sdk/_async/cron.py @@ -18,6 +18,7 @@ from langgraph_sdk.schema import ( CronSortBy, Durability, Input, + Json, OnCompletionBehavior, QueryParamTypes, Run, @@ -413,6 +414,7 @@ class CronClient: assistant_id: str | None = None, thread_id: str | None = None, enabled: bool | None = None, + metadata: Json = None, limit: int = 10, offset: int = 0, sort_by: CronSortBy | None = None, @@ -427,6 +429,8 @@ class CronClient: assistant_id: The assistant ID or graph name to search for. thread_id: the thread ID to search for. enabled: The enabled status to search for. + metadata: Metadata to filter by. Exact match filter for each KV pair. + !!! version-added "Added in Agent Server version 0.9.0" limit: The maximum number of results to return. offset: The number of results to skip. headers: Optional custom headers to include with the request. @@ -481,6 +485,8 @@ class CronClient: "limit": limit, "offset": offset, } + if metadata: + payload["metadata"] = metadata if sort_by: payload["sort_by"] = sort_by if sort_order: @@ -497,6 +503,7 @@ class CronClient: *, assistant_id: str | None = None, thread_id: str | None = None, + metadata: Json = None, headers: Mapping[str, str] | None = None, params: QueryParamTypes | None = None, ) -> int: @@ -505,6 +512,8 @@ class CronClient: Args: assistant_id: Assistant ID to filter by. thread_id: Thread ID to filter by. + metadata: Metadata to filter by. Exact match filter for each KV pair. + !!! version-added "Added in Agent Server version 0.9.0" headers: Optional custom headers to include with the request. params: Optional query parameters to include with the request. @@ -516,6 +525,8 @@ class CronClient: payload["assistant_id"] = assistant_id if thread_id: payload["thread_id"] = thread_id + if metadata: + payload["metadata"] = metadata return await self.http.post( "/runs/crons/count", json=payload, headers=headers, params=params ) diff --git a/libs/sdk-py/langgraph_sdk/_sync/cron.py b/libs/sdk-py/langgraph_sdk/_sync/cron.py index f4a66d435..51310f0e9 100644 --- a/libs/sdk-py/langgraph_sdk/_sync/cron.py +++ b/libs/sdk-py/langgraph_sdk/_sync/cron.py @@ -18,6 +18,7 @@ from langgraph_sdk.schema import ( CronSortBy, Durability, Input, + Json, OnCompletionBehavior, QueryParamTypes, Run, @@ -402,6 +403,7 @@ class SyncCronClient: assistant_id: str | None = None, thread_id: str | None = None, enabled: bool | None = None, + metadata: Json = None, limit: int = 10, offset: int = 0, sort_by: CronSortBy | None = None, @@ -416,6 +418,8 @@ class SyncCronClient: assistant_id: The assistant ID or graph name to search for. thread_id: the thread ID to search for. enabled: Whether the cron job is enabled. + metadata: Metadata to filter by. Exact match filter for each KV pair. + !!! version-added "Added in Agent Server version 0.9.0" limit: The maximum number of results to return. offset: The number of results to skip. headers: Optional custom headers to include with the request. @@ -468,6 +472,8 @@ class SyncCronClient: "limit": limit, "offset": offset, } + if metadata: + payload["metadata"] = metadata if sort_by: payload["sort_by"] = sort_by if sort_order: @@ -484,6 +490,7 @@ class SyncCronClient: *, assistant_id: str | None = None, thread_id: str | None = None, + metadata: Json = None, headers: Mapping[str, str] | None = None, params: QueryParamTypes | None = None, ) -> int: @@ -492,6 +499,8 @@ class SyncCronClient: Args: assistant_id: Assistant ID to filter by. thread_id: Thread ID to filter by. + metadata: Metadata to filter by. Exact match filter for each KV pair. + !!! version-added "Added in Agent Server version 0.9.0" headers: Optional custom headers to include with the request. params: Optional query parameters to include with the request. @@ -503,6 +512,8 @@ class SyncCronClient: payload["assistant_id"] = assistant_id if thread_id: payload["thread_id"] = thread_id + if metadata: + payload["metadata"] = metadata return self.http.post( "/runs/crons/count", json=payload, headers=headers, params=params ) diff --git a/libs/sdk-py/tests/test_crons_client.py b/libs/sdk-py/tests/test_crons_client.py index ea2f2a269..122dda638 100644 --- a/libs/sdk-py/tests/test_crons_client.py +++ b/libs/sdk-py/tests/test_crons_client.py @@ -485,3 +485,165 @@ def test_sync_update_with_enabled_parameter(enabled_value): ) assert result == cron + + +@pytest.mark.asyncio +async def test_async_search_with_metadata(): + """Test that CronClient.search forwards metadata in the request body.""" + cron = _cron_response() + + async def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/runs/crons/search" + + body = json.loads(request.content) + assert body["metadata"] == {"owner": "alice"} + assert body["limit"] == 10 + assert body["offset"] == 0 + + return httpx.Response(200, json=[cron]) + + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, base_url="https://example.com" + ) as client: + http_client = HttpClient(client) + cron_client = CronClient(http_client) + result = await cron_client.search(metadata={"owner": "alice"}) + + assert result == [cron] + + +@pytest.mark.asyncio +async def test_async_search_omits_empty_metadata(): + """Test that CronClient.search does not send metadata when not provided.""" + cron = _cron_response() + + async def handler(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + assert "metadata" not in body + return httpx.Response(200, json=[cron]) + + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, base_url="https://example.com" + ) as client: + http_client = HttpClient(client) + cron_client = CronClient(http_client) + await cron_client.search() + + +@pytest.mark.asyncio +async def test_async_count_with_metadata(): + """Test that CronClient.count forwards metadata in the request body.""" + + async def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/runs/crons/count" + + body = json.loads(request.content) + assert body["metadata"] == {"team": "infra"} + + return httpx.Response(200, json=2) + + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, base_url="https://example.com" + ) as client: + http_client = HttpClient(client) + cron_client = CronClient(http_client) + result = await cron_client.count(metadata={"team": "infra"}) + + assert result == 2 + + +@pytest.mark.asyncio +async def test_async_count_omits_empty_metadata(): + """Test that CronClient.count does not send metadata when not provided.""" + + async def handler(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + assert "metadata" not in body + return httpx.Response(200, json=0) + + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient( + transport=transport, base_url="https://example.com" + ) as client: + http_client = HttpClient(client) + cron_client = CronClient(http_client) + await cron_client.count() + + +def test_sync_search_with_metadata(): + """Test that SyncCronClient.search forwards metadata in the request body.""" + cron = _cron_response() + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/runs/crons/search" + + body = json.loads(request.content) + assert body["metadata"] == {"owner": "alice"} + + return httpx.Response(200, json=[cron]) + + transport = httpx.MockTransport(handler) + with httpx.Client(transport=transport, base_url="https://example.com") as client: + http_client = SyncHttpClient(client) + cron_client = SyncCronClient(http_client) + result = cron_client.search(metadata={"owner": "alice"}) + + assert result == [cron] + + +def test_sync_search_omits_empty_metadata(): + """Test that SyncCronClient.search does not send metadata when not provided.""" + cron = _cron_response() + + def handler(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + assert "metadata" not in body + return httpx.Response(200, json=[cron]) + + transport = httpx.MockTransport(handler) + with httpx.Client(transport=transport, base_url="https://example.com") as client: + http_client = SyncHttpClient(client) + cron_client = SyncCronClient(http_client) + cron_client.search() + + +def test_sync_count_with_metadata(): + """Test that SyncCronClient.count forwards metadata in the request body.""" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/runs/crons/count" + + body = json.loads(request.content) + assert body["metadata"] == {"team": "infra"} + + return httpx.Response(200, json=2) + + transport = httpx.MockTransport(handler) + with httpx.Client(transport=transport, base_url="https://example.com") as client: + http_client = SyncHttpClient(client) + cron_client = SyncCronClient(http_client) + result = cron_client.count(metadata={"team": "infra"}) + + assert result == 2 + + +def test_sync_count_omits_empty_metadata(): + """Test that SyncCronClient.count does not send metadata when not provided.""" + + def handler(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + assert "metadata" not in body + return httpx.Response(200, json=0) + + transport = httpx.MockTransport(handler) + with httpx.Client(transport=transport, base_url="https://example.com") as client: + http_client = SyncHttpClient(client) + cron_client = SyncCronClient(http_client) + cron_client.count()