diff --git a/packages/tangle-cli/src/tangle_cli/__init__.py b/packages/tangle-cli/src/tangle_cli/__init__.py index eaedcd3..e78b9c0 100644 --- a/packages/tangle-cli/src/tangle_cli/__init__.py +++ b/packages/tangle-cli/src/tangle_cli/__init__.py @@ -14,6 +14,6 @@ try: __version__ = metadata_version("tangle-cli") except PackageNotFoundError: - __version__ = "0.1.29" + __version__ = "0.1.30" __all__ = ["TangleDynamicDiscoveryClient", "__version__"] diff --git a/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py b/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py index 1d74d6b..ba9b5b4 100644 --- a/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +++ b/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py @@ -138,6 +138,7 @@ def __init__( *, base_url: str | None = None, client_factory: Callable[[], Any] | None = None, + extract_subgraphs: bool = True, uri_readers: Mapping[str, UriReader] | None = None, uri_writers: Mapping[str, UriWriter] | None = None, ) -> None: @@ -148,6 +149,7 @@ def __init__( base_url=base_url or DEFAULT_API_URL, ) self.remembered_choices = dict(remembered_choices or {}) + self.extract_subgraphs = extract_subgraphs self.output_file = output_file self.component_extension = component_extension or ".yaml" @@ -756,7 +758,14 @@ def dehydrate(self, data: dict[str, Any]) -> dict[str, Any]: # Every choice that promises a dehydrated document must replace nested # graph boundaries too; otherwise they keep their inline spec. A nested # graph has no canonical URL of its own, so URL extracts it to a file. - if default_choice in (DehydrateChoice.AUTO, DehydrateChoice.FILE, DehydrateChoice.URL): + # + # ``extract_subgraphs=False`` keeps every boundary inline and dehydrates + # only the leaf components inside it, in place. It is for callers that + # represent nested graphs themselves (the decompiler turns each into + # Python), so the output is deliberately not a fully dehydrated document. + if not self.extract_subgraphs: + pass + elif default_choice in (DehydrateChoice.AUTO, DehydrateChoice.FILE, DehydrateChoice.URL): self._extract_subgraphs_to_files(working) elif default_choice in (DehydrateChoice.DIGEST, DehydrateChoice.NAME): self._extract_subgraphs_portable(working, default_choice) diff --git a/pyproject.toml b/pyproject.toml index 8a7eb43..3a6b0ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tangle-cli" -version = "0.1.29" +version = "0.1.30" description = "CLI for Tangle, the open-source ML pipeline orchestration platform" readme = "README.md" authors = [ diff --git a/tests/test_dehydrator_portable_refs.py b/tests/test_dehydrator_portable_refs.py index 71e63ba..85325ac 100644 --- a/tests/test_dehydrator_portable_refs.py +++ b/tests/test_dehydrator_portable_refs.py @@ -416,3 +416,22 @@ def test_errors_still_propagate_outside_the_generated_fallback_contract( with pytest.raises(ConnectionError): PipelineHydrator(client=Library(error=ConnectionError("down"))).hydrate_file(pipeline) + + +@pytest.mark.parametrize("choice", [DehydrateChoice.DIGEST, DehydrateChoice.FILE, DehydrateChoice.AUTO]) +def test_extract_subgraphs_false_dehydrates_only_the_leaves_in_place(tmp_path: Path, choice: str) -> None: + """A caller that represents nested graphs itself keeps every boundary + inline; only the leaf components inside it are dehydrated, in place, with + no subgraph file written.""" + document = _pipeline({"spec": INNER}, task="sub") + + result = PipelineDehydrator( + {"": choice}, output_file=tmp_path / "out.yaml", client=Library({}), extract_subgraphs=False + ).dehydrate(document) + + boundary = result["implementation"]["graph"]["tasks"]["sub"]["componentRef"] + assert boundary["spec"]["name"] == "Inner", "the graph boundary stays inline" + leaf = boundary["spec"]["implementation"]["graph"]["tasks"]["leaf"]["componentRef"] + assert "spec" not in leaf and set(leaf) == {"url"}, "the leaf inside it is dehydrated" + assert not (tmp_path / "subgraphs").exists() + assert not [p for p in tmp_path.rglob("*.yaml") if "inner" in p.name.lower()] diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 3f41ed4..75c5369 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -183,7 +183,7 @@ def test_tangle_cli_wheel_supports_expert_no_deps_import_path_without_tangle_api requires_dist = [line for line in metadata.splitlines() if line.startswith("Requires-Dist: ")] assert not any(name.startswith("tangle_api/") for name in names) assert "tangle_cli/openapi/openapi.json" not in names - assert "Version: 0.1.29" in metadata + assert "Version: 0.1.30" in metadata assert "Requires-Dist: tangle-api==0.1.1" in requires_dist assert not any("extra == 'native'" in line for line in requires_dist) assert "Provides-Extra: native" in metadata diff --git a/uv.lock b/uv.lock index 296015c..2b8f7ad 100644 --- a/uv.lock +++ b/uv.lock @@ -2083,7 +2083,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.0" }] [[package]] name = "tangle-cli" -version = "0.1.29" +version = "0.1.30" source = { editable = "." } dependencies = [ { name = "cloud-pipelines" },