tangle-cli 0.1.7__tar.gz → 0.1.10__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.7 → tangle_cli-0.1.10}/PKG-INFO +141 -1
  2. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/README.md +140 -0
  3. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
  4. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/api_cli.py +14 -2
  5. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/api_schema.py +8 -0
  6. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/api_transport.py +126 -0
  7. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +1 -0
  8. tangle_cli-0.1.10/packages/tangle-cli/src/tangle_cli/cli.py +118 -0
  9. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/cli_options.py +24 -0
  10. tangle_cli-0.1.10/packages/tangle-cli/src/tangle_cli/client.py +1153 -0
  11. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +15 -0
  12. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +37 -15
  13. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +1 -0
  14. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +576 -27
  15. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +134 -4
  16. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +28 -0
  17. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipelines.py +5 -0
  18. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +1 -0
  19. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +8 -1
  20. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +212 -3
  21. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +22 -3
  22. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +37 -6
  23. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +34 -1
  24. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/schema_validation.py +18 -0
  25. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +14 -6
  26. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -6
  27. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +5 -2
  28. tangle_cli-0.1.10/pyproject.toml +88 -0
  29. tangle_cli-0.1.7/pyproject.toml → tangle_cli-0.1.10/pyproject.toml.orig +1 -1
  30. tangle_cli-0.1.7/packages/tangle-cli/src/tangle_cli/cli.py +0 -57
  31. tangle_cli-0.1.7/packages/tangle-cli/src/tangle_cli/client.py +0 -684
  32. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
  33. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
  34. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
  35. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/component_from_func.py +0 -0
  36. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
  37. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
  38. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
  39. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
  40. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
  41. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
  42. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
  43. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
  44. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
  45. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
  46. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
  47. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
  48. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +0 -0
  49. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
  50. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
  51. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
  52. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
  53. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
  54. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
  55. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
  56. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
  57. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
  58. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
  59. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
  60. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
  61. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
  62. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
  63. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
  64. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
  65. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
  66. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +0 -0
  67. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
  68. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
  69. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
  70. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
  71. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
  72. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
  73. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
  74. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
  75. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/OSS-CONVENTIONS.md +0 -0
  76. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/PORT-README.md +0 -0
  77. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/SKILL.md +0 -0
  78. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/auth-wizard.md +0 -0
  79. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/builder.md +0 -0
  80. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/debugger.md +0 -0
  81. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/reporter.md +0 -0
  82. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/researcher.md +0 -0
  83. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/reviewer.md +0 -0
  84. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/agents/scenario-builder.md +0 -0
  85. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/data-sources.md +0 -0
  86. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/event-log.md +0 -0
  87. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
  88. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
  89. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
  90. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/iterating-on-runs.md +0 -0
  91. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/knowledge-corpus.md +0 -0
  92. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/secrets.md +0 -0
  93. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/setup.md +0 -0
  94. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-0-initialize.md +0 -0
  95. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-1-analyze.md +0 -0
  96. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-2-hypothesize.md +0 -0
  97. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-3-submit.md +0 -0
  98. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-4-monitor.md +0 -0
  99. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-5-evaluate.md +0 -0
  100. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-6-synthesize.md +0 -0
  101. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/step-7-decide.md +0 -0
  102. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/skills/tangent/references/tangle-tools.md +0 -0
  103. {tangle_cli-0.1.7 → tangle_cli-0.1.10}/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.7
3
+ Version: 0.1.10
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
 
@@ -349,6 +400,95 @@ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
349
400
 
350
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.
351
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 execution options and caching
443
+
444
+ Tangle caches task results, so a task that reads state which changes between runs (a run's `createdBy`, wall-clock time, an external table that the graph does not depend on) must opt out of caching explicitly. Use the reserved task-call metadata keyword `max_cache_staleness=`:
445
+
446
+ ```python
447
+ @pipeline("Scheduled-run gate")
448
+ def scheduled_gate() -> Out[str]:
449
+ is_scheduled = read_runtime_state(
450
+ name="CLOUD_PIPELINES_PIPELINE_RUN_CREATED_BY",
451
+ max_cache_staleness="P0D",
452
+ )
453
+ return is_scheduled.Output
454
+ ```
455
+
456
+ This emits the canonical task field rather than a component argument:
457
+
458
+ ```yaml
459
+ executionOptions:
460
+ cachingStrategy:
461
+ maxCacheStaleness: P0D
462
+ ```
463
+
464
+ `P0D` means "never reuse a cached result"; any other ISO-8601 duration (`P7D`) caps how stale a reusable result may be. For the rest of `ExecutionOptionsSpec`, use the general `execution_options=` passthrough:
465
+
466
+ ```python
467
+ uploaded = flaky_upload(
468
+ payload=data.Output,
469
+ execution_options={"retryStrategy": {"maxRetries": 3}},
470
+ )
471
+ ```
472
+
473
+ Both keywords may be combined; `max_cache_staleness=` wins over a `cachingStrategy.maxCacheStaleness` supplied through `execution_options=`, and every other passthrough field is preserved. A mapping passed as `execution_options=` is never mutated, so one shared constant can be reused across tasks.
474
+
475
+ Tangle models exactly two execution-option groups today — `cachingStrategy.maxCacheStaleness` and `retryStrategy.maxRetries` (required whenever `retryStrategy` is present). Any other key is rejected at compile time: the backend ignores unmodeled keys silently, so accepting one would advertise a setting that never takes effect.
476
+
477
+ Execution options are STATIC compile-time settings, so `max_cache_staleness` takes an RFC3339 duration string and `retryStrategy.maxRetries` a non-negative integer. Graph inputs, task outputs, `dynamic_secret(...)`, and `raw(...)` values are rejected because the backend does not resolve them for `executionOptions`. Passing an empty `execution_options={}` is an error — omit the keyword instead.
478
+
479
+ If a component itself declares an input named `max_cache_staleness` or `execution_options`, bind that component argument separately while using the call-site keyword for task metadata:
480
+
481
+ ```python
482
+ result = work.bind(max_cache_staleness="component-input-value")(
483
+ message="hello",
484
+ max_cache_staleness="P0D",
485
+ )
486
+ ```
487
+
488
+ The bound value remains under `arguments.max_cache_staleness`; the call-site value emits as `executionOptions`. Tangle applies caching and retries to container-component tasks, so `subpipeline(...)(max_cache_staleness=...)` and `subpipeline(...)(execution_options=...)` are rejected with guidance to set them on tasks inside the child pipeline. A child graph input with either name remains available through `subpipeline(...).bind(...)`.
489
+
490
+ See `examples/python_pipeline/execution_options_pipeline.py` for a runnable example.
491
+
352
492
  ##### Task images, dependencies, and image IDs
353
493
 
354
494
  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`:
@@ -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
 
@@ -323,6 +374,95 @@ def greeting_pipeline(who: In[str], cfg) -> Out[str]:
323
374
 
324
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.
325
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 execution options and caching
417
+
418
+ Tangle caches task results, so a task that reads state which changes between runs (a run's `createdBy`, wall-clock time, an external table that the graph does not depend on) must opt out of caching explicitly. Use the reserved task-call metadata keyword `max_cache_staleness=`:
419
+
420
+ ```python
421
+ @pipeline("Scheduled-run gate")
422
+ def scheduled_gate() -> Out[str]:
423
+ is_scheduled = read_runtime_state(
424
+ name="CLOUD_PIPELINES_PIPELINE_RUN_CREATED_BY",
425
+ max_cache_staleness="P0D",
426
+ )
427
+ return is_scheduled.Output
428
+ ```
429
+
430
+ This emits the canonical task field rather than a component argument:
431
+
432
+ ```yaml
433
+ executionOptions:
434
+ cachingStrategy:
435
+ maxCacheStaleness: P0D
436
+ ```
437
+
438
+ `P0D` means "never reuse a cached result"; any other ISO-8601 duration (`P7D`) caps how stale a reusable result may be. For the rest of `ExecutionOptionsSpec`, use the general `execution_options=` passthrough:
439
+
440
+ ```python
441
+ uploaded = flaky_upload(
442
+ payload=data.Output,
443
+ execution_options={"retryStrategy": {"maxRetries": 3}},
444
+ )
445
+ ```
446
+
447
+ Both keywords may be combined; `max_cache_staleness=` wins over a `cachingStrategy.maxCacheStaleness` supplied through `execution_options=`, and every other passthrough field is preserved. A mapping passed as `execution_options=` is never mutated, so one shared constant can be reused across tasks.
448
+
449
+ Tangle models exactly two execution-option groups today — `cachingStrategy.maxCacheStaleness` and `retryStrategy.maxRetries` (required whenever `retryStrategy` is present). Any other key is rejected at compile time: the backend ignores unmodeled keys silently, so accepting one would advertise a setting that never takes effect.
450
+
451
+ Execution options are STATIC compile-time settings, so `max_cache_staleness` takes an RFC3339 duration string and `retryStrategy.maxRetries` a non-negative integer. Graph inputs, task outputs, `dynamic_secret(...)`, and `raw(...)` values are rejected because the backend does not resolve them for `executionOptions`. Passing an empty `execution_options={}` is an error — omit the keyword instead.
452
+
453
+ If a component itself declares an input named `max_cache_staleness` or `execution_options`, bind that component argument separately while using the call-site keyword for task metadata:
454
+
455
+ ```python
456
+ result = work.bind(max_cache_staleness="component-input-value")(
457
+ message="hello",
458
+ max_cache_staleness="P0D",
459
+ )
460
+ ```
461
+
462
+ The bound value remains under `arguments.max_cache_staleness`; the call-site value emits as `executionOptions`. Tangle applies caching and retries to container-component tasks, so `subpipeline(...)(max_cache_staleness=...)` and `subpipeline(...)(execution_options=...)` are rejected with guidance to set them on tasks inside the child pipeline. A child graph input with either name remains available through `subpipeline(...).bind(...)`.
463
+
464
+ See `examples/python_pipeline/execution_options_pipeline.py` for a runnable example.
465
+
326
466
  ##### Task images, dependencies, and image IDs
327
467
 
328
468
  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`:
@@ -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.7"
17
+ __version__ = "0.1.10"
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
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
+ import ssl
8
9
  import sys
9
10
  import urllib.parse
10
11
  from pathlib import Path
@@ -16,6 +17,20 @@ DEFAULT_API_URL = "http://localhost:8000"
16
17
  DEFAULT_TIMEOUT_SECONDS = 30.0
17
18
  _HEADER_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
18
19
  _MISSING = object()
20
+
21
+ # Canonical resolved TLS verification value: ``True``/``False`` or a CA bundle
22
+ # path. Both requests and httpx transports adapt this single contract.
23
+ VerifyValue = bool | str
24
+ VerifyArgument = bool | str | os.PathLike[str] | None
25
+ _VERIFY_UNSET = object()
26
+ _TLS_FALSE_VALUES = frozenset({"0", "false", "no"})
27
+
28
+ # Process-wide TLS override set from global CLI flags (``--ca-bundle`` /
29
+ # ``--verify-tls`` / ``--no-verify-tls``). It sits between an explicit
30
+ # ``verify=`` argument and the environment variables in :func:`resolve_verify`,
31
+ # so a single resolver serves every transport, including the schema discovery
32
+ # that runs before normal command dispatch.
33
+ _CLI_VERIFY_OVERRIDE: Any = _VERIFY_UNSET
19
34
  _SENSITIVE_HEADER_NAMES = {"authorization", "cloud-auth", "cookie", "x-api-key"}
20
35
  _SENSITIVE_KEY_RE = re.compile(
21
36
  r"(authorization|authentication|(^|[-_])auth($|[-_])|cloud[-_]?auth|cookie|x[-_]?api[-_]?key|token|secret|password|credential|pre[-_]?signed[-_]?url|signed[-_]?url)",
@@ -40,6 +55,115 @@ def tangle_verbose_enabled() -> bool:
40
55
  return value.strip().lower() in {"1", "true", "yes", "on"}
41
56
 
42
57
 
58
+ def _parse_verify_flag(raw: str) -> bool:
59
+ """Interpret ``TANGLE_API_VERIFY_TLS``.
60
+
61
+ Only the case/space-insensitive values ``0``, ``false``, and ``no`` disable
62
+ verification; any other nonempty value keeps it enabled.
63
+ """
64
+
65
+ return raw.strip().lower() not in _TLS_FALSE_VALUES
66
+
67
+
68
+ def _validate_ca_bundle(path: str, source: str) -> str:
69
+ """Return an existing CA bundle path or fail before any network request."""
70
+
71
+ candidate = Path(path).expanduser()
72
+ if not candidate.is_file():
73
+ raise SystemExit(
74
+ f"{source} points to a CA bundle that does not exist: {path!r}. "
75
+ "Provide a path to an existing PEM file, or unset it to use the "
76
+ "system trust store."
77
+ )
78
+ return str(candidate)
79
+
80
+
81
+ def _coerce_explicit_verify(value: bool | str | os.PathLike[str]) -> VerifyValue:
82
+ if isinstance(value, bool):
83
+ return value
84
+ if isinstance(value, (str, os.PathLike)):
85
+ return _validate_ca_bundle(os.fspath(value), "verify")
86
+ raise SystemExit("verify must be a bool or a path to a CA bundle file")
87
+
88
+
89
+ def configure_cli_verify(
90
+ ca_bundle: str | os.PathLike[str] | None = None,
91
+ verify_tls: bool | None = None,
92
+ ) -> None:
93
+ """Install (or clear) the process-wide TLS override from global CLI flags.
94
+
95
+ ``ca_bundle`` is a path to a PEM trust store; ``verify_tls`` is the tri-state
96
+ ``--verify-tls`` / ``--no-verify-tls`` flag where ``None`` means the flag was
97
+ not supplied. A ``--ca-bundle`` combined with an explicit
98
+ ``--no-verify-tls`` is contradictory and fails fast before any network
99
+ request. Passing neither clears any previously installed override.
100
+
101
+ The resolved value is validated here so an invalid or missing CA bundle
102
+ fails before dynamic schema discovery. It is consulted by
103
+ :func:`resolve_verify` for every transport.
104
+ """
105
+
106
+ global _CLI_VERIFY_OVERRIDE
107
+ if ca_bundle is not None:
108
+ if verify_tls is False:
109
+ raise SystemExit(
110
+ "--ca-bundle cannot be combined with --no-verify-tls: a CA "
111
+ "bundle only takes effect when verification is enabled. Drop "
112
+ "one of the two flags."
113
+ )
114
+ _CLI_VERIFY_OVERRIDE = _validate_ca_bundle(os.fspath(ca_bundle), "--ca-bundle")
115
+ return
116
+ if verify_tls is not None:
117
+ _CLI_VERIFY_OVERRIDE = bool(verify_tls)
118
+ return
119
+ _CLI_VERIFY_OVERRIDE = _VERIFY_UNSET
120
+
121
+
122
+ def resolve_verify(verify: Any = _VERIFY_UNSET) -> Any:
123
+ """Resolve the effective TLS verification setting.
124
+
125
+ Precedence, highest to lowest: an explicit ``verify`` argument, the global
126
+ CLI override (``--ca-bundle`` / ``--verify-tls`` / ``--no-verify-tls``), a
127
+ nonempty ``TANGLE_API_CA_BUNDLE``, ``TANGLE_API_VERIFY_TLS``, then a secure
128
+ enabled default. When none of these are set, ``_VERIFY_UNSET`` is returned
129
+ so callers can preserve library and caller defaults (for example requests'
130
+ ``REQUESTS_CA_BUNDLE``/``CURL_CA_BUNDLE`` handling and a caller-supplied
131
+ ``Session.verify``). Empty environment values are treated as unset.
132
+ """
133
+
134
+ if verify is not _VERIFY_UNSET and verify is not None:
135
+ return _coerce_explicit_verify(verify)
136
+ if _CLI_VERIFY_OVERRIDE is not _VERIFY_UNSET:
137
+ return _CLI_VERIFY_OVERRIDE
138
+ ca_bundle = os.environ.get("TANGLE_API_CA_BUNDLE", "")
139
+ if ca_bundle.strip():
140
+ return _validate_ca_bundle(ca_bundle.strip(), "TANGLE_API_CA_BUNDLE")
141
+ flag = os.environ.get("TANGLE_API_VERIFY_TLS", "")
142
+ if flag.strip():
143
+ return _parse_verify_flag(flag)
144
+ return _VERIFY_UNSET
145
+
146
+
147
+ def resolve_verify_default(verify: Any = _VERIFY_UNSET) -> VerifyValue:
148
+ """Like :func:`resolve_verify`, but fall back to the secure ``True`` default."""
149
+
150
+ resolved = resolve_verify(verify)
151
+ return True if resolved is _VERIFY_UNSET else resolved
152
+
153
+
154
+ def httpx_verify(verify: Any = _VERIFY_UNSET) -> bool | ssl.SSLContext:
155
+ """Adapt the resolved verify value to what httpx expects.
156
+
157
+ httpx 0.28 deprecates string CA-bundle paths, so a path is turned into an
158
+ :class:`ssl.SSLContext`; booleans pass through unchanged.
159
+ """
160
+
161
+ resolved = resolve_verify_default(verify)
162
+ if isinstance(resolved, bool):
163
+ return resolved
164
+ return ssl.create_default_context(cafile=resolved)
165
+
166
+
43
167
  def _redact_headers(headers: dict[str, Any] | None) -> dict[str, Any]:
44
168
  redacted: dict[str, Any] = {}
45
169
  for name, value in (headers or {}).items():
@@ -280,6 +404,7 @@ def request_operation(
280
404
  timeout: float = DEFAULT_TIMEOUT_SECONDS,
281
405
  allow_body_file_references: bool = False,
282
406
  include_env_credentials: bool = True,
407
+ verify: Any = _VERIFY_UNSET,
283
408
  ) -> httpx.Response:
284
409
  """Dispatch one normalized OpenAPI operation as an HTTP request.
285
410
 
@@ -306,6 +431,7 @@ def request_operation(
306
431
  content=content,
307
432
  headers=request_headers,
308
433
  timeout=timeout,
434
+ verify=httpx_verify(verify),
309
435
  )
310
436
  if tangle_verbose_enabled():
311
437
  log_http_exchange(
@@ -80,6 +80,7 @@ def artifacts_get(
80
80
  header=args.header,
81
81
  include_env_credentials=include_env_credentials_for_args(args, base_url),
82
82
  command_name="artifact commands",
83
+ logger=logger,
83
84
  )
84
85
  if require_available := getattr(client, "require_available", None):
85
86
  require_available()