tangle-cli 0.1.7__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.
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/PKG-INFO +91 -1
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/README.md +90 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_cli.py +14 -2
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_schema.py +8 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/api_transport.py +126 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +1 -0
- tangle_cli-0.1.9/packages/tangle-cli/src/tangle_cli/cli.py +118 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/cli_options.py +24 -0
- tangle_cli-0.1.9/packages/tangle-cli/src/tangle_cli/client.py +1153 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +15 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +1 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +576 -27
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +134 -4
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +28 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipelines.py +5 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +1 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +8 -1
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +36 -3
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +10 -3
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +19 -6
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +18 -1
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schema_validation.py +18 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +10 -1
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -6
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +5 -2
- tangle_cli-0.1.9/pyproject.toml +88 -0
- tangle_cli-0.1.7/pyproject.toml → tangle_cli-0.1.9/pyproject.toml.orig +1 -1
- tangle_cli-0.1.7/packages/tangle-cli/src/tangle_cli/cli.py +0 -57
- tangle_cli-0.1.7/packages/tangle-cli/src/tangle_cli/client.py +0 -684
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_from_func.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/OSS-CONVENTIONS.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/PORT-README.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/SKILL.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/auth-wizard.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/builder.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/debugger.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/reporter.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/researcher.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/reviewer.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/agents/scenario-builder.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/data-sources.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/event-log.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/iterating-on-runs.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/knowledge-corpus.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/secrets.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/setup.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-0-initialize.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-1-analyze.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-2-hypothesize.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-3-submit.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-4-monitor.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-5-evaluate.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-6-synthesize.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/step-7-decide.md +0 -0
- {tangle_cli-0.1.7 → tangle_cli-0.1.9}/skills/tangent/references/tangle-tools.md +0 -0
- {tangle_cli-0.1.7 → 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.
|
|
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
|
|
|
@@ -349,6 +400,45 @@ 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
|
+
|
|
352
442
|
##### Task images, dependencies, and image IDs
|
|
353
443
|
|
|
354
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`:
|
|
@@ -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,45 @@ 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
|
+
|
|
326
416
|
##### Task images, dependencies, and image IDs
|
|
327
417
|
|
|
328
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`:
|
|
@@ -14,6 +14,6 @@ from tangle_cli.dynamic_discovery_client import TangleDynamicDiscoveryClient
|
|
|
14
14
|
try:
|
|
15
15
|
__version__ = metadata_version("tangle-cli")
|
|
16
16
|
except PackageNotFoundError:
|
|
17
|
-
__version__ = "0.1.
|
|
17
|
+
__version__ = "0.1.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
|
-
|
|
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()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from cyclopts import App, Parameter
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
__version__,
|
|
10
|
+
api_cli,
|
|
11
|
+
artifacts_cli,
|
|
12
|
+
components_cli,
|
|
13
|
+
pipeline_runs_cli,
|
|
14
|
+
pipelines_cli,
|
|
15
|
+
published_components_cli,
|
|
16
|
+
quickstart,
|
|
17
|
+
secrets_cli,
|
|
18
|
+
)
|
|
19
|
+
from .api_transport import configure_cli_verify
|
|
20
|
+
from .cli_options import CaBundleOption, VerifyTlsOption
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def version() -> None:
|
|
24
|
+
"""Print the installed tangle-cli package version."""
|
|
25
|
+
|
|
26
|
+
print(__version__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _configure_tls_from_argv(argv: list[str]) -> None:
|
|
30
|
+
"""Parse the global TLS flags and install the process-wide override.
|
|
31
|
+
|
|
32
|
+
Runs before the `api` app is built so the override is in place for the
|
|
33
|
+
dynamic schema discovery that happens during command construction, ahead of
|
|
34
|
+
normal Cyclopts dispatch. Only tokens before the first subcommand are
|
|
35
|
+
considered; flag validation and conflict detection live in
|
|
36
|
+
:func:`configure_cli_verify`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
ca_bundle: str | None = None
|
|
40
|
+
verify_tls: bool | None = None
|
|
41
|
+
index = 1
|
|
42
|
+
while index < len(argv):
|
|
43
|
+
arg = argv[index]
|
|
44
|
+
if arg == "--ca-bundle" and index + 1 < len(argv):
|
|
45
|
+
ca_bundle = argv[index + 1]
|
|
46
|
+
index += 2
|
|
47
|
+
continue
|
|
48
|
+
if arg.startswith("--ca-bundle="):
|
|
49
|
+
ca_bundle = arg.split("=", 1)[1]
|
|
50
|
+
index += 1
|
|
51
|
+
continue
|
|
52
|
+
if arg == "--verify-tls":
|
|
53
|
+
verify_tls = True
|
|
54
|
+
index += 1
|
|
55
|
+
continue
|
|
56
|
+
if arg == "--no-verify-tls":
|
|
57
|
+
verify_tls = False
|
|
58
|
+
index += 1
|
|
59
|
+
continue
|
|
60
|
+
break
|
|
61
|
+
configure_cli_verify(ca_bundle, verify_tls)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_sdk_app() -> App:
|
|
65
|
+
"""Build the SDK command group."""
|
|
66
|
+
|
|
67
|
+
sdk_app = App(
|
|
68
|
+
name="sdk",
|
|
69
|
+
help="Work with local Tangle SDK resources and scaffolding.",
|
|
70
|
+
)
|
|
71
|
+
sdk_app.command(artifacts_cli.app)
|
|
72
|
+
sdk_app.command(components_cli.app)
|
|
73
|
+
sdk_app.command(pipelines_cli.app)
|
|
74
|
+
sdk_app.command(pipeline_runs_cli.app)
|
|
75
|
+
sdk_app.command(published_components_cli.app)
|
|
76
|
+
sdk_app.command(secrets_cli.app)
|
|
77
|
+
return sdk_app
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_app(argv: list[str] | None = None) -> App:
|
|
81
|
+
"""Build the root CLI app lazily for the current invocation.
|
|
82
|
+
|
|
83
|
+
Global TLS flags are parsed from *argv* (defaulting to ``sys.argv``) and
|
|
84
|
+
installed before the `api` app is built, because building it can trigger
|
|
85
|
+
dynamic OpenAPI schema discovery over the network.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_configure_tls_from_argv(sys.argv if argv is None else argv)
|
|
89
|
+
|
|
90
|
+
app = App(
|
|
91
|
+
help="CLI for Tangle, the open-source ML pipeline orchestration platform.",
|
|
92
|
+
version=__version__,
|
|
93
|
+
)
|
|
94
|
+
app.command(name="version")(version)
|
|
95
|
+
app.command(quickstart.app)
|
|
96
|
+
app.command(api_cli.build_app())
|
|
97
|
+
app.command(build_sdk_app())
|
|
98
|
+
|
|
99
|
+
@app.meta.default
|
|
100
|
+
def launcher(
|
|
101
|
+
*tokens: Annotated[str, Parameter(allow_leading_hyphen=True)],
|
|
102
|
+
ca_bundle: CaBundleOption = None,
|
|
103
|
+
verify_tls: VerifyTlsOption = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Apply global TLS options, then dispatch the requested command."""
|
|
106
|
+
|
|
107
|
+
configure_cli_verify(ca_bundle, verify_tls)
|
|
108
|
+
app(tokens)
|
|
109
|
+
|
|
110
|
+
return app
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main() -> None:
|
|
114
|
+
build_app().meta()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Annotated
|
|
6
7
|
|
|
7
8
|
from cyclopts import Parameter
|
|
@@ -50,3 +51,26 @@ LogTypeOption = Annotated[
|
|
|
50
51
|
str,
|
|
51
52
|
Parameter(help="Log output: console, none, file."),
|
|
52
53
|
]
|
|
54
|
+
CaBundleOption = Annotated[
|
|
55
|
+
Path | None,
|
|
56
|
+
Parameter(
|
|
57
|
+
name="--ca-bundle",
|
|
58
|
+
help=(
|
|
59
|
+
"Path to a PEM CA bundle used as the TLS trust store for every "
|
|
60
|
+
"transport. Overrides TANGLE_API_CA_BUNDLE. Place before the "
|
|
61
|
+
"subcommand, e.g. `tangle --ca-bundle ca.pem api ...`."
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
VerifyTlsOption = Annotated[
|
|
66
|
+
bool | None,
|
|
67
|
+
Parameter(
|
|
68
|
+
name="--verify-tls",
|
|
69
|
+
help=(
|
|
70
|
+
"Enable (--verify-tls) or disable (--no-verify-tls) TLS "
|
|
71
|
+
"certificate verification for every transport. Overrides "
|
|
72
|
+
"TANGLE_API_VERIFY_TLS. --no-verify-tls is for local development "
|
|
73
|
+
"only. Place before the subcommand."
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
]
|