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.
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/PKG-INFO +1 -1
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/__init__.py +1 -1
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/client.py +13 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/errors.py +1 -1
- langgraph_sdk-0.3.3/tests/test_crons_client.py +278 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_errors.py +36 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/.gitignore +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/LICENSE +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/Makefile +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/README.md +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/__init__.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/exceptions.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/auth/types.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/encryption/__init__.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/encryption/types.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/py.typed +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/schema.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/langgraph_sdk/sse.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/pyproject.toml +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/fixtures/response.txt +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_api_parity.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_assistants_client.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_client_stream.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_encryption.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_serde.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_serde_schema.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/tests/test_skip_auto_load_api_key.py +0 -0
- {langgraph_sdk-0.3.2 → langgraph_sdk-0.3.3}/uv.lock +0 -0
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|