tangle-cli 0.1.6__tar.gz → 0.1.7__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.
Files changed (100) hide show
  1. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/PKG-INFO +127 -2
  2. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/README.md +126 -1
  3. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
  4. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/component_from_func.py +325 -4
  5. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/component_generator.py +93 -4
  6. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +103 -17
  7. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +28 -1
  8. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +7 -0
  9. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +131 -7
  10. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +36 -0
  11. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/pyproject.toml +1 -1
  12. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/api_cli.py +0 -0
  13. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
  14. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
  15. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
  16. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
  17. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
  18. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
  19. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
  20. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
  21. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/client.py +0 -0
  22. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
  23. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
  24. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
  25. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
  26. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
  27. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
  28. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
  29. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
  30. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
  31. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
  32. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
  33. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
  34. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
  35. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
  36. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
  37. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
  38. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +0 -0
  39. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
  40. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
  41. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +0 -0
  42. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
  43. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +0 -0
  44. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipelines.py +0 -0
  45. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +0 -0
  46. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +0 -0
  47. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
  48. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
  49. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
  50. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
  51. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
  52. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +0 -0
  53. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
  54. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
  55. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
  56. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
  57. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
  58. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
  59. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +0 -0
  60. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
  61. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
  62. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
  63. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
  64. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/schema_validation.py +0 -0
  65. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
  66. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +0 -0
  67. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -0
  68. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
  69. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
  70. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
  71. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
  72. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/OSS-CONVENTIONS.md +0 -0
  73. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/PORT-README.md +0 -0
  74. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/SKILL.md +0 -0
  75. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/auth-wizard.md +0 -0
  76. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/builder.md +0 -0
  77. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/debugger.md +0 -0
  78. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/reporter.md +0 -0
  79. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/researcher.md +0 -0
  80. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/reviewer.md +0 -0
  81. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/agents/scenario-builder.md +0 -0
  82. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/data-sources.md +0 -0
  83. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/event-log.md +0 -0
  84. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
  85. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
  86. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
  87. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/iterating-on-runs.md +0 -0
  88. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/knowledge-corpus.md +0 -0
  89. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/secrets.md +0 -0
  90. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/setup.md +0 -0
  91. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-0-initialize.md +0 -0
  92. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-1-analyze.md +0 -0
  93. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-2-hypothesize.md +0 -0
  94. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-3-submit.md +0 -0
  95. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-4-monitor.md +0 -0
  96. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-5-evaluate.md +0 -0
  97. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-6-synthesize.md +0 -0
  98. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/step-7-decide.md +0 -0
  99. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/skills/tangent/references/tangle-tools.md +0 -0
  100. {tangle_cli-0.1.6 → tangle_cli-0.1.7}/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.6
3
+ Version: 0.1.7
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>
@@ -317,7 +317,132 @@ uv run tangle sdk pipeline-runs annotations set RUN_ID key value
317
317
  uv run tangle sdk pipeline-runs export RUN_ID --output pipeline.yaml
318
318
  ```
319
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.
320
+ #### Python pipeline authoring
321
+
322
+ Python-authored pipelines live in normal `.py` files and compile with:
323
+
324
+ ```bash
325
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
326
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
327
+ ```
328
+
329
+ 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.
330
+
331
+ ```python
332
+ from cloud_pipelines import components
333
+ from tangle_cli.python_pipeline import In, Out, pipeline, task
334
+
335
+
336
+ @task(image="python:3.12")
337
+ def write_greeting(out: components.OutputPath("Text"), who: str, greeting: str = "hello"):
338
+ with open(out, "w") as fh:
339
+ fh.write(f"{greeting} {who}")
340
+
341
+
342
+ @pipeline("Greeting pipeline", output_name="greeting_file")
343
+ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
344
+ greeting = write_greeting(who=who, greeting=cfg.greeting)
345
+ return greeting.out
346
+ ```
347
+
348
+ `In[T]` parameters become runtime graph inputs. A single `-> Out[T]` return exposes one graph output; use `@pipeline(output_name=...)` to name that output. For multiple outputs, define a frozen dataclass subclass of `Outputs` with `Out[T]` fields and return an instance. A pipeline that accepts a `cfg` parameter reads `config.yaml` (or the path passed via `@pipeline(config="...")`) at compile time, with `--override key=value` values overlaid by the compile command.
349
+
350
+ Task IDs default from the left-hand variable name at the call site, converted to title case. If there is no simple left-hand variable, or if you want a stable explicit label, call `.named("Task Id")` before invoking the task. Use `.bind(...)` to pre-fill task arguments and `.with_annotations({...})` to add per-task annotations.
351
+
352
+ ##### Task images, dependencies, and image IDs
353
+
354
+ Use `@task(image="...")` to write the component image directly. Use `dependencies_from="pyproject.toml"` when generated components need to install Python dependencies. Several tasks can share one authoring-only `TaskEnv`:
355
+
356
+ ```python
357
+ from tangle_cli.python_pipeline import TaskEnv, task
358
+
359
+ EVAL = TaskEnv(image="python:3.12", dependencies_from="pyproject.toml")
360
+
361
+ @task(env=EVAL)
362
+ def score(...):
363
+ ...
364
+ ```
365
+
366
+ Use `@task(image_id="eval-slim")` when source should carry a logical image name instead of a concrete registry reference. Downstream code can register defaults with `register_image_id(...)`, and callers can override at compile time with repeatable `--image ID=REF`:
367
+
368
+ ```bash
369
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml \
370
+ --image eval-slim=registry.example/eval-slim@sha256:...
371
+ ```
372
+
373
+ An explicit `image="..."` wins over `image_id=...`. If an `image_id` has neither a registered default nor a `--image` override, compile fails.
374
+
375
+ ##### Subpipelines and existing components
376
+
377
+ Use `subpipeline(child_pipeline)(...)` to call another Python `@pipeline` as one task in a parent graph. The child compiles to a subgraph sidecar under `<output>.subgraphs/`, and the returned handle exposes the child pipeline's declared outputs.
378
+
379
+ ```python
380
+ from tangle_cli.python_pipeline import In, Out, pipeline, subpipeline
381
+
382
+ @pipeline("Child")
383
+ def child(seed: In[str]) -> Out[str]:
384
+ ...
385
+
386
+ @pipeline("Parent")
387
+ def parent(seed: In[str]) -> Out[str]:
388
+ child_result = subpipeline(child).named("Run child")(seed=seed)
389
+ return child_result.wait_for_output
390
+ ```
391
+
392
+ Use `ref(url=...)`, `ref(name=...)`, or `ref(digest=...)` to call an existing component YAML or published component instead of authoring a local `@task`. Use `@registered(fragment=..., gen_config=...)` for operation wrappers that are already present in an existing `gen_config.yaml`; the compiler rewrites those calls to `resolve://...#fragment` without generating a new sidecar.
393
+
394
+ ##### Dynamic arguments and runtime placeholders
395
+
396
+ Task argument values can be literals, graph inputs, task outputs, or supported dynamic data. Use `dynamic_secret("NAME")` to emit a runtime secret reference:
397
+
398
+ ```python
399
+ from tangle_cli.python_pipeline import dynamic_secret
400
+
401
+ call_api(api_key=dynamic_secret("OPENAI_API_KEY"))
402
+ ```
403
+
404
+ Use `raw("...")` only for string values that intentionally contain a `{{name}}` runtime placeholder substituted by the component itself. `raw()` is not a compile-time Jinja escape hatch; `{% ... %}` and `{# ... #}` are rejected.
405
+
406
+ ##### Unwrapped dict task inputs
407
+
408
+ Python-authored pipelines can mark one or more `dict[str, T]` task parameters for unwrapping:
409
+
410
+ ```python
411
+ from cloud_pipelines import components
412
+ from tangle_cli.python_pipeline import Out, pipeline, task
413
+
414
+
415
+ @task(image="python:3.12")
416
+ def make_greeting(name: str) -> str:
417
+ return f"hello {name}"
418
+
419
+
420
+ @task(image="python:3.12", unwrap="items")
421
+ def join_greetings(out: components.OutputPath("Text"), items: dict[str, str], prefix: str = "joined"):
422
+ with open(out, "w") as fh:
423
+ fh.write(f"{prefix}: " + " | ".join(items[key] for key in sorted(items)))
424
+
425
+
426
+ @pipeline("Greeting pipeline")
427
+ def greeting_pipeline() -> Out[str]:
428
+ world = make_greeting.named("world_source")(name="world")
429
+ tangle = make_greeting.named("tangle_source")(name="tangle")
430
+ joined = join_greetings.named("join")(
431
+ prefix="demo",
432
+ items={
433
+ "who_1": world.Output,
434
+ "who_2": tangle.Output,
435
+ "literal": "plain value",
436
+ },
437
+ )
438
+ return joined.out
439
+ ```
440
+
441
+ `unwrap="items"` tells the compiler that the caller-provided entries in `items` should become explicit component inputs. The call above compiles the consumer task arguments as `items__who_1`, `items__who_2`, and `items__literal`; task-output values remain normal graph edges and literal values remain literals. The generated components sidecar persists the exact flattened schema under `local_from_python.unwrapped_inputs`, including the generated input names and inferred value type. Hydrate passes that schema back into Python component generation so the regenerated component has the same flattened inputs even though hydrate no longer has access to the original Python call-site dict. The generated runtime wrapper then re-wraps those CLI arguments back into the original `items` dict before calling `join_greetings(...)`.
442
+
443
+ Use `unwrap=["items", "metadata"]` to unwrap multiple dict parameters. The caller owns the key names; keys may contain letters, numbers, `_`, and `-`, and become `param__<key>` component inputs. Empty dicts are rejected because they do not define a component interface. If a generated name would collide with a fixed parameter or another generated name, compile fails before writing artifacts. Equivalent key sets are canonicalized for schema hashing, so two call sites with the same keys in different insertion orders dedupe to the same component fragment.
444
+
445
+ #### Pipeline run submission and validation
321
446
 
322
447
  `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.
323
448
 
@@ -291,7 +291,132 @@ 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.
294
+ #### Python pipeline authoring
295
+
296
+ Python-authored pipelines live in normal `.py` files and compile with:
297
+
298
+ ```bash
299
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
300
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
301
+ ```
302
+
303
+ 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.
304
+
305
+ ```python
306
+ from cloud_pipelines import components
307
+ from tangle_cli.python_pipeline import In, Out, pipeline, task
308
+
309
+
310
+ @task(image="python:3.12")
311
+ def write_greeting(out: components.OutputPath("Text"), who: str, greeting: str = "hello"):
312
+ with open(out, "w") as fh:
313
+ fh.write(f"{greeting} {who}")
314
+
315
+
316
+ @pipeline("Greeting pipeline", output_name="greeting_file")
317
+ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
318
+ greeting = write_greeting(who=who, greeting=cfg.greeting)
319
+ return greeting.out
320
+ ```
321
+
322
+ `In[T]` parameters become runtime graph inputs. A single `-> Out[T]` return exposes one graph output; use `@pipeline(output_name=...)` to name that output. For multiple outputs, define a frozen dataclass subclass of `Outputs` with `Out[T]` fields and return an instance. A pipeline that accepts a `cfg` parameter reads `config.yaml` (or the path passed via `@pipeline(config="...")`) at compile time, with `--override key=value` values overlaid by the compile command.
323
+
324
+ Task IDs default from the left-hand variable name at the call site, converted to title case. If there is no simple left-hand variable, or if you want a stable explicit label, call `.named("Task Id")` before invoking the task. Use `.bind(...)` to pre-fill task arguments and `.with_annotations({...})` to add per-task annotations.
325
+
326
+ ##### Task images, dependencies, and image IDs
327
+
328
+ Use `@task(image="...")` to write the component image directly. Use `dependencies_from="pyproject.toml"` when generated components need to install Python dependencies. Several tasks can share one authoring-only `TaskEnv`:
329
+
330
+ ```python
331
+ from tangle_cli.python_pipeline import TaskEnv, task
332
+
333
+ EVAL = TaskEnv(image="python:3.12", dependencies_from="pyproject.toml")
334
+
335
+ @task(env=EVAL)
336
+ def score(...):
337
+ ...
338
+ ```
339
+
340
+ Use `@task(image_id="eval-slim")` when source should carry a logical image name instead of a concrete registry reference. Downstream code can register defaults with `register_image_id(...)`, and callers can override at compile time with repeatable `--image ID=REF`:
341
+
342
+ ```bash
343
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml \
344
+ --image eval-slim=registry.example/eval-slim@sha256:...
345
+ ```
346
+
347
+ An explicit `image="..."` wins over `image_id=...`. If an `image_id` has neither a registered default nor a `--image` override, compile fails.
348
+
349
+ ##### Subpipelines and existing components
350
+
351
+ Use `subpipeline(child_pipeline)(...)` to call another Python `@pipeline` as one task in a parent graph. The child compiles to a subgraph sidecar under `<output>.subgraphs/`, and the returned handle exposes the child pipeline's declared outputs.
352
+
353
+ ```python
354
+ from tangle_cli.python_pipeline import In, Out, pipeline, subpipeline
355
+
356
+ @pipeline("Child")
357
+ def child(seed: In[str]) -> Out[str]:
358
+ ...
359
+
360
+ @pipeline("Parent")
361
+ def parent(seed: In[str]) -> Out[str]:
362
+ child_result = subpipeline(child).named("Run child")(seed=seed)
363
+ return child_result.wait_for_output
364
+ ```
365
+
366
+ Use `ref(url=...)`, `ref(name=...)`, or `ref(digest=...)` to call an existing component YAML or published component instead of authoring a local `@task`. Use `@registered(fragment=..., gen_config=...)` for operation wrappers that are already present in an existing `gen_config.yaml`; the compiler rewrites those calls to `resolve://...#fragment` without generating a new sidecar.
367
+
368
+ ##### Dynamic arguments and runtime placeholders
369
+
370
+ Task argument values can be literals, graph inputs, task outputs, or supported dynamic data. Use `dynamic_secret("NAME")` to emit a runtime secret reference:
371
+
372
+ ```python
373
+ from tangle_cli.python_pipeline import dynamic_secret
374
+
375
+ call_api(api_key=dynamic_secret("OPENAI_API_KEY"))
376
+ ```
377
+
378
+ Use `raw("...")` only for string values that intentionally contain a `{{name}}` runtime placeholder substituted by the component itself. `raw()` is not a compile-time Jinja escape hatch; `{% ... %}` and `{# ... #}` are rejected.
379
+
380
+ ##### Unwrapped dict task inputs
381
+
382
+ Python-authored pipelines can mark one or more `dict[str, T]` task parameters for unwrapping:
383
+
384
+ ```python
385
+ from cloud_pipelines import components
386
+ from tangle_cli.python_pipeline import Out, pipeline, task
387
+
388
+
389
+ @task(image="python:3.12")
390
+ def make_greeting(name: str) -> str:
391
+ return f"hello {name}"
392
+
393
+
394
+ @task(image="python:3.12", unwrap="items")
395
+ def join_greetings(out: components.OutputPath("Text"), items: dict[str, str], prefix: str = "joined"):
396
+ with open(out, "w") as fh:
397
+ fh.write(f"{prefix}: " + " | ".join(items[key] for key in sorted(items)))
398
+
399
+
400
+ @pipeline("Greeting pipeline")
401
+ def greeting_pipeline() -> Out[str]:
402
+ world = make_greeting.named("world_source")(name="world")
403
+ tangle = make_greeting.named("tangle_source")(name="tangle")
404
+ joined = join_greetings.named("join")(
405
+ prefix="demo",
406
+ items={
407
+ "who_1": world.Output,
408
+ "who_2": tangle.Output,
409
+ "literal": "plain value",
410
+ },
411
+ )
412
+ return joined.out
413
+ ```
414
+
415
+ `unwrap="items"` tells the compiler that the caller-provided entries in `items` should become explicit component inputs. The call above compiles the consumer task arguments as `items__who_1`, `items__who_2`, and `items__literal`; task-output values remain normal graph edges and literal values remain literals. The generated components sidecar persists the exact flattened schema under `local_from_python.unwrapped_inputs`, including the generated input names and inferred value type. Hydrate passes that schema back into Python component generation so the regenerated component has the same flattened inputs even though hydrate no longer has access to the original Python call-site dict. The generated runtime wrapper then re-wraps those CLI arguments back into the original `items` dict before calling `join_greetings(...)`.
416
+
417
+ Use `unwrap=["items", "metadata"]` to unwrap multiple dict parameters. The caller owns the key names; keys may contain letters, numbers, `_`, and `-`, and become `param__<key>` component inputs. Empty dicts are rejected because they do not define a component interface. If a generated name would collide with a fixed parameter or another generated name, compile fails before writing artifacts. Equivalent key sets are canonicalized for schema hashing, so two call sites with the same keys in different insertion orders dedupe to the same component fragment.
418
+
419
+ #### Pipeline run submission and validation
295
420
 
296
421
  `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.
297
422
 
@@ -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.6"
17
+ __version__ = "0.1.7"
18
18
 
19
19
  __all__ = ["TangleDynamicDiscoveryClient", "__version__"]