tangle-cli 0.1.11__tar.gz → 0.1.12__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.11 → tangle_cli-0.1.12}/PKG-INFO +55 -1
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/README.md +54 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +33 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +411 -1
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +9 -27
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/pyproject.toml +1 -1
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/pyproject.toml.orig +1 -1
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/OSS-CONVENTIONS.md +1 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/tangle-tools.md +23 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/api_cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/client.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_from_func.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipelines.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schema_validation.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/PORT-README.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/SKILL.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/auth-wizard.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/builder.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/debugger.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/reporter.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/researcher.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/reviewer.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/agents/scenario-builder.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/data-sources.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/event-log.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/iterating-on-runs.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/knowledge-corpus.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/secrets.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/setup.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-0-initialize.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-1-analyze.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-2-hypothesize.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-3-submit.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-4-monitor.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-5-evaluate.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-6-synthesize.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/step-7-decide.md +0 -0
- {tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/uploading-artifacts.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: tangle-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: CLI for Tangle, the open-source ML pipeline orchestration platform
|
|
5
5
|
Author: Alexey Volkov, Tangle authors, Volv Grebennikov
|
|
6
6
|
Author-email: Alexey Volkov <alexey.volkov@ark-kun.com>, Volv Grebennikov <volv.grebennikov@shopify.com>
|
|
@@ -412,6 +412,7 @@ Pipeline run API/submit commands live under `sdk pipeline-runs`:
|
|
|
412
412
|
```bash
|
|
413
413
|
uv run tangle sdk pipeline-runs submit pipeline.yaml --dry-run
|
|
414
414
|
uv run tangle sdk pipeline-runs submit pipeline.yaml --arg key=value --annotation owner=team
|
|
415
|
+
uv run tangle sdk pipeline-runs submit-from-python pipeline.py --arg key=value
|
|
415
416
|
uv run tangle sdk pipeline-runs wait RUN_ID --max-wait 600 --poll-interval 10
|
|
416
417
|
uv run tangle sdk pipeline-runs logs EXECUTION_ID
|
|
417
418
|
uv run tangle sdk pipeline-runs annotations set RUN_ID key value
|
|
@@ -427,6 +428,59 @@ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
|
|
|
427
428
|
uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
|
|
428
429
|
```
|
|
429
430
|
|
|
431
|
+
To compile and submit in one step, without keeping the compiled YAML around, use
|
|
432
|
+
`pipeline-runs submit-from-python`:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
uv run tangle sdk pipeline-runs submit-from-python pipeline.py \
|
|
436
|
+
--override batch_size=100 \
|
|
437
|
+
--image eval-slim=registry.example/eval-slim@sha256:... \
|
|
438
|
+
--arg shop=acme --annotation owner=team
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
It compiles the script, hydrates the bundle, submits the run, and removes the
|
|
442
|
+
compiled artifacts again — including on dry runs and failures. Its run-tier
|
|
443
|
+
flags are the same as `pipeline-runs submit`; the extra compile-tier flags are
|
|
444
|
+
`--pipeline`, repeatable `--override KEY=VALUE`, and repeatable `--image ID=REF`.
|
|
445
|
+
Use `pipelines compile` instead when the compiled YAML itself is what you want.
|
|
446
|
+
|
|
447
|
+
The two value tiers are distinct and easy to confuse:
|
|
448
|
+
|
|
449
|
+
| Flag | Tier | Meaning |
|
|
450
|
+
| --- | --- | --- |
|
|
451
|
+
| `--override KEY=VALUE` | compile | `cfg` value used while the graph is built |
|
|
452
|
+
| `--image ID=REF` | compile | resolves `@task(image_id=ID)` to a registry ref |
|
|
453
|
+
| `--arg` / `--args-json` / `--arg-secret` | run | pipeline arguments for the created run |
|
|
454
|
+
|
|
455
|
+
Notes:
|
|
456
|
+
|
|
457
|
+
- Hydration is always on: the compiled bundle references local sidecars
|
|
458
|
+
(`resolve://./<stem>.components.yaml#…`, `file://<stem>.subgraphs/…`) that the
|
|
459
|
+
server cannot read. There is no `--no-hydrate` here.
|
|
460
|
+
- The bundle is compiled next to the script (never in `/tmp`) under a unique
|
|
461
|
+
hidden `.tangle-submit-*.yaml` name, because relative refs such as
|
|
462
|
+
`ref(url="file://./component.yaml")` resolve against the *output* directory.
|
|
463
|
+
The name is allocated with an exclusive create, so concurrent compiles of the
|
|
464
|
+
same script never collide or overwrite a sibling YAML. A symlinked script
|
|
465
|
+
compiles next to its resolved target, where its `config.yaml` and relative
|
|
466
|
+
refs actually live.
|
|
467
|
+
- The bundle stays readable until every run has been submitted, so
|
|
468
|
+
run-lifecycle hooks still see an existing `pipeline_path`; cleanup then
|
|
469
|
+
removes exactly that stem's files (`.yaml`, `.components.yaml`,
|
|
470
|
+
`.subgraphs/`) and nothing else.
|
|
471
|
+
- Submission never waits; use `pipeline-runs wait RUN_ID`.
|
|
472
|
+
- With a multi-entry `--config` file, entries may carry different `--override` /
|
|
473
|
+
`--image` values, and **every entry is fully prepared before any run is
|
|
474
|
+
created**: each is compiled exactly once, hydrated, merged with its run
|
|
475
|
+
arguments and secrets, validated, and frozen into a submit body; only then are
|
|
476
|
+
the frozen bodies submitted in order (no recompilation). An unsupported config
|
|
477
|
+
key (`hydrate:`, `pipeline_path:`), a malformed `override` / `image` /
|
|
478
|
+
`arg-secret` value, an input given as both `--arg` and `--arg-secret`, a
|
|
479
|
+
missing script, or a compile/hydrate error in *any* entry therefore creates no
|
|
480
|
+
runs and leaves no artifacts behind. A runtime failure while submitting entry
|
|
481
|
+
N can still follow the successful submits of entries 1..N-1 — that is inherent
|
|
482
|
+
to creating N runs.
|
|
483
|
+
|
|
430
484
|
A minimal graph uses `@pipeline` for the graph and `@task` for local Python components. `@task` functions are not executed at compile time; the compiler records call sites, emits a sibling `<output>.components.yaml` with `local_from_python` entries, and rewrites task component refs to that sidecar. Hydrate later regenerates the same component YAML from the Python source.
|
|
431
485
|
|
|
432
486
|
```python
|
|
@@ -386,6 +386,7 @@ Pipeline run API/submit commands live under `sdk pipeline-runs`:
|
|
|
386
386
|
```bash
|
|
387
387
|
uv run tangle sdk pipeline-runs submit pipeline.yaml --dry-run
|
|
388
388
|
uv run tangle sdk pipeline-runs submit pipeline.yaml --arg key=value --annotation owner=team
|
|
389
|
+
uv run tangle sdk pipeline-runs submit-from-python pipeline.py --arg key=value
|
|
389
390
|
uv run tangle sdk pipeline-runs wait RUN_ID --max-wait 600 --poll-interval 10
|
|
390
391
|
uv run tangle sdk pipeline-runs logs EXECUTION_ID
|
|
391
392
|
uv run tangle sdk pipeline-runs annotations set RUN_ID key value
|
|
@@ -401,6 +402,59 @@ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
|
|
|
401
402
|
uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
|
|
402
403
|
```
|
|
403
404
|
|
|
405
|
+
To compile and submit in one step, without keeping the compiled YAML around, use
|
|
406
|
+
`pipeline-runs submit-from-python`:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
uv run tangle sdk pipeline-runs submit-from-python pipeline.py \
|
|
410
|
+
--override batch_size=100 \
|
|
411
|
+
--image eval-slim=registry.example/eval-slim@sha256:... \
|
|
412
|
+
--arg shop=acme --annotation owner=team
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
It compiles the script, hydrates the bundle, submits the run, and removes the
|
|
416
|
+
compiled artifacts again — including on dry runs and failures. Its run-tier
|
|
417
|
+
flags are the same as `pipeline-runs submit`; the extra compile-tier flags are
|
|
418
|
+
`--pipeline`, repeatable `--override KEY=VALUE`, and repeatable `--image ID=REF`.
|
|
419
|
+
Use `pipelines compile` instead when the compiled YAML itself is what you want.
|
|
420
|
+
|
|
421
|
+
The two value tiers are distinct and easy to confuse:
|
|
422
|
+
|
|
423
|
+
| Flag | Tier | Meaning |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| `--override KEY=VALUE` | compile | `cfg` value used while the graph is built |
|
|
426
|
+
| `--image ID=REF` | compile | resolves `@task(image_id=ID)` to a registry ref |
|
|
427
|
+
| `--arg` / `--args-json` / `--arg-secret` | run | pipeline arguments for the created run |
|
|
428
|
+
|
|
429
|
+
Notes:
|
|
430
|
+
|
|
431
|
+
- Hydration is always on: the compiled bundle references local sidecars
|
|
432
|
+
(`resolve://./<stem>.components.yaml#…`, `file://<stem>.subgraphs/…`) that the
|
|
433
|
+
server cannot read. There is no `--no-hydrate` here.
|
|
434
|
+
- The bundle is compiled next to the script (never in `/tmp`) under a unique
|
|
435
|
+
hidden `.tangle-submit-*.yaml` name, because relative refs such as
|
|
436
|
+
`ref(url="file://./component.yaml")` resolve against the *output* directory.
|
|
437
|
+
The name is allocated with an exclusive create, so concurrent compiles of the
|
|
438
|
+
same script never collide or overwrite a sibling YAML. A symlinked script
|
|
439
|
+
compiles next to its resolved target, where its `config.yaml` and relative
|
|
440
|
+
refs actually live.
|
|
441
|
+
- The bundle stays readable until every run has been submitted, so
|
|
442
|
+
run-lifecycle hooks still see an existing `pipeline_path`; cleanup then
|
|
443
|
+
removes exactly that stem's files (`.yaml`, `.components.yaml`,
|
|
444
|
+
`.subgraphs/`) and nothing else.
|
|
445
|
+
- Submission never waits; use `pipeline-runs wait RUN_ID`.
|
|
446
|
+
- With a multi-entry `--config` file, entries may carry different `--override` /
|
|
447
|
+
`--image` values, and **every entry is fully prepared before any run is
|
|
448
|
+
created**: each is compiled exactly once, hydrated, merged with its run
|
|
449
|
+
arguments and secrets, validated, and frozen into a submit body; only then are
|
|
450
|
+
the frozen bodies submitted in order (no recompilation). An unsupported config
|
|
451
|
+
key (`hydrate:`, `pipeline_path:`), a malformed `override` / `image` /
|
|
452
|
+
`arg-secret` value, an input given as both `--arg` and `--arg-secret`, a
|
|
453
|
+
missing script, or a compile/hydrate error in *any* entry therefore creates no
|
|
454
|
+
runs and leaves no artifacts behind. A runtime failure while submitting entry
|
|
455
|
+
N can still follow the successful submits of entries 1..N-1 — that is inherent
|
|
456
|
+
to creating N runs.
|
|
457
|
+
|
|
404
458
|
A minimal graph uses `@pipeline` for the graph and `@task` for local Python components. `@task` functions are not executed at compile time; the compiler records call sites, emits a sibling `<output>.components.yaml` with `local_from_python` entries, and rewrites task component refs to that sidecar. Hydrate later regenerates the same component YAML from the Python source.
|
|
405
459
|
|
|
406
460
|
```python
|
|
@@ -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.12"
|
|
18
18
|
|
|
19
19
|
__all__ = ["TangleDynamicDiscoveryClient", "__version__"]
|
|
@@ -46,6 +46,39 @@ def optional_path(value: str | pathlib.Path | object | None) -> pathlib.Path | N
|
|
|
46
46
|
return None
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def parse_overrides(values: list[str] | None) -> dict[str, str]:
|
|
50
|
+
"""Parse repeatable ``--override KEY=VALUE`` compile-time cfg overrides.
|
|
51
|
+
|
|
52
|
+
Shared verbatim by ``sdk pipelines compile`` and ``sdk pipeline-runs
|
|
53
|
+
submit-from-python`` so both accept exactly the same syntax and reject the
|
|
54
|
+
same mistakes. An empty VALUE is allowed (``--override note=``).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
parsed: dict[str, str] = {}
|
|
58
|
+
for value in values or []:
|
|
59
|
+
if "=" not in value:
|
|
60
|
+
raise SystemExit("--override entries must use KEY=VALUE syntax")
|
|
61
|
+
key, parsed_value = value.split("=", 1)
|
|
62
|
+
if not key:
|
|
63
|
+
raise SystemExit("--override entries must use KEY=VALUE syntax")
|
|
64
|
+
parsed[key] = parsed_value
|
|
65
|
+
return parsed
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_image_overrides(values: list[str] | None) -> dict[str, str]:
|
|
69
|
+
"""Parse repeatable ``--image ID=REF`` compile-time image-id overrides."""
|
|
70
|
+
|
|
71
|
+
parsed: dict[str, str] = {}
|
|
72
|
+
for value in values or []:
|
|
73
|
+
if "=" not in value:
|
|
74
|
+
raise SystemExit("--image entries must use ID=REF syntax")
|
|
75
|
+
image_id, image_ref = value.split("=", 1)
|
|
76
|
+
if not image_id or not image_ref:
|
|
77
|
+
raise SystemExit("--image entries must use ID=REF syntax")
|
|
78
|
+
parsed[image_id] = image_ref
|
|
79
|
+
return parsed
|
|
80
|
+
|
|
81
|
+
|
|
49
82
|
def api_arg_specs(
|
|
50
83
|
*,
|
|
51
84
|
base_url: str | None = None,
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py
RENAMED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import difflib
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import pathlib
|
|
9
|
+
import shutil
|
|
8
10
|
import sys
|
|
9
|
-
|
|
11
|
+
import tempfile
|
|
12
|
+
from contextlib import ExitStack, contextmanager, suppress
|
|
13
|
+
from typing import TYPE_CHECKING, Annotated, Any, Callable, Iterator
|
|
10
14
|
|
|
11
15
|
from cyclopts import App, Parameter
|
|
12
16
|
|
|
@@ -17,6 +21,8 @@ from .cli_helpers import (
|
|
|
17
21
|
include_env_credentials_for_args,
|
|
18
22
|
load_args_or_exit,
|
|
19
23
|
optional_path,
|
|
24
|
+
parse_image_overrides,
|
|
25
|
+
parse_overrides,
|
|
20
26
|
print_json,
|
|
21
27
|
)
|
|
22
28
|
from .cli_options import (
|
|
@@ -42,6 +48,10 @@ from .pipeline_run_manager import (
|
|
|
42
48
|
parse_key_value_entries,
|
|
43
49
|
)
|
|
44
50
|
from .pipeline_run_search import normalize_query_input, parse_annotation
|
|
51
|
+
from .pipelines import PipelineValidationError, compile_pipeline_file
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
54
|
+
from .pipeline_compiler import CompileResult
|
|
45
55
|
|
|
46
56
|
app = App(name="pipeline-runs", help="Submit and inspect Tangle pipeline runs.")
|
|
47
57
|
annotations_app = App(name="annotations", help="Work with pipeline-run annotations.")
|
|
@@ -120,6 +130,56 @@ def _run_manager_action(config: str | None, cli_base_url: str | None, specs: dic
|
|
|
120
130
|
finalize_logs()
|
|
121
131
|
|
|
122
132
|
|
|
133
|
+
def _run_prepared_manager_actions(
|
|
134
|
+
config: str | None,
|
|
135
|
+
cli_base_url: str | None,
|
|
136
|
+
specs: dict[str, tuple[Any, ...]],
|
|
137
|
+
prepare,
|
|
138
|
+
submit,
|
|
139
|
+
*,
|
|
140
|
+
precheck: Callable[[ArgsContainer], None] | None = None,
|
|
141
|
+
):
|
|
142
|
+
"""Prepare EVERY config entry, then submit the prepared results in order.
|
|
143
|
+
|
|
144
|
+
``prepare(manager, args, artifacts)`` returns one entry's frozen payload and
|
|
145
|
+
may register cleanup on the shared ``artifacts`` stack; ``submit`` performs
|
|
146
|
+
the single API write. Splitting the phases means a failure while preparing
|
|
147
|
+
any entry submits nothing. Artifacts outlive preparation because run
|
|
148
|
+
lifecycle hooks read the compiled path during submission.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
loaded = load_args_or_exit(config, **specs)
|
|
152
|
+
if precheck is not None:
|
|
153
|
+
for args in loaded:
|
|
154
|
+
precheck(args)
|
|
155
|
+
|
|
156
|
+
with ExitStack() as log_finalizers:
|
|
157
|
+
entries: list[tuple[ArgsContainer, PipelineRunManager]] = []
|
|
158
|
+
for args in loaded:
|
|
159
|
+
try:
|
|
160
|
+
logger, finalize_logs = logger_for_log_type(getattr(args, "log_type", "console"))
|
|
161
|
+
except ValueError as exc:
|
|
162
|
+
raise SystemExit(str(exc)) from exc
|
|
163
|
+
log_finalizers.callback(finalize_logs)
|
|
164
|
+
entries.append((args, _manager(args, cli_base_url=cli_base_url, logger=logger)))
|
|
165
|
+
|
|
166
|
+
prepared: list[Any] = []
|
|
167
|
+
with ExitStack() as artifacts:
|
|
168
|
+
for args, manager in entries:
|
|
169
|
+
try:
|
|
170
|
+
prepared.append(prepare(manager, args, artifacts))
|
|
171
|
+
except PipelineRunError as exc:
|
|
172
|
+
raise SystemExit(str(exc)) from exc
|
|
173
|
+
|
|
174
|
+
for (args, manager), payload in zip(entries, prepared):
|
|
175
|
+
try:
|
|
176
|
+
result = submit(manager, args, payload)
|
|
177
|
+
except PipelineRunError as exc:
|
|
178
|
+
raise SystemExit(str(exc)) from exc
|
|
179
|
+
if result is not None:
|
|
180
|
+
print_json(result)
|
|
181
|
+
|
|
182
|
+
|
|
123
183
|
def _run_annotation_action(config: str | None, cli_base_url: str | None, specs: dict[str, tuple[Any, ...]], fn):
|
|
124
184
|
for args in load_args_or_exit(config, **specs):
|
|
125
185
|
try:
|
|
@@ -247,6 +307,356 @@ def pipeline_runs_submit(
|
|
|
247
307
|
_run_manager_action(config, base_url, specs, action)
|
|
248
308
|
|
|
249
309
|
|
|
310
|
+
#: Config fields this command understands. Used only to catch MISSPELLINGS of
|
|
311
|
+
#: supported keys (`overide:`), which would otherwise be dropped silently and
|
|
312
|
+
#: submit something other than what was authored. Unrelated keys in a shared
|
|
313
|
+
#: config file are deliberately left alone.
|
|
314
|
+
_SUBMIT_FROM_PYTHON_CONFIG_KEYS = frozenset(
|
|
315
|
+
"script_path pipeline override image arg args args_json arg_secret arg_secrets "
|
|
316
|
+
"annotation dry_run run_as trusted_source trusted_hydration trusted_hydration_cli "
|
|
317
|
+
"submit_recovery_attempts log_type base_url token auth_header header".split()
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
#: Keys that are valid for other commands but must never be honored here,
|
|
321
|
+
#: because silently ignoring them would change what actually gets submitted.
|
|
322
|
+
_SUBMIT_FROM_PYTHON_REJECTED_CONFIG_KEYS = {
|
|
323
|
+
"hydrate": (
|
|
324
|
+
"submit-from-python always hydrates the compiled bundle; "
|
|
325
|
+
"its relative refs are meaningless to the server otherwise"
|
|
326
|
+
),
|
|
327
|
+
"pipeline_path": (
|
|
328
|
+
"submit-from-python compiles a Python script; "
|
|
329
|
+
"use script_path (and --pipeline to pick the root @pipeline function)"
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_LOG_TYPES = ("console", "none", "file")
|
|
334
|
+
|
|
335
|
+
#: Dot-prefixed so the throwaway bundle stays hidden next to the user's sources.
|
|
336
|
+
_TEMP_BUNDLE_PREFIX = ".tangle-submit-"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@contextmanager
|
|
340
|
+
def _compiled_bundle(
|
|
341
|
+
script: pathlib.Path,
|
|
342
|
+
*,
|
|
343
|
+
overrides: dict[str, str],
|
|
344
|
+
image_overrides: dict[str, str],
|
|
345
|
+
pipeline_name: str | None,
|
|
346
|
+
logger: Logger,
|
|
347
|
+
) -> Iterator[CompileResult]:
|
|
348
|
+
"""Compile *script* to a throwaway bundle beside it, always removing it.
|
|
349
|
+
|
|
350
|
+
The bundle cannot live in a temp directory: the compiler derives every
|
|
351
|
+
relative URL from the output path, so author-written refs such as
|
|
352
|
+
``ref(url="file://./component.yaml")`` only resolve when the bundle sits in
|
|
353
|
+
the script's own (physically resolved) directory. The name is allocated
|
|
354
|
+
with an exclusive create, so concurrent compiles never collide.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
script_path = script.resolve()
|
|
358
|
+
try:
|
|
359
|
+
handle = tempfile.NamedTemporaryFile(
|
|
360
|
+
dir=script_path.parent, prefix=_TEMP_BUNDLE_PREFIX, suffix=".yaml", delete=False
|
|
361
|
+
)
|
|
362
|
+
except OSError as exc:
|
|
363
|
+
raise PipelineValidationError(
|
|
364
|
+
f"Cannot create a compile output in {script_path.parent}: {exc}"
|
|
365
|
+
) from exc
|
|
366
|
+
root = pathlib.Path(handle.name)
|
|
367
|
+
closed = False
|
|
368
|
+
try:
|
|
369
|
+
handle.close()
|
|
370
|
+
closed = True
|
|
371
|
+
result = compile_pipeline_file(
|
|
372
|
+
script_path,
|
|
373
|
+
root,
|
|
374
|
+
overrides=overrides,
|
|
375
|
+
pipeline_name=pipeline_name,
|
|
376
|
+
image_overrides=image_overrides,
|
|
377
|
+
logger=logger,
|
|
378
|
+
)
|
|
379
|
+
for warning in result.warnings:
|
|
380
|
+
logger.warn(warning)
|
|
381
|
+
yield result
|
|
382
|
+
finally:
|
|
383
|
+
# Cleanup is stem-scoped and must never mask the primary error.
|
|
384
|
+
if not closed:
|
|
385
|
+
with suppress(Exception):
|
|
386
|
+
handle.close()
|
|
387
|
+
for path in (root, root.with_name(root.stem + ".components.yaml")):
|
|
388
|
+
try:
|
|
389
|
+
path.unlink()
|
|
390
|
+
except FileNotFoundError:
|
|
391
|
+
pass
|
|
392
|
+
except OSError as exc:
|
|
393
|
+
logger.warn(f"Could not remove compile artifact {path}: {exc}")
|
|
394
|
+
shutil.rmtree(root.with_name(root.stem + ".subgraphs"), ignore_errors=True)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _misspelled_config_key(key: str) -> str | None:
|
|
398
|
+
"""Return the supported field *key* looks like a typo of, if any."""
|
|
399
|
+
|
|
400
|
+
if key.startswith("_") or key in _SUBMIT_FROM_PYTHON_CONFIG_KEYS:
|
|
401
|
+
return None
|
|
402
|
+
normalized = "".join(char for char in key.lower() if char.isalnum())
|
|
403
|
+
for known in _SUBMIT_FROM_PYTHON_CONFIG_KEYS:
|
|
404
|
+
if normalized == known.replace("_", ""):
|
|
405
|
+
return known
|
|
406
|
+
close = difflib.get_close_matches(key, sorted(_SUBMIT_FROM_PYTHON_CONFIG_KEYS), n=1, cutoff=0.85)
|
|
407
|
+
return close[0] if close else None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _resolve_run_arguments(args: ArgsContainer) -> dict[str, Any]:
|
|
411
|
+
"""Resolve one entry's run arguments: literals plus secret bindings.
|
|
412
|
+
|
|
413
|
+
Pure, so the precheck and the preparation phase share one implementation
|
|
414
|
+
and an ``--arg``/``--arg-secret`` conflict is caught before any submit.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
run_args = parse_json_or_key_values(args.args_json or args.args_config, args.arg)
|
|
418
|
+
secret_names = normalize_arg_secret_config(args.arg_secrets_config)
|
|
419
|
+
secret_names.update(parse_arg_secret_entries(args.arg_secret))
|
|
420
|
+
return merge_secret_run_args(run_args, secret_names)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _check_submit_from_python_inputs(args: ArgsContainer) -> None:
|
|
424
|
+
"""Validate one entry before ANY entry is compiled or submitted.
|
|
425
|
+
|
|
426
|
+
Only deterministic, side-effect-free checks belong here: they run for every
|
|
427
|
+
config entry up front, so a malformed value in the last entry cannot land
|
|
428
|
+
after earlier entries were already submitted.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
config = getattr(args, "_config", {})
|
|
432
|
+
if isinstance(config, dict):
|
|
433
|
+
for key, reason in _SUBMIT_FROM_PYTHON_REJECTED_CONFIG_KEYS.items():
|
|
434
|
+
if key in config:
|
|
435
|
+
raise SystemExit(
|
|
436
|
+
f"Config error: '{key}' is not supported by submit-from-python: {reason}"
|
|
437
|
+
)
|
|
438
|
+
for key in config:
|
|
439
|
+
if (suggestion := _misspelled_config_key(str(key))) is not None:
|
|
440
|
+
raise SystemExit(
|
|
441
|
+
f"Config error: unknown submit-from-python field '{key}'; "
|
|
442
|
+
f"did you mean '{suggestion}'?"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Safety-sensitive values reach retry/trust logic, where a wrong type is
|
|
446
|
+
# worse than a rejected config: a non-int recovery budget crashes an
|
|
447
|
+
# ambiguous submit before its recovery lookup, and a truthy string silently
|
|
448
|
+
# enables allow-all hydration.
|
|
449
|
+
attempts = getattr(args, "submit_recovery_attempts", None)
|
|
450
|
+
if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 0:
|
|
451
|
+
raise SystemExit("Config error: submit_recovery_attempts must be a non-negative integer")
|
|
452
|
+
trusted_hydration = getattr(args, "trusted_hydration_cli", None)
|
|
453
|
+
if trusted_hydration is not None and not isinstance(trusted_hydration, bool):
|
|
454
|
+
raise SystemExit("Config error: trusted_hydration_cli must be a boolean")
|
|
455
|
+
|
|
456
|
+
if getattr(args, "log_type", "console") not in _LOG_TYPES:
|
|
457
|
+
raise SystemExit(f"--log-type must be one of: {', '.join(_LOG_TYPES)}")
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
parse_overrides(args.override)
|
|
461
|
+
parse_image_overrides(args.image)
|
|
462
|
+
_resolve_run_arguments(args)
|
|
463
|
+
parse_key_value_entries(args.annotation)
|
|
464
|
+
except PipelineRunError as exc:
|
|
465
|
+
raise SystemExit(str(exc)) from exc
|
|
466
|
+
|
|
467
|
+
if args.script_path is not None and not pathlib.Path(args.script_path).exists():
|
|
468
|
+
raise SystemExit(f"Pipeline script not found: {args.script_path}")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@app.command(name="submit-from-python")
|
|
472
|
+
def pipeline_runs_submit_from_python(
|
|
473
|
+
script_path: pathlib.Path | None = None,
|
|
474
|
+
*,
|
|
475
|
+
pipeline: Annotated[
|
|
476
|
+
str | None,
|
|
477
|
+
Parameter(
|
|
478
|
+
name="--pipeline",
|
|
479
|
+
help="Select the root @pipeline function by name when the file defines several.",
|
|
480
|
+
),
|
|
481
|
+
] = None,
|
|
482
|
+
override: Annotated[
|
|
483
|
+
list[str] | None,
|
|
484
|
+
Parameter(
|
|
485
|
+
name="--override",
|
|
486
|
+
help="Compile-time config override as KEY=VALUE. Repeat for multiple.",
|
|
487
|
+
negative_iterable=(),
|
|
488
|
+
),
|
|
489
|
+
] = None,
|
|
490
|
+
image: Annotated[
|
|
491
|
+
list[str] | None,
|
|
492
|
+
Parameter(
|
|
493
|
+
name="--image",
|
|
494
|
+
help=(
|
|
495
|
+
"Compile-time image-id override as ID=REF for @task(image_id=ID). "
|
|
496
|
+
"Repeat for multiple IDs."
|
|
497
|
+
),
|
|
498
|
+
negative_iterable=(),
|
|
499
|
+
),
|
|
500
|
+
] = None,
|
|
501
|
+
arg: Annotated[
|
|
502
|
+
list[str] | None,
|
|
503
|
+
Parameter(help="Pipeline argument as KEY=VALUE. Repeat for multiple.", negative_iterable=()),
|
|
504
|
+
] = None,
|
|
505
|
+
args_json: Annotated[str | None, Parameter(help="Pipeline arguments as a JSON object.")] = None,
|
|
506
|
+
arg_secret: Annotated[
|
|
507
|
+
list[str] | None,
|
|
508
|
+
Parameter(
|
|
509
|
+
name="--arg-secret",
|
|
510
|
+
help=(
|
|
511
|
+
"Pipeline argument bound to a Tangle secret as INPUT=SECRET_NAME. "
|
|
512
|
+
"Repeat for multiple."
|
|
513
|
+
),
|
|
514
|
+
negative_iterable=(),
|
|
515
|
+
),
|
|
516
|
+
] = None,
|
|
517
|
+
annotation: Annotated[
|
|
518
|
+
list[str] | None,
|
|
519
|
+
Parameter(help="Run annotation as KEY=VALUE. Repeat for multiple.", negative_iterable=()),
|
|
520
|
+
] = None,
|
|
521
|
+
dry_run: Annotated[
|
|
522
|
+
bool | None,
|
|
523
|
+
Parameter(help="Compile, hydrate and print the submit payload without creating a run."),
|
|
524
|
+
] = None,
|
|
525
|
+
run_as: Annotated[
|
|
526
|
+
str | None,
|
|
527
|
+
Parameter(help="Downstream extension point; unsupported by the OSS default hooks."),
|
|
528
|
+
] = None,
|
|
529
|
+
trusted_source: Annotated[
|
|
530
|
+
list[str] | None,
|
|
531
|
+
Parameter(
|
|
532
|
+
name="--trusted-source",
|
|
533
|
+
help="Trusted local_from_python source root or glob. Repeat for multiple.",
|
|
534
|
+
negative_iterable=(),
|
|
535
|
+
),
|
|
536
|
+
] = None,
|
|
537
|
+
trusted_hydration: Annotated[
|
|
538
|
+
bool | None,
|
|
539
|
+
Parameter(
|
|
540
|
+
name="--trusted-hydration",
|
|
541
|
+
help="Allow all local_from_python execution during hydration for trusted inputs.",
|
|
542
|
+
),
|
|
543
|
+
] = None,
|
|
544
|
+
base_url: BaseUrlOption = None,
|
|
545
|
+
token: TokenOption = None,
|
|
546
|
+
auth_header: AuthHeaderOption = None,
|
|
547
|
+
header: HeaderOption = None,
|
|
548
|
+
config: ConfigOption = None,
|
|
549
|
+
submit_recovery_attempts: Annotated[
|
|
550
|
+
int,
|
|
551
|
+
Parameter(
|
|
552
|
+
help=(
|
|
553
|
+
"Number of post-failed-submit recovery lookups before resubmitting; "
|
|
554
|
+
"higher values wait longer for delayed run registration."
|
|
555
|
+
)
|
|
556
|
+
),
|
|
557
|
+
] = _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS,
|
|
558
|
+
log_type: LogTypeOption = "console",
|
|
559
|
+
) -> None:
|
|
560
|
+
"""Compile a Python-authored pipeline and submit it as a run.
|
|
561
|
+
|
|
562
|
+
Equivalent to ``pipelines compile`` followed by ``pipeline-runs submit``,
|
|
563
|
+
without leaving compiled YAML behind. The bundle is compiled next to the
|
|
564
|
+
script (so relative ``file://`` refs keep resolving), hydrated, submitted,
|
|
565
|
+
and then removed — also on dry runs and failures. Use ``pipelines compile``
|
|
566
|
+
when the compiled YAML itself is what you want.
|
|
567
|
+
|
|
568
|
+
Compile-time values and run-time values are distinct: ``--override
|
|
569
|
+
KEY=VALUE`` sets ``cfg`` values consumed while the graph is built and
|
|
570
|
+
``--image ID=REF`` resolves ``@task(image_id=ID)``, while ``--arg`` /
|
|
571
|
+
``--args-json`` / ``--arg-secret`` set pipeline arguments for the run.
|
|
572
|
+
|
|
573
|
+
Hydration is always on: the compiled bundle's refs point at local sidecars
|
|
574
|
+
the server cannot read. Submission never waits; use ``pipeline-runs wait``.
|
|
575
|
+
|
|
576
|
+
A multi-entry ``--config`` prepares EVERY entry first — compile once,
|
|
577
|
+
hydrate, merge run arguments and secrets, validate, freeze the submit body
|
|
578
|
+
— and only then submits the frozen bodies in order. A failure while
|
|
579
|
+
preparing any entry therefore creates no runs and leaves no artifacts.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
specs = {
|
|
583
|
+
"script_path": ("script_path", script_path, None, False, True, optional_path),
|
|
584
|
+
"pipeline": (pipeline, None),
|
|
585
|
+
"override": (override, None),
|
|
586
|
+
"image": (image, None),
|
|
587
|
+
"arg": (arg, None),
|
|
588
|
+
"args_json": (args_json, None),
|
|
589
|
+
"args_config": ("args", None, None, True),
|
|
590
|
+
"arg_secret": (arg_secret, None),
|
|
591
|
+
"arg_secrets_config": ("arg_secrets", None, None, True),
|
|
592
|
+
"annotation": (annotation, None),
|
|
593
|
+
"dry_run": (dry_run, None),
|
|
594
|
+
"run_as": (run_as, None),
|
|
595
|
+
"trusted_source": (trusted_source, None),
|
|
596
|
+
"trusted_hydration_cli": ("trusted_hydration_cli", trusted_hydration, None, False),
|
|
597
|
+
"submit_recovery_attempts": (submit_recovery_attempts, _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS),
|
|
598
|
+
"log_type": (log_type, "console"),
|
|
599
|
+
**api_arg_specs(base_url=base_url, token=token, auth_header=auth_header, header=header),
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
def prepare(
|
|
603
|
+
manager: PipelineRunManager, args: ArgsContainer, artifacts: ExitStack
|
|
604
|
+
) -> tuple[dict[str, Any], pathlib.Path]:
|
|
605
|
+
"""Compile once, hydrate, validate, and freeze this entry's submit body.
|
|
606
|
+
|
|
607
|
+
The compiled path travels with the body so run lifecycle hooks keep the
|
|
608
|
+
path-backed context they had when submission happened inside the
|
|
609
|
+
bundle; that is why the bundle outlives preparation.
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
try:
|
|
613
|
+
compiled = artifacts.enter_context(
|
|
614
|
+
_compiled_bundle(
|
|
615
|
+
args.script_path,
|
|
616
|
+
overrides=parse_overrides(args.override),
|
|
617
|
+
image_overrides=parse_image_overrides(args.image),
|
|
618
|
+
pipeline_name=args.pipeline,
|
|
619
|
+
logger=manager.logger,
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
body = manager.build_submit_body(
|
|
623
|
+
compiled.pipeline_path,
|
|
624
|
+
run_args=_resolve_run_arguments(args),
|
|
625
|
+
annotations=parse_key_value_entries(args.annotation),
|
|
626
|
+
hydrate=True,
|
|
627
|
+
run_as=args.run_as,
|
|
628
|
+
)
|
|
629
|
+
return body, compiled.pipeline_path
|
|
630
|
+
except PipelineValidationError as exc:
|
|
631
|
+
raise SystemExit(str(exc)) from exc
|
|
632
|
+
|
|
633
|
+
def submit(
|
|
634
|
+
manager: PipelineRunManager,
|
|
635
|
+
args: ArgsContainer,
|
|
636
|
+
prepared: tuple[dict[str, Any], pathlib.Path],
|
|
637
|
+
) -> dict[str, Any]:
|
|
638
|
+
body, pipeline_path = prepared
|
|
639
|
+
if args.dry_run:
|
|
640
|
+
return body
|
|
641
|
+
# The body is already hydrated and validated, so the run is created
|
|
642
|
+
# without recompiling; submission-id stamping and the ambiguous-submit
|
|
643
|
+
# recovery lookup still happen inside the run lifecycle.
|
|
644
|
+
return manager.run_prepared_body(
|
|
645
|
+
body,
|
|
646
|
+
pipeline_path=pipeline_path,
|
|
647
|
+
submit_recovery_attempts=args.submit_recovery_attempts,
|
|
648
|
+
)["response"]
|
|
649
|
+
|
|
650
|
+
_run_prepared_manager_actions(
|
|
651
|
+
config,
|
|
652
|
+
base_url,
|
|
653
|
+
specs,
|
|
654
|
+
prepare,
|
|
655
|
+
submit,
|
|
656
|
+
precheck=_check_submit_from_python_inputs,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
250
660
|
@app.command(name="details")
|
|
251
661
|
def pipeline_runs_details(
|
|
252
662
|
run_id: str | None = None,
|
|
@@ -7,7 +7,13 @@ from typing import Annotated, Any
|
|
|
7
7
|
|
|
8
8
|
from cyclopts import App, Parameter
|
|
9
9
|
|
|
10
|
-
from .cli_helpers import
|
|
10
|
+
from .cli_helpers import (
|
|
11
|
+
LazyTangleApiClient,
|
|
12
|
+
load_config_or_exit,
|
|
13
|
+
optional_path,
|
|
14
|
+
parse_image_overrides,
|
|
15
|
+
parse_overrides,
|
|
16
|
+
)
|
|
11
17
|
from .cli_options import (
|
|
12
18
|
AuthHeaderOption,
|
|
13
19
|
BaseUrlOption,
|
|
@@ -126,30 +132,6 @@ def _parse_vars(values: list[str] | dict[str, object] | None) -> dict[str, str]:
|
|
|
126
132
|
return parsed
|
|
127
133
|
|
|
128
134
|
|
|
129
|
-
def _parse_overrides(values: list[str] | None) -> dict[str, str]:
|
|
130
|
-
parsed: dict[str, str] = {}
|
|
131
|
-
for value in values or []:
|
|
132
|
-
if "=" not in value:
|
|
133
|
-
raise SystemExit("--override entries must use KEY=VALUE syntax")
|
|
134
|
-
key, parsed_value = value.split("=", 1)
|
|
135
|
-
if not key:
|
|
136
|
-
raise SystemExit("--override entries must use KEY=VALUE syntax")
|
|
137
|
-
parsed[key] = parsed_value
|
|
138
|
-
return parsed
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _parse_image_overrides(values: list[str] | None) -> dict[str, str]:
|
|
142
|
-
parsed: dict[str, str] = {}
|
|
143
|
-
for value in values or []:
|
|
144
|
-
if "=" not in value:
|
|
145
|
-
raise SystemExit("--image entries must use ID=REF syntax")
|
|
146
|
-
image_id, image_ref = value.split("=", 1)
|
|
147
|
-
if not image_id or not image_ref:
|
|
148
|
-
raise SystemExit("--image entries must use ID=REF syntax")
|
|
149
|
-
parsed[image_id] = image_ref
|
|
150
|
-
return parsed
|
|
151
|
-
|
|
152
|
-
|
|
153
135
|
@app.command(name="hydrate")
|
|
154
136
|
def pipelines_hydrate(
|
|
155
137
|
pipeline_path: pathlib.Path,
|
|
@@ -299,8 +281,8 @@ def pipelines_compile(
|
|
|
299
281
|
result = compile_pipeline_file(
|
|
300
282
|
pipeline_path,
|
|
301
283
|
output,
|
|
302
|
-
overrides=
|
|
303
|
-
image_overrides=
|
|
284
|
+
overrides=parse_overrides(override),
|
|
285
|
+
image_overrides=parse_image_overrides(image),
|
|
304
286
|
pipeline_name=pipeline,
|
|
305
287
|
logger=logger,
|
|
306
288
|
)
|
|
@@ -84,6 +84,7 @@ OSS replacement. **Verbs/flags below were verified against the `tangle-cli` sour
|
|
|
84
84
|
| `<deploy-cli> pipeline-run submit p.yaml -f c.yaml --hydrate --no-wait` | `tangle sdk pipeline-runs submit p.yaml [--arg K=V \| --args-json JSON] [--annotation K=V]` — **hydrate is the default; there is NO `--no-wait` (submit never waits); there is NO `-f config.yaml` (use `--arg`/`--args-json`, or `--config` for CLI-option defaults)** |
|
|
85
85
|
| `… submit … (submit-as-is, no version resolution)` | `tangle sdk pipeline-runs submit p.yaml --no-hydrate` |
|
|
86
86
|
| `… submit … --dry-run` (preview payload) | `tangle sdk pipeline-runs submit p.yaml --dry-run` (prints the submit body, creates no run) |
|
|
87
|
+
| `… ship/submit a Python-authored pipeline` | `tangle sdk pipeline-runs submit-from-python p.py [--pipeline NAME] [--override K=V] [--image ID=REF]` plus the usual run flags — compiles, hydrates (always), submits, then deletes the compiled bundle; never waits (use `pipelines compile` to keep the YAML) |
|
|
87
88
|
| `<deploy-cli> pipeline-run details RUN_ID --state` | `tangle sdk pipeline-runs details RUN_ID --include-execution-state` |
|
|
88
89
|
| `… details … --implementations` | `… details … --include-implementations` |
|
|
89
90
|
| `… details … --include-annotations` | `… details … --include-annotations` (unchanged) |
|
|
@@ -157,6 +157,29 @@ tangle sdk pipeline-runs submit pipeline.yaml \
|
|
|
157
157
|
--arg K=V --annotation session=YYYY-MM-DD-scenario
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
### Submitting a Python-authored pipeline directly
|
|
161
|
+
|
|
162
|
+
`submit-from-python` compiles a Python pipeline script, hydrates it, submits the
|
|
163
|
+
run, and removes the compiled YAML again (also on `--dry-run` and on failure):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
tangle sdk pipeline-runs submit-from-python pipeline.py \
|
|
167
|
+
--override batch_size=100 \
|
|
168
|
+
--arg model=baseline --annotation session=YYYY-MM-DD-scenario
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- Compile-tier flags: `--pipeline NAME` (pick the root `@pipeline` when the file
|
|
172
|
+
defines several), `--override KEY=VALUE` (`cfg` values), `--image ID=REF`
|
|
173
|
+
(`@task(image_id=…)`). The bundle is always removed; use `pipelines compile`
|
|
174
|
+
when you want to keep the YAML.
|
|
175
|
+
- Run-tier flags are identical to `submit` (`--arg`, `--args-json`,
|
|
176
|
+
`--arg-secret`, `--annotation`, `--dry-run`, auth/config/log options).
|
|
177
|
+
- Hydration is forced (no `--no-hydrate`) and submit still never waits.
|
|
178
|
+
- A multi-entry `--config` prepares every entry (compile once, hydrate, merge
|
|
179
|
+
args/secrets, validate, freeze the body) before creating any run, so a bad
|
|
180
|
+
value or compile error in the last entry submits nothing.
|
|
181
|
+
- Do not confuse `--override` (compile time) with `--arg` (run time).
|
|
182
|
+
|
|
160
183
|
## Validating & Editing Pipelines
|
|
161
184
|
|
|
162
185
|
Local pipeline operations live under `pipelines` (NOT `pipeline-runs`):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/args_container.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_from_func.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_generator.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_inspector.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/component_publisher.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/components_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/hydration_trust.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/module_bundler.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/openapi/parser.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schema_validation.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/packages/tangle-cli/src/tangle_cli/version_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.11 → tangle_cli-0.1.12}/skills/tangent/references/example-scenarios/INDEX.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|