tangle-cli 0.1.6__tar.gz → 0.1.9__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 (103) hide show
  1. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/PKG-INFO +217 -2
  2. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/README.md +216 -1
  3. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
  4. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_cli.py +14 -2
  5. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_schema.py +8 -0
  6. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_transport.py +126 -0
  7. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +1 -0
  8. tangle_cli-0.1.9/packages/tangle-cli/src/tangle_cli/cli.py +118 -0
  9. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/cli_options.py +24 -0
  10. tangle_cli-0.1.9/packages/tangle-cli/src/tangle_cli/client.py +1153 -0
  11. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_from_func.py +325 -4
  12. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_generator.py +93 -4
  13. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +15 -0
  14. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +103 -17
  15. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +29 -1
  16. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +576 -27
  17. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +134 -4
  18. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +28 -0
  19. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipelines.py +5 -0
  20. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +1 -0
  21. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +8 -1
  22. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +36 -3
  23. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +17 -3
  24. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +150 -13
  25. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +18 -1
  26. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +36 -0
  27. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schema_validation.py +18 -0
  28. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +10 -1
  29. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -6
  30. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +5 -2
  31. tangle_cli-0.1.9/pyproject.toml +88 -0
  32. tangle_cli-0.1.6/pyproject.toml → tangle_cli-0.1.9/pyproject.toml.orig +1 -1
  33. tangle_cli-0.1.6/packages/tangle-cli/src/tangle_cli/cli.py +0 -57
  34. tangle_cli-0.1.6/packages/tangle-cli/src/tangle_cli/client.py +0 -684
  35. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
  36. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
  37. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
  38. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
  39. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
  40. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
  41. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
  42. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
  43. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
  44. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
  45. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
  46. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
  47. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
  48. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
  49. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
  50. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
  51. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
  52. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
  53. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
  54. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
  55. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
  56. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
  57. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
  58. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
  59. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
  60. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
  61. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
  62. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
  63. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
  64. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
  65. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
  66. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
  67. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
  68. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
  69. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
  70. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
  71. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
  72. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
  73. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
  74. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
  75. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/OSS-CONVENTIONS.md +0 -0
  76. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/PORT-README.md +0 -0
  77. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/SKILL.md +0 -0
  78. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/auth-wizard.md +0 -0
  79. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/builder.md +0 -0
  80. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/debugger.md +0 -0
  81. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/reporter.md +0 -0
  82. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/researcher.md +0 -0
  83. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/reviewer.md +0 -0
  84. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/agents/scenario-builder.md +0 -0
  85. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/data-sources.md +0 -0
  86. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/event-log.md +0 -0
  87. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
  88. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
  89. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
  90. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/iterating-on-runs.md +0 -0
  91. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/knowledge-corpus.md +0 -0
  92. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/secrets.md +0 -0
  93. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/setup.md +0 -0
  94. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-0-initialize.md +0 -0
  95. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-1-analyze.md +0 -0
  96. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-2-hypothesize.md +0 -0
  97. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-3-submit.md +0 -0
  98. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-4-monitor.md +0 -0
  99. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-5-evaluate.md +0 -0
  100. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-6-synthesize.md +0 -0
  101. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/step-7-decide.md +0 -0
  102. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/skills/tangent/references/tangle-tools.md +0 -0
  103. {tangle_cli-0.1.6 → tangle_cli-0.1.9}/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.9
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>
@@ -86,6 +86,57 @@ API-backed commands commonly accept these options. Explicit CLI options win over
86
86
  | `--config` | YAML/JSON defaults. Many commands accept a single object, a list of objects, or `_defaults` + `configs`. |
87
87
  | `--log-type` | SDK progress logs: `console`, `none`, or `file`. Logs go to stderr or a temp log file so structured stdout stays parseable. |
88
88
  | `TANGLE_VERBOSE=1` | Redacted HTTP request/response diagnostics only. This is separate from normal progress logging. |
89
+ | `--ca-bundle` | Global CLI flag: path to a PEM CA bundle used as the TLS trust store for every transport. Overrides `TANGLE_API_CA_BUNDLE`. Place before the subcommand. |
90
+ | `--verify-tls` / `--no-verify-tls` | Global CLI flag: enable or disable TLS verification for every transport. Overrides `TANGLE_API_VERIFY_TLS`. `--no-verify-tls` is local-development only. Place before the subcommand. |
91
+ | `TANGLE_API_CA_BUNDLE` | Path to a PEM CA bundle used to verify TLS for every transport. Use this to trust a private or corporate CA without disabling verification. |
92
+ | `TANGLE_API_VERIFY_TLS` | TLS verification toggle. Values `0`, `false`, or `no` (case/space-insensitive) disable verification; any other nonempty value keeps it on. |
93
+
94
+ ### TLS verification
95
+
96
+ TLS certificate verification is enabled by default for all HTTP transports (schema
97
+ fetches, `tangle api` calls, and the programmatic clients). The effective setting is
98
+ resolved with the following precedence, highest to lowest:
99
+
100
+ 1. An explicit `verify=` argument to the Python clients (a `bool` or a path to a CA bundle).
101
+ 2. The global CLI flags `--ca-bundle` / `--verify-tls` / `--no-verify-tls`.
102
+ 3. `TANGLE_API_CA_BUNDLE` — verify against the given CA bundle.
103
+ 4. `TANGLE_API_VERIFY_TLS` — enable or disable verification.
104
+ 5. The secure default: verification enabled against the system trust store.
105
+
106
+ The global CLI flags are true root options that apply to every command — the static
107
+ `tangle sdk ...` clients, the dynamic `tangle api ...` commands, and `tangle api refresh`.
108
+ Place them **before** the subcommand, for example `tangle --ca-bundle ca.pem api ...` or
109
+ `tangle --no-verify-tls sdk ...`. They are honored even by the dynamic OpenAPI schema
110
+ discovery that runs before command dispatch. A defaulted (absent) flag does not override
111
+ the environment: when a flag is not supplied, the `TANGLE_API_*` variables and the standard
112
+ `REQUESTS_CA_BUNDLE` / `CURL_CA_BUNDLE` handling still apply. `--ca-bundle` combined with an
113
+ explicit `--no-verify-tls` is contradictory and fails fast before any request; `--ca-bundle`
114
+ with `--verify-tls` is redundant but accepted.
115
+
116
+ If both env vars are set, `TANGLE_API_CA_BUNDLE` wins and TLS stays verified against the
117
+ bundle. Empty values are treated as unset. A `--ca-bundle` or `TANGLE_API_CA_BUNDLE` that
118
+ does not point to an existing file fails fast with an actionable error before any request is
119
+ made. When no Tangle-specific setting is provided, the standard `REQUESTS_CA_BUNDLE` /
120
+ `CURL_CA_BUNDLE` handling and any caller-supplied `requests.Session.verify` are left
121
+ untouched.
122
+
123
+ For a private CA, prefer `--ca-bundle` / `TANGLE_API_CA_BUNDLE` over disabling verification:
124
+ it keeps certificates verified against a trusted root. `--no-verify-tls` /
125
+ `TANGLE_API_VERIFY_TLS=0` disables verification entirely and is intended for local
126
+ development only — never use it against production endpoints.
127
+
128
+ ```bash
129
+ # Trust a private CA with the global flag (recommended for internal/self-hosted APIs)
130
+ uv run tangle --ca-bundle /etc/ssl/private-ca.pem \
131
+ api refresh --base-url https://internal.example
132
+
133
+ # Or via environment variable
134
+ TANGLE_API_CA_BUNDLE=/etc/ssl/private-ca.pem \
135
+ uv run tangle api refresh --base-url https://internal.example
136
+
137
+ # Disable verification (local development only)
138
+ uv run tangle --no-verify-tls api refresh --base-url https://localhost:8443
139
+ ```
89
140
 
90
141
  Examples for protected APIs:
91
142
 
@@ -317,7 +368,171 @@ uv run tangle sdk pipeline-runs annotations set RUN_ID key value
317
368
  uv run tangle sdk pipeline-runs export RUN_ID --output pipeline.yaml
318
369
  ```
319
370
 
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.
371
+ #### Python pipeline authoring
372
+
373
+ Python-authored pipelines live in normal `.py` files and compile with:
374
+
375
+ ```bash
376
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
377
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
378
+ ```
379
+
380
+ 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.
381
+
382
+ ```python
383
+ from cloud_pipelines import components
384
+ from tangle_cli.python_pipeline import In, Out, pipeline, task
385
+
386
+
387
+ @task(image="python:3.12")
388
+ def write_greeting(out: components.OutputPath("Text"), who: str, greeting: str = "hello"):
389
+ with open(out, "w") as fh:
390
+ fh.write(f"{greeting} {who}")
391
+
392
+
393
+ @pipeline("Greeting pipeline", output_name="greeting_file")
394
+ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
395
+ greeting = write_greeting(who=who, greeting=cfg.greeting)
396
+ return greeting.out
397
+ ```
398
+
399
+ `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.
400
+
401
+ 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.
402
+
403
+ ##### Conditional task execution
404
+
405
+ Pipeline inputs used as conditions are ordinary `In[str]` values; there is no special conditional input annotation. Pass the value through the reserved task-call metadata keyword `is_enabled=`:
406
+
407
+ ```python
408
+ @pipeline("Conditional greeting")
409
+ def conditional_greeting(enabled: In[str]) -> Out[str]:
410
+ greeting = write_greeting(who="world", is_enabled=enabled)
411
+ return greeting.out
412
+ ```
413
+
414
+ This emits the canonical task field rather than a component argument:
415
+
416
+ ```yaml
417
+ isEnabled:
418
+ graphInput:
419
+ inputName: enabled
420
+ ```
421
+
422
+ `is_enabled=` supports Python booleans (serialized as lowercase `"true"` / `"false"` strings), string constants, `In[...]` graph inputs, and previous task outputs such as `is_enabled=gate.Output`. It is container-component task metadata; component function parameters are not implicitly conditions.
423
+
424
+ If a component itself declares an input named `is_enabled`, bind that component argument separately while using the call-site keyword for task metadata:
425
+
426
+ ```python
427
+ @task(image="python:3.12")
428
+ def work(is_enabled: str, message: str) -> str:
429
+ return message
430
+
431
+ @pipeline("Input-name collision")
432
+ def collision(runtime_condition: In[str]) -> Out[str]:
433
+ result = work.bind(is_enabled="component-input-value")(
434
+ message="hello",
435
+ is_enabled=runtime_condition,
436
+ )
437
+ return result.Output
438
+ ```
439
+
440
+ The bound value remains under `arguments.is_enabled`; the call-site value emits as `isEnabled`. Tangle does not evaluate conditions on graph-component tasks, so `subpipeline(...)(is_enabled=...)` is rejected with guidance to condition tasks inside the child pipeline. A child graph input with that name remains available through `subpipeline(...).bind(is_enabled=...)(...)`. There is no `condition` alias.
441
+
442
+ ##### Task images, dependencies, and image IDs
443
+
444
+ 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`:
445
+
446
+ ```python
447
+ from tangle_cli.python_pipeline import TaskEnv, task
448
+
449
+ EVAL = TaskEnv(image="python:3.12", dependencies_from="pyproject.toml")
450
+
451
+ @task(env=EVAL)
452
+ def score(...):
453
+ ...
454
+ ```
455
+
456
+ 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`:
457
+
458
+ ```bash
459
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml \
460
+ --image eval-slim=registry.example/eval-slim@sha256:...
461
+ ```
462
+
463
+ An explicit `image="..."` wins over `image_id=...`. If an `image_id` has neither a registered default nor a `--image` override, compile fails.
464
+
465
+ ##### Subpipelines and existing components
466
+
467
+ 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.
468
+
469
+ ```python
470
+ from tangle_cli.python_pipeline import In, Out, pipeline, subpipeline
471
+
472
+ @pipeline("Child")
473
+ def child(seed: In[str]) -> Out[str]:
474
+ ...
475
+
476
+ @pipeline("Parent")
477
+ def parent(seed: In[str]) -> Out[str]:
478
+ child_result = subpipeline(child).named("Run child")(seed=seed)
479
+ return child_result.wait_for_output
480
+ ```
481
+
482
+ 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.
483
+
484
+ ##### Dynamic arguments and runtime placeholders
485
+
486
+ Task argument values can be literals, graph inputs, task outputs, or supported dynamic data. Use `dynamic_secret("NAME")` to emit a runtime secret reference:
487
+
488
+ ```python
489
+ from tangle_cli.python_pipeline import dynamic_secret
490
+
491
+ call_api(api_key=dynamic_secret("OPENAI_API_KEY"))
492
+ ```
493
+
494
+ 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.
495
+
496
+ ##### Unwrapped dict task inputs
497
+
498
+ Python-authored pipelines can mark one or more `dict[str, T]` task parameters for unwrapping:
499
+
500
+ ```python
501
+ from cloud_pipelines import components
502
+ from tangle_cli.python_pipeline import Out, pipeline, task
503
+
504
+
505
+ @task(image="python:3.12")
506
+ def make_greeting(name: str) -> str:
507
+ return f"hello {name}"
508
+
509
+
510
+ @task(image="python:3.12", unwrap="items")
511
+ def join_greetings(out: components.OutputPath("Text"), items: dict[str, str], prefix: str = "joined"):
512
+ with open(out, "w") as fh:
513
+ fh.write(f"{prefix}: " + " | ".join(items[key] for key in sorted(items)))
514
+
515
+
516
+ @pipeline("Greeting pipeline")
517
+ def greeting_pipeline() -> Out[str]:
518
+ world = make_greeting.named("world_source")(name="world")
519
+ tangle = make_greeting.named("tangle_source")(name="tangle")
520
+ joined = join_greetings.named("join")(
521
+ prefix="demo",
522
+ items={
523
+ "who_1": world.Output,
524
+ "who_2": tangle.Output,
525
+ "literal": "plain value",
526
+ },
527
+ )
528
+ return joined.out
529
+ ```
530
+
531
+ `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(...)`.
532
+
533
+ 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.
534
+
535
+ #### Pipeline run submission and validation
321
536
 
322
537
  `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
538
 
@@ -60,6 +60,57 @@ API-backed commands commonly accept these options. Explicit CLI options win over
60
60
  | `--config` | YAML/JSON defaults. Many commands accept a single object, a list of objects, or `_defaults` + `configs`. |
61
61
  | `--log-type` | SDK progress logs: `console`, `none`, or `file`. Logs go to stderr or a temp log file so structured stdout stays parseable. |
62
62
  | `TANGLE_VERBOSE=1` | Redacted HTTP request/response diagnostics only. This is separate from normal progress logging. |
63
+ | `--ca-bundle` | Global CLI flag: path to a PEM CA bundle used as the TLS trust store for every transport. Overrides `TANGLE_API_CA_BUNDLE`. Place before the subcommand. |
64
+ | `--verify-tls` / `--no-verify-tls` | Global CLI flag: enable or disable TLS verification for every transport. Overrides `TANGLE_API_VERIFY_TLS`. `--no-verify-tls` is local-development only. Place before the subcommand. |
65
+ | `TANGLE_API_CA_BUNDLE` | Path to a PEM CA bundle used to verify TLS for every transport. Use this to trust a private or corporate CA without disabling verification. |
66
+ | `TANGLE_API_VERIFY_TLS` | TLS verification toggle. Values `0`, `false`, or `no` (case/space-insensitive) disable verification; any other nonempty value keeps it on. |
67
+
68
+ ### TLS verification
69
+
70
+ TLS certificate verification is enabled by default for all HTTP transports (schema
71
+ fetches, `tangle api` calls, and the programmatic clients). The effective setting is
72
+ resolved with the following precedence, highest to lowest:
73
+
74
+ 1. An explicit `verify=` argument to the Python clients (a `bool` or a path to a CA bundle).
75
+ 2. The global CLI flags `--ca-bundle` / `--verify-tls` / `--no-verify-tls`.
76
+ 3. `TANGLE_API_CA_BUNDLE` — verify against the given CA bundle.
77
+ 4. `TANGLE_API_VERIFY_TLS` — enable or disable verification.
78
+ 5. The secure default: verification enabled against the system trust store.
79
+
80
+ The global CLI flags are true root options that apply to every command — the static
81
+ `tangle sdk ...` clients, the dynamic `tangle api ...` commands, and `tangle api refresh`.
82
+ Place them **before** the subcommand, for example `tangle --ca-bundle ca.pem api ...` or
83
+ `tangle --no-verify-tls sdk ...`. They are honored even by the dynamic OpenAPI schema
84
+ discovery that runs before command dispatch. A defaulted (absent) flag does not override
85
+ the environment: when a flag is not supplied, the `TANGLE_API_*` variables and the standard
86
+ `REQUESTS_CA_BUNDLE` / `CURL_CA_BUNDLE` handling still apply. `--ca-bundle` combined with an
87
+ explicit `--no-verify-tls` is contradictory and fails fast before any request; `--ca-bundle`
88
+ with `--verify-tls` is redundant but accepted.
89
+
90
+ If both env vars are set, `TANGLE_API_CA_BUNDLE` wins and TLS stays verified against the
91
+ bundle. Empty values are treated as unset. A `--ca-bundle` or `TANGLE_API_CA_BUNDLE` that
92
+ does not point to an existing file fails fast with an actionable error before any request is
93
+ made. When no Tangle-specific setting is provided, the standard `REQUESTS_CA_BUNDLE` /
94
+ `CURL_CA_BUNDLE` handling and any caller-supplied `requests.Session.verify` are left
95
+ untouched.
96
+
97
+ For a private CA, prefer `--ca-bundle` / `TANGLE_API_CA_BUNDLE` over disabling verification:
98
+ it keeps certificates verified against a trusted root. `--no-verify-tls` /
99
+ `TANGLE_API_VERIFY_TLS=0` disables verification entirely and is intended for local
100
+ development only — never use it against production endpoints.
101
+
102
+ ```bash
103
+ # Trust a private CA with the global flag (recommended for internal/self-hosted APIs)
104
+ uv run tangle --ca-bundle /etc/ssl/private-ca.pem \
105
+ api refresh --base-url https://internal.example
106
+
107
+ # Or via environment variable
108
+ TANGLE_API_CA_BUNDLE=/etc/ssl/private-ca.pem \
109
+ uv run tangle api refresh --base-url https://internal.example
110
+
111
+ # Disable verification (local development only)
112
+ uv run tangle --no-verify-tls api refresh --base-url https://localhost:8443
113
+ ```
63
114
 
64
115
  Examples for protected APIs:
65
116
 
@@ -291,7 +342,171 @@ uv run tangle sdk pipeline-runs annotations set RUN_ID key value
291
342
  uv run tangle sdk pipeline-runs export RUN_ID --output pipeline.yaml
292
343
  ```
293
344
 
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.
345
+ #### Python pipeline authoring
346
+
347
+ Python-authored pipelines live in normal `.py` files and compile with:
348
+
349
+ ```bash
350
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml
351
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml --pipeline pipeline_fn_name
352
+ ```
353
+
354
+ 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.
355
+
356
+ ```python
357
+ from cloud_pipelines import components
358
+ from tangle_cli.python_pipeline import In, Out, pipeline, task
359
+
360
+
361
+ @task(image="python:3.12")
362
+ def write_greeting(out: components.OutputPath("Text"), who: str, greeting: str = "hello"):
363
+ with open(out, "w") as fh:
364
+ fh.write(f"{greeting} {who}")
365
+
366
+
367
+ @pipeline("Greeting pipeline", output_name="greeting_file")
368
+ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
369
+ greeting = write_greeting(who=who, greeting=cfg.greeting)
370
+ return greeting.out
371
+ ```
372
+
373
+ `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.
374
+
375
+ 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.
376
+
377
+ ##### Conditional task execution
378
+
379
+ Pipeline inputs used as conditions are ordinary `In[str]` values; there is no special conditional input annotation. Pass the value through the reserved task-call metadata keyword `is_enabled=`:
380
+
381
+ ```python
382
+ @pipeline("Conditional greeting")
383
+ def conditional_greeting(enabled: In[str]) -> Out[str]:
384
+ greeting = write_greeting(who="world", is_enabled=enabled)
385
+ return greeting.out
386
+ ```
387
+
388
+ This emits the canonical task field rather than a component argument:
389
+
390
+ ```yaml
391
+ isEnabled:
392
+ graphInput:
393
+ inputName: enabled
394
+ ```
395
+
396
+ `is_enabled=` supports Python booleans (serialized as lowercase `"true"` / `"false"` strings), string constants, `In[...]` graph inputs, and previous task outputs such as `is_enabled=gate.Output`. It is container-component task metadata; component function parameters are not implicitly conditions.
397
+
398
+ If a component itself declares an input named `is_enabled`, bind that component argument separately while using the call-site keyword for task metadata:
399
+
400
+ ```python
401
+ @task(image="python:3.12")
402
+ def work(is_enabled: str, message: str) -> str:
403
+ return message
404
+
405
+ @pipeline("Input-name collision")
406
+ def collision(runtime_condition: In[str]) -> Out[str]:
407
+ result = work.bind(is_enabled="component-input-value")(
408
+ message="hello",
409
+ is_enabled=runtime_condition,
410
+ )
411
+ return result.Output
412
+ ```
413
+
414
+ The bound value remains under `arguments.is_enabled`; the call-site value emits as `isEnabled`. Tangle does not evaluate conditions on graph-component tasks, so `subpipeline(...)(is_enabled=...)` is rejected with guidance to condition tasks inside the child pipeline. A child graph input with that name remains available through `subpipeline(...).bind(is_enabled=...)(...)`. There is no `condition` alias.
415
+
416
+ ##### Task images, dependencies, and image IDs
417
+
418
+ 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`:
419
+
420
+ ```python
421
+ from tangle_cli.python_pipeline import TaskEnv, task
422
+
423
+ EVAL = TaskEnv(image="python:3.12", dependencies_from="pyproject.toml")
424
+
425
+ @task(env=EVAL)
426
+ def score(...):
427
+ ...
428
+ ```
429
+
430
+ 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`:
431
+
432
+ ```bash
433
+ uv run tangle sdk pipelines compile pipeline.py -o pipeline.yaml \
434
+ --image eval-slim=registry.example/eval-slim@sha256:...
435
+ ```
436
+
437
+ An explicit `image="..."` wins over `image_id=...`. If an `image_id` has neither a registered default nor a `--image` override, compile fails.
438
+
439
+ ##### Subpipelines and existing components
440
+
441
+ 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.
442
+
443
+ ```python
444
+ from tangle_cli.python_pipeline import In, Out, pipeline, subpipeline
445
+
446
+ @pipeline("Child")
447
+ def child(seed: In[str]) -> Out[str]:
448
+ ...
449
+
450
+ @pipeline("Parent")
451
+ def parent(seed: In[str]) -> Out[str]:
452
+ child_result = subpipeline(child).named("Run child")(seed=seed)
453
+ return child_result.wait_for_output
454
+ ```
455
+
456
+ 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.
457
+
458
+ ##### Dynamic arguments and runtime placeholders
459
+
460
+ Task argument values can be literals, graph inputs, task outputs, or supported dynamic data. Use `dynamic_secret("NAME")` to emit a runtime secret reference:
461
+
462
+ ```python
463
+ from tangle_cli.python_pipeline import dynamic_secret
464
+
465
+ call_api(api_key=dynamic_secret("OPENAI_API_KEY"))
466
+ ```
467
+
468
+ 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.
469
+
470
+ ##### Unwrapped dict task inputs
471
+
472
+ Python-authored pipelines can mark one or more `dict[str, T]` task parameters for unwrapping:
473
+
474
+ ```python
475
+ from cloud_pipelines import components
476
+ from tangle_cli.python_pipeline import Out, pipeline, task
477
+
478
+
479
+ @task(image="python:3.12")
480
+ def make_greeting(name: str) -> str:
481
+ return f"hello {name}"
482
+
483
+
484
+ @task(image="python:3.12", unwrap="items")
485
+ def join_greetings(out: components.OutputPath("Text"), items: dict[str, str], prefix: str = "joined"):
486
+ with open(out, "w") as fh:
487
+ fh.write(f"{prefix}: " + " | ".join(items[key] for key in sorted(items)))
488
+
489
+
490
+ @pipeline("Greeting pipeline")
491
+ def greeting_pipeline() -> Out[str]:
492
+ world = make_greeting.named("world_source")(name="world")
493
+ tangle = make_greeting.named("tangle_source")(name="tangle")
494
+ joined = join_greetings.named("join")(
495
+ prefix="demo",
496
+ items={
497
+ "who_1": world.Output,
498
+ "who_2": tangle.Output,
499
+ "literal": "plain value",
500
+ },
501
+ )
502
+ return joined.out
503
+ ```
504
+
505
+ `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(...)`.
506
+
507
+ 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.
508
+
509
+ #### Pipeline run submission and validation
295
510
 
296
511
  `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
512
 
@@ -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.9"
18
18
 
19
19
  __all__ = ["TangleDynamicDiscoveryClient", "__version__"]
@@ -640,10 +640,22 @@ def _argv_dispatches_dynamic_command(argv: list[str]) -> bool:
640
640
 
641
641
 
642
642
  def _api_argv_tail(argv: list[str]) -> list[str] | None:
643
- """Return args after the root `api` command, or None for non-API invocations."""
643
+ """Return args after the root `api` command, or None for non-API invocations.
644
+
645
+ Global TLS flags (``--ca-bundle``/``--verify-tls``/``--no-verify-tls``) may
646
+ precede the subcommand, so they are skipped before locating `api`.
647
+ """
644
648
 
645
649
  args = list(argv[1:])
646
- for index, arg in enumerate(args):
650
+ index = 0
651
+ while index < len(args):
652
+ arg = args[index]
653
+ if arg == "--ca-bundle" and index + 1 < len(args):
654
+ index += 2
655
+ continue
656
+ if arg.startswith("--ca-bundle=") or arg in {"--verify-tls", "--no-verify-tls"}:
657
+ index += 1
658
+ continue
647
659
  if arg == "--":
648
660
  if index + 1 < len(args) and args[index + 1] == "api":
649
661
  return args[index + 2 :]
@@ -18,7 +18,9 @@ from .api_transport import (
18
18
  _normalize_base_url,
19
19
  _openapi_url,
20
20
  _request_headers,
21
+ _VERIFY_UNSET,
21
22
  default_base_url,
23
+ httpx_verify,
22
24
  )
23
25
 
24
26
  SUPPORTED_METHODS = {"get", "post", "put", "patch", "delete"}
@@ -122,6 +124,7 @@ def fetch_schema(
122
124
  auth_header: str | None = None,
123
125
  headers: dict[str, str] | None = None,
124
126
  include_env_credentials: bool = True,
127
+ verify: Any = _VERIFY_UNSET,
125
128
  ) -> dict[str, Any]:
126
129
  """Fetch ``/openapi.json``, applying bearer and custom auth headers."""
127
130
 
@@ -136,6 +139,7 @@ def fetch_schema(
136
139
  include_env_credentials=include_env_credentials,
137
140
  ),
138
141
  timeout=DEFAULT_TIMEOUT_SECONDS,
142
+ verify=httpx_verify(verify),
139
143
  )
140
144
  response.raise_for_status()
141
145
  payload = response.text
@@ -152,6 +156,7 @@ def refresh_schema(
152
156
  auth_header: str | None = None,
153
157
  headers: dict[str, str] | None = None,
154
158
  include_env_credentials: bool = True,
159
+ verify: Any = _VERIFY_UNSET,
155
160
  ) -> tuple[dict[str, Any], Path]:
156
161
  """Fetch and cache the latest schema for a backend."""
157
162
 
@@ -163,6 +168,7 @@ def refresh_schema(
163
168
  auth_header,
164
169
  headers,
165
170
  include_env_credentials=include_env_credentials,
171
+ verify=verify,
166
172
  )
167
173
  path = write_cached_schema(schema, base_url)
168
174
  return schema, path
@@ -175,6 +181,7 @@ def load_or_fetch_schema(
175
181
  auth_header: str | None = None,
176
182
  headers: dict[str, str] | None = None,
177
183
  include_env_credentials: bool = True,
184
+ verify: Any = _VERIFY_UNSET,
178
185
  ) -> dict[str, Any]:
179
186
  """Use a cached schema when available, otherwise fetch once and cache it."""
180
187
 
@@ -188,6 +195,7 @@ def load_or_fetch_schema(
188
195
  auth_header,
189
196
  headers,
190
197
  include_env_credentials=include_env_credentials,
198
+ verify=verify,
191
199
  )
192
200
  return schema
193
201