vellum-ai 0.14.6__py3-none-any.whl → 0.14.8__py3-none-any.whl

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 (65) hide show
  1. vellum/__init__.py +14 -0
  2. vellum/client/core/client_wrapper.py +1 -1
  3. vellum/client/types/__init__.py +14 -0
  4. vellum/client/types/array_chat_message_content_item.py +6 -1
  5. vellum/client/types/array_chat_message_content_item_request.py +2 -0
  6. vellum/client/types/chat_message_content.py +2 -0
  7. vellum/client/types/chat_message_content_request.py +2 -0
  8. vellum/client/types/document_chat_message_content.py +25 -0
  9. vellum/client/types/document_chat_message_content_request.py +25 -0
  10. vellum/client/types/document_prompt_block.py +29 -0
  11. vellum/client/types/document_vellum_value.py +25 -0
  12. vellum/client/types/document_vellum_value_request.py +25 -0
  13. vellum/client/types/prompt_block.py +2 -0
  14. vellum/client/types/vellum_document.py +20 -0
  15. vellum/client/types/vellum_document_request.py +20 -0
  16. vellum/client/types/vellum_value.py +2 -0
  17. vellum/client/types/vellum_value_request.py +2 -0
  18. vellum/client/types/vellum_variable_type.py +1 -0
  19. vellum/types/document_chat_message_content.py +3 -0
  20. vellum/types/document_chat_message_content_request.py +3 -0
  21. vellum/types/document_prompt_block.py +3 -0
  22. vellum/types/document_vellum_value.py +3 -0
  23. vellum/types/document_vellum_value_request.py +3 -0
  24. vellum/types/vellum_document.py +3 -0
  25. vellum/types/vellum_document_request.py +3 -0
  26. vellum/workflows/exceptions.py +18 -0
  27. vellum/workflows/inputs/base.py +27 -1
  28. vellum/workflows/inputs/tests/__init__.py +0 -0
  29. vellum/workflows/inputs/tests/test_inputs.py +49 -0
  30. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -1
  31. vellum/workflows/nodes/core/map_node/node.py +7 -7
  32. vellum/workflows/nodes/core/try_node/node.py +1 -1
  33. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +33 -0
  34. vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
  35. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +2 -2
  36. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +5 -3
  37. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +5 -4
  38. vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py +4 -4
  39. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +39 -15
  40. vellum/workflows/nodes/displayable/subworkflow_deployment_node/tests/test_node.py +142 -0
  41. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +3 -1
  42. vellum/workflows/outputs/base.py +1 -1
  43. vellum/workflows/runner/runner.py +16 -10
  44. vellum/workflows/state/context.py +7 -7
  45. vellum/workflows/workflows/base.py +16 -5
  46. vellum/workflows/workflows/tests/test_base_workflow.py +131 -40
  47. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/METADATA +1 -1
  48. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/RECORD +65 -47
  49. vellum_cli/__init__.py +43 -0
  50. vellum_cli/config.py +1 -0
  51. vellum_cli/init.py +132 -0
  52. vellum_cli/pull.py +7 -3
  53. vellum_cli/tests/test_init.py +473 -0
  54. vellum_cli/tests/test_pull.py +135 -0
  55. vellum_cli/tests/test_push.py +1 -0
  56. vellum_ee/workflows/display/nodes/base_node_display.py +4 -4
  57. vellum_ee/workflows/display/tests/test_vellum_workflow_display.py +83 -0
  58. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +118 -3
  59. vellum_ee/workflows/display/vellum.py +0 -4
  60. vellum_ee/workflows/display/workflows/base_workflow_display.py +46 -13
  61. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +29 -0
  62. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +12 -0
  63. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/LICENSE +0 -0
  64. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/WHEEL +0 -0
  65. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,473 @@
1
+ import io
2
+ import json
3
+ import os
4
+ from unittest.mock import patch
5
+ import zipfile
6
+
7
+ from click.testing import CliRunner
8
+ from pydash import snake_case
9
+
10
+ from vellum_cli import main as cli_main
11
+
12
+
13
+ def _zip_file_map(file_map: dict[str, str]) -> bytes:
14
+ # Create an in-memory bytes buffer to store the zip
15
+ zip_buffer = io.BytesIO()
16
+
17
+ # Create zip file and add files from file_map
18
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
19
+ for filename, content in file_map.items():
20
+ zip_file.writestr(filename, content)
21
+
22
+ # Get the bytes from the buffer
23
+ zip_bytes = zip_buffer.getvalue()
24
+ zip_buffer.close()
25
+
26
+ return zip_bytes
27
+
28
+
29
+ class MockTemplate:
30
+ def __init__(self, id, label):
31
+ self.id = id
32
+ self.label = label
33
+
34
+
35
+ def test_init_command(vellum_client, mock_module):
36
+ # GIVEN a module on the user's filesystem
37
+ temp_dir = mock_module.temp_dir
38
+ mock_module.set_pyproject_toml({"workflows": []})
39
+ # GIVEN the vellum client returns a list of template workflows
40
+ fake_templates = [
41
+ MockTemplate(id="template-1", label="Example Workflow"),
42
+ MockTemplate(id="template-2", label="Another Workflow"),
43
+ ]
44
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
45
+
46
+ # AND the workflow pull API call returns a zip file
47
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
48
+
49
+ # WHEN the user runs the `init` command and selects the first template
50
+ runner = CliRunner()
51
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
52
+
53
+ # THEN the command returns successfully
54
+ assert result.exit_code == 0
55
+
56
+ # AND `vellum_client.workflows.pull` is called with the selected template ID
57
+ vellum_client.workflows.pull.assert_called_once_with(
58
+ "template-1",
59
+ request_options={"additional_query_parameters": {"include_sandbox": True}},
60
+ )
61
+
62
+ # AND the `workflow.py` file should be created in the correct module directory
63
+ workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
64
+ assert os.path.exists(workflow_py)
65
+ with open(workflow_py) as f:
66
+ assert f.read() == "print('hello')"
67
+
68
+ # AND the vellum.lock.json file should be created
69
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
70
+ assert os.path.exists(vellum_lock_json)
71
+ with open(vellum_lock_json) as f:
72
+ lock_data = json.load(f)
73
+ assert lock_data["workflows"] == [
74
+ {
75
+ "module": "example_workflow",
76
+ "workflow_sandbox_id": "template-1",
77
+ "ignore": None,
78
+ "deployments": [],
79
+ "container_image_name": None,
80
+ "container_image_tag": None,
81
+ "workspace": "default",
82
+ "target_directory": None,
83
+ }
84
+ ]
85
+
86
+
87
+ def test_init_command__invalid_template_id(vellum_client, mock_module):
88
+ # GIVEN a module on the user's filesystem
89
+ temp_dir = mock_module.temp_dir
90
+ mock_module.set_pyproject_toml({"workflows": []})
91
+ # GIVEN the vellum client returns a list of template workflows
92
+ fake_templates = [
93
+ MockTemplate(id="template-1", label="Example Workflow"),
94
+ MockTemplate(id="template-2", label="Another Workflow"),
95
+ ]
96
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
97
+
98
+ # WHEN the user runs the `init` command, enters invalid input and then cancels
99
+ runner = CliRunner()
100
+ # Mock click.prompt to raise a KeyboardInterrupt (simulating Ctrl+C)
101
+ with patch("click.prompt", side_effect=KeyboardInterrupt):
102
+ runner = CliRunner()
103
+ result = runner.invoke(cli_main, ["workflows", "init"])
104
+
105
+ # THEN the command is aborted
106
+ assert result.exit_code != 0
107
+ assert "Aborted!" in result.output # Click shows this message on Ctrl+C
108
+
109
+ # AND `vellum_client.workflows.pull` is not called
110
+ vellum_client.workflows.pull.assert_not_called()
111
+
112
+ # AND no workflow files are created
113
+ workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
114
+ assert not os.path.exists(workflow_py)
115
+
116
+ # AND the lock file remains empty
117
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
118
+ if os.path.exists(vellum_lock_json):
119
+ with open(vellum_lock_json) as f:
120
+ lock_data = json.load(f)
121
+ assert lock_data["workflows"] == []
122
+
123
+
124
+ def test_init_command__no_templates(vellum_client, mock_module):
125
+ # GIVEN a module on the user's filesystem
126
+ temp_dir = mock_module.temp_dir
127
+ mock_module.set_pyproject_toml({"workflows": []})
128
+ # GIVEN the vellum client returns no template workflows
129
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = []
130
+
131
+ # WHEN the user runs the `init` command
132
+ runner = CliRunner()
133
+ result = runner.invoke(cli_main, ["workflows", "init"])
134
+
135
+ # THEN the command gracefully exits
136
+ assert result.exit_code == 0
137
+ assert "No templates available" in result.output
138
+
139
+ # AND `vellum_client.workflows.pull` is not called
140
+ vellum_client.workflows.pull.assert_not_called()
141
+
142
+ # AND no workflow files are created
143
+ workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
144
+ assert not os.path.exists(workflow_py)
145
+
146
+ # AND the lock file remains empty
147
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
148
+ if os.path.exists(vellum_lock_json):
149
+ with open(vellum_lock_json) as f:
150
+ lock_data = json.load(f)
151
+ assert lock_data["workflows"] == []
152
+
153
+
154
+ def test_init_command_target_directory_exists(vellum_client, mock_module):
155
+ """
156
+ GIVEN a target directory already exists
157
+ WHEN the user tries to run the `init` command
158
+ THEN the command should fail and exit without modifying existing files
159
+ """
160
+ temp_dir = mock_module.temp_dir
161
+ existing_workflow_dir = os.path.join(temp_dir, "example_workflow")
162
+
163
+ # Create the target directory to simulate it already existing
164
+ os.makedirs(existing_workflow_dir, exist_ok=True)
165
+
166
+ # Ensure directory exists before command execution
167
+ assert os.path.exists(existing_workflow_dir)
168
+
169
+ # GIVEN the vellum client returns a list of template workflows
170
+ fake_templates = [
171
+ MockTemplate(id="template-1", label="Example Workflow"),
172
+ ]
173
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
174
+
175
+ # AND the workflow pull API call returns a zip file
176
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
177
+
178
+ # WHEN the user runs the `init` command and selects the template
179
+ runner = CliRunner()
180
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
181
+
182
+ # THEN the command should detect the existing directory and abort
183
+ assert result.exit_code == 0
184
+ assert f"{existing_workflow_dir} already exists." in result.output
185
+
186
+ # Ensure the directory still exists (wasn't deleted or modified)
187
+ assert os.path.exists(existing_workflow_dir)
188
+
189
+ # AND `vellum_client.workflows.pull` is not called
190
+ vellum_client.workflows.pull.assert_not_called()
191
+
192
+ # AND no workflow files are created
193
+ workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
194
+ assert not os.path.exists(workflow_py)
195
+
196
+ # AND the lock file remains empty
197
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
198
+ if os.path.exists(vellum_lock_json):
199
+ with open(vellum_lock_json) as f:
200
+ lock_data = json.load(f)
201
+ assert lock_data["workflows"] == []
202
+
203
+
204
+ def test_init_command_with_template_name(vellum_client, mock_module):
205
+ # GIVEN a module on the user's filesystem
206
+ temp_dir = mock_module.temp_dir
207
+ mock_module.set_pyproject_toml({"workflows": []})
208
+
209
+ # GIVEN the vellum client returns a list of template workflows
210
+ fake_templates = [
211
+ MockTemplate(id="template-1", label="Example Workflow"),
212
+ MockTemplate(id="template-2", label="Another Workflow"),
213
+ ]
214
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
215
+
216
+ # AND the workflow pull API call returns a zip file
217
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
218
+
219
+ # WHEN the user runs the `init` command with a specific template name
220
+ template_name = snake_case("Another Workflow")
221
+ runner = CliRunner()
222
+ result = runner.invoke(cli_main, ["workflows", "init", template_name])
223
+
224
+ # THEN the command returns successfully
225
+ assert result.exit_code == 0
226
+
227
+ # AND `vellum_client.workflows.pull` is called with the correct template ID
228
+ vellum_client.workflows.pull.assert_called_once_with(
229
+ "template-2", # ID of "Another Workflow"
230
+ request_options={"additional_query_parameters": {"include_sandbox": True}},
231
+ )
232
+
233
+ # AND the workflow files should be created in the correct module directory
234
+ workflow_py = os.path.join(temp_dir, "another_workflow", "workflow.py")
235
+
236
+ assert os.path.exists(workflow_py)
237
+
238
+ with open(workflow_py) as f:
239
+ assert f.read() == "print('hello')"
240
+
241
+ # AND the vellum.lock.json file should be created with the correct data
242
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
243
+ assert os.path.exists(vellum_lock_json)
244
+
245
+ with open(vellum_lock_json) as f:
246
+ lock_data = json.load(f)
247
+ assert lock_data["workflows"] == [
248
+ {
249
+ "module": "another_workflow",
250
+ "workflow_sandbox_id": "template-2",
251
+ "ignore": None,
252
+ "deployments": [],
253
+ "container_image_name": None,
254
+ "container_image_tag": None,
255
+ "workspace": "default",
256
+ "target_directory": None,
257
+ }
258
+ ]
259
+
260
+
261
+ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module):
262
+ # GIVEN a module on the user's filesystem
263
+ temp_dir = mock_module.temp_dir
264
+ mock_module.set_pyproject_toml({"workflows": []})
265
+
266
+ # GIVEN the vellum client returns a list of template workflows
267
+ fake_templates = [
268
+ MockTemplate(id="template-1", label="Example Workflow"),
269
+ MockTemplate(id="template-2", label="Another Workflow"),
270
+ ]
271
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
272
+
273
+ # WHEN the user runs the `init` command with a non-existent template name
274
+ nonexistent_template = "nonexistent_template"
275
+ runner = CliRunner()
276
+ result = runner.invoke(cli_main, ["workflows", "init", nonexistent_template])
277
+
278
+ # THEN the command should indicate the template was not found
279
+ assert result.exit_code == 0
280
+ assert f"Template {nonexistent_template} not found" in result.output
281
+
282
+ # AND `vellum_client.workflows.pull` is not called
283
+ vellum_client.workflows.pull.assert_not_called()
284
+
285
+ # AND no workflow files are created
286
+ example_workflow_dir = os.path.join(temp_dir, "example_workflow")
287
+ nonexistent_workflow_dir = os.path.join(temp_dir, nonexistent_template)
288
+
289
+ assert not os.path.exists(example_workflow_dir)
290
+ assert not os.path.exists(nonexistent_workflow_dir)
291
+
292
+ # AND the lock file remains empty
293
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
294
+ if os.path.exists(vellum_lock_json):
295
+ with open(vellum_lock_json) as f:
296
+ lock_data = json.load(f)
297
+ assert lock_data["workflows"] == []
298
+
299
+
300
+ def test_init__with_target_dir(vellum_client, mock_module):
301
+ # GIVEN a module on the user's filesystem
302
+ temp_dir = mock_module.temp_dir
303
+ mock_module.set_pyproject_toml({"workflows": []})
304
+
305
+ # GIVEN the vellum client returns a list of template workflows
306
+ fake_templates = [
307
+ MockTemplate(id="template-1", label="Example Workflow"),
308
+ ]
309
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
310
+
311
+ # AND the workflow pull API call returns a zip file
312
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
313
+
314
+ # AND a target directory
315
+ target_dir = os.path.join(temp_dir, "dir")
316
+ os.makedirs(target_dir, exist_ok=True)
317
+
318
+ # WHEN the user runs the init command with target-dir
319
+ runner = CliRunner()
320
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", target_dir], input="1\n")
321
+
322
+ # THEN the command returns successfully
323
+ assert result.exit_code == 0
324
+
325
+ # AND the `workflow.py` file should be created in the target directory
326
+ module_path = os.path.join(target_dir, "example_workflow")
327
+ workflow_py = os.path.join(module_path, "workflow.py")
328
+ assert os.path.exists(workflow_py)
329
+ with open(workflow_py) as f:
330
+ assert f.read() == "print('hello')"
331
+
332
+ # AND the files are not in the default module directory
333
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
334
+ assert not os.path.exists(default_module_path)
335
+
336
+ # AND the vellum.lock.json file should be created in the original directory
337
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
338
+ assert os.path.exists(vellum_lock_json)
339
+ with open(vellum_lock_json) as f:
340
+ lock_data = json.load(f)
341
+ assert lock_data["workflows"] == [
342
+ {
343
+ "module": "example_workflow",
344
+ "workflow_sandbox_id": "template-1",
345
+ "ignore": None,
346
+ "deployments": [],
347
+ "container_image_name": None,
348
+ "container_image_tag": None,
349
+ "workspace": "default",
350
+ "target_directory": module_path,
351
+ }
352
+ ]
353
+
354
+
355
+ def test_init__with_nested_target_dir(vellum_client, mock_module):
356
+ # GIVEN a module on the user's filesystem
357
+ temp_dir = mock_module.temp_dir
358
+ mock_module.set_pyproject_toml({"workflows": []})
359
+
360
+ # GIVEN the vellum client returns a list of template workflows
361
+ fake_templates = [
362
+ MockTemplate(id="template-1", label="Example Workflow"),
363
+ ]
364
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
365
+
366
+ # AND the workflow pull API call returns a zip file
367
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
368
+
369
+ # AND a nested target directory that doesn't exist yet
370
+ nested_target_dir = os.path.join(temp_dir, "dir-1", "dir-2")
371
+
372
+ # WHEN the user runs the init command with nested target-dir
373
+ runner = CliRunner()
374
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", nested_target_dir], input="1\n")
375
+
376
+ # THEN the command returns successfully
377
+ assert result.exit_code == 0
378
+
379
+ # AND the nested directory with module subdirectory should be created
380
+ module_path = os.path.join(nested_target_dir, "example_workflow")
381
+ assert os.path.exists(module_path)
382
+
383
+ # AND the workflow.py file is written to the nested target directory
384
+ workflow_py = os.path.join(module_path, "workflow.py")
385
+ assert os.path.exists(workflow_py)
386
+ with open(workflow_py) as f:
387
+ assert f.read() == "print('hello')"
388
+
389
+ # AND the files are not in the default module directory
390
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
391
+ assert not os.path.exists(default_module_path)
392
+
393
+ # AND the vellum.lock.json file is still updated
394
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
395
+ assert os.path.exists(vellum_lock_json)
396
+ with open(vellum_lock_json) as f:
397
+ lock_data = json.load(f)
398
+ assert lock_data["workflows"] == [
399
+ {
400
+ "module": "example_workflow",
401
+ "workflow_sandbox_id": "template-1",
402
+ "ignore": None,
403
+ "deployments": [],
404
+ "container_image_name": None,
405
+ "container_image_tag": None,
406
+ "workspace": "default",
407
+ "target_directory": module_path,
408
+ }
409
+ ]
410
+
411
+
412
+ def test_init__with_template_name_and_target_dir(vellum_client, mock_module):
413
+ # GIVEN a module on the user's filesystem
414
+ temp_dir = mock_module.temp_dir
415
+ mock_module.set_pyproject_toml({"workflows": []})
416
+
417
+ # GIVEN the vellum client returns a list of template workflows
418
+ fake_templates = [
419
+ MockTemplate(id="template-1", label="Example Workflow"),
420
+ MockTemplate(id="template-2", label="Another Workflow"),
421
+ ]
422
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
423
+
424
+ # AND the workflow pull API call returns a zip file
425
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
426
+
427
+ # AND a target directory
428
+ target_dir = os.path.join(temp_dir, "dir")
429
+ os.makedirs(target_dir, exist_ok=True)
430
+
431
+ # WHEN the user runs the init command with a specific template name and target-dir
432
+ template_name = snake_case("Another Workflow")
433
+ runner = CliRunner()
434
+ result = runner.invoke(cli_main, ["workflows", "init", template_name, "--target-dir", target_dir])
435
+
436
+ # THEN the command returns successfully
437
+ assert result.exit_code == 0
438
+
439
+ # AND `vellum_client.workflows.pull` is called with the correct template ID
440
+ vellum_client.workflows.pull.assert_called_once_with(
441
+ "template-2", # ID of "Another Workflow"
442
+ request_options={"additional_query_parameters": {"include_sandbox": True}},
443
+ )
444
+
445
+ # AND the workflow files should be created in the target directory with the correct module subdirectory
446
+ module_path = os.path.join(target_dir, "another_workflow")
447
+ workflow_py = os.path.join(module_path, "workflow.py")
448
+ assert os.path.exists(workflow_py)
449
+ with open(workflow_py) as f:
450
+ assert f.read() == "print('hello')"
451
+
452
+ # AND the files are not in the default module directory
453
+ default_module_path = os.path.join(temp_dir, "another_workflow", "workflow.py")
454
+ assert not os.path.exists(default_module_path)
455
+
456
+ # AND the vellum.lock.json file should be created with the correct data
457
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
458
+ assert os.path.exists(vellum_lock_json)
459
+
460
+ with open(vellum_lock_json) as f:
461
+ lock_data = json.load(f)
462
+ assert lock_data["workflows"] == [
463
+ {
464
+ "module": "another_workflow",
465
+ "workflow_sandbox_id": "template-2",
466
+ "ignore": None,
467
+ "deployments": [],
468
+ "container_image_name": None,
469
+ "container_image_tag": None,
470
+ "workspace": "default",
471
+ "target_directory": module_path,
472
+ }
473
+ ]
@@ -73,6 +73,7 @@ def test_pull(vellum_client, mock_module, base_command):
73
73
  "ignore": None,
74
74
  "deployments": [],
75
75
  "workspace": "default",
76
+ "target_directory": None,
76
77
  }
77
78
  ],
78
79
  "workspaces": [],
@@ -112,6 +113,135 @@ def test_pull__second_module(vellum_client, mock_module):
112
113
  assert f.read() == "print('hello')"
113
114
 
114
115
 
116
+ @pytest.mark.parametrize(
117
+ "base_command",
118
+ [
119
+ ["pull"],
120
+ ["workflows", "pull"],
121
+ ],
122
+ ids=["pull", "workflows_pull"],
123
+ )
124
+ def test_pull__with_target_dir(vellum_client, mock_module, base_command):
125
+ # GIVEN a module on the user's filesystem
126
+ temp_dir = mock_module.temp_dir
127
+ module = mock_module.module
128
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
129
+
130
+ # AND a target directory
131
+ target_dir = os.path.join(temp_dir, "dir")
132
+ os.makedirs(target_dir, exist_ok=True)
133
+
134
+ # AND the workflow pull API call returns a zip file
135
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
136
+
137
+ # WHEN the user runs the pull command with target-dir
138
+ runner = CliRunner()
139
+ result = runner.invoke(cli_main, base_command + [module, "--target-dir", target_dir])
140
+
141
+ # THEN the command returns successfully
142
+ assert result.exit_code == 0
143
+
144
+ # AND the workflow.py file is written to the target directory
145
+ module_path = os.path.join(target_dir, *module.split("."))
146
+ workflow_py = os.path.join(module_path, "workflow.py")
147
+ assert os.path.exists(workflow_py)
148
+ with open(workflow_py) as f:
149
+ assert f.read() == "print('hello')"
150
+
151
+ # AND the files are not in the default module directory
152
+ default_module_path = os.path.join(temp_dir, *module.split("."), "workflow.py")
153
+ assert not os.path.exists(default_module_path)
154
+
155
+ # AND the vellum.lock.json file is still updated
156
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
157
+ assert os.path.exists(vellum_lock_json)
158
+ with open(vellum_lock_json) as f:
159
+ lock_data = json.load(f)
160
+ assert lock_data == {
161
+ "version": "1.0",
162
+ "workflows": [
163
+ {
164
+ "module": module,
165
+ "workflow_sandbox_id": workflow_sandbox_id,
166
+ "container_image_name": None,
167
+ "container_image_tag": None,
168
+ "ignore": None,
169
+ "deployments": [],
170
+ "workspace": "default",
171
+ "target_directory": module_path,
172
+ }
173
+ ],
174
+ "workspaces": [],
175
+ }
176
+
177
+
178
+ @pytest.mark.parametrize(
179
+ "base_command",
180
+ [
181
+ ["pull"],
182
+ ["workflows", "pull"],
183
+ ],
184
+ ids=["pull", "workflows_pull"],
185
+ )
186
+ def test_pull__with_nested_target_dir(vellum_client, mock_module, base_command):
187
+ # GIVEN a module on the user's filesystem
188
+ temp_dir = mock_module.temp_dir
189
+ module = mock_module.module
190
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
191
+
192
+ # AND a nested target directory that doesn't exist yet
193
+ nested_target_dir = os.path.join(temp_dir, "dir-1", "dir-2")
194
+
195
+ # AND the workflow pull API call returns a zip file
196
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
197
+
198
+ # WHEN the user runs the pull command with nested target-dir
199
+ runner = CliRunner()
200
+ result = runner.invoke(cli_main, base_command + [module, "--target-dir", nested_target_dir])
201
+
202
+ # THEN the command returns successfully
203
+ assert result.exit_code == 0
204
+
205
+ # AND the nested directory with module subdirectory should be created
206
+ module_path = os.path.join(nested_target_dir, *module.split("."))
207
+ assert os.path.exists(module_path)
208
+
209
+ # AND the nested directory should be created
210
+ assert os.path.exists(module_path)
211
+
212
+ # AND the workflow.py file is written to the nested target directory
213
+ workflow_py = os.path.join(module_path, "workflow.py")
214
+ assert os.path.exists(workflow_py)
215
+ with open(workflow_py) as f:
216
+ assert f.read() == "print('hello')"
217
+
218
+ # AND the files are not in the default module directory
219
+ default_module_path = os.path.join(temp_dir, *module.split("."), "workflow.py")
220
+ assert not os.path.exists(default_module_path)
221
+
222
+ # AND the vellum.lock.json file is still updated
223
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
224
+ assert os.path.exists(vellum_lock_json)
225
+ with open(vellum_lock_json) as f:
226
+ lock_data = json.load(f)
227
+ assert lock_data == {
228
+ "version": "1.0",
229
+ "workflows": [
230
+ {
231
+ "module": module,
232
+ "workflow_sandbox_id": workflow_sandbox_id,
233
+ "container_image_name": None,
234
+ "container_image_tag": None,
235
+ "ignore": None,
236
+ "deployments": [],
237
+ "workspace": "default",
238
+ "target_directory": module_path,
239
+ }
240
+ ],
241
+ "workspaces": [],
242
+ }
243
+
244
+
115
245
  def test_pull__sandbox_id_with_no_config(vellum_client):
116
246
  # GIVEN a workflow sandbox id
117
247
  workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
@@ -162,6 +292,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
162
292
  "container_image_tag": None,
163
293
  "container_image_name": None,
164
294
  "workspace": "default",
295
+ "target_directory": None,
165
296
  }
166
297
  ],
167
298
  }
@@ -245,6 +376,7 @@ def test_pull__workflow_deployment_with_no_config(vellum_client):
245
376
  "container_image_tag": None,
246
377
  "container_image_name": None,
247
378
  "workspace": "default",
379
+ "target_directory": None,
248
380
  }
249
381
  ],
250
382
  "workspaces": [],
@@ -492,6 +624,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
492
624
  "container_image_name": None,
493
625
  "container_image_tag": None,
494
626
  "workspace": "default",
627
+ "target_directory": None,
495
628
  },
496
629
  {
497
630
  "module": "super_cool_workflow",
@@ -501,6 +634,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
501
634
  "container_image_name": "test",
502
635
  "container_image_tag": "1.0",
503
636
  "workspace": "default",
637
+ "target_directory": None,
504
638
  },
505
639
  ]
506
640
 
@@ -644,6 +778,7 @@ def test_pull__module_not_in_config(vellum_client, mock_module):
644
778
  "container_image_name": None,
645
779
  "container_image_tag": None,
646
780
  "workspace": "default",
781
+ "target_directory": None,
647
782
  }
648
783
  ]
649
784
 
@@ -508,6 +508,7 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
508
508
  "container_image_tag": None,
509
509
  "deployments": [],
510
510
  "ignore": None,
511
+ "target_directory": None,
511
512
  }
512
513
 
513
514
 
@@ -47,7 +47,7 @@ from vellum_ee.workflows.display.nodes.get_node_display_class import get_node_di
47
47
  from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay, PortDisplay, PortDisplayOverrides
48
48
  from vellum_ee.workflows.display.utils.expressions import get_child_descriptor
49
49
  from vellum_ee.workflows.display.utils.vellum import convert_descriptor_to_operator, primitive_to_vellum_value
50
- from vellum_ee.workflows.display.vellum import CodeResourceDefinition, GenericNodeDisplayData
50
+ from vellum_ee.workflows.display.vellum import CodeResourceDefinition, NodeDisplayData
51
51
 
52
52
  if TYPE_CHECKING:
53
53
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
@@ -295,9 +295,9 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
295
295
 
296
296
  cls._node_display_registry[node_class] = cls
297
297
 
298
- def _get_generic_node_display_data(self) -> GenericNodeDisplayData:
299
- explicit_value = self._get_explicit_node_display_attr("display_data", GenericNodeDisplayData)
300
- return explicit_value if explicit_value else GenericNodeDisplayData()
298
+ def _get_generic_node_display_data(self) -> NodeDisplayData:
299
+ explicit_value = self._get_explicit_node_display_attr("display_data", NodeDisplayData)
300
+ return explicit_value if explicit_value else NodeDisplayData()
301
301
 
302
302
  def serialize_condition(self, display_context: "WorkflowDisplayContext", condition: BaseDescriptor) -> JsonObject:
303
303
  if isinstance(