vellum-ai 0.14.6__py3-none-any.whl → 0.14.7__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.
- vellum/__init__.py +12 -0
- vellum/client/core/client_wrapper.py +1 -1
- vellum/client/types/__init__.py +12 -0
- vellum/client/types/array_chat_message_content_item.py +6 -1
- vellum/client/types/array_chat_message_content_item_request.py +2 -0
- vellum/client/types/chat_message_content.py +2 -0
- vellum/client/types/chat_message_content_request.py +2 -0
- vellum/client/types/document_chat_message_content.py +25 -0
- vellum/client/types/document_chat_message_content_request.py +25 -0
- vellum/client/types/document_vellum_value.py +25 -0
- vellum/client/types/document_vellum_value_request.py +25 -0
- vellum/client/types/vellum_document.py +20 -0
- vellum/client/types/vellum_document_request.py +20 -0
- vellum/client/types/vellum_value.py +2 -0
- vellum/client/types/vellum_value_request.py +2 -0
- vellum/client/types/vellum_variable_type.py +1 -0
- vellum/types/document_chat_message_content.py +3 -0
- vellum/types/document_chat_message_content_request.py +3 -0
- vellum/types/document_vellum_value.py +3 -0
- vellum/types/document_vellum_value_request.py +3 -0
- vellum/types/vellum_document.py +3 -0
- vellum/types/vellum_document_request.py +3 -0
- vellum/workflows/exceptions.py +18 -0
- vellum/workflows/inputs/base.py +27 -1
- vellum/workflows/inputs/tests/__init__.py +0 -0
- vellum/workflows/inputs/tests/test_inputs.py +49 -0
- vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -1
- vellum/workflows/nodes/core/map_node/node.py +7 -7
- vellum/workflows/nodes/core/try_node/node.py +1 -1
- vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +2 -2
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +5 -3
- vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +5 -4
- vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py +4 -4
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +39 -15
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/tests/test_node.py +142 -0
- vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +3 -1
- vellum/workflows/outputs/base.py +1 -1
- vellum/workflows/runner/runner.py +16 -10
- vellum/workflows/state/context.py +7 -7
- vellum/workflows/workflows/base.py +16 -5
- vellum/workflows/workflows/tests/test_base_workflow.py +131 -40
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.7.dist-info}/METADATA +1 -1
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.7.dist-info}/RECORD +54 -38
- vellum_cli/__init__.py +36 -0
- vellum_cli/init.py +128 -0
- vellum_cli/pull.py +6 -3
- vellum_cli/tests/test_init.py +355 -0
- vellum_cli/tests/test_pull.py +127 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +4 -4
- vellum_ee/workflows/display/vellum.py +0 -4
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +29 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.7.dist-info}/LICENSE +0 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.7.dist-info}/WHEEL +0 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.7.dist-info}/entry_points.txt +0 -0
vellum_cli/__init__.py
CHANGED
@@ -4,6 +4,7 @@ import click
|
|
4
4
|
|
5
5
|
from vellum_cli.aliased_group import ClickAliasedGroup
|
6
6
|
from vellum_cli.image_push import image_push_command
|
7
|
+
from vellum_cli.init import init_command
|
7
8
|
from vellum_cli.ping import ping_command
|
8
9
|
from vellum_cli.pull import pull_command
|
9
10
|
from vellum_cli.push import push_command
|
@@ -193,12 +194,20 @@ Should only be used for debugging purposes.""",
|
|
193
194
|
help="""Generates a runnable sandbox.py file containing test data from the Resource's sandbox. \
|
194
195
|
Helpful for running and debugging workflows locally.""",
|
195
196
|
)
|
197
|
+
@click.option(
|
198
|
+
"--target-dir",
|
199
|
+
"target_directory", # Internal parameter name is target_directory
|
200
|
+
type=str,
|
201
|
+
help="""Directory to pull the workflow into. If not specified, \
|
202
|
+
the workflow will be pulled into the current working directory.""",
|
203
|
+
)
|
196
204
|
def pull(
|
197
205
|
ctx: click.Context,
|
198
206
|
include_json: Optional[bool],
|
199
207
|
exclude_code: Optional[bool],
|
200
208
|
strict: Optional[bool],
|
201
209
|
include_sandbox: Optional[bool],
|
210
|
+
target_directory: Optional[str],
|
202
211
|
) -> None:
|
203
212
|
"""Pull Resources from Vellum"""
|
204
213
|
|
@@ -208,6 +217,7 @@ def pull(
|
|
208
217
|
exclude_code=exclude_code,
|
209
218
|
strict=strict,
|
210
219
|
include_sandbox=include_sandbox,
|
220
|
+
target_directory=target_directory,
|
211
221
|
)
|
212
222
|
|
213
223
|
|
@@ -242,6 +252,13 @@ Should only be used for debugging purposes.""",
|
|
242
252
|
help="""Generates a runnable sandbox.py file containing test data from the Resource's sandbox. \
|
243
253
|
Helpful for running and debugging workflows locally.""",
|
244
254
|
)
|
255
|
+
@click.option(
|
256
|
+
"--target-dir",
|
257
|
+
"target_directory", # Internal parameter name is target_directory
|
258
|
+
type=str,
|
259
|
+
help="""Directory to pull the workflow into. If not specified, \
|
260
|
+
the workflow will be pulled into the current working directory.""",
|
261
|
+
)
|
245
262
|
def workflows_pull(
|
246
263
|
module: Optional[str],
|
247
264
|
include_json: Optional[bool],
|
@@ -250,6 +267,7 @@ def workflows_pull(
|
|
250
267
|
exclude_code: Optional[bool],
|
251
268
|
strict: Optional[bool],
|
252
269
|
include_sandbox: Optional[bool],
|
270
|
+
target_directory: Optional[str],
|
253
271
|
) -> None:
|
254
272
|
"""
|
255
273
|
Pull Workflows from Vellum. If a module is provided, only the Workflow for that module will be pulled.
|
@@ -264,6 +282,7 @@ def workflows_pull(
|
|
264
282
|
exclude_code=exclude_code,
|
265
283
|
strict=strict,
|
266
284
|
include_sandbox=include_sandbox,
|
285
|
+
target_directory=target_directory,
|
267
286
|
)
|
268
287
|
|
269
288
|
|
@@ -292,12 +311,20 @@ Should only be used for debugging purposes.""",
|
|
292
311
|
help="""Generates a runnable sandbox.py file containing test data from the Resource's sandbox. \
|
293
312
|
Helpful for running and debugging resources locally.""",
|
294
313
|
)
|
314
|
+
@click.option(
|
315
|
+
"--target-dir",
|
316
|
+
"target_directory", # Internal parameter name is target_directory
|
317
|
+
type=str,
|
318
|
+
help="""Directory to pull the workflow into. If not specified, \
|
319
|
+
the workflow will be pulled into the current working directory.""",
|
320
|
+
)
|
295
321
|
def pull_module(
|
296
322
|
ctx: click.Context,
|
297
323
|
include_json: Optional[bool],
|
298
324
|
exclude_code: Optional[bool],
|
299
325
|
strict: Optional[bool],
|
300
326
|
include_sandbox: Optional[bool],
|
327
|
+
target_directory: Optional[str],
|
301
328
|
) -> None:
|
302
329
|
"""Pull a specific module from Vellum"""
|
303
330
|
|
@@ -308,6 +335,7 @@ def pull_module(
|
|
308
335
|
exclude_code=exclude_code,
|
309
336
|
strict=strict,
|
310
337
|
include_sandbox=include_sandbox,
|
338
|
+
target_directory=target_directory,
|
311
339
|
)
|
312
340
|
|
313
341
|
|
@@ -331,5 +359,13 @@ def image_push(image: str, tag: Optional[List[str]] = None) -> None:
|
|
331
359
|
image_push_command(image, tag)
|
332
360
|
|
333
361
|
|
362
|
+
@workflows.command(name="init")
|
363
|
+
@click.argument("template_name", required=False)
|
364
|
+
def workflows_init(template_name: Optional[str] = None) -> None:
|
365
|
+
"""Initialize a new Vellum Workflow using a predefined template"""
|
366
|
+
|
367
|
+
init_command(template_name=template_name)
|
368
|
+
|
369
|
+
|
334
370
|
if __name__ == "__main__":
|
335
371
|
main()
|
vellum_cli/init.py
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import zipfile
|
5
|
+
from typing import Optional, cast
|
6
|
+
|
7
|
+
import click
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
from pydash import snake_case
|
10
|
+
|
11
|
+
from vellum.client.types.workflow_sandbox_example import WorkflowSandboxExample
|
12
|
+
from vellum.workflows.vellum_client import create_vellum_client
|
13
|
+
from vellum_cli.config import WorkflowConfig, load_vellum_cli_config
|
14
|
+
from vellum_cli.logger import load_cli_logger
|
15
|
+
from vellum_cli.pull import PullContentsMetadata, WorkflowConfigResolutionResult
|
16
|
+
|
17
|
+
ERROR_LOG_FILE_NAME = "error.log"
|
18
|
+
METADATA_FILE_NAME = "metadata.json"
|
19
|
+
|
20
|
+
|
21
|
+
def init_command(template_name: Optional[str] = None):
|
22
|
+
load_dotenv()
|
23
|
+
logger = load_cli_logger()
|
24
|
+
config = load_vellum_cli_config()
|
25
|
+
|
26
|
+
client = create_vellum_client()
|
27
|
+
templates_response = client.workflow_sandboxes.list_workflow_sandbox_examples(tag="TEMPLATES")
|
28
|
+
|
29
|
+
templates = templates_response.results
|
30
|
+
if not templates:
|
31
|
+
logger.error("No templates available")
|
32
|
+
return
|
33
|
+
|
34
|
+
if template_name:
|
35
|
+
selected_template = next((t for t in templates if snake_case(t.label) == template_name), None)
|
36
|
+
if not selected_template:
|
37
|
+
logger.error(f"Template {template_name} not found")
|
38
|
+
return
|
39
|
+
else:
|
40
|
+
click.echo(click.style("Available Templates", bold=True, fg="green"))
|
41
|
+
for idx, template in enumerate(templates, 1):
|
42
|
+
click.echo(f"{idx}. {template.label}")
|
43
|
+
|
44
|
+
choice = click.prompt(
|
45
|
+
f"Please select a template number (1-{len(templates)})", type=click.IntRange(1, len(templates))
|
46
|
+
)
|
47
|
+
selected_template = cast(WorkflowSandboxExample, templates[choice - 1])
|
48
|
+
|
49
|
+
click.echo(click.style(f"\nYou selected: {selected_template.label}\n", bold=True, fg="cyan"))
|
50
|
+
|
51
|
+
# Create workflow config with module name from template label
|
52
|
+
workflow_config = WorkflowConfig(
|
53
|
+
workflow_sandbox_id=selected_template.id,
|
54
|
+
module=snake_case(selected_template.label), # Set module name directly from template
|
55
|
+
)
|
56
|
+
config.workflows.append(workflow_config)
|
57
|
+
|
58
|
+
workflow_config_result = WorkflowConfigResolutionResult(
|
59
|
+
workflow_config=workflow_config,
|
60
|
+
pk=selected_template.id,
|
61
|
+
)
|
62
|
+
|
63
|
+
pk = workflow_config_result.pk
|
64
|
+
if not pk:
|
65
|
+
raise ValueError("No workflow sandbox ID found in project to pull from.")
|
66
|
+
|
67
|
+
target_dir = os.path.join(os.getcwd(), workflow_config.module)
|
68
|
+
if os.path.exists(target_dir):
|
69
|
+
click.echo(click.style(f"{target_dir} already exists.", fg="red"))
|
70
|
+
return
|
71
|
+
|
72
|
+
logger.info(f"Pulling workflow into {workflow_config.module}...")
|
73
|
+
|
74
|
+
query_parameters = {
|
75
|
+
"include_sandbox": True,
|
76
|
+
}
|
77
|
+
|
78
|
+
response = client.workflows.pull(
|
79
|
+
pk,
|
80
|
+
request_options={"additional_query_parameters": query_parameters},
|
81
|
+
)
|
82
|
+
|
83
|
+
zip_bytes = b"".join(response)
|
84
|
+
zip_buffer = io.BytesIO(zip_bytes)
|
85
|
+
|
86
|
+
error_content = ""
|
87
|
+
|
88
|
+
with zipfile.ZipFile(zip_buffer) as zip_file:
|
89
|
+
if METADATA_FILE_NAME in zip_file.namelist():
|
90
|
+
metadata_json: Optional[dict] = None
|
91
|
+
with zip_file.open(METADATA_FILE_NAME) as source:
|
92
|
+
metadata_json = json.load(source)
|
93
|
+
|
94
|
+
pull_contents_metadata = PullContentsMetadata.model_validate(metadata_json)
|
95
|
+
|
96
|
+
if pull_contents_metadata.runner_config:
|
97
|
+
workflow_config.container_image_name = pull_contents_metadata.runner_config.container_image_name
|
98
|
+
workflow_config.container_image_tag = pull_contents_metadata.runner_config.container_image_tag
|
99
|
+
if workflow_config.container_image_name and not workflow_config.container_image_tag:
|
100
|
+
workflow_config.container_image_tag = "latest"
|
101
|
+
|
102
|
+
if not workflow_config.module and pull_contents_metadata.label:
|
103
|
+
workflow_config.module = snake_case(pull_contents_metadata.label)
|
104
|
+
|
105
|
+
if not workflow_config.module:
|
106
|
+
raise ValueError(f"Failed to resolve a module name for Workflow {pk}")
|
107
|
+
|
108
|
+
for file_name in zip_file.namelist():
|
109
|
+
with zip_file.open(file_name) as source:
|
110
|
+
content = source.read().decode("utf-8")
|
111
|
+
if file_name == ERROR_LOG_FILE_NAME:
|
112
|
+
error_content = content
|
113
|
+
continue
|
114
|
+
if file_name == METADATA_FILE_NAME:
|
115
|
+
continue
|
116
|
+
|
117
|
+
target_file = os.path.join(target_dir, file_name)
|
118
|
+
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
119
|
+
with open(target_file, "w") as target:
|
120
|
+
logger.info(f"Writing to {target_file}...")
|
121
|
+
target.write(content)
|
122
|
+
|
123
|
+
config.save()
|
124
|
+
|
125
|
+
if error_content:
|
126
|
+
logger.error(error_content)
|
127
|
+
else:
|
128
|
+
logger.info(f"Successfully pulled Workflow into {target_dir}")
|
vellum_cli/pull.py
CHANGED
@@ -108,6 +108,7 @@ def pull_command(
|
|
108
108
|
exclude_code: Optional[bool] = None,
|
109
109
|
strict: Optional[bool] = None,
|
110
110
|
include_sandbox: Optional[bool] = None,
|
111
|
+
target_directory: Optional[str] = None,
|
111
112
|
) -> None:
|
112
113
|
load_dotenv()
|
113
114
|
logger = load_cli_logger()
|
@@ -129,7 +130,7 @@ def pull_command(
|
|
129
130
|
raise ValueError("No workflow sandbox ID found in project to pull from.")
|
130
131
|
|
131
132
|
if workflow_config.module:
|
132
|
-
logger.info(f"Pulling workflow
|
133
|
+
logger.info(f"Pulling workflow {workflow_config.module}...")
|
133
134
|
else:
|
134
135
|
logger.info(f"Pulling workflow from {pk}...")
|
135
136
|
|
@@ -175,7 +176,9 @@ def pull_command(
|
|
175
176
|
if not workflow_config.module:
|
176
177
|
raise ValueError(f"Failed to resolve a module name for Workflow {pk}")
|
177
178
|
|
178
|
-
|
179
|
+
# Use target_directory if provided, otherwise use current working directory
|
180
|
+
base_dir = os.path.join(os.getcwd(), target_directory) if target_directory else os.getcwd()
|
181
|
+
target_dir = os.path.join(base_dir, *workflow_config.module.split("."))
|
179
182
|
|
180
183
|
# Delete files in target_dir that aren't in the zip file
|
181
184
|
if os.path.exists(target_dir):
|
@@ -233,4 +236,4 @@ Its schema should be considered unstable and subject to change at any time."""
|
|
233
236
|
if error_content:
|
234
237
|
logger.error(error_content)
|
235
238
|
else:
|
236
|
-
logger.info(f"Successfully pulled Workflow into {
|
239
|
+
logger.info(f"Successfully pulled Workflow into {target_dir}")
|
@@ -0,0 +1,355 @@
|
|
1
|
+
import pytest
|
2
|
+
import io
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from unittest.mock import patch
|
6
|
+
import zipfile
|
7
|
+
|
8
|
+
from click.testing import CliRunner
|
9
|
+
from pydash import snake_case
|
10
|
+
|
11
|
+
from vellum_cli import main as cli_main
|
12
|
+
|
13
|
+
|
14
|
+
def _zip_file_map(file_map: dict[str, str]) -> bytes:
|
15
|
+
# Create an in-memory bytes buffer to store the zip
|
16
|
+
zip_buffer = io.BytesIO()
|
17
|
+
|
18
|
+
# Create zip file and add files from file_map
|
19
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
20
|
+
for filename, content in file_map.items():
|
21
|
+
zip_file.writestr(filename, content)
|
22
|
+
|
23
|
+
# Get the bytes from the buffer
|
24
|
+
zip_bytes = zip_buffer.getvalue()
|
25
|
+
zip_buffer.close()
|
26
|
+
|
27
|
+
return zip_bytes
|
28
|
+
|
29
|
+
|
30
|
+
class MockTemplate:
|
31
|
+
def __init__(self, id, label):
|
32
|
+
self.id = id
|
33
|
+
self.label = label
|
34
|
+
|
35
|
+
|
36
|
+
@pytest.mark.parametrize(
|
37
|
+
"base_command",
|
38
|
+
[
|
39
|
+
["workflows", "init"],
|
40
|
+
],
|
41
|
+
ids=["workflows_init"],
|
42
|
+
)
|
43
|
+
def test_init_command(vellum_client, mock_module, base_command):
|
44
|
+
# GIVEN a module on the user's filesystem
|
45
|
+
temp_dir = mock_module.temp_dir
|
46
|
+
mock_module.set_pyproject_toml({"workflows": []})
|
47
|
+
# GIVEN the vellum client returns a list of template workflows
|
48
|
+
fake_templates = [
|
49
|
+
MockTemplate(id="template-1", label="Example Workflow"),
|
50
|
+
MockTemplate(id="template-2", label="Another Workflow"),
|
51
|
+
]
|
52
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
|
53
|
+
|
54
|
+
# AND the workflow pull API call returns a zip file
|
55
|
+
vellum_client.workflows.pull.return_value = iter(
|
56
|
+
[
|
57
|
+
_zip_file_map(
|
58
|
+
{
|
59
|
+
"workflow.py": "print('hello')",
|
60
|
+
}
|
61
|
+
)
|
62
|
+
]
|
63
|
+
)
|
64
|
+
# WHEN the user runs the `init` command and selects the first template
|
65
|
+
runner = CliRunner()
|
66
|
+
result = runner.invoke(cli_main, base_command, input="1\n")
|
67
|
+
|
68
|
+
# THEN the command returns successfully
|
69
|
+
assert result.exit_code == 0
|
70
|
+
|
71
|
+
# AND `vellum_client.workflows.pull` is called with the selected template ID
|
72
|
+
vellum_client.workflows.pull.assert_called_once_with(
|
73
|
+
"template-1",
|
74
|
+
request_options={"additional_query_parameters": {"include_sandbox": True}},
|
75
|
+
)
|
76
|
+
|
77
|
+
# AND the `workflow.py` file should be created in the correct module directory
|
78
|
+
workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
|
79
|
+
assert os.path.exists(workflow_py)
|
80
|
+
with open(workflow_py) as f:
|
81
|
+
assert f.read() == "print('hello')"
|
82
|
+
|
83
|
+
# AND the vellum.lock.json file should be created
|
84
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
85
|
+
assert os.path.exists(vellum_lock_json)
|
86
|
+
with open(vellum_lock_json) as f:
|
87
|
+
lock_data = json.load(f)
|
88
|
+
assert lock_data["workflows"] == [
|
89
|
+
{
|
90
|
+
"module": "example_workflow",
|
91
|
+
"workflow_sandbox_id": "template-1",
|
92
|
+
"ignore": None,
|
93
|
+
"deployments": [],
|
94
|
+
"container_image_name": None,
|
95
|
+
"container_image_tag": None,
|
96
|
+
"workspace": "default",
|
97
|
+
}
|
98
|
+
]
|
99
|
+
|
100
|
+
|
101
|
+
@pytest.mark.parametrize(
|
102
|
+
"base_command",
|
103
|
+
[
|
104
|
+
["workflows", "init"],
|
105
|
+
],
|
106
|
+
ids=["workflows_init"],
|
107
|
+
)
|
108
|
+
def test_init_command__invalid_template_id(vellum_client, mock_module, base_command):
|
109
|
+
# GIVEN a module on the user's filesystem
|
110
|
+
temp_dir = mock_module.temp_dir
|
111
|
+
mock_module.set_pyproject_toml({"workflows": []})
|
112
|
+
# GIVEN the vellum client returns a list of template workflows
|
113
|
+
fake_templates = [
|
114
|
+
MockTemplate(id="template-1", label="Example Workflow"),
|
115
|
+
MockTemplate(id="template-2", label="Another Workflow"),
|
116
|
+
]
|
117
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
|
118
|
+
|
119
|
+
# WHEN the user runs the `init` command, enters invalid input and then cancels
|
120
|
+
runner = CliRunner()
|
121
|
+
# Mock click.prompt to raise a KeyboardInterrupt (simulating Ctrl+C)
|
122
|
+
with patch("click.prompt", side_effect=KeyboardInterrupt):
|
123
|
+
runner = CliRunner()
|
124
|
+
result = runner.invoke(cli_main, base_command)
|
125
|
+
|
126
|
+
# THEN the command is aborted
|
127
|
+
assert result.exit_code != 0
|
128
|
+
assert "Aborted!" in result.output # Click shows this message on Ctrl+C
|
129
|
+
|
130
|
+
# AND `vellum_client.workflows.pull` is not called
|
131
|
+
vellum_client.workflows.pull.assert_not_called()
|
132
|
+
|
133
|
+
# AND no workflow files are created
|
134
|
+
workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
|
135
|
+
assert not os.path.exists(workflow_py)
|
136
|
+
|
137
|
+
# AND the lock file remains empty
|
138
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
139
|
+
if os.path.exists(vellum_lock_json):
|
140
|
+
with open(vellum_lock_json) as f:
|
141
|
+
lock_data = json.load(f)
|
142
|
+
assert lock_data["workflows"] == []
|
143
|
+
|
144
|
+
|
145
|
+
@pytest.mark.parametrize(
|
146
|
+
"base_command",
|
147
|
+
[
|
148
|
+
["workflows", "init"],
|
149
|
+
],
|
150
|
+
ids=["workflows_init"],
|
151
|
+
)
|
152
|
+
def test_init_command__no_templates(vellum_client, mock_module, base_command):
|
153
|
+
# GIVEN a module on the user's filesystem
|
154
|
+
temp_dir = mock_module.temp_dir
|
155
|
+
mock_module.set_pyproject_toml({"workflows": []})
|
156
|
+
# GIVEN the vellum client returns no template workflows
|
157
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = []
|
158
|
+
|
159
|
+
# WHEN the user runs the `init` command
|
160
|
+
runner = CliRunner()
|
161
|
+
result = runner.invoke(cli_main, base_command)
|
162
|
+
|
163
|
+
# THEN the command gracefully exits
|
164
|
+
assert result.exit_code == 0
|
165
|
+
assert "No templates available" in result.output
|
166
|
+
|
167
|
+
# AND `vellum_client.workflows.pull` is not called
|
168
|
+
vellum_client.workflows.pull.assert_not_called()
|
169
|
+
|
170
|
+
# AND no workflow files are created
|
171
|
+
workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
|
172
|
+
assert not os.path.exists(workflow_py)
|
173
|
+
|
174
|
+
# AND the lock file remains empty
|
175
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
176
|
+
if os.path.exists(vellum_lock_json):
|
177
|
+
with open(vellum_lock_json) as f:
|
178
|
+
lock_data = json.load(f)
|
179
|
+
assert lock_data["workflows"] == []
|
180
|
+
|
181
|
+
|
182
|
+
@pytest.mark.parametrize(
|
183
|
+
"base_command",
|
184
|
+
[
|
185
|
+
["workflows", "init"],
|
186
|
+
],
|
187
|
+
ids=["workflows_init"],
|
188
|
+
)
|
189
|
+
def test_init_command_target_directory_exists(vellum_client, mock_module, base_command):
|
190
|
+
"""
|
191
|
+
GIVEN a target directory already exists
|
192
|
+
WHEN the user tries to run the `init` command
|
193
|
+
THEN the command should fail and exit without modifying existing files
|
194
|
+
"""
|
195
|
+
temp_dir = mock_module.temp_dir
|
196
|
+
existing_workflow_dir = os.path.join(temp_dir, "example_workflow")
|
197
|
+
|
198
|
+
# Create the target directory to simulate it already existing
|
199
|
+
os.makedirs(existing_workflow_dir, exist_ok=True)
|
200
|
+
|
201
|
+
# Ensure directory exists before command execution
|
202
|
+
assert os.path.exists(existing_workflow_dir)
|
203
|
+
|
204
|
+
# GIVEN the vellum client returns a list of template workflows
|
205
|
+
fake_templates = [
|
206
|
+
MockTemplate(id="template-1", label="Example Workflow"),
|
207
|
+
]
|
208
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
|
209
|
+
|
210
|
+
# AND the workflow pull API call returns a zip file
|
211
|
+
vellum_client.workflows.pull.return_value = iter(
|
212
|
+
[
|
213
|
+
_zip_file_map(
|
214
|
+
{
|
215
|
+
"workflow.py": "print('hello')",
|
216
|
+
}
|
217
|
+
)
|
218
|
+
]
|
219
|
+
)
|
220
|
+
|
221
|
+
# WHEN the user runs the `init` command and selects the template
|
222
|
+
runner = CliRunner()
|
223
|
+
result = runner.invoke(cli_main, base_command, input="1\n")
|
224
|
+
|
225
|
+
# THEN the command should detect the existing directory and abort
|
226
|
+
assert result.exit_code == 0
|
227
|
+
assert f"{existing_workflow_dir} already exists." in result.output
|
228
|
+
|
229
|
+
# Ensure the directory still exists (wasn't deleted or modified)
|
230
|
+
assert os.path.exists(existing_workflow_dir)
|
231
|
+
|
232
|
+
# AND `vellum_client.workflows.pull` is not called
|
233
|
+
vellum_client.workflows.pull.assert_not_called()
|
234
|
+
|
235
|
+
# AND no workflow files are created
|
236
|
+
workflow_py = os.path.join(temp_dir, "example_workflow", "workflow.py")
|
237
|
+
assert not os.path.exists(workflow_py)
|
238
|
+
|
239
|
+
# AND the lock file remains empty
|
240
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
241
|
+
if os.path.exists(vellum_lock_json):
|
242
|
+
with open(vellum_lock_json) as f:
|
243
|
+
lock_data = json.load(f)
|
244
|
+
assert lock_data["workflows"] == []
|
245
|
+
|
246
|
+
|
247
|
+
@pytest.mark.parametrize(
|
248
|
+
"base_command",
|
249
|
+
[
|
250
|
+
["workflows", "init"],
|
251
|
+
],
|
252
|
+
ids=["workflows_init"],
|
253
|
+
)
|
254
|
+
def test_init_command_with_template_name(vellum_client, mock_module, base_command):
|
255
|
+
# GIVEN a module on the user's filesystem
|
256
|
+
temp_dir = mock_module.temp_dir
|
257
|
+
mock_module.set_pyproject_toml({"workflows": []})
|
258
|
+
|
259
|
+
# GIVEN the vellum client returns a list of template workflows
|
260
|
+
fake_templates = [
|
261
|
+
MockTemplate(id="template-1", label="Example Workflow"),
|
262
|
+
MockTemplate(id="template-2", label="Another Workflow"),
|
263
|
+
]
|
264
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
|
265
|
+
|
266
|
+
# AND the workflow pull API call returns a zip file
|
267
|
+
vellum_client.workflows.pull.return_value = iter(
|
268
|
+
[_zip_file_map({"workflow.py": "print('hello')", "README.md": "# Another Workflow\nThis is a test template."})]
|
269
|
+
)
|
270
|
+
|
271
|
+
# WHEN the user runs the `init` command with a specific template name
|
272
|
+
template_name = snake_case("Another Workflow")
|
273
|
+
runner = CliRunner()
|
274
|
+
result = runner.invoke(cli_main, base_command + [template_name])
|
275
|
+
|
276
|
+
# THEN the command returns successfully
|
277
|
+
assert result.exit_code == 0
|
278
|
+
|
279
|
+
# AND `vellum_client.workflows.pull` is called with the correct template ID
|
280
|
+
vellum_client.workflows.pull.assert_called_once_with(
|
281
|
+
"template-2", # ID of "Another Workflow"
|
282
|
+
request_options={"additional_query_parameters": {"include_sandbox": True}},
|
283
|
+
)
|
284
|
+
|
285
|
+
# AND the workflow files should be created in the correct module directory
|
286
|
+
workflow_py = os.path.join(temp_dir, "another_workflow", "workflow.py")
|
287
|
+
|
288
|
+
assert os.path.exists(workflow_py)
|
289
|
+
|
290
|
+
with open(workflow_py) as f:
|
291
|
+
assert f.read() == "print('hello')"
|
292
|
+
|
293
|
+
# AND the vellum.lock.json file should be created with the correct data
|
294
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
295
|
+
assert os.path.exists(vellum_lock_json)
|
296
|
+
|
297
|
+
with open(vellum_lock_json) as f:
|
298
|
+
lock_data = json.load(f)
|
299
|
+
assert lock_data["workflows"] == [
|
300
|
+
{
|
301
|
+
"module": "another_workflow",
|
302
|
+
"workflow_sandbox_id": "template-2",
|
303
|
+
"ignore": None,
|
304
|
+
"deployments": [],
|
305
|
+
"container_image_name": None,
|
306
|
+
"container_image_tag": None,
|
307
|
+
"workspace": "default",
|
308
|
+
}
|
309
|
+
]
|
310
|
+
|
311
|
+
|
312
|
+
@pytest.mark.parametrize(
|
313
|
+
"base_command",
|
314
|
+
[
|
315
|
+
["workflows", "init"],
|
316
|
+
],
|
317
|
+
ids=["workflows_init"],
|
318
|
+
)
|
319
|
+
def test_init_command_with_nonexistent_template_name(vellum_client, mock_module, base_command):
|
320
|
+
# GIVEN a module on the user's filesystem
|
321
|
+
temp_dir = mock_module.temp_dir
|
322
|
+
mock_module.set_pyproject_toml({"workflows": []})
|
323
|
+
|
324
|
+
# GIVEN the vellum client returns a list of template workflows
|
325
|
+
fake_templates = [
|
326
|
+
MockTemplate(id="template-1", label="Example Workflow"),
|
327
|
+
MockTemplate(id="template-2", label="Another Workflow"),
|
328
|
+
]
|
329
|
+
vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
|
330
|
+
|
331
|
+
# WHEN the user runs the `init` command with a non-existent template name
|
332
|
+
nonexistent_template = "nonexistent_template"
|
333
|
+
runner = CliRunner()
|
334
|
+
result = runner.invoke(cli_main, base_command + [nonexistent_template])
|
335
|
+
|
336
|
+
# THEN the command should indicate the template was not found
|
337
|
+
assert result.exit_code == 0
|
338
|
+
assert f"Template {nonexistent_template} not found" in result.output
|
339
|
+
|
340
|
+
# AND `vellum_client.workflows.pull` is not called
|
341
|
+
vellum_client.workflows.pull.assert_not_called()
|
342
|
+
|
343
|
+
# AND no workflow files are created
|
344
|
+
example_workflow_dir = os.path.join(temp_dir, "example_workflow")
|
345
|
+
nonexistent_workflow_dir = os.path.join(temp_dir, nonexistent_template)
|
346
|
+
|
347
|
+
assert not os.path.exists(example_workflow_dir)
|
348
|
+
assert not os.path.exists(nonexistent_workflow_dir)
|
349
|
+
|
350
|
+
# AND the lock file remains empty
|
351
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
352
|
+
if os.path.exists(vellum_lock_json):
|
353
|
+
with open(vellum_lock_json) as f:
|
354
|
+
lock_data = json.load(f)
|
355
|
+
assert lock_data["workflows"] == []
|