mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
fix(sdk-py): support clearing cron end_time via update(end_time=None) (#8334)
`CronClient.update` / `SyncCronClient.update` stripped all `None`-valued fields from the PATCH body, so `update(end_time=None)` could never clear a previously set cron end time. This routes an explicit `None` through as `end_time: null` (via the existing `NOT_PROVIDED` sentinel), while an omitted argument still leaves the value unchanged. Verified with `make format`, `make lint`, and `make test` in `libs/sdk-py` (493 passed), including two new tests asserting the PATCH body carries an explicit null `end_time`.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user