langgraph-sdk 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/PKG-INFO +1 -1
  2. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/__init__.py +1 -1
  3. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/client.py +13 -0
  4. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/errors.py +1 -1
  5. langgraph_sdk-0.3.3/tests/test_crons_client.py +278 -0
  6. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_errors.py +36 -0
  7. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/.gitignore +0 -0
  8. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/LICENSE +0 -0
  9. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/Makefile +0 -0
  10. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/README.md +0 -0
  11. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/__init__.py +0 -0
  12. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/exceptions.py +0 -0
  13. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/types.py +0 -0
  14. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/encryption/__init__.py +0 -0
  15. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/encryption/types.py +0 -0
  16. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/py.typed +0 -0
  17. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/schema.py +0 -0
  18. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/sse.py +0 -0
  19. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/pyproject.toml +0 -0
  20. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/fixtures/response.txt +0 -0
  21. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_api_parity.py +0 -0
  22. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_assistants_client.py +0 -0
  23. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_client_stream.py +0 -0
  24. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_encryption.py +0 -0
  25. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_serde.py +0 -0
  26. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_serde_schema.py +0 -0
  27. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_skip_auto_load_api_key.py +0 -0
  28. {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-sdk
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: SDK for interacting with LangGraph API
5
5
  Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-py
6
6
  Project-URL: Twitter, https://x.com/LangChainAI
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
3
3
  from langgraph_sdk.encryption import Encryption
4
4
  from langgraph_sdk.encryption.types import EncryptionContext
5
5
 
6
- __version__ = "0.3.2"
6
+ __version__ = "0.3.3"
7
7
 
8
8
  __all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
@@ -17,6 +17,7 @@ import re
17
17
  import sys
18
18
  import warnings
19
19
  from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
20
+ from datetime import datetime
20
21
  from types import TracebackType
21
22
  from typing import (
22
23
  Any,
@@ -2982,6 +2983,7 @@ class CronClient:
2982
2983
  interrupt_after: All | list[str] | None = None,
2983
2984
  webhook: str | None = None,
2984
2985
  multitask_strategy: str | None = None,
2986
+ end_time: datetime | None = None,
2985
2987
  headers: Mapping[str, str] | None = None,
2986
2988
  params: QueryParamTypes | None = None,
2987
2989
  ) -> Run:
@@ -3005,6 +3007,7 @@ class CronClient:
3005
3007
  webhook: Webhook to call after LangGraph API call is done.
3006
3008
  multitask_strategy: Multitask strategy to use.
3007
3009
  Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
3010
+ end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
3008
3011
  headers: Optional custom headers to include with the request.
3009
3012
  params: Optional query parameters to include with the request.
3010
3013
 
@@ -3040,6 +3043,7 @@ class CronClient:
3040
3043
  "interrupt_before": interrupt_before,
3041
3044
  "interrupt_after": interrupt_after,
3042
3045
  "webhook": webhook,
3046
+ "end_time": end_time.isoformat() if end_time else None,
3043
3047
  }
3044
3048
  if multitask_strategy:
3045
3049
  payload["multitask_strategy"] = multitask_strategy
@@ -3066,6 +3070,7 @@ class CronClient:
3066
3070
  webhook: str | None = None,
3067
3071
  on_run_completed: OnCompletionBehavior | None = None,
3068
3072
  multitask_strategy: str | None = None,
3073
+ end_time: datetime | None = None,
3069
3074
  headers: Mapping[str, str] | None = None,
3070
3075
  params: QueryParamTypes | None = None,
3071
3076
  ) -> Run:
@@ -3090,6 +3095,7 @@ class CronClient:
3090
3095
  clean them up. Clients are responsible for cleaning up kept threads.
3091
3096
  multitask_strategy: Multitask strategy to use.
3092
3097
  Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
3098
+ end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
3093
3099
  headers: Optional custom headers to include with the request.
3094
3100
  params: Optional query parameters to include with the request.
3095
3101
 
@@ -3126,6 +3132,7 @@ class CronClient:
3126
3132
  "interrupt_after": interrupt_after,
3127
3133
  "webhook": webhook,
3128
3134
  "on_run_completed": on_run_completed,
3135
+ "end_time": end_time.isoformat() if end_time else None,
3129
3136
  }
3130
3137
  if multitask_strategy:
3131
3138
  payload["multitask_strategy"] = multitask_strategy
@@ -6284,6 +6291,7 @@ class SyncCronClient:
6284
6291
  interrupt_after: All | list[str] | None = None,
6285
6292
  webhook: str | None = None,
6286
6293
  multitask_strategy: str | None = None,
6294
+ end_time: datetime | None = None,
6287
6295
  headers: Mapping[str, str] | None = None,
6288
6296
  params: QueryParamTypes | None = None,
6289
6297
  ) -> Run:
@@ -6305,6 +6313,7 @@ class SyncCronClient:
6305
6313
  webhook: Webhook to call after LangGraph API call is done.
6306
6314
  multitask_strategy: Multitask strategy to use.
6307
6315
  Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
6316
+ end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
6308
6317
  headers: Optional custom headers to include with the request.
6309
6318
 
6310
6319
  Returns:
@@ -6340,6 +6349,7 @@ class SyncCronClient:
6340
6349
  "checkpoint_during": checkpoint_during,
6341
6350
  "webhook": webhook,
6342
6351
  "multitask_strategy": multitask_strategy,
6352
+ "end_time": end_time.isoformat() if end_time else None,
6343
6353
  }
6344
6354
  payload = {k: v for k, v in payload.items() if v is not None}
6345
6355
  return self.http.post(
@@ -6364,6 +6374,7 @@ class SyncCronClient:
6364
6374
  webhook: str | None = None,
6365
6375
  on_run_completed: OnCompletionBehavior | None = None,
6366
6376
  multitask_strategy: str | None = None,
6377
+ end_time: datetime | None = None,
6367
6378
  headers: Mapping[str, str] | None = None,
6368
6379
  params: QueryParamTypes | None = None,
6369
6380
  ) -> Run:
@@ -6388,6 +6399,7 @@ class SyncCronClient:
6388
6399
  clean them up. Clients are responsible for cleaning up kept threads.
6389
6400
  multitask_strategy: Multitask strategy to use.
6390
6401
  Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
6402
+ end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
6391
6403
  headers: Optional custom headers to include with the request.
6392
6404
 
6393
6405
  Returns:
@@ -6425,6 +6437,7 @@ class SyncCronClient:
6425
6437
  "checkpoint_during": checkpoint_during,
6426
6438
  "on_run_completed": on_run_completed,
6427
6439
  "multitask_strategy": multitask_strategy,
6440
+ "end_time": end_time.isoformat() if end_time else None,
6428
6441
  }
6429
6442
  payload = {k: v for k, v in payload.items() if v is not None}
6430
6443
  return self.http.post(
@@ -38,7 +38,7 @@ class APIError(httpx.HTTPStatusError, LangGraphError):
38
38
  response = None
39
39
 
40
40
  httpx.HTTPStatusError.__init__(self, message, request=req, response=response) # type: ignore[arg-type]
41
- LangGraphError.__init__(self)
41
+ LangGraphError.__init__(self, message)
42
42
 
43
43
  self.request = req
44
44
  self.message = message
@@ -0,0 +1,278 @@
1
+ """Tests for the crons client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from langgraph_sdk.client import (
12
+ CronClient,
13
+ HttpClient,
14
+ SyncCronClient,
15
+ SyncHttpClient,
16
+ )
17
+
18
+
19
+ def _cron_payload() -> dict[str, object]:
20
+ """Return a mock cron response payload."""
21
+ return {
22
+ "run_id": "run_123",
23
+ "thread_id": "thread_123",
24
+ "assistant_id": "asst_123",
25
+ "created_at": "2024-01-01T00:00:00Z",
26
+ "updated_at": "2024-01-02T00:00:00Z",
27
+ "status": "success",
28
+ "metadata": {},
29
+ "multitask_strategy": "reject",
30
+ }
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_async_create_for_thread():
35
+ """Test that CronClient.create_for_thread works without end_time."""
36
+ cron = _cron_payload()
37
+
38
+ async def handler(request: httpx.Request) -> httpx.Response:
39
+ assert request.method == "POST"
40
+ assert request.url.path == "/threads/thread_123/runs/crons"
41
+
42
+ # Parse the request body
43
+ body = json.loads(request.content)
44
+ assert body["schedule"] == "0 0 * * *"
45
+ assert body["assistant_id"] == "asst_123"
46
+ assert "end_time" not in body # Should be filtered out by the None check
47
+
48
+ return httpx.Response(200, json=cron)
49
+
50
+ transport = httpx.MockTransport(handler)
51
+ async with httpx.AsyncClient(
52
+ transport=transport, base_url="https://example.com"
53
+ ) as client:
54
+ http_client = HttpClient(client)
55
+ cron_client = CronClient(http_client)
56
+ result = await cron_client.create_for_thread(
57
+ thread_id="thread_123",
58
+ assistant_id="asst_123",
59
+ schedule="0 0 * * *",
60
+ )
61
+
62
+ assert result == cron
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_async_create_for_thread_with_end_time():
67
+ """Test that CronClient.create_for_thread includes end_time in the payload."""
68
+ cron = _cron_payload()
69
+ end_time = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
70
+
71
+ async def handler(request: httpx.Request) -> httpx.Response:
72
+ assert request.method == "POST"
73
+ assert request.url.path == "/threads/thread_123/runs/crons"
74
+
75
+ # Parse the request body
76
+ body = json.loads(request.content)
77
+ assert body["schedule"] == "0 0 * * *"
78
+ assert body["assistant_id"] == "asst_123"
79
+ assert body["end_time"] == "2025-12-31T23:59:59+00:00"
80
+
81
+ return httpx.Response(200, json=cron)
82
+
83
+ transport = httpx.MockTransport(handler)
84
+ async with httpx.AsyncClient(
85
+ transport=transport, base_url="https://example.com"
86
+ ) as client:
87
+ http_client = HttpClient(client)
88
+ cron_client = CronClient(http_client)
89
+ result = await cron_client.create_for_thread(
90
+ thread_id="thread_123",
91
+ assistant_id="asst_123",
92
+ schedule="0 0 * * *",
93
+ end_time=end_time,
94
+ )
95
+
96
+ assert result == cron
97
+
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_async_create():
101
+ """Test that CronClient.create works without end_time."""
102
+ cron = _cron_payload()
103
+
104
+ async def handler(request: httpx.Request) -> httpx.Response:
105
+ assert request.method == "POST"
106
+ assert request.url.path == "/runs/crons"
107
+
108
+ # Parse the request body
109
+ body = json.loads(request.content)
110
+ assert body["schedule"] == "0 12 * * *"
111
+ assert body["assistant_id"] == "asst_456"
112
+ assert "end_time" not in body # Should be filtered out by the None check
113
+
114
+ return httpx.Response(200, json=cron)
115
+
116
+ transport = httpx.MockTransport(handler)
117
+ async with httpx.AsyncClient(
118
+ transport=transport, base_url="https://example.com"
119
+ ) as client:
120
+ http_client = HttpClient(client)
121
+ cron_client = CronClient(http_client)
122
+ result = await cron_client.create(
123
+ assistant_id="asst_456",
124
+ schedule="0 12 * * *",
125
+ )
126
+
127
+ assert result == cron
128
+
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_async_create_with_end_time():
132
+ """Test that CronClient.create includes end_time in the payload."""
133
+ cron = _cron_payload()
134
+ end_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
135
+
136
+ async def handler(request: httpx.Request) -> httpx.Response:
137
+ assert request.method == "POST"
138
+ assert request.url.path == "/runs/crons"
139
+
140
+ # Parse the request body
141
+ body = json.loads(request.content)
142
+ assert body["schedule"] == "0 12 * * *"
143
+ assert body["assistant_id"] == "asst_456"
144
+ assert body["end_time"] == "2025-06-15T12:00:00+00:00"
145
+
146
+ return httpx.Response(200, json=cron)
147
+
148
+ transport = httpx.MockTransport(handler)
149
+ async with httpx.AsyncClient(
150
+ transport=transport, base_url="https://example.com"
151
+ ) as client:
152
+ http_client = HttpClient(client)
153
+ cron_client = CronClient(http_client)
154
+ result = await cron_client.create(
155
+ assistant_id="asst_456",
156
+ schedule="0 12 * * *",
157
+ end_time=end_time,
158
+ )
159
+
160
+ assert result == cron
161
+
162
+
163
+ def test_sync_create_for_thread():
164
+ """Test that SyncCronClient.create_for_thread works without end_time."""
165
+ cron = _cron_payload()
166
+
167
+ def handler(request: httpx.Request) -> httpx.Response:
168
+ assert request.method == "POST"
169
+ assert request.url.path == "/threads/thread_123/runs/crons"
170
+
171
+ # Parse the request body
172
+ body = json.loads(request.content)
173
+ assert body["schedule"] == "0 0 * * *"
174
+ assert body["assistant_id"] == "asst_123"
175
+ assert "end_time" not in body # Should be filtered out by the None check
176
+
177
+ return httpx.Response(200, json=cron)
178
+
179
+ transport = httpx.MockTransport(handler)
180
+ with httpx.Client(transport=transport, base_url="https://example.com") as client:
181
+ http_client = SyncHttpClient(client)
182
+ cron_client = SyncCronClient(http_client)
183
+ result = cron_client.create_for_thread(
184
+ thread_id="thread_123",
185
+ assistant_id="asst_123",
186
+ schedule="0 0 * * *",
187
+ )
188
+
189
+ assert result == cron
190
+
191
+
192
+ def test_sync_create_for_thread_with_end_time():
193
+ """Test that SyncCronClient.create_for_thread includes end_time in the payload."""
194
+ cron = _cron_payload()
195
+ end_time = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
196
+
197
+ def handler(request: httpx.Request) -> httpx.Response:
198
+ assert request.method == "POST"
199
+ assert request.url.path == "/threads/thread_123/runs/crons"
200
+
201
+ # Parse the request body
202
+ body = json.loads(request.content)
203
+ assert body["schedule"] == "0 0 * * *"
204
+ assert body["assistant_id"] == "asst_123"
205
+ assert body["end_time"] == "2025-12-31T23:59:59+00:00"
206
+
207
+ return httpx.Response(200, json=cron)
208
+
209
+ transport = httpx.MockTransport(handler)
210
+ with httpx.Client(transport=transport, base_url="https://example.com") as client:
211
+ http_client = SyncHttpClient(client)
212
+ cron_client = SyncCronClient(http_client)
213
+ result = cron_client.create_for_thread(
214
+ thread_id="thread_123",
215
+ assistant_id="asst_123",
216
+ schedule="0 0 * * *",
217
+ end_time=end_time,
218
+ )
219
+
220
+ assert result == cron
221
+
222
+
223
+ def test_sync_create():
224
+ """Test that SyncCronClient.create works without end_time."""
225
+ cron = _cron_payload()
226
+
227
+ def handler(request: httpx.Request) -> httpx.Response:
228
+ assert request.method == "POST"
229
+ assert request.url.path == "/runs/crons"
230
+
231
+ # Parse the request body
232
+ body = json.loads(request.content)
233
+ assert body["schedule"] == "0 12 * * *"
234
+ assert body["assistant_id"] == "asst_456"
235
+ assert "end_time" not in body # Should be filtered out by the None check
236
+
237
+ return httpx.Response(200, json=cron)
238
+
239
+ transport = httpx.MockTransport(handler)
240
+ with httpx.Client(transport=transport, base_url="https://example.com") as client:
241
+ http_client = SyncHttpClient(client)
242
+ cron_client = SyncCronClient(http_client)
243
+ result = cron_client.create(
244
+ assistant_id="asst_456",
245
+ schedule="0 12 * * *",
246
+ )
247
+
248
+ assert result == cron
249
+
250
+
251
+ def test_sync_create_with_end_time():
252
+ """Test that SyncCronClient.create includes end_time in the payload."""
253
+ cron = _cron_payload()
254
+ end_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
255
+
256
+ def handler(request: httpx.Request) -> httpx.Response:
257
+ assert request.method == "POST"
258
+ assert request.url.path == "/runs/crons"
259
+
260
+ # Parse the request body
261
+ body = json.loads(request.content)
262
+ assert body["schedule"] == "0 12 * * *"
263
+ assert body["assistant_id"] == "asst_456"
264
+ assert body["end_time"] == "2025-06-15T12:00:00+00:00"
265
+
266
+ return httpx.Response(200, json=cron)
267
+
268
+ transport = httpx.MockTransport(handler)
269
+ with httpx.Client(transport=transport, base_url="https://example.com") as client:
270
+ http_client = SyncHttpClient(client)
271
+ cron_client = SyncCronClient(http_client)
272
+ result = cron_client.create(
273
+ assistant_id="asst_456",
274
+ schedule="0 12 * * *",
275
+ end_time=end_time,
276
+ )
277
+
278
+ assert result == cron
@@ -106,3 +106,39 @@ def test_field_extraction_from_json_body() -> None:
106
106
  assert err.code == "invalid_param"
107
107
  assert err.param == "limit"
108
108
  assert err.type == "invalid_request_error"
109
+
110
+
111
+ def test_error_message_in_str_and_args() -> None:
112
+ """Test that error message is accessible via str() and args."""
113
+ r = make_response(422, json_body={"message": "Validation failed"})
114
+ with pytest.raises(UnprocessableEntityError) as ei:
115
+ _raise_for_status_typed(r)
116
+ err = cast("UnprocessableEntityError", ei.value)
117
+ assert str(err) == "Validation failed"
118
+ assert err.args == ("Validation failed",)
119
+ assert err.message == "Validation failed"
120
+
121
+
122
+ @pytest.mark.parametrize(
123
+ "status,exc_type",
124
+ [
125
+ (400, BadRequestError),
126
+ (401, AuthenticationError),
127
+ (403, PermissionDeniedError),
128
+ (404, NotFoundError),
129
+ (409, ConflictError),
130
+ (422, UnprocessableEntityError),
131
+ (429, RateLimitError),
132
+ (500, InternalServerError),
133
+ ],
134
+ )
135
+ def test_all_error_types_display_message(
136
+ status: int, exc_type: type[APIStatusError]
137
+ ) -> None:
138
+ """Test that all error subclasses properly display their message."""
139
+ r = make_response(status, json_body={"message": "test error message"})
140
+ with pytest.raises(exc_type) as ei:
141
+ _raise_for_status_typed(r)
142
+ err = ei.value
143
+ assert str(err) == "test error message"
144
+ assert "test error message" in err.args
File without changes
File without changes
File without changes
File without changes
File without changes