vellum-ai 0.11.8__py3-none-any.whl → 0.11.10__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.8",
20
+ "X-Fern-SDK-Version": "0.11.10",
21
21
  }
22
22
  headers["X_API_KEY"] = self.api_key
23
23
  return headers
@@ -73,7 +73,8 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
73
73
  request_options: Optional[RequestOptions] = None - The request options to use for the custom script.
74
74
  """
75
75
 
76
- filepath: ClassVar[str]
76
+ filepath: ClassVar[Optional[str]] = None
77
+ code: ClassVar[Optional[str]] = None
77
78
 
78
79
  code_inputs: ClassVar[EntityInputsInterface]
79
80
  runtime: CodeExecutionRuntime = "PYTHON_3_11_6"
@@ -190,6 +191,21 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
190
191
  return compiled_inputs
191
192
 
192
193
  def _resolve_code(self) -> str:
194
+ if self.code and self.filepath:
195
+ raise NodeException(
196
+ message="Cannot specify both `code` and `filepath` for a CodeExecutionNode",
197
+ code=VellumErrorCode.INVALID_INPUTS,
198
+ )
199
+
200
+ if self.code:
201
+ return self.code
202
+
203
+ if not self.filepath:
204
+ raise NodeException(
205
+ message="Must specify either `code` or `filepath` for a CodeExecutionNode",
206
+ code=VellumErrorCode.INVALID_INPUTS,
207
+ )
208
+
193
209
  root = inspect.getfile(self.__class__)
194
210
  code = read_file_from_path(node_filepath=root, script_filepath=self.filepath)
195
211
  if not code:
@@ -1,6 +1,8 @@
1
+ import pytest
1
2
  import os
2
3
 
3
4
  from vellum import CodeExecutorResponse, NumberVellumValue, StringInput
5
+ from vellum.workflows.exceptions import NodeException
4
6
  from vellum.workflows.inputs.base import BaseInputs
5
7
  from vellum.workflows.nodes.displayable.code_execution_node import CodeExecutionNode
6
8
  from vellum.workflows.references.vellum_secret import VellumSecretReference
@@ -62,6 +64,145 @@ def main(word: str) -> int:
62
64
  )
63
65
 
64
66
 
67
+ def test_run_workflow__code_attribute(vellum_client):
68
+ """Confirm that CodeExecutionNodes can use the `code` attribute to specify the code to execute."""
69
+
70
+ # GIVEN a node that subclasses CodeExecutionNode
71
+ class Inputs(BaseInputs):
72
+ word: str
73
+
74
+ class State(BaseState):
75
+ pass
76
+
77
+ class ExampleCodeExecutionNode(CodeExecutionNode[State, int]):
78
+ code = """\
79
+ def main(word: str) -> int:
80
+ print(word) # noqa: T201
81
+ return len(word)
82
+ """
83
+ runtime = "PYTHON_3_11_6"
84
+
85
+ code_inputs = {
86
+ "word": Inputs.word,
87
+ }
88
+
89
+ # AND we know what the Code Execution Node will respond with
90
+ mock_code_execution = CodeExecutorResponse(
91
+ log="hello",
92
+ output=NumberVellumValue(value=5),
93
+ )
94
+ vellum_client.execute_code.return_value = mock_code_execution
95
+
96
+ # WHEN we run the node
97
+ node = ExampleCodeExecutionNode(
98
+ state=State(
99
+ meta=StateMeta(workflow_inputs=Inputs(word="hello")),
100
+ )
101
+ )
102
+ outputs = node.run()
103
+
104
+ # THEN the node should have produced the outputs we expect
105
+ assert outputs == {"result": 5, "log": "hello"}
106
+
107
+ # AND we should have invoked the Code with the expected inputs
108
+ vellum_client.execute_code.assert_called_once_with(
109
+ input_values=[
110
+ StringInput(name="word", value="hello"),
111
+ ],
112
+ code="""\
113
+ def main(word: str) -> int:
114
+ print(word) # noqa: T201
115
+ return len(word)
116
+ """,
117
+ runtime="PYTHON_3_11_6",
118
+ output_type="NUMBER",
119
+ packages=[],
120
+ request_options=None,
121
+ )
122
+
123
+
124
+ def test_run_workflow__code_and_filepath_defined(vellum_client):
125
+ """Confirm that CodeExecutionNodes raise an error if both `code` and `filepath` are defined."""
126
+
127
+ # GIVEN a node that subclasses CodeExecutionNode
128
+ class Inputs(BaseInputs):
129
+ word: str
130
+
131
+ class State(BaseState):
132
+ pass
133
+
134
+ fixture = os.path.abspath(os.path.join(__file__, "../fixtures/main.py"))
135
+
136
+ class ExampleCodeExecutionNode(CodeExecutionNode[State, int]):
137
+ filepath = fixture
138
+ code = """\
139
+ def main(word: str) -> int:
140
+ print(word) # noqa: T201
141
+ return len(word)
142
+ """
143
+ runtime = "PYTHON_3_11_6"
144
+
145
+ code_inputs = {
146
+ "word": Inputs.word,
147
+ }
148
+
149
+ # AND we know what the Code Execution Node will respond with
150
+ mock_code_execution = CodeExecutorResponse(
151
+ log="hello",
152
+ output=NumberVellumValue(value=5),
153
+ )
154
+ vellum_client.execute_code.return_value = mock_code_execution
155
+
156
+ # WHEN we run the node
157
+ node = ExampleCodeExecutionNode(
158
+ state=State(
159
+ meta=StateMeta(workflow_inputs=Inputs(word="hello")),
160
+ )
161
+ )
162
+ with pytest.raises(NodeException) as exc_info:
163
+ node.run()
164
+
165
+ # THEN the node should have produced the exception we expected
166
+ assert exc_info.value.message == "Cannot specify both `code` and `filepath` for a CodeExecutionNode"
167
+
168
+
169
+ def test_run_workflow__code_and_filepath_not_defined(vellum_client):
170
+ """Confirm that CodeExecutionNodes raise an error if neither `code` nor `filepath` are defined."""
171
+
172
+ # GIVEN a node that subclasses CodeExecutionNode
173
+ class Inputs(BaseInputs):
174
+ word: str
175
+
176
+ class State(BaseState):
177
+ pass
178
+
179
+ class ExampleCodeExecutionNode(CodeExecutionNode[State, int]):
180
+ runtime = "PYTHON_3_11_6"
181
+
182
+ code_inputs = {
183
+ "word": Inputs.word,
184
+ }
185
+
186
+ # AND we know what the Code Execution Node will respond with
187
+ mock_code_execution = CodeExecutorResponse(
188
+ log="hello",
189
+ output=NumberVellumValue(value=5),
190
+ )
191
+ vellum_client.execute_code.return_value = mock_code_execution
192
+
193
+ # WHEN we run the node
194
+ node = ExampleCodeExecutionNode(
195
+ state=State(
196
+ meta=StateMeta(workflow_inputs=Inputs(word="hello")),
197
+ )
198
+ )
199
+ with pytest.raises(NodeException) as exc_info:
200
+ node.run()
201
+
202
+ # THEN the node should have produced the exception we expected
203
+ assert exc_info.value.message == "Must specify either `code` or `filepath` for a CodeExecutionNode"
204
+
205
+
65
206
  def test_run_workflow__vellum_secret(vellum_client):
66
207
  """Confirm that CodeExecutionNodes can use Vellum Secrets"""
67
208
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.11.8
3
+ Version: 0.11.10
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -1,17 +1,17 @@
1
1
  vellum_cli/CONTRIBUTING.md,sha256=FtDC7BGxSeMnwCXAUssFsAIElXtmJE-O5Z7BpolcgvI,2935
2
2
  vellum_cli/README.md,sha256=2NudRoLzWxNKqnuVy1JuQ7DerIaxWGYkrH8kMd-asIE,90
3
- vellum_cli/__init__.py,sha256=pftUQ6FiyfebNEB8xcfwzLjpfFDCAiH15xHBU6xr_wY,6733
3
+ vellum_cli/__init__.py,sha256=XG6aC1NSPfBjNFzqiy9Lbk4DnELCKO8lpIZ3jeJR0r0,6990
4
4
  vellum_cli/aliased_group.py,sha256=ugW498j0yv4ALJ8vS9MsO7ctDW7Jlir9j6nE_uHAP8c,3363
5
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
- vellum_cli/pull.py,sha256=6wIiorqSx2rmR6atZJHHBuLSviocxK_n0DQxEDGmCzo,4008
8
+ vellum_cli/pull.py,sha256=EkQirsTe0KCpJ8oHTE3S9_M2XHSrb2wrXNq4hTkMZng,5770
9
9
  vellum_cli/push.py,sha256=kbvlzZ9KnkS5DxxKHQP5ZvHHk1-CbCDg9LqnIRAWyt4,5258
10
10
  vellum_cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
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=N6ZphvHYGokclbpbTpgOmpu_m2GtocDEesbdeHFjO5Y,13194
14
+ vellum_cli/tests/test_pull.py,sha256=pGqpBkahC360UITDE8BlcJ7LPd9DyRPn4NrCNdkQmAQ,15907
15
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
@@ -20,7 +20,7 @@ vellum_ee/workflows/display/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
20
20
  vellum_ee/workflows/display/base.py,sha256=3ZFUYRNKL24fBqXhKpa_Dq2W1a-a86J20dmJYA3H2eY,1755
21
21
  vellum_ee/workflows/display/nodes/__init__.py,sha256=5XOcZJXYUgaLS55QgRJzyQ_W1tpeprjnYAeYVezqoGw,160
22
22
  vellum_ee/workflows/display/nodes/base_node_display.py,sha256=3W7X1V2Lv0k6djYp60LDu-0lYVMNsEjPXmNmIQ4UW6s,5961
23
- vellum_ee/workflows/display/nodes/base_node_vellum_display.py,sha256=HoD3AGCMXKoHyyRJteUYlQ7DR26Srjhlrv4fZlLCyKc,1649
23
+ vellum_ee/workflows/display/nodes/base_node_vellum_display.py,sha256=F30B7L3HJLdeYzQmn17KVduWWXPb3KFvFFEVltYWxvY,2124
24
24
  vellum_ee/workflows/display/nodes/get_node_display_class.py,sha256=xg6DWEm8CDiOn_fX74fT2XtSEDCnq06dHKj7HX9JnJw,907
25
25
  vellum_ee/workflows/display/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=0y20AAqDivg58hQjHbiCPjEdEghfWmWr-NdYw3u-AwM,1054
@@ -28,7 +28,7 @@ vellum_ee/workflows/display/nodes/types.py,sha256=St1BB6no528OyELGiyRabWao0GGw6m
28
28
  vellum_ee/workflows/display/nodes/utils.py,sha256=sloya5TpXsnot1HURc9L51INwflRqUzHxRVnCS9Cd-4,973
29
29
  vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=nmPLj8vkbVCS46XQqmHq8Xj8Mr36wCK_vWf26A9KIkw,1505
30
30
  vellum_ee/workflows/display/nodes/vellum/api_node.py,sha256=4SSQGecKWHuoGy5YIGJeOZVHGKwTs_8Y-gf3GvsHb0M,8506
31
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=HzB4zQ6MYlRII9GsZcBPzOswFUuwBjn-b3FuDLNyujg,4025
31
+ vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=qJXUTVGfL88Katxu6HQb4yCPUzBFTBFDXcf8pdId3U8,4076
32
32
  vellum_ee/workflows/display/nodes/vellum/conditional_node.py,sha256=gUbSP8_oSAMNIb0CGiefd2FMYgoO6wMoG6iA1FakMjk,13293
33
33
  vellum_ee/workflows/display/nodes/vellum/error_node.py,sha256=ygTjSjYDI4DtkxADWub5rhBnRWItMKWF6fezBrgpOKA,1979
34
34
  vellum_ee/workflows/display/nodes/vellum/final_output_node.py,sha256=UezalObmZ3mcg7Nou2RgiI_0cmc7_tSdZLNB591iCcI,2772
@@ -73,13 +73,13 @@ vellum_ee/workflows/display/vellum.py,sha256=OSv0ZS50h1zJbunJ9TH7VEWFw-exXdK_Zsd
73
73
  vellum_ee/workflows/display/workflows/__init__.py,sha256=kapXsC67VJcgSuiBMa86FdePG5A9kMB5Pi4Uy1O2ob4,207
74
74
  vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=HkakkrNgVFoHlUP7yHlQjHOvii3CZ90iyU1062PfoW4,12819
75
75
  vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=AMxNnTm2z3LIR5rqxoCAfuy37F2FTuSRDVtKUoezO8M,1184
76
- vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=UiE1vJ4nzt1tHAxvXSt8qnV101I__gd5YdiPuKA4TWk,17370
76
+ vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=DWaiYdP138kN3fbQvloh6VEBPlxzd6zSLydc6NpYu3s,17145
77
77
  vellum/__init__.py,sha256=QmGeEPXeFxgkZa849KKK3wH3Y641wyt00Rytfay6KiM,35520
78
78
  vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
79
79
  vellum/client/__init__.py,sha256=o4m7iRZWEV8rP3GkdaztHAjNmjxjWERlarviFoHzuKI,110927
80
80
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
81
81
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
82
- vellum/client/core/client_wrapper.py,sha256=KtxhfH7jJZXfEPTJ_JeglwXX43TbY0JGd81hjEJN-Gw,1890
82
+ vellum/client/core/client_wrapper.py,sha256=6zb4goHdzGRX1BY9bkyHFFaNstiWKUbnXvOxT_2cidM,1891
83
83
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
84
84
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
85
85
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -1302,11 +1302,11 @@ vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py,sha256=1_OXD
1302
1302
  vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py,sha256=MdrAKN8QGPk_JnNjbEBaVVKwVLPE2judbBcWuYJgbkY,4964
1303
1303
  vellum/workflows/nodes/displayable/bases/search_node.py,sha256=S7J8tTW681O4wcWYerGOfH6h-_BlE8-JMJHpW8eCVG0,3564
1304
1304
  vellum/workflows/nodes/displayable/code_execution_node/__init__.py,sha256=0FLWMMktpzSnmBMizQglBpcPrP80fzVsoJwJgf822Cg,76
1305
- vellum/workflows/nodes/displayable/code_execution_node/node.py,sha256=uwT_sn-XT0uYe7E-0DcJfcb3X33pwE0sw-2ri7FhoTo,8073
1305
+ vellum/workflows/nodes/displayable/code_execution_node/node.py,sha256=OHU9SnB0JDTTSFIp7io-9Jckelqtq84AP-QOcZBRNXY,8640
1306
1306
  vellum/workflows/nodes/displayable/code_execution_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1307
1307
  vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1308
1308
  vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/main.py,sha256=5QsbmkzSlSbcbWTG_JmIqcP-JNJzOPTKxGzdHos19W4,79
1309
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py,sha256=2Kr7fKtjc1fW5z_6z6noKfWoETIVJbYi0AGhhSw-hsU,3376
1309
+ vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py,sha256=s93M_EnU-4n60iSKv3FCf0kppwzFH5FNi9o9E58fQ3I,7510
1310
1310
  vellum/workflows/nodes/displayable/code_execution_node/utils.py,sha256=LfI3kj2zQz6UGMld_uA9z2LjZobqRcgxQO4jdUWkg7o,376
1311
1311
  vellum/workflows/nodes/displayable/conditional_node/__init__.py,sha256=AS_EIqFdU1F9t8aLmbZU-rLh9ry6LCJ0uj0D8F0L5Uw,72
1312
1312
  vellum/workflows/nodes/displayable/conditional_node/node.py,sha256=REFZdEVetXGyOK1RbIN1T6yRblrP0hfyZUls2KfjTKg,1016
@@ -1378,8 +1378,8 @@ vellum/workflows/vellum_client.py,sha256=ODrq_TSl-drX2aezXegf7pizpWDVJuTXH-j6528
1378
1378
  vellum/workflows/workflows/__init__.py,sha256=KY45TqvavCCvXIkyCFMEc0dc6jTMOUci93U2DUrlZYc,66
1379
1379
  vellum/workflows/workflows/base.py,sha256=mnI-kZ78yt7u6NFSTUo-tYjDnarP-RJ7uZjwjCn6PCQ,16795
1380
1380
  vellum/workflows/workflows/event_filters.py,sha256=-uQcMB7IpPd-idMku8f2QNVhPXPFWo6FZLlGjRf8rCo,1996
1381
- vellum_ai-0.11.8.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1382
- vellum_ai-0.11.8.dist-info/METADATA,sha256=7H-xm-0FDR9QYaVdqlM8w4rVbWfsK9-0osBbv0npgWk,5128
1383
- vellum_ai-0.11.8.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
1384
- vellum_ai-0.11.8.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1385
- vellum_ai-0.11.8.dist-info/RECORD,,
1381
+ vellum_ai-0.11.10.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1382
+ vellum_ai-0.11.10.dist-info/METADATA,sha256=mPMns1QolQ74Dw64M1w0SbtZXWFMMpEfK7VBYRrAmj8,5129
1383
+ vellum_ai-0.11.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1384
+ vellum_ai-0.11.10.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1385
+ vellum_ai-0.11.10.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
vellum_cli/__init__.py CHANGED
@@ -148,6 +148,11 @@ def pull(
148
148
  Should only be used for debugging purposes.""",
149
149
  )
150
150
  @click.option("--workflow-sandbox-id", type=str, help="Pull the Workflow from a specific Sandbox ID")
151
+ @click.option(
152
+ "--workflow-deployment",
153
+ type=str,
154
+ help="""Pull the Workflow from a specific Deployment. Can use the name or the ID of the Deployment.""",
155
+ )
151
156
  @click.option(
152
157
  "--exclude-code",
153
158
  is_flag=True,
@@ -158,6 +163,7 @@ def workflows_pull(
158
163
  module: Optional[str],
159
164
  include_json: Optional[bool],
160
165
  workflow_sandbox_id: Optional[str],
166
+ workflow_deployment: Optional[str],
161
167
  exclude_code: Optional[bool],
162
168
  ) -> None:
163
169
  """
@@ -169,6 +175,7 @@ def workflows_pull(
169
175
  module=module,
170
176
  include_json=include_json,
171
177
  workflow_sandbox_id=workflow_sandbox_id,
178
+ workflow_deployment=workflow_deployment,
172
179
  exclude_code=exclude_code,
173
180
  )
174
181
 
vellum_cli/pull.py CHANGED
@@ -1,39 +1,84 @@
1
1
  import io
2
2
  import os
3
3
  from pathlib import Path
4
+ from uuid import UUID
4
5
  import zipfile
5
- from typing import Optional
6
+ from typing import Optional, Union
6
7
 
7
8
  from dotenv import load_dotenv
9
+ from pydash import snake_case
8
10
 
11
+ from vellum.client.core.pydantic_utilities import UniversalBaseModel
9
12
  from vellum.workflows.vellum_client import create_vellum_client
10
13
  from vellum_cli.config import VellumCliConfig, WorkflowConfig, load_vellum_cli_config
11
14
  from vellum_cli.logger import load_cli_logger
12
15
 
13
16
 
14
- def resolve_workflow_config(
17
+ def _is_valid_uuid(val: Union[str, UUID, None]) -> bool:
18
+ try:
19
+ UUID(str(val))
20
+ return True
21
+ except (ValueError, TypeError):
22
+ return False
23
+
24
+
25
+ class WorkflowConfigResolutionResult(UniversalBaseModel):
26
+ workflow_config: Optional[WorkflowConfig] = None
27
+ pk: Optional[str] = None
28
+
29
+
30
+ def _resolve_workflow_config(
15
31
  config: VellumCliConfig,
16
32
  module: Optional[str] = None,
17
33
  workflow_sandbox_id: Optional[str] = None,
18
- ) -> Optional[WorkflowConfig]:
34
+ workflow_deployment: Optional[str] = None,
35
+ ) -> WorkflowConfigResolutionResult:
36
+ if workflow_sandbox_id and workflow_deployment:
37
+ raise ValueError("Cannot specify both workflow_sandbox_id and workflow_deployment")
38
+
19
39
  if module:
20
- return next((w for w in config.workflows if w.module == module), None)
40
+ workflow_config = next((w for w in config.workflows if w.module == module), None)
41
+ return WorkflowConfigResolutionResult(
42
+ workflow_config=workflow_config,
43
+ pk=workflow_config.workflow_sandbox_id if workflow_config else None,
44
+ )
21
45
  elif workflow_sandbox_id:
22
46
  workflow_config = WorkflowConfig(
23
47
  workflow_sandbox_id=workflow_sandbox_id,
24
48
  module=f"workflow_{workflow_sandbox_id.split('-')[0]}",
25
49
  )
26
50
  config.workflows.append(workflow_config)
27
- return workflow_config
51
+ return WorkflowConfigResolutionResult(
52
+ workflow_config=workflow_config,
53
+ pk=workflow_config.workflow_sandbox_id,
54
+ )
55
+ elif workflow_deployment:
56
+ module = (
57
+ f"workflow_{workflow_deployment.split('-')[0]}"
58
+ if _is_valid_uuid(workflow_deployment)
59
+ else snake_case(workflow_deployment)
60
+ )
61
+ workflow_config = WorkflowConfig(
62
+ module=module,
63
+ )
64
+ config.workflows.append(workflow_config)
65
+ return WorkflowConfigResolutionResult(
66
+ workflow_config=workflow_config,
67
+ pk=workflow_deployment,
68
+ )
28
69
  elif config.workflows:
29
- return config.workflows[0]
70
+ return WorkflowConfigResolutionResult(
71
+ workflow_config=config.workflows[0],
72
+ pk=config.workflows[0].workflow_sandbox_id,
73
+ )
30
74
 
31
- return None
75
+ return WorkflowConfigResolutionResult()
32
76
 
33
77
 
34
78
  def pull_command(
35
79
  module: Optional[str] = None,
36
80
  workflow_sandbox_id: Optional[str] = None,
81
+ workflow_deployment: Optional[str] = None,
37
82
  include_json: Optional[bool] = None,
38
83
  exclude_code: Optional[bool] = None,
39
84
  ) -> None:
@@ -41,17 +86,20 @@ def pull_command(
41
86
  logger = load_cli_logger()
42
87
  config = load_vellum_cli_config()
43
88
 
44
- workflow_config = resolve_workflow_config(
45
- config,
46
- module,
47
- workflow_sandbox_id,
89
+ workflow_config_result = _resolve_workflow_config(
90
+ config=config,
91
+ module=module,
92
+ workflow_sandbox_id=workflow_sandbox_id,
93
+ workflow_deployment=workflow_deployment,
48
94
  )
49
95
  save_lock_file = not module
50
96
 
97
+ workflow_config = workflow_config_result.workflow_config
51
98
  if not workflow_config:
52
99
  raise ValueError("No workflow config found in project to pull from.")
53
100
 
54
- if not workflow_config.workflow_sandbox_id:
101
+ pk = workflow_config_result.pk
102
+ if not pk:
55
103
  raise ValueError("No workflow sandbox ID found in project to pull from.")
56
104
 
57
105
  logger.info(f"Pulling workflow into {workflow_config.module}")
@@ -63,7 +111,7 @@ def pull_command(
63
111
  query_parameters["exclude_code"] = exclude_code
64
112
 
65
113
  response = client.workflows.pull(
66
- workflow_config.workflow_sandbox_id,
114
+ pk,
67
115
  request_options={"additional_query_parameters": query_parameters},
68
116
  )
69
117
 
@@ -164,6 +164,86 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
164
164
  assert f.read() == "print('hello')"
165
165
 
166
166
 
167
+ def test_pull__workflow_deployment_with_no_config(vellum_client):
168
+ # GIVEN a workflow deployment
169
+ workflow_deployment = "my-deployment"
170
+
171
+ # AND the workflow pull API call returns a zip file
172
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
173
+
174
+ # AND we are currently in a new directory
175
+ current_dir = os.getcwd()
176
+ temp_dir = tempfile.mkdtemp()
177
+ os.chdir(temp_dir)
178
+
179
+ # WHEN the user runs the pull command with the workflow deployment
180
+ runner = CliRunner()
181
+ result = runner.invoke(cli_main, ["workflows", "pull", "--workflow-deployment", workflow_deployment])
182
+ os.chdir(current_dir)
183
+
184
+ # THEN the command returns successfully
185
+ assert result.exit_code == 0
186
+
187
+ # AND the pull api is called with the workflow deployment
188
+ vellum_client.workflows.pull.assert_called_once()
189
+ workflow_py = os.path.join(temp_dir, "my_deployment", "workflow.py")
190
+ assert os.path.exists(workflow_py)
191
+ with open(workflow_py) as f:
192
+ assert f.read() == "print('hello')"
193
+
194
+ # AND the vellum.lock.json file is created
195
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
196
+ assert os.path.exists(vellum_lock_json)
197
+ with open(vellum_lock_json) as f:
198
+ lock_data = json.loads(f.read())
199
+ assert lock_data == {
200
+ "version": "1.0",
201
+ "workflows": [
202
+ {
203
+ "module": "my_deployment",
204
+ "workflow_sandbox_id": None,
205
+ "ignore": None,
206
+ "deployments": [],
207
+ }
208
+ ],
209
+ }
210
+
211
+
212
+ def test_pull__both_workflow_sandbox_id_and_deployment(vellum_client):
213
+ # GIVEN a workflow sandbox id
214
+ workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
215
+
216
+ # AND a workflow deployment
217
+ workflow_deployment = "my-deployment"
218
+
219
+ # AND the workflow pull API call returns a zip file
220
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
221
+
222
+ # AND we are currently in a new directory
223
+ current_dir = os.getcwd()
224
+ temp_dir = tempfile.mkdtemp()
225
+ os.chdir(temp_dir)
226
+
227
+ # WHEN the user runs the pull command with the workflow deployment
228
+ runner = CliRunner()
229
+ result = runner.invoke(
230
+ cli_main,
231
+ [
232
+ "workflows",
233
+ "pull",
234
+ "--workflow-sandbox-id",
235
+ workflow_sandbox_id,
236
+ "--workflow-deployment",
237
+ workflow_deployment,
238
+ ],
239
+ )
240
+ os.chdir(current_dir)
241
+
242
+ # THEN the command returns successfully
243
+ assert result.exit_code == 1
244
+ assert "Cannot specify both workflow_sandbox_id and workflow_deployment" == str(result.exception)
245
+
246
+
167
247
  def test_pull__remove_missing_files(vellum_client, mock_module):
168
248
  # GIVEN a module on the user's filesystem
169
249
  temp_dir = mock_module.temp_dir
@@ -30,6 +30,15 @@ class BaseNodeVellumDisplay(BaseNodeDisplay[NodeType]):
30
30
  def get_target_handle_id(self) -> UUID:
31
31
  return self._get_node_display_uuid("target_handle_id")
32
32
 
33
+ def get_target_handle_id_by_source_node_id(self, source_node_id: UUID) -> UUID:
34
+ """
35
+ In the vast majority of cases, nodes will only have a single target handle and can be retrieved independently
36
+ of the source node. However, in rare cases (such as legacy Merge nodes), this method can be overridden to
37
+ account for the case of retrieving one amongst multiple target handles on a node.
38
+ """
39
+
40
+ return self.get_target_handle_id()
41
+
33
42
  def get_source_handle_id(self, port_displays: Dict[Port, PortDisplay]) -> UUID:
34
43
  default_port = self._node.Ports.default
35
44
  default_port_display = port_displays[default_port]
@@ -30,7 +30,10 @@ class BaseCodeExecutionNodeDisplay(BaseNodeVellumDisplay[_CodeExecutionNodeType]
30
30
  node_id = self.node_id
31
31
 
32
32
  node_file_path = inspect.getfile(node)
33
- code = read_file_from_path(node_filepath=node_file_path, script_filepath=(raise_if_descriptor(node.filepath)))
33
+ code = read_file_from_path(
34
+ node_filepath=node_file_path,
35
+ script_filepath=(raise_if_descriptor(node.filepath)), # type: ignore
36
+ )
34
37
  code_inputs = raise_if_descriptor(node.code_inputs)
35
38
 
36
39
  inputs = [
@@ -12,7 +12,6 @@ from vellum.workflows.references import WorkflowInputReference
12
12
  from vellum.workflows.references.output import OutputReference
13
13
  from vellum.workflows.types.core import JsonArray, JsonObject
14
14
  from vellum.workflows.types.generics import WorkflowType
15
- from vellum_ee.workflows.display.nodes import BaseMergeNodeDisplay
16
15
  from vellum_ee.workflows.display.nodes.base_node_vellum_display import BaseNodeVellumDisplay
17
16
  from vellum_ee.workflows.display.nodes.types import PortDisplay
18
17
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
@@ -369,10 +368,7 @@ class VellumWorkflowDisplay(
369
368
  target_node_id = target_node_display.node_id
370
369
 
371
370
  target_handle_id: UUID
372
- if isinstance(target_node_display, BaseMergeNodeDisplay):
373
- target_handle_id = target_node_display.get_target_handle_id_by_source_node_id(source_node_id)
374
- else:
375
- target_handle_id = target_node_display.get_target_handle_id()
371
+ target_handle_id = target_node_display.get_target_handle_id_by_source_node_id(source_node_id)
376
372
 
377
373
  return self._generate_edge_display_from_source(
378
374
  source_node_id, source_handle_id, target_node_id, target_handle_id, overrides