vellum-ai 0.11.8__py3-none-any.whl → 0.11.10__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.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