langgraph-sdk 0.3.0__tar.gz → 0.3.2__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/.gitignore +102 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/PKG-INFO +1 -1
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/__init__.py +1 -1
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/client.py +12 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/encryption/__init__.py +137 -225
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/schema.py +2 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_encryption.py +0 -20
- langgraph_sdk-0.3.0/.gitignore +0 -184
- langgraph_sdk-0.3.0/tests/test_select_fields_sync.py +0 -76
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/LICENSE +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/Makefile +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/README.md +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/auth/__init__.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/auth/exceptions.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/auth/types.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/encryption/types.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/errors.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/py.typed +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/langgraph_sdk/sse.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/pyproject.toml +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/fixtures/response.txt +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_api_parity.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_assistants_client.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_client_stream.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_errors.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_serde.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_serde_schema.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/tests/test_skip_auto_load_api_key.py +0 -0
- {langgraph_sdk-0.3.0 → langgraph_sdk-0.3.2}/uv.lock +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
.vs/
|
|
2
|
+
.vscode/
|
|
3
|
+
.idea/
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
db.sqlite3
|
|
58
|
+
db.sqlite3-journal
|
|
59
|
+
|
|
60
|
+
# PyBuilder
|
|
61
|
+
target/
|
|
62
|
+
|
|
63
|
+
# Jupyter Notebook
|
|
64
|
+
.ipynb_checkpoints
|
|
65
|
+
notebooks/
|
|
66
|
+
|
|
67
|
+
# IPython
|
|
68
|
+
profile_default/
|
|
69
|
+
ipython_config.py
|
|
70
|
+
|
|
71
|
+
# pyenv
|
|
72
|
+
.python-version
|
|
73
|
+
|
|
74
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
75
|
+
__pypackages__/
|
|
76
|
+
|
|
77
|
+
# Environments
|
|
78
|
+
.env
|
|
79
|
+
.envrc
|
|
80
|
+
.venv
|
|
81
|
+
.venvs
|
|
82
|
+
env/
|
|
83
|
+
venv/
|
|
84
|
+
ENV/
|
|
85
|
+
env.bak/
|
|
86
|
+
venv.bak/
|
|
87
|
+
|
|
88
|
+
# mypy
|
|
89
|
+
.mypy_cache/
|
|
90
|
+
.dmypy.json
|
|
91
|
+
dmypy.json
|
|
92
|
+
|
|
93
|
+
# Pyre type checker
|
|
94
|
+
.pyre/
|
|
95
|
+
|
|
96
|
+
# macOS display setting files
|
|
97
|
+
.DS_Store
|
|
98
|
+
|
|
99
|
+
.vercel
|
|
100
|
+
.turbo
|
|
101
|
+
.editorconfig
|
|
102
|
+
.scratch
|
|
@@ -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.2"
|
|
7
7
|
|
|
8
8
|
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
|
|
@@ -3064,6 +3064,7 @@ class CronClient:
|
|
|
3064
3064
|
interrupt_before: All | list[str] | None = None,
|
|
3065
3065
|
interrupt_after: All | list[str] | None = None,
|
|
3066
3066
|
webhook: str | None = None,
|
|
3067
|
+
on_run_completed: OnCompletionBehavior | None = None,
|
|
3067
3068
|
multitask_strategy: str | None = None,
|
|
3068
3069
|
headers: Mapping[str, str] | None = None,
|
|
3069
3070
|
params: QueryParamTypes | None = None,
|
|
@@ -3083,6 +3084,10 @@ class CronClient:
|
|
|
3083
3084
|
interrupt_before: Nodes to interrupt immediately before they get executed.
|
|
3084
3085
|
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
|
3085
3086
|
webhook: Webhook to call after LangGraph API call is done.
|
|
3087
|
+
on_run_completed: What to do with the thread after the run completes.
|
|
3088
|
+
Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread
|
|
3089
|
+
after execution. 'keep' creates a new thread for each execution but does not
|
|
3090
|
+
clean them up. Clients are responsible for cleaning up kept threads.
|
|
3086
3091
|
multitask_strategy: Multitask strategy to use.
|
|
3087
3092
|
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
|
3088
3093
|
headers: Optional custom headers to include with the request.
|
|
@@ -3120,6 +3125,7 @@ class CronClient:
|
|
|
3120
3125
|
"interrupt_before": interrupt_before,
|
|
3121
3126
|
"interrupt_after": interrupt_after,
|
|
3122
3127
|
"webhook": webhook,
|
|
3128
|
+
"on_run_completed": on_run_completed,
|
|
3123
3129
|
}
|
|
3124
3130
|
if multitask_strategy:
|
|
3125
3131
|
payload["multitask_strategy"] = multitask_strategy
|
|
@@ -6356,6 +6362,7 @@ class SyncCronClient:
|
|
|
6356
6362
|
interrupt_before: All | list[str] | None = None,
|
|
6357
6363
|
interrupt_after: All | list[str] | None = None,
|
|
6358
6364
|
webhook: str | None = None,
|
|
6365
|
+
on_run_completed: OnCompletionBehavior | None = None,
|
|
6359
6366
|
multitask_strategy: str | None = None,
|
|
6360
6367
|
headers: Mapping[str, str] | None = None,
|
|
6361
6368
|
params: QueryParamTypes | None = None,
|
|
@@ -6375,6 +6382,10 @@ class SyncCronClient:
|
|
|
6375
6382
|
interrupt_before: Nodes to interrupt immediately before they get executed.
|
|
6376
6383
|
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
|
6377
6384
|
webhook: Webhook to call after LangGraph API call is done.
|
|
6385
|
+
on_run_completed: What to do with the thread after the run completes.
|
|
6386
|
+
Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread
|
|
6387
|
+
after execution. 'keep' creates a new thread for each execution but does not
|
|
6388
|
+
clean them up. Clients are responsible for cleaning up kept threads.
|
|
6378
6389
|
multitask_strategy: Multitask strategy to use.
|
|
6379
6390
|
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
|
6380
6391
|
headers: Optional custom headers to include with the request.
|
|
@@ -6412,6 +6423,7 @@ class SyncCronClient:
|
|
|
6412
6423
|
"interrupt_after": interrupt_after,
|
|
6413
6424
|
"webhook": webhook,
|
|
6414
6425
|
"checkpoint_during": checkpoint_during,
|
|
6426
|
+
"on_run_completed": on_run_completed,
|
|
6415
6427
|
"multitask_strategy": multitask_strategy,
|
|
6416
6428
|
}
|
|
6417
6429
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
@@ -64,114 +64,6 @@ def _validate_handler(fn: typing.Callable, handler_type: str) -> None:
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class _JsonEncryptDecorators:
|
|
68
|
-
"""Dynamic decorator factory for JSON encryption handlers.
|
|
69
|
-
|
|
70
|
-
Supports both default and model-specific handlers:
|
|
71
|
-
- @encrypt.json - default handler for all models
|
|
72
|
-
- @encrypt.json.thread - handler for thread model
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
def __init__(self, parent: Encryption):
|
|
76
|
-
self._parent = parent
|
|
77
|
-
|
|
78
|
-
def __call__(self, fn: types.JsonEncryptor) -> types.JsonEncryptor:
|
|
79
|
-
"""Register the default JSON encryption handler.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
fn: The handler function
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
The registered handler function
|
|
86
|
-
|
|
87
|
-
Raises:
|
|
88
|
-
DuplicateHandlerError: If handler already registered
|
|
89
|
-
TypeError: If handler has invalid signature
|
|
90
|
-
"""
|
|
91
|
-
if self._parent._json_encryptor is not None:
|
|
92
|
-
raise DuplicateHandlerError("Default JSON encryptor already registered")
|
|
93
|
-
_validate_handler(fn, "Default JSON encryptor")
|
|
94
|
-
self._parent._json_encryptor = fn
|
|
95
|
-
return fn
|
|
96
|
-
|
|
97
|
-
def __getattr__(
|
|
98
|
-
self, model: str
|
|
99
|
-
) -> typing.Callable[[types.JsonEncryptor], types.JsonEncryptor]:
|
|
100
|
-
"""Dynamic attribute access for model-specific handlers.
|
|
101
|
-
|
|
102
|
-
Allows @encryption.encrypt.json.thread, @encryption.encrypt.json.assistant, etc.
|
|
103
|
-
|
|
104
|
-
Raises:
|
|
105
|
-
DuplicateHandlerError: If handler already registered for this model
|
|
106
|
-
TypeError: If handler has invalid signature
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
def decorator(fn: types.JsonEncryptor) -> types.JsonEncryptor:
|
|
110
|
-
if model in self._parent._json_encryptors:
|
|
111
|
-
raise DuplicateHandlerError(
|
|
112
|
-
f"JSON encryptor for model '{model}' already registered"
|
|
113
|
-
)
|
|
114
|
-
_validate_handler(fn, f"JSON encryptor for model '{model}'")
|
|
115
|
-
self._parent._json_encryptors[model] = fn
|
|
116
|
-
return fn
|
|
117
|
-
|
|
118
|
-
return decorator
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class _JsonDecryptDecorators:
|
|
122
|
-
"""Dynamic decorator factory for JSON decryption handlers.
|
|
123
|
-
|
|
124
|
-
Supports both default and model-specific handlers:
|
|
125
|
-
- @encryption.decrypt.json - default handler for all models
|
|
126
|
-
- @encryption.decrypt.json.thread - handler for thread model
|
|
127
|
-
"""
|
|
128
|
-
|
|
129
|
-
def __init__(self, parent: Encryption):
|
|
130
|
-
self._parent = parent
|
|
131
|
-
|
|
132
|
-
def __call__(self, fn: types.JsonDecryptor) -> types.JsonDecryptor:
|
|
133
|
-
"""Register the default JSON decryption handler.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
fn: The handler function
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
The registered handler function
|
|
140
|
-
|
|
141
|
-
Raises:
|
|
142
|
-
DuplicateHandlerError: If handler already registered
|
|
143
|
-
TypeError: If handler has invalid signature
|
|
144
|
-
"""
|
|
145
|
-
if self._parent._json_decryptor is not None:
|
|
146
|
-
raise DuplicateHandlerError("Default JSON decryptor already registered")
|
|
147
|
-
_validate_handler(fn, "Default JSON decryptor")
|
|
148
|
-
self._parent._json_decryptor = fn
|
|
149
|
-
return fn
|
|
150
|
-
|
|
151
|
-
def __getattr__(
|
|
152
|
-
self, model: str
|
|
153
|
-
) -> typing.Callable[[types.JsonDecryptor], types.JsonDecryptor]:
|
|
154
|
-
"""Dynamic attribute access for model-specific handlers.
|
|
155
|
-
|
|
156
|
-
Allows @encryption.decrypt.json.thread, @encryption.decrypt.json.assistant, etc.
|
|
157
|
-
|
|
158
|
-
Raises:
|
|
159
|
-
DuplicateHandlerError: If handler already registered for this model
|
|
160
|
-
TypeError: If handler has invalid signature
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
def decorator(fn: types.JsonDecryptor) -> types.JsonDecryptor:
|
|
164
|
-
if model in self._parent._json_decryptors:
|
|
165
|
-
raise DuplicateHandlerError(
|
|
166
|
-
f"JSON decryptor for model '{model}' already registered"
|
|
167
|
-
)
|
|
168
|
-
_validate_handler(fn, f"JSON decryptor for model '{model}'")
|
|
169
|
-
self._parent._json_decryptors[model] = fn
|
|
170
|
-
return fn
|
|
171
|
-
|
|
172
|
-
return decorator
|
|
173
|
-
|
|
174
|
-
|
|
175
67
|
class _EncryptDecorators:
|
|
176
68
|
"""Decorators for encryption handlers.
|
|
177
69
|
|
|
@@ -181,7 +73,6 @@ class _EncryptDecorators:
|
|
|
181
73
|
|
|
182
74
|
def __init__(self, parent: Encryption):
|
|
183
75
|
self._parent = parent
|
|
184
|
-
self._json = _JsonEncryptDecorators(parent)
|
|
185
76
|
|
|
186
77
|
def blob(self, fn: types.BlobEncryptor) -> types.BlobEncryptor:
|
|
187
78
|
"""Register a blob encryption handler.
|
|
@@ -212,29 +103,32 @@ class _EncryptDecorators:
|
|
|
212
103
|
self._parent._blob_encryptor = fn
|
|
213
104
|
return fn
|
|
214
105
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"""Access JSON encryption decorators.
|
|
218
|
-
|
|
219
|
-
Supports model-specific handlers:
|
|
220
|
-
- @encryption.encrypt.json - default handler for all models
|
|
221
|
-
- @encryption.encrypt.json.thread - handler for thread model only
|
|
222
|
-
- @encryption.encrypt.json.assistant - handler for assistant model only
|
|
106
|
+
def json(self, fn: types.JsonEncryptor) -> types.JsonEncryptor:
|
|
107
|
+
"""Register the JSON encryption handler.
|
|
223
108
|
|
|
224
109
|
Example:
|
|
225
110
|
```python
|
|
226
111
|
@encryption.encrypt.json
|
|
227
|
-
async def
|
|
228
|
-
#
|
|
112
|
+
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
113
|
+
# Encrypt the data
|
|
229
114
|
return encrypt_data(data)
|
|
230
|
-
|
|
231
|
-
@encryption.encrypt.json.thread
|
|
232
|
-
async def encrypt_thread(ctx: EncryptionContext, data: dict) -> dict:
|
|
233
|
-
# Special encryption for thread model only
|
|
234
|
-
return encrypt_thread_data(data)
|
|
235
115
|
```
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
fn: The encryption handler function
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The registered handler function
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
DuplicateHandlerError: If JSON encryptor already registered
|
|
125
|
+
TypeError: If handler has invalid signature
|
|
236
126
|
"""
|
|
237
|
-
|
|
127
|
+
if self._parent._json_encryptor is not None:
|
|
128
|
+
raise DuplicateHandlerError("JSON encryptor already registered")
|
|
129
|
+
_validate_handler(fn, "JSON encryptor")
|
|
130
|
+
self._parent._json_encryptor = fn
|
|
131
|
+
return fn
|
|
238
132
|
|
|
239
133
|
|
|
240
134
|
class _DecryptDecorators:
|
|
@@ -246,7 +140,6 @@ class _DecryptDecorators:
|
|
|
246
140
|
|
|
247
141
|
def __init__(self, parent: Encryption):
|
|
248
142
|
self._parent = parent
|
|
249
|
-
self._json = _JsonDecryptDecorators(parent)
|
|
250
143
|
|
|
251
144
|
def blob(self, fn: types.BlobDecryptor) -> types.BlobDecryptor:
|
|
252
145
|
"""Register a blob decryption handler.
|
|
@@ -277,29 +170,32 @@ class _DecryptDecorators:
|
|
|
277
170
|
self._parent._blob_decryptor = fn
|
|
278
171
|
return fn
|
|
279
172
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"""Access JSON decryption decorators.
|
|
283
|
-
|
|
284
|
-
Supports model-specific handlers:
|
|
285
|
-
- @encryption.decrypt.json - default handler for all models
|
|
286
|
-
- @encryption.decrypt.json.thread - handler for thread model only
|
|
287
|
-
- @encryption.decrypt.json.assistant - handler for assistant model only
|
|
173
|
+
def json(self, fn: types.JsonDecryptor) -> types.JsonDecryptor:
|
|
174
|
+
"""Register the JSON decryption handler.
|
|
288
175
|
|
|
289
176
|
Example:
|
|
290
177
|
```python
|
|
291
178
|
@encryption.decrypt.json
|
|
292
|
-
async def
|
|
293
|
-
#
|
|
179
|
+
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
180
|
+
# Decrypt the data
|
|
294
181
|
return decrypt_data(data)
|
|
295
|
-
|
|
296
|
-
@encryption.decrypt.json.thread
|
|
297
|
-
async def decrypt_thread(ctx: EncryptionContext, data: dict) -> dict:
|
|
298
|
-
# Special decryption for thread model only
|
|
299
|
-
return decrypt_thread_data(data)
|
|
300
182
|
```
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
fn: The decryption handler function
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The registered handler function
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
DuplicateHandlerError: If JSON decryptor already registered
|
|
192
|
+
TypeError: If handler has invalid signature
|
|
301
193
|
"""
|
|
302
|
-
|
|
194
|
+
if self._parent._json_decryptor is not None:
|
|
195
|
+
raise DuplicateHandlerError("JSON decryptor already registered")
|
|
196
|
+
_validate_handler(fn, "JSON decryptor")
|
|
197
|
+
self._parent._json_decryptor = fn
|
|
198
|
+
return fn
|
|
303
199
|
|
|
304
200
|
|
|
305
201
|
class Encryption:
|
|
@@ -336,6 +232,28 @@ class Encryption:
|
|
|
336
232
|
Then the LangGraph server will load your encryption file and use it to
|
|
337
233
|
encrypt/decrypt data at rest.
|
|
338
234
|
|
|
235
|
+
!!! warning "JSON Encryptors Must Preserve Keys"
|
|
236
|
+
|
|
237
|
+
JSON encryptors **must not add or remove keys** from the input dict.
|
|
238
|
+
Only values may be transformed. This constraint is **enforced at runtime
|
|
239
|
+
by the server** and exists because SQL JSONB merge operations (used for
|
|
240
|
+
partial updates) work at the key level.
|
|
241
|
+
|
|
242
|
+
**Correct (per-key encryption):**
|
|
243
|
+
```python
|
|
244
|
+
# Input: {"secret": "value", "plain": "x"}
|
|
245
|
+
# Output: {"secret": "<encrypted>", "plain": "x"} ✓ Keys preserved
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Incorrect (key consolidation):**
|
|
249
|
+
```python
|
|
250
|
+
# Input: {"secret": "value", "plain": "x"}
|
|
251
|
+
# Output: {"__encrypted__": "<blob>", "plain": "x"} ✗ Key changed
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If your encryptor needs to store auxiliary data (DEK, IV, etc.), embed it
|
|
255
|
+
within the encrypted value itself, not as separate keys.
|
|
256
|
+
|
|
339
257
|
???+ example "Basic Usage"
|
|
340
258
|
|
|
341
259
|
```python
|
|
@@ -343,89 +261,48 @@ class Encryption:
|
|
|
343
261
|
|
|
344
262
|
my_encryption = Encryption()
|
|
345
263
|
|
|
264
|
+
SKIP_FIELDS = {"tenant_id", "owner", "thread_id", "assistant_id"}
|
|
265
|
+
ENCRYPTED_PREFIX = "encrypted:"
|
|
266
|
+
|
|
346
267
|
@my_encryption.encrypt.blob
|
|
347
268
|
async def encrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
|
348
|
-
|
|
349
|
-
return encrypted_blob
|
|
269
|
+
return your_encrypt_bytes(blob)
|
|
350
270
|
|
|
351
271
|
@my_encryption.decrypt.blob
|
|
352
272
|
async def decrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
|
353
|
-
|
|
354
|
-
return decrypted_blob
|
|
273
|
+
return your_decrypt_bytes(blob)
|
|
355
274
|
|
|
356
275
|
@my_encryption.encrypt.json
|
|
357
276
|
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
encrypted = {}
|
|
363
|
-
for key, value in data.items():
|
|
364
|
-
if key.startswith("my.customer.org/"):
|
|
365
|
-
# Encrypt VALUE for sensitive customer data
|
|
366
|
-
encrypted[key] = encrypt_value(value)
|
|
277
|
+
result = {}
|
|
278
|
+
for k, v in data.items():
|
|
279
|
+
if k in SKIP_FIELDS or v is None:
|
|
280
|
+
result[k] = v
|
|
367
281
|
else:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return encrypted
|
|
282
|
+
result[k] = ENCRYPTED_PREFIX + your_encrypt_string(v)
|
|
283
|
+
return result
|
|
371
284
|
|
|
372
285
|
@my_encryption.decrypt.json
|
|
373
286
|
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
decrypted[key] = decrypt_value(value)
|
|
287
|
+
result = {}
|
|
288
|
+
for k, v in data.items():
|
|
289
|
+
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX):
|
|
290
|
+
result[k] = your_decrypt_string(v[len(ENCRYPTED_PREFIX):])
|
|
379
291
|
else:
|
|
380
|
-
|
|
381
|
-
return
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
???+ example "Model-Specific Handlers"
|
|
385
|
-
|
|
386
|
-
You can register different encryption handlers for different model types
|
|
387
|
-
(thread, assistant, run, cron, checkpoint, etc.):
|
|
388
|
-
|
|
389
|
-
```python
|
|
390
|
-
from langgraph_sdk import Encryption, EncryptionContext
|
|
391
|
-
|
|
392
|
-
my_encryption = Encryption()
|
|
393
|
-
|
|
394
|
-
# Default handler for models without specific handlers
|
|
395
|
-
@my_encryption.encrypt.json
|
|
396
|
-
async def default_encrypt(ctx: EncryptionContext, data: dict) -> dict:
|
|
397
|
-
return standard_encrypt(data)
|
|
398
|
-
|
|
399
|
-
# Thread-specific handler (uses different KMS key)
|
|
400
|
-
@my_encryption.encrypt.json.thread
|
|
401
|
-
async def encrypt_thread(ctx: EncryptionContext, data: dict) -> dict:
|
|
402
|
-
return encrypt_with_thread_key(data)
|
|
403
|
-
|
|
404
|
-
# Assistant-specific handler
|
|
405
|
-
@my_encryption.encrypt.json.assistant
|
|
406
|
-
async def encrypt_assistant(ctx: EncryptionContext, data: dict) -> dict:
|
|
407
|
-
return encrypt_with_assistant_key(data)
|
|
408
|
-
|
|
409
|
-
# Same pattern for decryption
|
|
410
|
-
@my_encryption.decrypt.json
|
|
411
|
-
async def default_decrypt(ctx: EncryptionContext, data: dict) -> dict:
|
|
412
|
-
return standard_decrypt(data)
|
|
413
|
-
|
|
414
|
-
@my_encryption.decrypt.json.thread
|
|
415
|
-
async def decrypt_thread(ctx: EncryptionContext, data: dict) -> dict:
|
|
416
|
-
return decrypt_with_thread_key(data)
|
|
292
|
+
result[k] = v
|
|
293
|
+
return result
|
|
417
294
|
```
|
|
418
295
|
|
|
419
296
|
???+ example "Field-Specific Logic"
|
|
420
297
|
|
|
421
|
-
The `ctx.field`
|
|
422
|
-
|
|
298
|
+
The `ctx.model` and `ctx.field` attributes tell you which model type and
|
|
299
|
+
specific field is being encrypted, allowing different logic:
|
|
423
300
|
|
|
424
301
|
```python
|
|
425
|
-
@my_encryption.encrypt.json
|
|
426
|
-
async def
|
|
302
|
+
@my_encryption.encrypt.json
|
|
303
|
+
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
427
304
|
if ctx.field == "metadata":
|
|
428
|
-
#
|
|
305
|
+
# Metadata - standard encryption
|
|
429
306
|
return encrypt_standard(data)
|
|
430
307
|
elif ctx.field == "values":
|
|
431
308
|
# Thread values - more sensitive, use stronger encryption
|
|
@@ -433,6 +310,49 @@ class Encryption:
|
|
|
433
310
|
else:
|
|
434
311
|
return encrypt_standard(data)
|
|
435
312
|
```
|
|
313
|
+
|
|
314
|
+
!!! warning "Model/Field May Differ Between Encrypt and Decrypt"
|
|
315
|
+
|
|
316
|
+
Data encrypted with one `(model, field)` pair is **not guaranteed**
|
|
317
|
+
to be decrypted with the same pair. The server performs SQL JSONB
|
|
318
|
+
merges that can move encrypted values between models (e.g., cron
|
|
319
|
+
metadata → run metadata). Your decryption logic must handle data
|
|
320
|
+
regardless of the `ctx.model` or `ctx.field` values at decrypt time.
|
|
321
|
+
|
|
322
|
+
**Safe:** Use `ctx.model`/`ctx.field` for logging or metrics only.
|
|
323
|
+
|
|
324
|
+
**Safe:** Encrypt different keys based on `ctx.field`, but use a
|
|
325
|
+
single decrypt handler that decrypts any value with the encrypted
|
|
326
|
+
prefix (and passes through plaintext unchanged):
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
ENCRYPTED_PREFIX = "enc:"
|
|
330
|
+
|
|
331
|
+
@my_encryption.encrypt.json
|
|
332
|
+
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
333
|
+
# Encrypt different keys depending on the field
|
|
334
|
+
if ctx.field == "context":
|
|
335
|
+
keys_to_encrypt = {"api_key", "secret_token"}
|
|
336
|
+
else:
|
|
337
|
+
keys_to_encrypt = {"email", "ssn"}
|
|
338
|
+
return {
|
|
339
|
+
k: ENCRYPTED_PREFIX + encrypt(v) if k in keys_to_encrypt else v
|
|
340
|
+
for k, v in data.items()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@my_encryption.decrypt.json
|
|
344
|
+
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
|
345
|
+
# Decrypt ANY value with the prefix, regardless of model/field
|
|
346
|
+
return {
|
|
347
|
+
k: decrypt(v[len(ENCRYPTED_PREFIX):])
|
|
348
|
+
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX)
|
|
349
|
+
else v
|
|
350
|
+
for k, v in data.items()
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Unsafe:** Using different encryption keys or algorithms based on
|
|
355
|
+
`ctx.model`/`ctx.field` will cause decryption failures.
|
|
436
356
|
"""
|
|
437
357
|
|
|
438
358
|
__slots__ = (
|
|
@@ -440,9 +360,7 @@ class Encryption:
|
|
|
440
360
|
"_blob_encryptor",
|
|
441
361
|
"_context_handler",
|
|
442
362
|
"_json_decryptor",
|
|
443
|
-
"_json_decryptors",
|
|
444
363
|
"_json_encryptor",
|
|
445
|
-
"_json_encryptors",
|
|
446
364
|
"decrypt",
|
|
447
365
|
"encrypt",
|
|
448
366
|
)
|
|
@@ -464,8 +382,6 @@ class Encryption:
|
|
|
464
382
|
self._blob_decryptor: types.BlobDecryptor | None = None
|
|
465
383
|
self._json_encryptor: types.JsonEncryptor | None = None
|
|
466
384
|
self._json_decryptor: types.JsonDecryptor | None = None
|
|
467
|
-
self._json_encryptors: dict[str, types.JsonEncryptor] = {}
|
|
468
|
-
self._json_decryptors: dict[str, types.JsonDecryptor] = {}
|
|
469
385
|
self._context_handler: types.ContextHandler | None = None
|
|
470
386
|
|
|
471
387
|
def context(self, fn: types.ContextHandler) -> types.ContextHandler:
|
|
@@ -506,33 +422,33 @@ class Encryption:
|
|
|
506
422
|
return fn
|
|
507
423
|
|
|
508
424
|
def get_json_encryptor(
|
|
509
|
-
self,
|
|
425
|
+
self,
|
|
426
|
+
_model: str | None = None, # kept for langgraph-api compat
|
|
510
427
|
) -> types.JsonEncryptor | None:
|
|
511
|
-
"""Get the JSON encryptor
|
|
428
|
+
"""Get the JSON encryptor.
|
|
512
429
|
|
|
513
430
|
Args:
|
|
514
|
-
|
|
431
|
+
_model: Ignored. Kept for backwards compatibility with langgraph-api
|
|
432
|
+
which passes model_type to this method.
|
|
515
433
|
|
|
516
434
|
Returns:
|
|
517
|
-
|
|
435
|
+
The JSON encryptor, or None if not registered.
|
|
518
436
|
"""
|
|
519
|
-
if model and model in self._json_encryptors:
|
|
520
|
-
return self._json_encryptors[model]
|
|
521
437
|
return self._json_encryptor
|
|
522
438
|
|
|
523
439
|
def get_json_decryptor(
|
|
524
|
-
self,
|
|
440
|
+
self,
|
|
441
|
+
_model: str | None = None, # kept for langgraph-api compat
|
|
525
442
|
) -> types.JsonDecryptor | None:
|
|
526
|
-
"""Get the JSON decryptor
|
|
443
|
+
"""Get the JSON decryptor.
|
|
527
444
|
|
|
528
445
|
Args:
|
|
529
|
-
|
|
446
|
+
_model: Ignored. Kept for backwards compatibility with langgraph-api
|
|
447
|
+
which passes model_type to this method.
|
|
530
448
|
|
|
531
449
|
Returns:
|
|
532
|
-
|
|
450
|
+
The JSON decryptor, or None if not registered.
|
|
533
451
|
"""
|
|
534
|
-
if model and model in self._json_decryptors:
|
|
535
|
-
return self._json_decryptors[model]
|
|
536
452
|
return self._json_decryptor
|
|
537
453
|
|
|
538
454
|
def __repr__(self) -> str:
|
|
@@ -545,10 +461,6 @@ class Encryption:
|
|
|
545
461
|
handlers.append("json_encryptor")
|
|
546
462
|
if self._json_decryptor:
|
|
547
463
|
handlers.append("json_decryptor")
|
|
548
|
-
if self._json_encryptors:
|
|
549
|
-
handlers.append(f"json_encryptors({list(self._json_encryptors.keys())})")
|
|
550
|
-
if self._json_decryptors:
|
|
551
|
-
handlers.append(f"json_decryptors({list(self._json_decryptors.keys())})")
|
|
552
464
|
if self._context_handler:
|
|
553
465
|
handlers.append("context_handler")
|
|
554
466
|
return f"Encryption(handlers=[{', '.join(handlers)}])"
|
|
@@ -354,6 +354,8 @@ class Cron(TypedDict):
|
|
|
354
354
|
"""The ID of the assistant."""
|
|
355
355
|
thread_id: str | None
|
|
356
356
|
"""The ID of the thread."""
|
|
357
|
+
on_run_completed: OnCompletionBehavior | None
|
|
358
|
+
"""What to do with the thread after the run completes. Only applicable for stateless crons."""
|
|
357
359
|
end_time: datetime | None
|
|
358
360
|
"""The end date to stop running the cron."""
|
|
359
361
|
schedule: str
|
|
@@ -26,14 +26,6 @@ class TestHandlerValidation:
|
|
|
26
26
|
async def json_dec(_ctx, data):
|
|
27
27
|
return data
|
|
28
28
|
|
|
29
|
-
@encryption.encrypt.json.thread
|
|
30
|
-
async def thread_enc(_ctx, data):
|
|
31
|
-
return data
|
|
32
|
-
|
|
33
|
-
@encryption.decrypt.json.custom
|
|
34
|
-
async def custom_dec(_ctx, data):
|
|
35
|
-
return data
|
|
36
|
-
|
|
37
29
|
# All duplicates should raise
|
|
38
30
|
with pytest.raises(DuplicateHandlerError):
|
|
39
31
|
|
|
@@ -59,18 +51,6 @@ class TestHandlerValidation:
|
|
|
59
51
|
async def dup(_ctx, data):
|
|
60
52
|
return data
|
|
61
53
|
|
|
62
|
-
with pytest.raises(DuplicateHandlerError):
|
|
63
|
-
|
|
64
|
-
@encryption.encrypt.json.thread
|
|
65
|
-
async def dup(_ctx, data):
|
|
66
|
-
return data
|
|
67
|
-
|
|
68
|
-
with pytest.raises(DuplicateHandlerError):
|
|
69
|
-
|
|
70
|
-
@encryption.decrypt.json.custom
|
|
71
|
-
async def dup(_ctx, data):
|
|
72
|
-
return data
|
|
73
|
-
|
|
74
54
|
def test_handlers_must_be_async(self):
|
|
75
55
|
"""Sync functions raise TypeError."""
|
|
76
56
|
encryption = Encryption()
|
langgraph_sdk-0.3.0/.gitignore
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
.vs/
|
|
2
|
-
.vscode/
|
|
3
|
-
.idea/
|
|
4
|
-
# Byte-compiled / optimized / DLL files
|
|
5
|
-
__pycache__/
|
|
6
|
-
*.py[cod]
|
|
7
|
-
*$py.class
|
|
8
|
-
|
|
9
|
-
# C extensions
|
|
10
|
-
*.so
|
|
11
|
-
|
|
12
|
-
# Distribution / packaging
|
|
13
|
-
.Python
|
|
14
|
-
build/
|
|
15
|
-
develop-eggs/
|
|
16
|
-
dist/
|
|
17
|
-
downloads/
|
|
18
|
-
eggs/
|
|
19
|
-
.eggs/
|
|
20
|
-
lib/
|
|
21
|
-
lib64/
|
|
22
|
-
parts/
|
|
23
|
-
sdist/
|
|
24
|
-
var/
|
|
25
|
-
wheels/
|
|
26
|
-
pip-wheel-metadata/
|
|
27
|
-
share/python-wheels/
|
|
28
|
-
*.egg-info/
|
|
29
|
-
.installed.cfg
|
|
30
|
-
*.egg
|
|
31
|
-
MANIFEST
|
|
32
|
-
|
|
33
|
-
# PyInstaller
|
|
34
|
-
# Usually these files are written by a python script from a template
|
|
35
|
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
-
*.manifest
|
|
37
|
-
*.spec
|
|
38
|
-
|
|
39
|
-
# Installer logs
|
|
40
|
-
pip-log.txt
|
|
41
|
-
pip-delete-this-directory.txt
|
|
42
|
-
|
|
43
|
-
# Unit test / coverage reports
|
|
44
|
-
htmlcov/
|
|
45
|
-
.tox/
|
|
46
|
-
.nox/
|
|
47
|
-
.coverage
|
|
48
|
-
.coverage.*
|
|
49
|
-
.cache
|
|
50
|
-
nosetests.xml
|
|
51
|
-
coverage.xml
|
|
52
|
-
*.cover
|
|
53
|
-
*.py,cover
|
|
54
|
-
.hypothesis/
|
|
55
|
-
.pytest_cache/
|
|
56
|
-
|
|
57
|
-
# Translations
|
|
58
|
-
*.mo
|
|
59
|
-
*.pot
|
|
60
|
-
|
|
61
|
-
# Django stuff:
|
|
62
|
-
*.log
|
|
63
|
-
local_settings.py
|
|
64
|
-
db.sqlite3
|
|
65
|
-
db.sqlite3-journal
|
|
66
|
-
|
|
67
|
-
# Flask stuff:
|
|
68
|
-
instance/
|
|
69
|
-
.webassets-cache
|
|
70
|
-
|
|
71
|
-
# Scrapy stuff:
|
|
72
|
-
.scrapy
|
|
73
|
-
|
|
74
|
-
# Sphinx documentation
|
|
75
|
-
docs/_build/
|
|
76
|
-
docs/docs/_build/
|
|
77
|
-
|
|
78
|
-
# PyBuilder
|
|
79
|
-
target/
|
|
80
|
-
|
|
81
|
-
# Jupyter Notebook
|
|
82
|
-
.ipynb_checkpoints
|
|
83
|
-
notebooks/
|
|
84
|
-
|
|
85
|
-
# IPython
|
|
86
|
-
profile_default/
|
|
87
|
-
ipython_config.py
|
|
88
|
-
|
|
89
|
-
# pyenv
|
|
90
|
-
.python-version
|
|
91
|
-
|
|
92
|
-
# pipenv
|
|
93
|
-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
-
# install all needed dependencies.
|
|
97
|
-
#Pipfile.lock
|
|
98
|
-
|
|
99
|
-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
100
|
-
__pypackages__/
|
|
101
|
-
|
|
102
|
-
# Celery stuff
|
|
103
|
-
celerybeat-schedule
|
|
104
|
-
celerybeat.pid
|
|
105
|
-
|
|
106
|
-
# SageMath parsed files
|
|
107
|
-
*.sage.py
|
|
108
|
-
|
|
109
|
-
# Environments
|
|
110
|
-
.env
|
|
111
|
-
.envrc
|
|
112
|
-
.venv
|
|
113
|
-
.venvs
|
|
114
|
-
env/
|
|
115
|
-
venv/
|
|
116
|
-
ENV/
|
|
117
|
-
env.bak/
|
|
118
|
-
venv.bak/
|
|
119
|
-
|
|
120
|
-
# Spyder project settings
|
|
121
|
-
.spyderproject
|
|
122
|
-
.spyproject
|
|
123
|
-
|
|
124
|
-
# Rope project settings
|
|
125
|
-
.ropeproject
|
|
126
|
-
|
|
127
|
-
# mkdocs documentation
|
|
128
|
-
/site
|
|
129
|
-
|
|
130
|
-
# mypy
|
|
131
|
-
.mypy_cache/
|
|
132
|
-
.dmypy.json
|
|
133
|
-
dmypy.json
|
|
134
|
-
|
|
135
|
-
# Pyre type checker
|
|
136
|
-
.pyre/
|
|
137
|
-
|
|
138
|
-
# macOS display setting files
|
|
139
|
-
.DS_Store
|
|
140
|
-
|
|
141
|
-
# Wandb directory
|
|
142
|
-
wandb/
|
|
143
|
-
|
|
144
|
-
# asdf tool versions
|
|
145
|
-
.tool-versions
|
|
146
|
-
/.ruff_cache/
|
|
147
|
-
|
|
148
|
-
*.pkl
|
|
149
|
-
*.bin
|
|
150
|
-
|
|
151
|
-
# integration test artifacts
|
|
152
|
-
data_map*
|
|
153
|
-
\[('_type', 'fake'), ('stop', None)]
|
|
154
|
-
|
|
155
|
-
# Replit files
|
|
156
|
-
*replit*
|
|
157
|
-
|
|
158
|
-
node_modules
|
|
159
|
-
docs/.yarn/
|
|
160
|
-
docs/node_modules/
|
|
161
|
-
docs/.docusaurus/
|
|
162
|
-
docs/.cache-loader/
|
|
163
|
-
docs/_dist
|
|
164
|
-
docs/api_reference/api_reference.rst
|
|
165
|
-
docs/api_reference/experimental_api_reference.rst
|
|
166
|
-
docs/api_reference/_build
|
|
167
|
-
docs/api_reference/*/
|
|
168
|
-
!docs/api_reference/_static/
|
|
169
|
-
!docs/api_reference/templates/
|
|
170
|
-
!docs/api_reference/themes/
|
|
171
|
-
docs/docs_skeleton/build
|
|
172
|
-
docs/docs_skeleton/node_modules
|
|
173
|
-
docs/docs_skeleton/yarn.lock
|
|
174
|
-
|
|
175
|
-
# Any new jupyter notebooks
|
|
176
|
-
# not intended for the repo
|
|
177
|
-
Untitled*.ipynb
|
|
178
|
-
|
|
179
|
-
Chinook.db
|
|
180
|
-
|
|
181
|
-
.vercel
|
|
182
|
-
.turbo
|
|
183
|
-
.editorconfig
|
|
184
|
-
.scratch
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import get_args
|
|
6
|
-
|
|
7
|
-
from langgraph_sdk.schema import (
|
|
8
|
-
AssistantSelectField,
|
|
9
|
-
CronSelectField,
|
|
10
|
-
RunSelectField,
|
|
11
|
-
ThreadSelectField,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@functools.cache
|
|
18
|
-
def _load_spec() -> dict:
|
|
19
|
-
with (
|
|
20
|
-
Path(current_dir).parents[2]
|
|
21
|
-
/ "docs"
|
|
22
|
-
/ "docs"
|
|
23
|
-
/ "cloud"
|
|
24
|
-
/ "reference"
|
|
25
|
-
/ "api"
|
|
26
|
-
/ "openapi.json"
|
|
27
|
-
).open() as f:
|
|
28
|
-
return json.load(f)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _enum_from_request_select(spec: dict, path: str, method: str) -> set[str]:
|
|
32
|
-
schema = spec["paths"][path][method]["requestBody"]["content"]["application/json"][
|
|
33
|
-
"schema"
|
|
34
|
-
]
|
|
35
|
-
if "properties" in schema:
|
|
36
|
-
props = schema["properties"]
|
|
37
|
-
elif "$ref" in schema:
|
|
38
|
-
component = spec
|
|
39
|
-
index = schema["$ref"].split("/")[1:]
|
|
40
|
-
for part in index:
|
|
41
|
-
component = component[part]
|
|
42
|
-
props = component["properties"]
|
|
43
|
-
else:
|
|
44
|
-
raise ValueError(f"Unknown schema: {schema}")
|
|
45
|
-
sel = props["select"]
|
|
46
|
-
return set(sel["items"]["enum"])
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _enum_from_query_select(spec: dict, path: str, method: str) -> set[str]:
|
|
50
|
-
params = spec["paths"][path][method]["parameters"]
|
|
51
|
-
sel = next(p for p in params if p["name"] == "select")
|
|
52
|
-
return set(sel["schema"]["items"]["enum"])
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_assistants_select_enum_matches_sdk():
|
|
56
|
-
spec = _load_spec()
|
|
57
|
-
expected = set(get_args(AssistantSelectField))
|
|
58
|
-
assert _enum_from_request_select(spec, "/assistants/search", "post") == expected
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_threads_select_enum_matches_sdk():
|
|
62
|
-
spec = _load_spec()
|
|
63
|
-
expected = set(get_args(ThreadSelectField))
|
|
64
|
-
assert _enum_from_request_select(spec, "/threads/search", "post") == expected
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_runs_select_enum_matches_sdk():
|
|
68
|
-
spec = _load_spec()
|
|
69
|
-
expected = set(get_args(RunSelectField))
|
|
70
|
-
assert _enum_from_query_select(spec, "/threads/{thread_id}/runs", "get") == expected
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_crons_select_enum_matches_sdk():
|
|
74
|
-
spec = _load_spec()
|
|
75
|
-
expected = set(get_args(CronSelectField))
|
|
76
|
-
assert _enum_from_request_select(spec, "/runs/crons/search", "post") == expected
|
|
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
|