tangle-cli 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/PKG-INFO +8 -3
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/README.md +4 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/component_from_func.py +133 -78
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +52 -2
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +1 -18
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +21 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +593 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipelines.py +29 -205
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +43 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +169 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +277 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +289 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +29 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +108 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +27 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +183 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +162 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +122 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +324 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +200 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +211 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +218 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +88 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +363 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +69 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
- tangle_cli-0.1.4/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +1949 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/pyproject.toml +3 -1
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/api_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/client.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/OSS-CONVENTIONS.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/PORT-README.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/SKILL.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/auth-wizard.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/builder.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/debugger.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/reporter.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/researcher.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/reviewer.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/agents/scenario-builder.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/data-sources.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/event-log.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/iterating-on-runs.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/knowledge-corpus.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/secrets.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/setup.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-0-initialize.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-1-analyze.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-2-hypothesize.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-3-submit.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-4-monitor.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-5-evaluate.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-6-synthesize.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/step-7-decide.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/tangle-tools.md +0 -0
- {tangle_cli-0.1.2 → tangle_cli-0.1.4}/skills/tangent/references/uploading-artifacts.md +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: tangle-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: CLI for Tangle, the open-source ML pipeline orchestration platform
|
|
5
|
-
Author: Alexey Volkov, Tangle authors
|
|
6
|
-
Author-email: Alexey Volkov <alexey.volkov@ark-kun.com>
|
|
5
|
+
Author: Alexey Volkov, Tangle authors, Volv Grebennikov
|
|
6
|
+
Author-email: Alexey Volkov <alexey.volkov@ark-kun.com>, Volv Grebennikov <volv.grebennikov@shopify.com>
|
|
7
7
|
Requires-Dist: cloud-pipelines>=0.26.3.12
|
|
8
8
|
Requires-Dist: cyclopts>=4.16.1
|
|
9
9
|
Requires-Dist: docstring-parser>=0.16
|
|
10
10
|
Requires-Dist: httpx>=0.28.1
|
|
11
11
|
Requires-Dist: jinja2>=3.1
|
|
12
|
+
Requires-Dist: jsonschema>=4.25.0
|
|
12
13
|
Requires-Dist: platformdirs>=4.10.0
|
|
13
14
|
Requires-Dist: pydantic>=2.0
|
|
14
15
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -316,8 +317,12 @@ uv run tangle sdk pipeline-runs annotations set RUN_ID key value
|
|
|
316
317
|
uv run tangle sdk pipeline-runs export RUN_ID --output pipeline.yaml
|
|
317
318
|
```
|
|
318
319
|
|
|
320
|
+
`pipelines validate` checks local graph shape, the packaged Tangle pipeline JSON schema, and component input wiring when component specs are present. It does not hydrate refs; validate a hydrated file when you need fully resolved component-input checks for remote refs.
|
|
321
|
+
|
|
319
322
|
`submit` hydrates refs by default and builds an API submit payload with `root_task.componentRef.spec`. Use `--no-hydrate` to submit the local YAML structure as-is. Use `--dry-run` to print the payload without creating a run.
|
|
320
323
|
|
|
324
|
+
Before creating a run—or printing a `--dry-run` payload—`submit` runs the same authoring validation as `tangle sdk pipelines validate` on the hydrated/resolved pipeline spec (or on the as-is spec when `--no-hydrate` is used). Invalid specs fail locally with `Pipeline validation failed` errors before the run-submission API call. For example, the pipeline root must be a graph (`implementation.graph`), so a bare `implementation.container` root is rejected before the run is submitted; missing required component inputs and invalid task output references are rejected when component specs are available.
|
|
325
|
+
|
|
321
326
|
## Programmatic client
|
|
322
327
|
|
|
323
328
|
The stable public wrapper for downstream Python tools is:
|
|
@@ -291,8 +291,12 @@ uv run tangle sdk pipeline-runs annotations set RUN_ID key value
|
|
|
291
291
|
uv run tangle sdk pipeline-runs export RUN_ID --output pipeline.yaml
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
+
`pipelines validate` checks local graph shape, the packaged Tangle pipeline JSON schema, and component input wiring when component specs are present. It does not hydrate refs; validate a hydrated file when you need fully resolved component-input checks for remote refs.
|
|
295
|
+
|
|
294
296
|
`submit` hydrates refs by default and builds an API submit payload with `root_task.componentRef.spec`. Use `--no-hydrate` to submit the local YAML structure as-is. Use `--dry-run` to print the payload without creating a run.
|
|
295
297
|
|
|
298
|
+
Before creating a run—or printing a `--dry-run` payload—`submit` runs the same authoring validation as `tangle sdk pipelines validate` on the hydrated/resolved pipeline spec (or on the as-is spec when `--no-hydrate` is used). Invalid specs fail locally with `Pipeline validation failed` errors before the run-submission API call. For example, the pipeline root must be a graph (`implementation.graph`), so a bare `implementation.container` root is rejected before the run is submitted; missing required component inputs and invalid task output references are rejected when component specs are available.
|
|
299
|
+
|
|
296
300
|
## Programmatic client
|
|
297
301
|
|
|
298
302
|
The stable public wrapper for downstream Python tools is:
|
|
@@ -14,6 +14,6 @@ from tangle_cli.dynamic_discovery_client import TangleDynamicDiscoveryClient
|
|
|
14
14
|
try:
|
|
15
15
|
__version__ = metadata_version("tangle-cli")
|
|
16
16
|
except PackageNotFoundError:
|
|
17
|
-
__version__ = "0.1.
|
|
17
|
+
__version__ = "0.1.4"
|
|
18
18
|
|
|
19
19
|
__all__ = ["TangleDynamicDiscoveryClient", "__version__"]
|
{tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/component_from_func.py
RENAMED
|
@@ -181,11 +181,18 @@ class FunctionSpec:
|
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
def _ensure_cloud_pipelines_shim() -> None:
|
|
184
|
-
"""Register import-time
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
"""Register the import-time ``cloud_pipelines`` shim used while introspecting
|
|
185
|
+
authoring files.
|
|
186
|
+
|
|
187
|
+
This lets us load Python files that use ``from cloud_pipelines import
|
|
188
|
+
components`` without requiring that authoring package to be installed; the
|
|
189
|
+
authoring constructs are stripped from the generated runtime code later.
|
|
190
|
+
|
|
191
|
+
OSS deliberately does NOT fabricate a shim for any *downstream* authoring
|
|
192
|
+
surface (e.g. a module a downstream package exposes to re-export the
|
|
193
|
+
authoring objects under its own import path). A downstream package that
|
|
194
|
+
wants its own authoring path recognised both makes that module importable
|
|
195
|
+
itself and registers it via :func:`register_authoring_import_module`.
|
|
189
196
|
"""
|
|
190
197
|
if "cloud_pipelines" not in sys.modules:
|
|
191
198
|
components_mod = types.ModuleType("cloud_pipelines.components")
|
|
@@ -198,41 +205,6 @@ def _ensure_cloud_pipelines_shim() -> None:
|
|
|
198
205
|
sys.modules["cloud_pipelines"] = cloud_pipelines_mod
|
|
199
206
|
sys.modules["cloud_pipelines.components"] = components_mod
|
|
200
207
|
|
|
201
|
-
_ensure_tangle_deploy_authoring_shim()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _identity_decorator(*args, **kwargs):
|
|
205
|
-
def decorate(func):
|
|
206
|
-
return func
|
|
207
|
-
|
|
208
|
-
return decorate
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class _AuthoringGeneric:
|
|
212
|
-
def __class_getitem__(cls, item):
|
|
213
|
-
return cls
|
|
214
|
-
|
|
215
|
-
def __init__(self, *args, **kwargs):
|
|
216
|
-
pass
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _ensure_tangle_deploy_authoring_shim() -> None:
|
|
220
|
-
"""Register a tiny shim for TD pipeline authoring imports if absent."""
|
|
221
|
-
if "tangle_deploy.python_pipeline" in sys.modules:
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
tangle_deploy_mod = sys.modules.get("tangle_deploy") or types.ModuleType("tangle_deploy")
|
|
225
|
-
python_pipeline_mod = types.ModuleType("tangle_deploy.python_pipeline")
|
|
226
|
-
for name in ("task", "pipeline", "subpipeline", "registered"):
|
|
227
|
-
setattr(python_pipeline_mod, name, _identity_decorator)
|
|
228
|
-
for name in ("In", "Out", "Outputs", "TaskEnv"):
|
|
229
|
-
setattr(python_pipeline_mod, name, _AuthoringGeneric)
|
|
230
|
-
setattr(python_pipeline_mod, "ref", lambda *args, **kwargs: None)
|
|
231
|
-
|
|
232
|
-
setattr(tangle_deploy_mod, "python_pipeline", python_pipeline_mod)
|
|
233
|
-
sys.modules.setdefault("tangle_deploy", tangle_deploy_mod)
|
|
234
|
-
sys.modules["tangle_deploy.python_pipeline"] = python_pipeline_mod
|
|
235
|
-
|
|
236
208
|
|
|
237
209
|
def load_python_module(file_path: Path, extra_sys_path: list[Path] | None = None) -> Any:
|
|
238
210
|
"""Dynamically import a Python module from a file path.
|
|
@@ -757,31 +729,66 @@ def _is_name_main_test(node: ast.expr) -> bool:
|
|
|
757
729
|
# too, exactly like @task.
|
|
758
730
|
_AUTHORING_DECORATOR_NAMES = frozenset({"task", "pipeline", "subpipeline", "registered"})
|
|
759
731
|
|
|
760
|
-
# The python-pipeline authoring
|
|
761
|
-
# submodules) are authoring-only and stripped from the baked source. We
|
|
762
|
-
# deliberately do NOT strip other
|
|
763
|
-
#
|
|
764
|
-
# ``@task`` body, and dropping them would raise ``NameError`` in the
|
|
765
|
-
# container.
|
|
766
|
-
|
|
732
|
+
# The python-pipeline authoring modules. ONLY imports of these modules (and
|
|
733
|
+
# their submodules) are authoring-only and stripped from the baked source. We
|
|
734
|
+
# deliberately do NOT strip other packages that merely share a top-level name
|
|
735
|
+
# (e.g. a downstream ``*.utils``): those may be legitimate runtime helpers used
|
|
736
|
+
# inside a ``@task`` body, and dropping them would raise ``NameError`` in the
|
|
737
|
+
# operation container.
|
|
738
|
+
#
|
|
739
|
+
# OSS recognises exactly one authoring surface out of the box: the canonical
|
|
740
|
+
# ``tangle_cli.python_pipeline`` path. A downstream package that re-exports the
|
|
741
|
+
# authoring objects under its own module path — so authors may write ``from
|
|
742
|
+
# <downstream>.python_pipeline import task`` — registers that path via
|
|
743
|
+
# :func:`register_authoring_import_module`; codegen then strips either import
|
|
744
|
+
# the same way. OSS never hardcodes a downstream module name (the dependency
|
|
745
|
+
# points inward), mirroring the resolver/reader registries in the hydrator.
|
|
746
|
+
_AUTHORING_IMPORT_MODULES: list[str] = ["tangle_cli.python_pipeline"]
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def register_authoring_import_module(module: str) -> None:
|
|
750
|
+
"""Register *module* as an additional python-pipeline authoring surface.
|
|
751
|
+
|
|
752
|
+
A downstream package that re-exports the ``tangle_cli.python_pipeline``
|
|
753
|
+
authoring objects under its own module path calls this (typically at import
|
|
754
|
+
time) so codegen strips ``from <module> import ...`` / ``import <module>``
|
|
755
|
+
lines — and their submodules — from baked runtime source exactly like the
|
|
756
|
+
canonical OSS surface. Idempotent: registering an already-known module is a
|
|
757
|
+
no-op, so repeated import-time registration is safe.
|
|
758
|
+
"""
|
|
759
|
+
if module not in _AUTHORING_IMPORT_MODULES:
|
|
760
|
+
_AUTHORING_IMPORT_MODULES.append(module)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def authoring_import_modules() -> tuple[str, ...]:
|
|
764
|
+
"""Return the python-pipeline authoring modules recognised by codegen."""
|
|
765
|
+
return tuple(_AUTHORING_IMPORT_MODULES)
|
|
767
766
|
|
|
768
767
|
# The authoring-only ``TaskEnv`` class name. A module-level ``X = TaskEnv(...)``
|
|
769
768
|
# (or ``X = <alias>.TaskEnv(...)``) declaration is authoring-only by contract and
|
|
770
769
|
# is stripped from the baked source by ``_strip_authoring_constructs``.
|
|
771
770
|
# Matched by trailing NAME only (like the authoring decorators), because in
|
|
772
|
-
# python-pipeline authoring files ``TaskEnv`` always
|
|
773
|
-
#
|
|
771
|
+
# python-pipeline authoring files ``TaskEnv`` always resolves to the
|
|
772
|
+
# python-pipeline authoring surface's ``TaskEnv``.
|
|
774
773
|
_AUTHORING_ENV_CLASS_NAME = "TaskEnv"
|
|
775
774
|
|
|
776
775
|
|
|
777
776
|
class AuthoringStripError(ValueError):
|
|
778
|
-
"""Raised when
|
|
777
|
+
"""Raised when authoring code cannot be safely stripped from baked source.
|
|
778
|
+
|
|
779
|
+
``_strip_authoring_constructs`` raises this rather than bake a broken
|
|
780
|
+
program when an authoring construct is entangled with runtime code and
|
|
781
|
+
cannot be removed by line range without corrupting the kept program:
|
|
779
782
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
783
|
+
- a comma-separated ``import tangle_cli.python_pipeline as tp, os`` that
|
|
784
|
+
mixes the authoring surface with a runtime module on one statement (the
|
|
785
|
+
authoring alias must be stripped, but line-deletion would take ``os`` with
|
|
786
|
+
it); or
|
|
787
|
+
- a ``@task(env=...)`` env binding entangled with runtime code — e.g. a
|
|
788
|
+
mixed ``from _envs import UPI, helper`` import whose ``helper`` is used at
|
|
789
|
+
runtime, or a collected env name referenced by the kept task body.
|
|
790
|
+
|
|
791
|
+
Failing fast here is intentional: silently baking a broken ``import os`` /
|
|
785
792
|
``from _envs import UPI`` / ``UPI = TaskEnv(...)`` would only surface as a
|
|
786
793
|
``NameError`` / ``ImportError`` at container start. The message tells the
|
|
787
794
|
author how to split the import or keep TaskEnv values authoring-only.
|
|
@@ -793,7 +800,7 @@ def _decorator_called_name(node: ast.expr) -> str | None:
|
|
|
793
800
|
|
|
794
801
|
Handles ``@name`` / ``@name(...)`` and ``@mod.name`` / ``@mod.name(...)``
|
|
795
802
|
forms, returning the trailing attribute/name (e.g. ``task`` for both
|
|
796
|
-
``@task(...)`` and ``@
|
|
803
|
+
``@task(...)`` and ``@tangle_cli.python_pipeline.task(...)``). Returns
|
|
797
804
|
``None`` for shapes we do not recognise so callers leave them untouched.
|
|
798
805
|
|
|
799
806
|
Limitation (v1, intentional): matching is by trailing NAME only, not by
|
|
@@ -812,36 +819,58 @@ def _decorator_called_name(node: ast.expr) -> str | None:
|
|
|
812
819
|
return None
|
|
813
820
|
|
|
814
821
|
|
|
822
|
+
def _is_authoring_module(name: str) -> bool:
|
|
823
|
+
"""Return True if *name* is an authoring module or a submodule of one."""
|
|
824
|
+
return any(name == mod or name.startswith(mod + ".") for mod in _AUTHORING_IMPORT_MODULES)
|
|
825
|
+
|
|
826
|
+
|
|
815
827
|
def _is_authoring_import(node: ast.stmt) -> bool:
|
|
816
828
|
"""Return True if *node* imports the python-pipeline authoring surface.
|
|
817
829
|
|
|
818
|
-
Matches ONLY the
|
|
819
|
-
|
|
830
|
+
Matches ONLY the registered authoring modules (and their submodules) — the
|
|
831
|
+
canonical ``tangle_cli.python_pipeline`` plus any registered via
|
|
832
|
+
:func:`register_authoring_import_module`:
|
|
820
833
|
|
|
821
|
-
- ``from
|
|
822
|
-
``from
|
|
823
|
-
and submodules like ``from
|
|
824
|
-
- ``import
|
|
825
|
-
|
|
834
|
+
- ``from tangle_cli.python_pipeline import ...`` (including the aliased
|
|
835
|
+
``from tangle_cli.python_pipeline import ref as operation_by_ref`` form
|
|
836
|
+
and submodules like ``from tangle_cli.python_pipeline.x import y``);
|
|
837
|
+
- ``import tangle_cli.python_pipeline`` / ``import
|
|
838
|
+
tangle_cli.python_pipeline as tp``;
|
|
839
|
+
- the equivalents for any registered downstream authoring path.
|
|
826
840
|
|
|
827
|
-
It does NOT match other
|
|
828
|
-
|
|
841
|
+
It does NOT match other packages that merely share a top-level name (e.g. a
|
|
842
|
+
downstream ``*.utils`` module) — those can be genuine runtime helpers
|
|
829
843
|
referenced inside a ``@task`` body and must survive into the baked program.
|
|
830
844
|
Relative imports (``from . import x``) are never authoring imports.
|
|
831
845
|
"""
|
|
832
846
|
if isinstance(node, ast.ImportFrom):
|
|
833
847
|
if node.level: # relative import — not the authoring package
|
|
834
848
|
return False
|
|
835
|
-
|
|
836
|
-
return module == _AUTHORING_IMPORT_MODULE or module.startswith(_AUTHORING_IMPORT_MODULE + ".")
|
|
849
|
+
return _is_authoring_module(node.module or "")
|
|
837
850
|
if isinstance(node, ast.Import):
|
|
838
|
-
return any(
|
|
839
|
-
alias.name == _AUTHORING_IMPORT_MODULE or alias.name.startswith(_AUTHORING_IMPORT_MODULE + ".")
|
|
840
|
-
for alias in node.names
|
|
841
|
-
)
|
|
851
|
+
return any(_is_authoring_module(alias.name) for alias in node.names)
|
|
842
852
|
return False
|
|
843
853
|
|
|
844
854
|
|
|
855
|
+
def _mixed_authoring_import_aliases(node: ast.stmt) -> tuple[list[str], list[str]] | None:
|
|
856
|
+
"""Classify a comma-separated ``import a, b`` that mixes import surfaces.
|
|
857
|
+
|
|
858
|
+
Only ``ast.Import`` (``import x, y``) can bind several modules in one
|
|
859
|
+
statement; a ``from X import ...`` binds a single module, so it is
|
|
860
|
+
all-or-nothing and never "mixed". Returns ``(authoring, runtime)`` alias
|
|
861
|
+
names when the statement mixes at least one authoring module with at least
|
|
862
|
+
one non-authoring one, else ``None`` (a pure-authoring or pure-runtime
|
|
863
|
+
``import``, which the caller drops whole or leaves untouched respectively).
|
|
864
|
+
"""
|
|
865
|
+
if not isinstance(node, ast.Import):
|
|
866
|
+
return None
|
|
867
|
+
authoring = [alias.name for alias in node.names if _is_authoring_module(alias.name)]
|
|
868
|
+
runtime = [alias.name for alias in node.names if not _is_authoring_module(alias.name)]
|
|
869
|
+
if authoring and runtime:
|
|
870
|
+
return authoring, runtime
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
|
|
845
874
|
def _attr_root_name(node: ast.expr) -> str | None:
|
|
846
875
|
"""Return the root ``Name`` id of an attribute chain (``a.b.c`` -> ``a``).
|
|
847
876
|
|
|
@@ -971,7 +1000,7 @@ def _strip_authoring_constructs(source_code: str) -> str:
|
|
|
971
1000
|
- re-running an ``@task`` / ``@pipeline`` / ``@subpipeline`` decorator
|
|
972
1001
|
replaces the function with a ``CallableRef`` recorder, which raises at
|
|
973
1002
|
call time because there is no active ``@pipeline`` trace context;
|
|
974
|
-
- on a thin image the ``from
|
|
1003
|
+
- on a thin image the ``from tangle_cli.python_pipeline import ...``
|
|
975
1004
|
import itself can fail with ``ImportError``.
|
|
976
1005
|
|
|
977
1006
|
This removes them via surgical AST line-range deletion (mirroring
|
|
@@ -986,9 +1015,12 @@ def _strip_authoring_constructs(source_code: str) -> str:
|
|
|
986
1015
|
|
|
987
1016
|
Scope of the strip (intentional v1 boundaries):
|
|
988
1017
|
|
|
989
|
-
- imports: only
|
|
990
|
-
dropped — see ``_is_authoring_import``. Other
|
|
991
|
-
|
|
1018
|
+
- imports: only the registered authoring modules (and submodules) are
|
|
1019
|
+
dropped — see ``_is_authoring_import``. Other runtime helpers that merely
|
|
1020
|
+
share a top-level name are preserved. A comma-separated ``import a, b``
|
|
1021
|
+
that mixes the authoring surface with a runtime module raises
|
|
1022
|
+
:class:`AuthoringStripError` (it cannot be partially line-deleted) — see
|
|
1023
|
+
``_mixed_authoring_import_aliases``.
|
|
992
1024
|
- decorators: matched by trailing NAME (``task`` / ``pipeline`` /
|
|
993
1025
|
``subpipeline``), not by import resolution — see ``_decorator_called_name``
|
|
994
1026
|
for the limitation. Unrelated decorators (``@functools.cache``,
|
|
@@ -1037,6 +1069,27 @@ def _strip_authoring_constructs(source_code: str) -> str:
|
|
|
1037
1069
|
for node in ast.walk(tree):
|
|
1038
1070
|
# Authoring imports — delete the whole (possibly multi-line) statement.
|
|
1039
1071
|
if isinstance(node, (ast.Import, ast.ImportFrom)) and _is_authoring_import(node):
|
|
1072
|
+
# A comma-separated ``import authoring_surface, os`` mixes the
|
|
1073
|
+
# authoring surface with a runtime module on one statement. We drop
|
|
1074
|
+
# authoring imports by line range, which cannot delete just part of
|
|
1075
|
+
# a statement — dropping the whole line would take the runtime
|
|
1076
|
+
# ``import os`` with it (silent NameError in the baked program),
|
|
1077
|
+
# while keeping it would leave the authoring import un-stripped. Fail
|
|
1078
|
+
# fast with split guidance rather than bake a broken program.
|
|
1079
|
+
mixed = _mixed_authoring_import_aliases(node)
|
|
1080
|
+
if mixed is not None:
|
|
1081
|
+
authoring_names, runtime_names = mixed
|
|
1082
|
+
raise AuthoringStripError(
|
|
1083
|
+
"import statement mixes the python-pipeline authoring "
|
|
1084
|
+
f"surface ({', '.join(authoring_names)}) with runtime "
|
|
1085
|
+
f"import(s) ({', '.join(runtime_names)}) on one line. The "
|
|
1086
|
+
"authoring import is stripped from the baked runtime "
|
|
1087
|
+
"program, but a comma-separated `import a, b` cannot be "
|
|
1088
|
+
"partially line-deleted without corrupting the kept import. "
|
|
1089
|
+
"Split the authoring import onto its own line (e.g. `import "
|
|
1090
|
+
"tangle_cli.python_pipeline as tp` separate from `import "
|
|
1091
|
+
"os`)."
|
|
1092
|
+
)
|
|
1040
1093
|
start = node.lineno
|
|
1041
1094
|
end = node.end_lineno or node.lineno
|
|
1042
1095
|
removed.update(range(start, end + 1))
|
|
@@ -1686,7 +1739,8 @@ def generate_component_yaml(
|
|
|
1686
1739
|
path_annotation_mode: ``"oss"`` always records source/YAML paths relative
|
|
1687
1740
|
to their common ancestor. ``"td_legacy"`` only uses that relative
|
|
1688
1741
|
common-root behavior inside a git checkout; outside git it records
|
|
1689
|
-
``file_path.name`` / ``output_path.name``
|
|
1742
|
+
``file_path.name`` / ``output_path.name`` to preserve the legacy
|
|
1743
|
+
downstream driver's historical basename-only snapshots.
|
|
1690
1744
|
|
|
1691
1745
|
Returns:
|
|
1692
1746
|
True on success, False on failure.
|
|
@@ -1764,8 +1818,9 @@ def generate_component_yaml(
|
|
|
1764
1818
|
# Use the common ancestor of source and output so both paths are clean
|
|
1765
1819
|
# forward references (no ".."). This lets later local maintenance
|
|
1766
1820
|
# commands find the source even when YAML is generated into a separate
|
|
1767
|
-
# output directory.
|
|
1768
|
-
# outside a git checkout to preserve historical
|
|
1821
|
+
# output directory. Legacy (``td_legacy``) compatibility keeps
|
|
1822
|
+
# basename-only paths outside a git checkout to preserve historical
|
|
1823
|
+
# snapshots.
|
|
1769
1824
|
resolved_source = file_path.resolve()
|
|
1770
1825
|
resolved_output = output_path.resolve()
|
|
1771
1826
|
common_dir = Path(os.path.commonpath([resolved_source, resolved_output]))
|
{tangle_cli-0.1.2 → tangle_cli-0.1.4}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py
RENAMED
|
@@ -28,6 +28,7 @@ from .logger import Logger, get_default_logger
|
|
|
28
28
|
from .pipeline_dehydrator import DehydrateChoice, PipelineDehydrator
|
|
29
29
|
from .pipeline_hydrator import HydrationError, PipelineHydrator
|
|
30
30
|
from .pipeline_run_details import PipelineRunDetails
|
|
31
|
+
from .pipelines import collect_pipeline_spec_errors
|
|
31
32
|
from .pipeline_run_search import PipelineRunSearch
|
|
32
33
|
from .utils import dump_yaml
|
|
33
34
|
|
|
@@ -378,6 +379,26 @@ class PipelineRunHooks:
|
|
|
378
379
|
"""Hook for TD JOB_CONFIG time input / scheduled runtime behavior."""
|
|
379
380
|
return run_args
|
|
380
381
|
|
|
382
|
+
def validate_pipeline_for_run(
|
|
383
|
+
self,
|
|
384
|
+
pipeline_spec: dict[str, Any],
|
|
385
|
+
*,
|
|
386
|
+
pipeline_path: str | Path | None,
|
|
387
|
+
effective_path: str | Path | None,
|
|
388
|
+
skip_validation: bool,
|
|
389
|
+
) -> list[str]:
|
|
390
|
+
"""Return submit-time validation errors for a prepared pipeline spec.
|
|
391
|
+
|
|
392
|
+
The OSS default enforces the same local authoring validator used by
|
|
393
|
+
``tangle pipeline validate``. Downstreams can override or extend this
|
|
394
|
+
hook with stricter schema/input validators.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
del pipeline_path, effective_path
|
|
398
|
+
if skip_validation:
|
|
399
|
+
return []
|
|
400
|
+
return collect_pipeline_spec_errors(pipeline_spec)
|
|
401
|
+
|
|
381
402
|
def transform_run_name(
|
|
382
403
|
self,
|
|
383
404
|
run_name: str,
|
|
@@ -834,6 +855,11 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
834
855
|
for parameter in parameters.values()
|
|
835
856
|
)
|
|
836
857
|
|
|
858
|
+
@staticmethod
|
|
859
|
+
def _raise_pipeline_validation_error(validation_errors: list[str]) -> None:
|
|
860
|
+
if validation_errors:
|
|
861
|
+
raise PipelineRunError("Pipeline validation failed:\n - " + "\n - ".join(validation_errors))
|
|
862
|
+
|
|
837
863
|
def load_pipeline_for_submit(
|
|
838
864
|
self,
|
|
839
865
|
pipeline_path: str | Path,
|
|
@@ -909,13 +935,15 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
909
935
|
pipeline_path: str | Path | None = None,
|
|
910
936
|
run_as: str | None = None,
|
|
911
937
|
hydrate: bool = True,
|
|
938
|
+
skip_validation: bool = False,
|
|
912
939
|
) -> PipelineSubmitPayload:
|
|
913
940
|
"""Prepare the generic submit payload from a pipeline spec.
|
|
914
941
|
|
|
915
942
|
The order here is the submit-body contract shared by OSS and TD:
|
|
916
943
|
prepare the spec, prepare runtime arguments, expand run-name templates,
|
|
917
|
-
convert/sanitize the payload, then
|
|
918
|
-
before caller-supplied annotations
|
|
944
|
+
validate the prepared authoring spec, convert/sanitize the payload, then
|
|
945
|
+
merge downstream/default annotations before caller-supplied annotations
|
|
946
|
+
override them.
|
|
919
947
|
"""
|
|
920
948
|
|
|
921
949
|
prepared_spec = self.prepare_pipeline_spec_for_submit(
|
|
@@ -926,6 +954,14 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
926
954
|
)
|
|
927
955
|
prepared_run_args = self.hooks.prepare_run_arguments(prepared_spec, run_args)
|
|
928
956
|
prepared_spec = self.apply_run_name_template(prepared_spec, prepared_run_args)
|
|
957
|
+
if not skip_validation:
|
|
958
|
+
validation_errors = self.hooks.validate_pipeline_for_run(
|
|
959
|
+
prepared_spec,
|
|
960
|
+
pipeline_path=pipeline_path,
|
|
961
|
+
effective_path=None,
|
|
962
|
+
skip_validation=False,
|
|
963
|
+
)
|
|
964
|
+
self._raise_pipeline_validation_error(validation_errors)
|
|
929
965
|
payload = self.convert_yaml_to_payload(copy.deepcopy(prepared_spec), prepared_run_args)
|
|
930
966
|
payload = self.sanitize_submit_payload(payload)
|
|
931
967
|
root_task = payload["root_task"]
|
|
@@ -961,6 +997,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
961
997
|
pipeline_path: str | Path | None = None,
|
|
962
998
|
run_as: str | None = None,
|
|
963
999
|
hydrate: bool = True,
|
|
1000
|
+
skip_validation: bool = False,
|
|
964
1001
|
) -> dict[str, Any]:
|
|
965
1002
|
"""Build a submit body from an already-prepared pipeline spec."""
|
|
966
1003
|
|
|
@@ -971,6 +1008,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
971
1008
|
pipeline_path=pipeline_path,
|
|
972
1009
|
run_as=run_as,
|
|
973
1010
|
hydrate=hydrate,
|
|
1011
|
+
skip_validation=skip_validation,
|
|
974
1012
|
).to_body()
|
|
975
1013
|
|
|
976
1014
|
def prepare_submit_payload(
|
|
@@ -982,6 +1020,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
982
1020
|
hydrate: bool = True,
|
|
983
1021
|
run_as: str | None = None,
|
|
984
1022
|
resolution_overrides: dict[str, Any] | None = None,
|
|
1023
|
+
skip_validation: bool = False,
|
|
985
1024
|
) -> PipelineSubmitPayload:
|
|
986
1025
|
pipeline_spec = self.load_pipeline_for_submit(
|
|
987
1026
|
pipeline_path,
|
|
@@ -995,6 +1034,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
995
1034
|
pipeline_path=pipeline_path,
|
|
996
1035
|
run_as=run_as,
|
|
997
1036
|
hydrate=hydrate,
|
|
1037
|
+
skip_validation=skip_validation,
|
|
998
1038
|
)
|
|
999
1039
|
|
|
1000
1040
|
def build_submit_body(
|
|
@@ -1006,6 +1046,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1006
1046
|
hydrate: bool = True,
|
|
1007
1047
|
run_as: str | None = None,
|
|
1008
1048
|
resolution_overrides: dict[str, Any] | None = None,
|
|
1049
|
+
skip_validation: bool = False,
|
|
1009
1050
|
) -> dict[str, Any]:
|
|
1010
1051
|
return self.prepare_submit_payload(
|
|
1011
1052
|
pipeline_path,
|
|
@@ -1014,6 +1055,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1014
1055
|
hydrate=hydrate,
|
|
1015
1056
|
run_as=run_as,
|
|
1016
1057
|
resolution_overrides=resolution_overrides,
|
|
1058
|
+
skip_validation=skip_validation,
|
|
1017
1059
|
).to_body()
|
|
1018
1060
|
|
|
1019
1061
|
@staticmethod
|
|
@@ -1117,6 +1159,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1117
1159
|
run_as: str | None = None,
|
|
1118
1160
|
hydrate: bool = True,
|
|
1119
1161
|
attempt: int = 1,
|
|
1162
|
+
skip_validation: bool = False,
|
|
1120
1163
|
) -> dict[str, Any]:
|
|
1121
1164
|
payload = self.prepare_submit_payload_from_spec(
|
|
1122
1165
|
pipeline_spec,
|
|
@@ -1125,6 +1168,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1125
1168
|
pipeline_path=pipeline_path,
|
|
1126
1169
|
run_as=run_as,
|
|
1127
1170
|
hydrate=hydrate,
|
|
1171
|
+
skip_validation=skip_validation,
|
|
1128
1172
|
)
|
|
1129
1173
|
return self.submit_prepared_payload(payload, pipeline_path=pipeline_path, attempt=attempt)
|
|
1130
1174
|
|
|
@@ -1138,6 +1182,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1138
1182
|
run_as: str | None = None,
|
|
1139
1183
|
resolution_overrides: dict[str, Any] | None = None,
|
|
1140
1184
|
attempt: int = 1,
|
|
1185
|
+
skip_validation: bool = False,
|
|
1141
1186
|
) -> dict[str, Any]:
|
|
1142
1187
|
payload = self.prepare_submit_payload(
|
|
1143
1188
|
pipeline_path,
|
|
@@ -1146,6 +1191,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1146
1191
|
hydrate=hydrate,
|
|
1147
1192
|
run_as=run_as,
|
|
1148
1193
|
resolution_overrides=resolution_overrides,
|
|
1194
|
+
skip_validation=skip_validation,
|
|
1149
1195
|
)
|
|
1150
1196
|
return self.submit_prepared_payload(payload, pipeline_path=pipeline_path, attempt=attempt)
|
|
1151
1197
|
|
|
@@ -1909,6 +1955,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1909
1955
|
exit_on_first_failure: bool = False,
|
|
1910
1956
|
metadata: dict[str, Any] | None = None,
|
|
1911
1957
|
submit_recovery_attempts: int = _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS,
|
|
1958
|
+
skip_validation: bool = False,
|
|
1912
1959
|
) -> dict[str, Any]:
|
|
1913
1960
|
"""Submit/wait/retry an already hydrated/validated in-memory spec."""
|
|
1914
1961
|
|
|
@@ -1924,6 +1971,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1924
1971
|
pipeline_path=pipeline_path,
|
|
1925
1972
|
run_as=run_as,
|
|
1926
1973
|
hydrate=hydrate,
|
|
1974
|
+
skip_validation=skip_validation,
|
|
1927
1975
|
).to_body()
|
|
1928
1976
|
|
|
1929
1977
|
return self._run_body_factory(
|
|
@@ -1960,6 +2008,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1960
2008
|
exit_on_first_failure: bool = False,
|
|
1961
2009
|
metadata: dict[str, Any] | None = None,
|
|
1962
2010
|
submit_recovery_attempts: int = _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS,
|
|
2011
|
+
skip_validation: bool = False,
|
|
1963
2012
|
) -> dict[str, Any]:
|
|
1964
2013
|
"""Submit (and optionally wait for) a pipeline with lifecycle hooks.
|
|
1965
2014
|
|
|
@@ -1980,6 +2029,7 @@ class PipelineRunManager(TangleCliHandler):
|
|
|
1980
2029
|
hydrate=hydrate,
|
|
1981
2030
|
run_as=run_as,
|
|
1982
2031
|
resolution_overrides=resolution_overrides,
|
|
2032
|
+
skip_validation=skip_validation,
|
|
1983
2033
|
).to_body()
|
|
1984
2034
|
|
|
1985
2035
|
return self._run_body_factory(
|
|
@@ -109,24 +109,6 @@ class PipelineRunnerHooks(PipelineRunHooks):
|
|
|
109
109
|
|
|
110
110
|
return pipeline_spec
|
|
111
111
|
|
|
112
|
-
def validate_pipeline_for_run(
|
|
113
|
-
self,
|
|
114
|
-
pipeline_spec: dict[str, Any],
|
|
115
|
-
*,
|
|
116
|
-
pipeline_path: str | Path,
|
|
117
|
-
effective_path: str | Path | None,
|
|
118
|
-
skip_validation: bool,
|
|
119
|
-
) -> list[str]:
|
|
120
|
-
"""Return validation errors for a prepared pipeline spec.
|
|
121
|
-
|
|
122
|
-
The OSS default intentionally does not enforce the local authoring
|
|
123
|
-
validator here: submit-time API validation remains the source of truth,
|
|
124
|
-
while downstreams can plug in stricter schema/input validators.
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
del pipeline_spec, pipeline_path, effective_path, skip_validation
|
|
128
|
-
return []
|
|
129
|
-
|
|
130
112
|
def has_layout(self, pipeline_spec: Mapping[str, Any]) -> bool:
|
|
131
113
|
"""Return True when a pipeline graph already has non-zero coordinates."""
|
|
132
114
|
|
|
@@ -547,6 +529,7 @@ class PipelineRunner(PipelineRunnerHooks, PipelineRunManager):
|
|
|
547
529
|
pipeline_path=pipeline_path,
|
|
548
530
|
run_as=run_as,
|
|
549
531
|
hydrate=False,
|
|
532
|
+
skip_validation=True,
|
|
550
533
|
)
|
|
551
534
|
submit_payloads[attempt] = submit_payload
|
|
552
535
|
return submit_payload.to_body()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared helpers for traversing Tangle pipeline spec dictionaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _extract_task_output_refs(value: Any) -> set[str]:
|
|
9
|
+
"""Return task ids referenced by nested taskOutput argument values."""
|
|
10
|
+
|
|
11
|
+
refs: set[str] = set()
|
|
12
|
+
if isinstance(value, Mapping):
|
|
13
|
+
task_output = value.get("taskOutput")
|
|
14
|
+
if isinstance(task_output, Mapping) and isinstance(task_output.get("taskId"), str):
|
|
15
|
+
refs.add(task_output["taskId"])
|
|
16
|
+
for nested in value.values():
|
|
17
|
+
refs.update(_extract_task_output_refs(nested))
|
|
18
|
+
elif isinstance(value, list):
|
|
19
|
+
for item in value:
|
|
20
|
+
refs.update(_extract_task_output_refs(item))
|
|
21
|
+
return refs
|