mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-26 17:42:24 +02:00
feat(sdk-py): add end-time to crons client (#6674)
**Description:** The crons create endpoint supports an end-time field that is not currently supported in the client. Adding that parameter here. **Issue:** N/A **Dependencies:** N/A **Twitter handle:** N/A
This commit is contained in:
@@ -17,6 +17,7 @@ import re
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -2982,6 +2983,7 @@ class CronClient:
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -3005,6 +3007,7 @@ class CronClient:
|
||||
webhook: Webhook to call after LangGraph API call is done.
|
||||
multitask_strategy: Multitask strategy to use.
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -3040,6 +3043,7 @@ class CronClient:
|
||||
"interrupt_before": interrupt_before,
|
||||
"interrupt_after": interrupt_after,
|
||||
"webhook": webhook,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
}
|
||||
if multitask_strategy:
|
||||
payload["multitask_strategy"] = multitask_strategy
|
||||
@@ -3066,6 +3070,7 @@ class CronClient:
|
||||
webhook: str | None = None,
|
||||
on_run_completed: OnCompletionBehavior | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -3090,6 +3095,7 @@ class CronClient:
|
||||
clean them up. Clients are responsible for cleaning up kept threads.
|
||||
multitask_strategy: Multitask strategy to use.
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -3126,6 +3132,7 @@ class CronClient:
|
||||
"interrupt_after": interrupt_after,
|
||||
"webhook": webhook,
|
||||
"on_run_completed": on_run_completed,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
}
|
||||
if multitask_strategy:
|
||||
payload["multitask_strategy"] = multitask_strategy
|
||||
@@ -6284,6 +6291,7 @@ class SyncCronClient:
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -6305,6 +6313,7 @@ class SyncCronClient:
|
||||
webhook: Webhook to call after LangGraph API call is done.
|
||||
multitask_strategy: Multitask strategy to use.
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
headers: Optional custom headers to include with the request.
|
||||
|
||||
Returns:
|
||||
@@ -6340,6 +6349,7 @@ class SyncCronClient:
|
||||
"checkpoint_during": checkpoint_during,
|
||||
"webhook": webhook,
|
||||
"multitask_strategy": multitask_strategy,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return self.http.post(
|
||||
@@ -6364,6 +6374,7 @@ class SyncCronClient:
|
||||
webhook: str | None = None,
|
||||
on_run_completed: OnCompletionBehavior | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -6388,6 +6399,7 @@ class SyncCronClient:
|
||||
clean them up. Clients are responsible for cleaning up kept threads.
|
||||
multitask_strategy: Multitask strategy to use.
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
headers: Optional custom headers to include with the request.
|
||||
|
||||
Returns:
|
||||
@@ -6425,6 +6437,7 @@ class SyncCronClient:
|
||||
"checkpoint_during": checkpoint_during,
|
||||
"on_run_completed": on_run_completed,
|
||||
"multitask_strategy": multitask_strategy,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return self.http.post(
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
"""Tests for the crons client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from langgraph_sdk.client import (
|
||||
CronClient,
|
||||
HttpClient,
|
||||
SyncCronClient,
|
||||
SyncHttpClient,
|
||||
)
|
||||
|
||||
|
||||
def _cron_payload() -> dict[str, object]:
|
||||
"""Return a mock cron response payload."""
|
||||
return {
|
||||
"run_id": "run_123",
|
||||
"thread_id": "thread_123",
|
||||
"assistant_id": "asst_123",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-02T00:00:00Z",
|
||||
"status": "success",
|
||||
"metadata": {},
|
||||
"multitask_strategy": "reject",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_for_thread():
|
||||
"""Test that CronClient.create_for_thread works without end_time."""
|
||||
cron = _cron_payload()
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/threads/thread_123/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 0 * * *"
|
||||
assert body["assistant_id"] == "asst_123"
|
||||
assert "end_time" not in body # Should be filtered out by the None check
|
||||
|
||||
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.create_for_thread(
|
||||
thread_id="thread_123",
|
||||
assistant_id="asst_123",
|
||||
schedule="0 0 * * *",
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_for_thread_with_end_time():
|
||||
"""Test that CronClient.create_for_thread includes end_time in the payload."""
|
||||
cron = _cron_payload()
|
||||
end_time = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/threads/thread_123/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 0 * * *"
|
||||
assert body["assistant_id"] == "asst_123"
|
||||
assert body["end_time"] == "2025-12-31T23:59:59+00:00"
|
||||
|
||||
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.create_for_thread(
|
||||
thread_id="thread_123",
|
||||
assistant_id="asst_123",
|
||||
schedule="0 0 * * *",
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create():
|
||||
"""Test that CronClient.create works without end_time."""
|
||||
cron = _cron_payload()
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 12 * * *"
|
||||
assert body["assistant_id"] == "asst_456"
|
||||
assert "end_time" not in body # Should be filtered out by the None check
|
||||
|
||||
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.create(
|
||||
assistant_id="asst_456",
|
||||
schedule="0 12 * * *",
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_with_end_time():
|
||||
"""Test that CronClient.create includes end_time in the payload."""
|
||||
cron = _cron_payload()
|
||||
end_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 12 * * *"
|
||||
assert body["assistant_id"] == "asst_456"
|
||||
assert body["end_time"] == "2025-06-15T12:00:00+00:00"
|
||||
|
||||
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.create(
|
||||
assistant_id="asst_456",
|
||||
schedule="0 12 * * *",
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
def test_sync_create_for_thread():
|
||||
"""Test that SyncCronClient.create_for_thread works without end_time."""
|
||||
cron = _cron_payload()
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/threads/thread_123/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 0 * * *"
|
||||
assert body["assistant_id"] == "asst_123"
|
||||
assert "end_time" not in body # Should be filtered out by the None check
|
||||
|
||||
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.create_for_thread(
|
||||
thread_id="thread_123",
|
||||
assistant_id="asst_123",
|
||||
schedule="0 0 * * *",
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
def test_sync_create_for_thread_with_end_time():
|
||||
"""Test that SyncCronClient.create_for_thread includes end_time in the payload."""
|
||||
cron = _cron_payload()
|
||||
end_time = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/threads/thread_123/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 0 * * *"
|
||||
assert body["assistant_id"] == "asst_123"
|
||||
assert body["end_time"] == "2025-12-31T23:59:59+00:00"
|
||||
|
||||
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.create_for_thread(
|
||||
thread_id="thread_123",
|
||||
assistant_id="asst_123",
|
||||
schedule="0 0 * * *",
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
def test_sync_create():
|
||||
"""Test that SyncCronClient.create works without end_time."""
|
||||
cron = _cron_payload()
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 12 * * *"
|
||||
assert body["assistant_id"] == "asst_456"
|
||||
assert "end_time" not in body # Should be filtered out by the None check
|
||||
|
||||
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.create(
|
||||
assistant_id="asst_456",
|
||||
schedule="0 12 * * *",
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
|
||||
|
||||
def test_sync_create_with_end_time():
|
||||
"""Test that SyncCronClient.create includes end_time in the payload."""
|
||||
cron = _cron_payload()
|
||||
end_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.method == "POST"
|
||||
assert request.url.path == "/runs/crons"
|
||||
|
||||
# Parse the request body
|
||||
body = json.loads(request.content)
|
||||
assert body["schedule"] == "0 12 * * *"
|
||||
assert body["assistant_id"] == "asst_456"
|
||||
assert body["end_time"] == "2025-06-15T12:00:00+00:00"
|
||||
|
||||
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.create(
|
||||
assistant_id="asst_456",
|
||||
schedule="0 12 * * *",
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
assert result == cron
|
||||
Reference in New Issue
Block a user