diff --git a/libs/sdk-py/langgraph_sdk/_async/cron.py b/libs/sdk-py/langgraph_sdk/_async/cron.py index 792fba5b0..824b86f55 100644 --- a/libs/sdk-py/langgraph_sdk/_async/cron.py +++ b/libs/sdk-py/langgraph_sdk/_async/cron.py @@ -8,7 +8,11 @@ from datetime import datetime, tzinfo from typing import Any from langgraph_sdk._async.http import HttpClient -from langgraph_sdk._shared.utilities import _quote_path_param, _resolve_timezone +from langgraph_sdk._shared.utilities import ( + NOT_PROVIDED, + _quote_path_param, + _resolve_timezone, +) from langgraph_sdk.schema import ( All, Config, @@ -324,7 +328,7 @@ class CronClient: cron_id: str, *, schedule: str | None = None, - end_time: datetime | None = None, + end_time: datetime | None = NOT_PROVIDED, input: Input | None = None, metadata: Mapping[str, Any] | None = None, config: Config | None = None, @@ -348,7 +352,8 @@ class CronClient: cron_id: The cron ID to update. schedule: The cron schedule to execute this job on. Schedules are interpreted in UTC unless a timezone is specified. - end_time: The end date to stop running the cron. + end_time: The end date to stop running the cron. Pass ``None`` to + clear a previously set end time; omit to leave it unchanged. input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. @@ -386,7 +391,6 @@ class CronClient: """ payload = { "schedule": schedule, - "end_time": end_time.isoformat() if end_time else None, "input": input, "metadata": metadata, "config": config, @@ -403,6 +407,10 @@ class CronClient: "durability": durability, } payload = {k: v for k, v in payload.items() if v is not None} + # An explicit end_time=None clears the end time; NOT_PROVIDED leaves it + # unchanged. Inject after the None-strip so the explicit null survives. + if end_time is not NOT_PROVIDED: + payload["end_time"] = end_time.isoformat() if end_time is not None else None return await self.http.patch( f"/runs/crons/{_quote_path_param(cron_id)}", json=payload, diff --git a/libs/sdk-py/langgraph_sdk/_sync/cron.py b/libs/sdk-py/langgraph_sdk/_sync/cron.py index 24acae5bd..464edb80d 100644 --- a/libs/sdk-py/langgraph_sdk/_sync/cron.py +++ b/libs/sdk-py/langgraph_sdk/_sync/cron.py @@ -7,7 +7,11 @@ from collections.abc import Mapping, Sequence from datetime import datetime, tzinfo from typing import Any -from langgraph_sdk._shared.utilities import _quote_path_param, _resolve_timezone +from langgraph_sdk._shared.utilities import ( + NOT_PROVIDED, + _quote_path_param, + _resolve_timezone, +) from langgraph_sdk._sync.http import SyncHttpClient from langgraph_sdk.schema import ( All, @@ -313,7 +317,7 @@ class SyncCronClient: cron_id: str, *, schedule: str | None = None, - end_time: datetime | None = None, + end_time: datetime | None = NOT_PROVIDED, input: Input | None = None, metadata: Mapping[str, Any] | None = None, config: Config | None = None, @@ -337,7 +341,8 @@ class SyncCronClient: cron_id: The cron ID to update. schedule: The cron schedule to execute this job on. Schedules are interpreted in UTC unless a timezone is specified. - end_time: The end date to stop running the cron. + end_time: The end date to stop running the cron. Pass ``None`` to + clear a previously set end time; omit to leave it unchanged. input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. @@ -375,7 +380,6 @@ class SyncCronClient: """ payload = { "schedule": schedule, - "end_time": end_time.isoformat() if end_time else None, "input": input, "metadata": metadata, "config": config, @@ -392,6 +396,10 @@ class SyncCronClient: "durability": durability, } payload = {k: v for k, v in payload.items() if v is not None} + # An explicit end_time=None clears the end time; NOT_PROVIDED leaves it + # unchanged. Inject after the None-strip so the explicit null survives. + if end_time is not NOT_PROVIDED: + payload["end_time"] = end_time.isoformat() if end_time is not None else None return self.http.patch( f"/runs/crons/{_quote_path_param(cron_id)}", json=payload, diff --git a/libs/sdk-py/tests/test_crons_client.py b/libs/sdk-py/tests/test_crons_client.py index 122dda638..7e38e25f1 100644 --- a/libs/sdk-py/tests/test_crons_client.py +++ b/libs/sdk-py/tests/test_crons_client.py @@ -396,6 +396,32 @@ async def test_async_update_with_end_time(): assert result == cron +@pytest.mark.asyncio +async def test_async_update_clears_end_time(): + """Test that CronClient.update sends an explicit null end_time to clear it.""" + cron = _cron_response() + + async def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "PATCH" + assert request.url.path == "/runs/crons/cron_123" + + body = json.loads(request.content) + assert "end_time" in body # Explicit None must survive the None-strip + assert body["end_time"] is None + + 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.update(cron_id="cron_123", end_time=None) + + assert result == cron + + def test_sync_update(): """Test that SyncCronClient.update works with schedule and enabled parameters.""" cron = _cron_response() @@ -456,6 +482,29 @@ def test_sync_update_with_end_time(): assert result == cron +def test_sync_update_clears_end_time(): + """Test that SyncCronClient.update sends an explicit null end_time to clear it.""" + cron = _cron_response() + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "PATCH" + assert request.url.path == "/runs/crons/cron_123" + + body = json.loads(request.content) + assert "end_time" in body # Explicit None must survive the None-strip + assert body["end_time"] is None + + 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.update(cron_id="cron_123", end_time=None) + + assert result == cron + + @pytest.mark.parametrize( "enabled_value", [True, False],