vellum-ai 0.11.3__py3-none-any.whl → 0.11.4__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.
@@ -17,7 +17,7 @@ class BaseClientWrapper:
17
17
  headers: typing.Dict[str, str] = {
18
18
  "X-Fern-Language": "Python",
19
19
  "X-Fern-SDK-Name": "vellum-ai",
20
- "X-Fern-SDK-Version": "0.11.2",
20
+ "X-Fern-SDK-Version": "0.11.4",
21
21
  }
22
22
  headers["X_API_KEY"] = self.api_key
23
23
  return headers
@@ -12,11 +12,13 @@ if TYPE_CHECKING:
12
12
  GraphTargetOfSets = Union[
13
13
  Set[NodeType],
14
14
  Set["Graph"],
15
- Set[Union[Type["BaseNode"], "Graph"]],
15
+ Set["Port"],
16
+ Set[Union[Type["BaseNode"], "Graph", "Port"]],
16
17
  ]
17
18
 
18
19
  GraphTarget = Union[
19
20
  Type["BaseNode"],
21
+ "Port",
20
22
  "Graph",
21
23
  GraphTargetOfSets,
22
24
  ]
@@ -53,9 +55,13 @@ class Graph:
53
55
  entrypoints.update(target._entrypoints)
54
56
  edges.update(target._edges)
55
57
  terminals.update(target._terminals)
56
- else:
58
+ elif hasattr(target, "Ports"):
57
59
  entrypoints.update({port for port in target.Ports})
58
60
  terminals.update({port for port in target.Ports})
61
+ else:
62
+ # target is a Port
63
+ entrypoints.update({target})
64
+ terminals.update({target})
59
65
 
60
66
  return Graph(entrypoints=entrypoints, edges=list(edges), terminals=terminals)
61
67
 
@@ -77,11 +83,16 @@ class Graph:
77
83
  self._extend_edges(elem.edges)
78
84
  for other_terminal in elem._terminals:
79
85
  new_terminals.add(other_terminal)
80
- else:
86
+ elif hasattr(elem, "Ports"):
81
87
  midgraph = final_output_node >> elem
82
88
  self._extend_edges(midgraph.edges)
83
89
  for other_terminal in elem.Ports:
84
90
  new_terminals.add(other_terminal)
91
+ else:
92
+ # elem is a Port
93
+ midgraph = final_output_node >> elem
94
+ self._extend_edges(midgraph.edges)
95
+ new_terminals.add(elem)
85
96
  self._terminals = new_terminals
86
97
  return self
87
98
 
@@ -93,10 +104,18 @@ class Graph:
93
104
  self._terminals = other._terminals
94
105
  return self
95
106
 
107
+ if hasattr(other, "Ports"):
108
+ for final_output_node in self._terminals:
109
+ subgraph = final_output_node >> other
110
+ self._extend_edges(subgraph.edges)
111
+ self._terminals = {port for port in other.Ports}
112
+ return self
113
+
114
+ # other is a Port
96
115
  for final_output_node in self._terminals:
97
116
  subgraph = final_output_node >> other
98
117
  self._extend_edges(subgraph.edges)
99
- self._terminals = {port for port in other.Ports}
118
+ self._terminals = {other}
100
119
  return self
101
120
 
102
121
  @property
@@ -435,3 +435,28 @@ def test_graph__set_to_node():
435
435
 
436
436
  # AND two edges
437
437
  assert len(list(graph.edges)) == 2
438
+
439
+
440
+ def test_graph__node_to_port():
441
+ # GIVEN two nodes, one with a port
442
+ class SourceNode(BaseNode):
443
+ pass
444
+
445
+ class MiddleNode(BaseNode):
446
+ class Ports(BaseNode.Ports):
447
+ custom = Port.on_else()
448
+
449
+ class TargetNode(BaseNode):
450
+ pass
451
+
452
+ # WHEN we create a graph from the source node to the target node
453
+ graph = SourceNode >> MiddleNode.Ports.custom >> TargetNode
454
+
455
+ # THEN the graph has the source node as the entrypoint
456
+ assert set(graph.entrypoints) == {SourceNode}
457
+
458
+ # AND three nodes
459
+ assert len(list(graph.nodes)) == 3
460
+
461
+ # AND two edges
462
+ assert len(list(graph.edges)) == 2
@@ -52,6 +52,9 @@ class Port:
52
52
  if isinstance(other, set) or isinstance(other, Graph):
53
53
  return Graph.from_port(self) >> other
54
54
 
55
+ if isinstance(other, Port):
56
+ return Graph.from_port(self) >> Graph.from_port(other)
57
+
55
58
  edge = Edge(from_port=self, to_node=other)
56
59
  if edge not in self._edges:
57
60
  self._edges.append(edge)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.11.3
3
+ Version: 0.11.4
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -2,17 +2,17 @@ vellum_cli/CONTRIBUTING.md,sha256=FtDC7BGxSeMnwCXAUssFsAIElXtmJE-O5Z7BpolcgvI,29
2
2
  vellum_cli/README.md,sha256=2NudRoLzWxNKqnuVy1JuQ7DerIaxWGYkrH8kMd-asIE,90
3
3
  vellum_cli/__init__.py,sha256=pftUQ6FiyfebNEB8xcfwzLjpfFDCAiH15xHBU6xr_wY,6733
4
4
  vellum_cli/aliased_group.py,sha256=ugW498j0yv4ALJ8vS9MsO7ctDW7Jlir9j6nE_uHAP8c,3363
5
- vellum_cli/config.py,sha256=uEdDqqpy7fh4Thlj2jSszfEmnXA9SGtzNzgR77fBjnY,3612
5
+ vellum_cli/config.py,sha256=wJQnv3tCgu1BOugg0AOP94yQ-x1yAg8juX_QoFN9Y7w,5223
6
6
  vellum_cli/image_push.py,sha256=SJwhwWJsLjwGNezNVd_oCVpFMfPsAB3dfLWmriZZUtw,4419
7
7
  vellum_cli/logger.py,sha256=PuRFa0WCh4sAGFS5aqWB0QIYpS6nBWwPJrIXpWxugV4,1022
8
8
  vellum_cli/pull.py,sha256=6wIiorqSx2rmR6atZJHHBuLSviocxK_n0DQxEDGmCzo,4008
9
- vellum_cli/push.py,sha256=7arl5Udk9iTVkMIqbUWHE7HqzteTlRe0Ix3cc5RNaHA,5130
9
+ vellum_cli/push.py,sha256=kbvlzZ9KnkS5DxxKHQP5ZvHHk1-CbCDg9LqnIRAWyt4,5258
10
10
  vellum_cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- vellum_cli/tests/conftest.py,sha256=j3P2g5vegxarIi_VoWr1RlGxLYEQPgw3V_p3M6Iyq0U,1007
11
+ vellum_cli/tests/conftest.py,sha256=eFGwBxib3Nki830lIFintB0b6r4x8T_KMnmzhlTY5x0,1337
12
12
  vellum_cli/tests/test_config.py,sha256=uvKGDc8BoVyT9_H0Z-g8469zVxomn6Oi3Zj-vK7O_wU,2631
13
13
  vellum_cli/tests/test_main.py,sha256=qDZG-aQauPwBwM6A2DIu1494n47v3pL28XakTbLGZ-k,272
14
- vellum_cli/tests/test_pull.py,sha256=PzB259psqyllgxbN7VwH7-wvY0-Z3hvdsqGdCil9vDs,10286
15
- vellum_cli/tests/test_push.py,sha256=TBw35SXyFt9PL2OJV2jOPnbq3T0cbSVXGQ5TS77ytgA,5830
14
+ vellum_cli/tests/test_pull.py,sha256=N6ZphvHYGokclbpbTpgOmpu_m2GtocDEesbdeHFjO5Y,13194
15
+ vellum_cli/tests/test_push.py,sha256=V2iGcskh2X3OHj2uV5Vx_BhmtyfmUkyx0lrp8DDOExc,5824
16
16
  vellum_ee/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  vellum_ee/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  vellum_ee/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -78,7 +78,7 @@ vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
78
78
  vellum/client/__init__.py,sha256=o4m7iRZWEV8rP3GkdaztHAjNmjxjWERlarviFoHzuKI,110927
79
79
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
80
80
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
81
- vellum/client/core/client_wrapper.py,sha256=XbAMOMcjdM9aXRo9_hecrehfft9oYmtBqckaIR0GAog,1890
81
+ vellum/client/core/client_wrapper.py,sha256=2kb_aZbadYgFs7fHp9vZCvmnzLko_U_VPmjLg8Jnnyw,1890
82
82
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
83
83
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
84
84
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -1251,9 +1251,9 @@ vellum/workflows/expressions/not_between.py,sha256=H2huRR95D9Qb7lCHmK7BcK-Ug-E1g
1251
1251
  vellum/workflows/expressions/not_in.py,sha256=OQkN5G1E6VoTDpoLvx7X3GbohLlqEAYHV0rVVUV7ow4,1049
1252
1252
  vellum/workflows/expressions/or_.py,sha256=s-8YdMSSCDS2yijR38kguwok3iqmDMMgDYKV93b4O4s,914
1253
1253
  vellum/workflows/graph/__init__.py,sha256=3sHlay5d_-uD7j3QJXiGl0WHFZZ_QScRvgyDhN2GhHY,74
1254
- vellum/workflows/graph/graph.py,sha256=etuop6xNRJcK8anQx-I31xaExw604f9wC-lRQFr2Xw8,4571
1254
+ vellum/workflows/graph/graph.py,sha256=rmPPs2gtQhaYIuZKETdWrdacgwQRvT6soj12UOl1Tm0,5316
1255
1255
  vellum/workflows/graph/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1256
- vellum/workflows/graph/tests/test_graph.py,sha256=zeBqfcMIlMS3AJn-t0YitKsa8O0r2fDnQ7qBPxexP7k,10620
1256
+ vellum/workflows/graph/tests/test_graph.py,sha256=PQI1lO2IY-izBSbkwxjKR5a1z-aN-ieHV_p0m-g5eAM,11256
1257
1257
  vellum/workflows/inputs/__init__.py,sha256=AbFEteIYEvCb14fM3EK7bhM-40-6s494rSlIhQ4Dsss,62
1258
1258
  vellum/workflows/inputs/base.py,sha256=1kMgr0WqCYdWUqgFvgSoAMw2067FAlgwhGXLgbIOrLY,2391
1259
1259
  vellum/workflows/logging.py,sha256=_a217XogktV4Ncz6xKFz7WfYmZAzkfVRVuC0rWob8ls,437
@@ -1334,7 +1334,7 @@ vellum/workflows/outputs/__init__.py,sha256=AyZ4pRh_ACQIGvkf0byJO46EDnSix1ZCAXfv
1334
1334
  vellum/workflows/outputs/base.py,sha256=a7W6rNSDSawwGAXYjNTF2iHb9lnZu7WFSOagZIyy__k,7976
1335
1335
  vellum/workflows/ports/__init__.py,sha256=bZuMt-R7z5bKwpu4uPW7LlJeePOQWmCcDSXe5frUY5g,101
1336
1336
  vellum/workflows/ports/node_ports.py,sha256=g4A-8iUAvEJSkaWppbvzAR8XU02R9U-qLN4rP2Kq4Aw,2743
1337
- vellum/workflows/ports/port.py,sha256=iLYzjIoFk5HWpNyhySf8E1uIuJ3MbAeJGD3yOnnLY70,2843
1337
+ vellum/workflows/ports/port.py,sha256=4vTvhe9FE5SO5WwwAmJm2ApBBBxZv9fsyUoikPIgiaI,2947
1338
1338
  vellum/workflows/ports/utils.py,sha256=pEjVNJKw9LhD_cFN-o0MWBOW2ejno7jv26qqzjLxwS4,1662
1339
1339
  vellum/workflows/references/__init__.py,sha256=glHFC1VfXmcbNvH5VzFbkT03d8_D7MMcvEcsUBrzLIs,591
1340
1340
  vellum/workflows/references/environment_variable.py,sha256=7FFtiKfc4eyVkkfUbhc666OBNDqvFlMoNQEYmGpEVVE,661
@@ -1377,8 +1377,8 @@ vellum/workflows/vellum_client.py,sha256=ODrq_TSl-drX2aezXegf7pizpWDVJuTXH-j6528
1377
1377
  vellum/workflows/workflows/__init__.py,sha256=KY45TqvavCCvXIkyCFMEc0dc6jTMOUci93U2DUrlZYc,66
1378
1378
  vellum/workflows/workflows/base.py,sha256=mnI-kZ78yt7u6NFSTUo-tYjDnarP-RJ7uZjwjCn6PCQ,16795
1379
1379
  vellum/workflows/workflows/event_filters.py,sha256=-uQcMB7IpPd-idMku8f2QNVhPXPFWo6FZLlGjRf8rCo,1996
1380
- vellum_ai-0.11.3.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1381
- vellum_ai-0.11.3.dist-info/METADATA,sha256=6F3pwCqaSVevjDTq4ErE748xs0VspO_aHNYnoOSq-dY,5128
1382
- vellum_ai-0.11.3.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
1383
- vellum_ai-0.11.3.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1384
- vellum_ai-0.11.3.dist-info/RECORD,,
1380
+ vellum_ai-0.11.4.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1381
+ vellum_ai-0.11.4.dist-info/METADATA,sha256=jsthltxAbW8_ohEuJ38axnhG6JBG-DLG6p2bwAgcAwA,5128
1382
+ vellum_ai-0.11.4.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
1383
+ vellum_ai-0.11.4.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1384
+ vellum_ai-0.11.4.dist-info/RECORD,,
vellum_cli/config.py CHANGED
@@ -20,6 +20,15 @@ class WorkflowDeploymentConfig(UniversalBaseModel):
20
20
  description: Optional[str] = None
21
21
  release_tags: Optional[List[str]] = None
22
22
 
23
+ def merge(self, other: "WorkflowDeploymentConfig") -> "WorkflowDeploymentConfig":
24
+ return WorkflowDeploymentConfig(
25
+ id=self.id or other.id,
26
+ label=self.label or other.label,
27
+ name=self.name or other.name,
28
+ description=self.description or other.description,
29
+ release_tags=self.release_tags or other.release_tags,
30
+ )
31
+
23
32
 
24
33
  class WorkflowConfig(UniversalBaseModel):
25
34
  module: str
@@ -28,10 +37,37 @@ class WorkflowConfig(UniversalBaseModel):
28
37
  deployments: List[WorkflowDeploymentConfig] = field(default_factory=list)
29
38
 
30
39
  def merge(self, other: "WorkflowConfig") -> "WorkflowConfig":
40
+ self_deployment_by_id = {
41
+ deployment.id: deployment for deployment in self.deployments if deployment.id is not None
42
+ }
43
+ other_deployment_by_id = {
44
+ deployment.id: deployment for deployment in other.deployments if deployment.id is not None
45
+ }
46
+ all_ids = sorted(set(self_deployment_by_id.keys()).union(set(other_deployment_by_id.keys())))
47
+ merged_deployments = []
48
+ for id in all_ids:
49
+ self_deployment = self_deployment_by_id.get(id)
50
+ other_deployment = other_deployment_by_id.get(id)
51
+ if self_deployment and other_deployment:
52
+ merged_deployments.append(self_deployment.merge(other_deployment))
53
+ elif self_deployment:
54
+ merged_deployments.append(self_deployment)
55
+ elif other_deployment:
56
+ merged_deployments.append(other_deployment)
57
+
58
+ for deployment in self.deployments:
59
+ if deployment.id is None:
60
+ merged_deployments.append(deployment)
61
+
62
+ for deployment in other.deployments:
63
+ if deployment.id is None:
64
+ merged_deployments.append(deployment)
65
+
31
66
  return WorkflowConfig(
32
67
  module=self.module,
33
68
  workflow_sandbox_id=self.workflow_sandbox_id or other.workflow_sandbox_id,
34
69
  ignore=self.ignore or other.ignore,
70
+ deployments=merged_deployments,
35
71
  )
36
72
 
37
73
 
vellum_cli/push.py CHANGED
@@ -1,3 +1,4 @@
1
+ from importlib import metadata
1
2
  import io
2
3
  import json
3
4
  import os
@@ -50,6 +51,9 @@ def push_command(
50
51
  workflow = BaseWorkflow.load_from_module(workflow_config.module)
51
52
  workflow_display = get_workflow_display(base_display_class=VellumWorkflowDisplay, workflow_class=workflow)
52
53
  exec_config = workflow_display.serialize()
54
+ exec_config["runner_config"] = {
55
+ "sdk_version": metadata.version("vellum-ai"),
56
+ }
53
57
 
54
58
  label = snake_to_title_case(workflow_config.module.split(".")[-1])
55
59
 
@@ -1,19 +1,29 @@
1
1
  import pytest
2
+ from dataclasses import dataclass
2
3
  import os
3
4
  import shutil
4
5
  import tempfile
5
6
  from uuid import uuid4
6
- from typing import Any, Callable, Dict, Generator, Tuple
7
+ from typing import Any, Callable, Dict, Generator
7
8
 
8
9
  import tomli_w
9
10
 
10
11
 
12
+ @dataclass
13
+ class MockModuleResult:
14
+ temp_dir: str
15
+ module: str
16
+ set_pyproject_toml: Callable[[Dict[str, Any]], None]
17
+ workflow_sandbox_id: str
18
+
19
+
11
20
  @pytest.fixture
12
- def mock_module() -> Generator[Tuple[str, str, Callable[[Dict[str, Any]], None]], None, None]:
21
+ def mock_module() -> Generator[MockModuleResult, None, None]:
13
22
  current_dir = os.getcwd()
14
23
  temp_dir = tempfile.mkdtemp()
15
24
  os.chdir(temp_dir)
16
25
  module = "examples.mock"
26
+ workflow_sandbox_id = str(uuid4())
17
27
 
18
28
  def set_pyproject_toml(vellum_config: Dict[str, Any]) -> None:
19
29
  pyproject_toml_path = os.path.join(temp_dir, "pyproject.toml")
@@ -28,13 +38,18 @@ def mock_module() -> Generator[Tuple[str, str, Callable[[Dict[str, Any]], None]]
28
38
  "workflows": [
29
39
  {
30
40
  "module": module,
31
- "workflow_sandbox_id": str(uuid4()),
41
+ "workflow_sandbox_id": workflow_sandbox_id,
32
42
  }
33
43
  ]
34
44
  }
35
45
  )
36
46
 
37
- yield temp_dir, module, set_pyproject_toml
47
+ yield MockModuleResult(
48
+ temp_dir=temp_dir,
49
+ module=module,
50
+ set_pyproject_toml=set_pyproject_toml,
51
+ workflow_sandbox_id=workflow_sandbox_id,
52
+ )
38
53
 
39
54
  os.chdir(current_dir)
40
55
  shutil.rmtree(temp_dir)
@@ -37,7 +37,8 @@ def _zip_file_map(file_map: dict[str, str]) -> bytes:
37
37
  )
38
38
  def test_pull(vellum_client, mock_module, base_command):
39
39
  # GIVEN a module on the user's filesystem
40
- temp_dir, module, _ = mock_module
40
+ temp_dir = mock_module.temp_dir
41
+ module = mock_module.module
41
42
 
42
43
  # AND the workflow pull API call returns a zip file
43
44
  vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
@@ -58,7 +59,9 @@ def test_pull(vellum_client, mock_module, base_command):
58
59
 
59
60
  def test_pull__second_module(vellum_client, mock_module):
60
61
  # GIVEN a module on the user's filesystem
61
- temp_dir, module, set_pyproject_toml = mock_module
62
+ temp_dir = mock_module.temp_dir
63
+ module = mock_module.module
64
+ set_pyproject_toml = mock_module.set_pyproject_toml
62
65
 
63
66
  # AND the workflow pull API call returns a zip file
64
67
  vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
@@ -134,7 +137,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
134
137
 
135
138
  def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_module):
136
139
  # GIVEN a pyproject.toml with a workflow configured
137
- temp_dir, _, _ = mock_module
140
+ temp_dir = mock_module.temp_dir
138
141
 
139
142
  # AND a different workflow sandbox id
140
143
  workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
@@ -163,7 +166,8 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
163
166
 
164
167
  def test_pull__remove_missing_files(vellum_client, mock_module):
165
168
  # GIVEN a module on the user's filesystem
166
- temp_dir, module, _ = mock_module
169
+ temp_dir = mock_module.temp_dir
170
+ module = mock_module.module
167
171
 
168
172
  # AND the workflow pull API call returns a zip file
169
173
  vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
@@ -192,7 +196,9 @@ def test_pull__remove_missing_files(vellum_client, mock_module):
192
196
 
193
197
  def test_pull__remove_missing_files__ignore_pattern(vellum_client, mock_module):
194
198
  # GIVEN a module on the user's filesystem
195
- temp_dir, module, set_pyproject_toml = mock_module
199
+ temp_dir = mock_module.temp_dir
200
+ module = mock_module.module
201
+ set_pyproject_toml = mock_module.set_pyproject_toml
196
202
 
197
203
  # AND the workflow pull API call returns a zip file
198
204
  vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
@@ -243,7 +249,7 @@ def test_pull__remove_missing_files__ignore_pattern(vellum_client, mock_module):
243
249
 
244
250
  def test_pull__include_json(vellum_client, mock_module):
245
251
  # GIVEN a module on the user's filesystem
246
- _, module, __ = mock_module
252
+ module = mock_module.module
247
253
 
248
254
  # AND the workflow pull API call returns a zip file
249
255
  vellum_client.workflows.pull.return_value = iter(
@@ -265,7 +271,7 @@ def test_pull__include_json(vellum_client, mock_module):
265
271
 
266
272
  def test_pull__exclude_code(vellum_client, mock_module):
267
273
  # GIVEN a module on the user's filesystem
268
- _, module, __ = mock_module
274
+ module = mock_module.module
269
275
 
270
276
  # AND the workflow pull API call returns a zip file
271
277
  vellum_client.workflows.pull.return_value = iter(
@@ -283,3 +289,75 @@ def test_pull__exclude_code(vellum_client, mock_module):
283
289
  vellum_client.workflows.pull.assert_called_once()
284
290
  call_args = vellum_client.workflows.pull.call_args.kwargs
285
291
  assert call_args["request_options"]["additional_query_parameters"] == {"exclude_code": True}
292
+
293
+
294
+ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client, mock_module):
295
+ # GIVEN a pyproject.toml with a workflow configured
296
+ temp_dir = mock_module.temp_dir
297
+ module = mock_module.module
298
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
299
+
300
+ # AND there's a workflow deployment in the lock file
301
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
302
+ with open(vellum_lock_json, "w") as f:
303
+ json.dump(
304
+ {
305
+ "version": "1.0",
306
+ "workflows": [
307
+ {
308
+ "module": module,
309
+ "workflow_sandbox_id": "0edc07cd-45b9-43e8-99bc-1f181972a857",
310
+ "ignore": "tests/*",
311
+ "deployments": [
312
+ {
313
+ "id": "7e5a7610-4c46-4bc9-b06e-0fc6a9e28959",
314
+ "label": None,
315
+ "name": None,
316
+ "description": None,
317
+ "release_tags": None,
318
+ }
319
+ ],
320
+ }
321
+ ],
322
+ },
323
+ f,
324
+ )
325
+
326
+ # AND a different workflow sandbox id
327
+ new_workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
328
+
329
+ # AND the workflow pull API call returns a zip file
330
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
331
+
332
+ # WHEN the user runs the pull command with the new workflow sandbox id
333
+ runner = CliRunner()
334
+ result = runner.invoke(cli_main, ["workflows", "pull", "--workflow-sandbox-id", new_workflow_sandbox_id])
335
+
336
+ # THEN the command returns successfully
337
+ assert result.exit_code == 0
338
+
339
+ # AND the lock file is updated to preserve the deployment and include the new workflow
340
+ with open(vellum_lock_json) as f:
341
+ lock_data = json.load(f)
342
+ assert lock_data["workflows"] == [
343
+ {
344
+ "module": module,
345
+ "workflow_sandbox_id": workflow_sandbox_id,
346
+ "ignore": "tests/*",
347
+ "deployments": [
348
+ {
349
+ "id": "7e5a7610-4c46-4bc9-b06e-0fc6a9e28959",
350
+ "label": None,
351
+ "name": None,
352
+ "description": None,
353
+ "release_tags": None,
354
+ },
355
+ ],
356
+ },
357
+ {
358
+ "module": "workflow_87654321",
359
+ "workflow_sandbox_id": new_workflow_sandbox_id,
360
+ "ignore": None,
361
+ "deployments": [],
362
+ },
363
+ ]
@@ -29,8 +29,7 @@ def _extract_tar_gz(tar_gz_bytes: bytes) -> dict[str, str]:
29
29
 
30
30
  def test_push__no_config(mock_module):
31
31
  # GIVEN no config file set
32
- _, _, set_pyproject_toml = mock_module
33
- set_pyproject_toml({"workflows": []})
32
+ mock_module.set_pyproject_toml({"workflows": []})
34
33
 
35
34
  # WHEN calling `vellum push`
36
35
  runner = CliRunner()
@@ -44,8 +43,7 @@ def test_push__no_config(mock_module):
44
43
 
45
44
  def test_push__multiple_workflows_configured__no_module_specified(mock_module):
46
45
  # GIVEN multiple workflows configured
47
- _, _, set_pyproject_toml = mock_module
48
- set_pyproject_toml({"workflows": [{"module": "examples.mock"}, {"module": "examples.mock2"}]})
46
+ mock_module.set_pyproject_toml({"workflows": [{"module": "examples.mock"}, {"module": "examples.mock2"}]})
49
47
 
50
48
  # WHEN calling `vellum push` without a module specified
51
49
  runner = CliRunner()
@@ -62,8 +60,8 @@ def test_push__multiple_workflows_configured__no_module_specified(mock_module):
62
60
 
63
61
  def test_push__multiple_workflows_configured__not_found_module(mock_module):
64
62
  # GIVEN multiple workflows configured
65
- _, module, set_pyproject_toml = mock_module
66
- set_pyproject_toml({"workflows": [{"module": "examples.mock2"}, {"module": "examples.mock3"}]})
63
+ module = mock_module.module
64
+ mock_module.set_pyproject_toml({"workflows": [{"module": "examples.mock2"}, {"module": "examples.mock3"}]})
67
65
 
68
66
  # WHEN calling `vellum push` with a module that doesn't exist
69
67
  runner = CliRunner()
@@ -85,7 +83,8 @@ def test_push__multiple_workflows_configured__not_found_module(mock_module):
85
83
  )
86
84
  def test_push__happy_path(mock_module, vellum_client, base_command):
87
85
  # GIVEN a single workflow configured
88
- temp_dir, module, _ = mock_module
86
+ temp_dir = mock_module.temp_dir
87
+ module = mock_module.module
89
88
 
90
89
  # AND a workflow exists in the module successfully
91
90
  base_dir = os.path.join(temp_dir, *module.split("."))
@@ -134,7 +133,8 @@ class ExampleWorkflow(BaseWorkflow):
134
133
  )
135
134
  def test_push__deployment(mock_module, vellum_client, base_command):
136
135
  # GIVEN a single workflow configured
137
- temp_dir, module, _ = mock_module
136
+ temp_dir = mock_module.temp_dir
137
+ module = mock_module.module
138
138
 
139
139
  # AND a workflow exists in the module successfully
140
140
  base_dir = os.path.join(temp_dir, *module.split("."))