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.
- vellum/client/core/client_wrapper.py +1 -1
- vellum/workflows/graph/graph.py +23 -4
- vellum/workflows/graph/tests/test_graph.py +25 -0
- vellum/workflows/ports/port.py +3 -0
- {vellum_ai-0.11.3.dist-info → vellum_ai-0.11.4.dist-info}/METADATA +1 -1
- {vellum_ai-0.11.3.dist-info → vellum_ai-0.11.4.dist-info}/RECORD +14 -14
- vellum_cli/config.py +36 -0
- vellum_cli/push.py +4 -0
- vellum_cli/tests/conftest.py +19 -4
- vellum_cli/tests/test_pull.py +85 -7
- vellum_cli/tests/test_push.py +8 -8
- {vellum_ai-0.11.3.dist-info → vellum_ai-0.11.4.dist-info}/LICENSE +0 -0
- {vellum_ai-0.11.3.dist-info → vellum_ai-0.11.4.dist-info}/WHEEL +0 -0
- {vellum_ai-0.11.3.dist-info → vellum_ai-0.11.4.dist-info}/entry_points.txt +0 -0
| @@ -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. | 
| 20 | 
            +
                        "X-Fern-SDK-Version": "0.11.4",
         | 
| 21 21 | 
             
                    }
         | 
| 22 22 | 
             
                    headers["X_API_KEY"] = self.api_key
         | 
| 23 23 | 
             
                    return headers
         | 
    
        vellum/workflows/graph/graph.py
    CHANGED
    
    | @@ -12,11 +12,13 @@ if TYPE_CHECKING: | |
| 12 12 | 
             
            GraphTargetOfSets = Union[
         | 
| 13 13 | 
             
                Set[NodeType],
         | 
| 14 14 | 
             
                Set["Graph"],
         | 
| 15 | 
            -
                Set[ | 
| 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 | 
            -
                         | 
| 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 | 
            -
                                 | 
| 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 = { | 
| 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
         | 
    
        vellum/workflows/ports/port.py
    CHANGED
    
    | @@ -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)
         | 
| @@ -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= | 
| 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= | 
| 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= | 
| 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= | 
| 15 | 
            -
            vellum_cli/tests/test_push.py,sha256= | 
| 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= | 
| 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= | 
| 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= | 
| 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= | 
| 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. | 
| 1381 | 
            -
            vellum_ai-0.11. | 
| 1382 | 
            -
            vellum_ai-0.11. | 
| 1383 | 
            -
            vellum_ai-0.11. | 
| 1384 | 
            -
            vellum_ai-0.11. | 
| 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 |  | 
    
        vellum_cli/tests/conftest.py
    CHANGED
    
    | @@ -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 | 
| 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[ | 
| 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":  | 
| 41 | 
            +
                                "workflow_sandbox_id": workflow_sandbox_id,
         | 
| 32 42 | 
             
                            }
         | 
| 33 43 | 
             
                        ]
         | 
| 34 44 | 
             
                    }
         | 
| 35 45 | 
             
                )
         | 
| 36 46 |  | 
| 37 | 
            -
                yield  | 
| 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)
         | 
    
        vellum_cli/tests/test_pull.py
    CHANGED
    
    | @@ -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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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 | 
            +
                    ]
         | 
    
        vellum_cli/tests/test_push.py
    CHANGED
    
    | @@ -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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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 | 
| 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 | 
| 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("."))
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |