tangle-cli 0.1.14__tar.gz → 0.1.16__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.14 → tangle_cli-0.1.16}/PKG-INFO +31 -2
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/README.md +30 -1
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
- tangle_cli-0.1.16/packages/tangle-cli/src/tangle_cli/authenticated_identity.py +77 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/client.py +10 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_from_func.py +44 -8
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_generator.py +33 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_publisher.py +335 -81
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +87 -5
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +14 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +12 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +2 -0
- tangle_cli-0.1.16/packages/tangle-cli/src/tangle_cli/python_pipeline/publish.py +142 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +10 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/pyproject.toml +1 -1
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/pyproject.toml.orig +1 -1
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/api_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipelines.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/schema_validation.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/OSS-CONVENTIONS.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/PORT-README.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/SKILL.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/auth-wizard.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/builder.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/debugger.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/reporter.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/researcher.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/reviewer.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/agents/scenario-builder.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/data-sources.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/event-log.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/iterating-on-runs.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/knowledge-corpus.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/secrets.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/setup.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-0-initialize.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-1-analyze.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-2-hypothesize.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-3-submit.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-4-monitor.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-5-evaluate.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-6-synthesize.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/step-7-decide.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/skills/tangent/references/tangle-tools.md +0 -0
- {tangle_cli-0.1.14 → tangle_cli-0.1.16}/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.16
|
|
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>
|
|
@@ -379,7 +379,36 @@ uv run tangle sdk published-components publish components/my-component.yaml --dr
|
|
|
379
379
|
uv run tangle sdk published-components deprecate sha256:old --superseded-by sha256:new
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
-
`publish` accepts `--image`, `--name`, `--description`, `--annotations` (JSON), `--dry-run`, `--published-by`, generic git metadata fields, generic API auth fields, `--log-type`, and `--config`. By default it scopes version checks and automatic old-version deprecation to the current authenticated user via `users_me()`; use `--published-by` to supply an explicit owner/publisher filter. Publishing fails closed if no owner can be determined.
|
|
382
|
+
`publish` accepts `--image`, `--name`, `--description`, `--annotations` (JSON), `--dry-run`, `--allow-downgrade`, `--published-by`, generic git metadata fields, generic API auth fields, `--log-type`, and `--config`. By default it scopes version checks and automatic old-version deprecation to the current authenticated user via `users_me()`; use `--published-by` to supply an explicit owner/publisher filter. Publishing fails closed if no owner can be determined.
|
|
383
|
+
|
|
384
|
+
#### Monotonic publishing and result digests
|
|
385
|
+
|
|
386
|
+
Publishing is monotonic against the highest **non-deprecated, owner-scoped** published version of the component (ordering comes from `compare_versions`, which zero-pads shorter versions so `1.0.1 > 1.0`):
|
|
387
|
+
|
|
388
|
+
| Local vs latest published | Outcome | Notes |
|
|
389
|
+
| --- | --- | --- |
|
|
390
|
+
| nothing published (no non-deprecated owner-scoped version) | `proceed` | first publish |
|
|
391
|
+
| local strictly newer | `proceed` | publishes, then deprecates owner-scoped versions proven older |
|
|
392
|
+
| local equal | `skip` | no create/deprecate calls |
|
|
393
|
+
| local strictly older | `skip` | no-op; never publishes an older version and never deprecates a newer one |
|
|
394
|
+
| published version unreadable, or ambiguous tie at the latest version | `error` | fails closed; no create/deprecate calls |
|
|
395
|
+
|
|
396
|
+
Every result carries the digest of the version it compared against:
|
|
397
|
+
|
|
398
|
+
- `digest` — digest of a **newly created** publication (SUCCESS only, unchanged meaning).
|
|
399
|
+
- `latest_digest` — exact digest of the selected latest published version (set on PROCEED/SKIP, and carried through the SUCCESS/ERROR results that follow a version check). JSON output includes it as `latest_digest`.
|
|
400
|
+
- `ProcessingResult.resolved_digest` — the digest a caller should pin: `digest or latest_digest`, but deliberately `None` for any outcome other than SUCCESS/SKIP, so a failed publish never hands back a stale-but-plausible digest.
|
|
401
|
+
|
|
402
|
+
The check **fails closed** (an `error`, with nothing published and nothing deprecated) whenever the published state cannot be read completely:
|
|
403
|
+
|
|
404
|
+
- any non-deprecated owner-scoped candidate whose digest is missing, or whose spec/version cannot be fetched or parsed — an unreadable row could be newer than the local version, and must never be deprecated sight-unseen;
|
|
405
|
+
- two or more non-deprecated candidates tied at the selected latest version, where no exact digest can be chosen. The reason names the tied digests instead of guessing from API ordering.
|
|
406
|
+
|
|
407
|
+
Deprecated components are never selected as "latest", and all digest lists in results/logs are sorted, so diagnostics do not depend on API response order.
|
|
408
|
+
|
|
409
|
+
The published state is re-read immediately before create, and the same policy is re-applied to that fresh observation: a version that appeared concurrently since the first check can still turn the publish into a skip or an error. After a successful create, only digests **proven strictly older** in that final observation are deprecated — a row first seen after the publish decision is never deprecated on the strength of the earlier one. A race after the final read is not preventable client-side and needs a server-side conditional/CAS operation.
|
|
410
|
+
|
|
411
|
+
**Contract change:** republishing an older version used to proceed (publishing the older spec and deprecating the newer one). It is now a skip. `--allow-downgrade` publishes the older spec but still never deprecates a strictly newer row; deprecate those explicitly with `published-components deprecate` if that is really intended. Deliberate downgrades must opt in with `--allow-downgrade` on the CLI, or `ComponentPublisher(allow_downgrade=True)` / `allow_downgrade=True` on the `publish_component_to_tangle` / `perform_version_check` wrappers. Republishing the same version is still a skip, as before.
|
|
383
412
|
|
|
384
413
|
There is no separate OSS `publish-all` command. To publish multiple components, pass a YAML/JSON config list, or `_defaults` + `configs`, to the same `published-components publish` command; the command aggregates results and exits nonzero if any component errors. A top-level `_select` node can choose between such documents per environment (see [Environment-selected configs](#environment-selected-configs-_select)).
|
|
385
414
|
|
|
@@ -353,7 +353,36 @@ uv run tangle sdk published-components publish components/my-component.yaml --dr
|
|
|
353
353
|
uv run tangle sdk published-components deprecate sha256:old --superseded-by sha256:new
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
-
`publish` accepts `--image`, `--name`, `--description`, `--annotations` (JSON), `--dry-run`, `--published-by`, generic git metadata fields, generic API auth fields, `--log-type`, and `--config`. By default it scopes version checks and automatic old-version deprecation to the current authenticated user via `users_me()`; use `--published-by` to supply an explicit owner/publisher filter. Publishing fails closed if no owner can be determined.
|
|
356
|
+
`publish` accepts `--image`, `--name`, `--description`, `--annotations` (JSON), `--dry-run`, `--allow-downgrade`, `--published-by`, generic git metadata fields, generic API auth fields, `--log-type`, and `--config`. By default it scopes version checks and automatic old-version deprecation to the current authenticated user via `users_me()`; use `--published-by` to supply an explicit owner/publisher filter. Publishing fails closed if no owner can be determined.
|
|
357
|
+
|
|
358
|
+
#### Monotonic publishing and result digests
|
|
359
|
+
|
|
360
|
+
Publishing is monotonic against the highest **non-deprecated, owner-scoped** published version of the component (ordering comes from `compare_versions`, which zero-pads shorter versions so `1.0.1 > 1.0`):
|
|
361
|
+
|
|
362
|
+
| Local vs latest published | Outcome | Notes |
|
|
363
|
+
| --- | --- | --- |
|
|
364
|
+
| nothing published (no non-deprecated owner-scoped version) | `proceed` | first publish |
|
|
365
|
+
| local strictly newer | `proceed` | publishes, then deprecates owner-scoped versions proven older |
|
|
366
|
+
| local equal | `skip` | no create/deprecate calls |
|
|
367
|
+
| local strictly older | `skip` | no-op; never publishes an older version and never deprecates a newer one |
|
|
368
|
+
| published version unreadable, or ambiguous tie at the latest version | `error` | fails closed; no create/deprecate calls |
|
|
369
|
+
|
|
370
|
+
Every result carries the digest of the version it compared against:
|
|
371
|
+
|
|
372
|
+
- `digest` — digest of a **newly created** publication (SUCCESS only, unchanged meaning).
|
|
373
|
+
- `latest_digest` — exact digest of the selected latest published version (set on PROCEED/SKIP, and carried through the SUCCESS/ERROR results that follow a version check). JSON output includes it as `latest_digest`.
|
|
374
|
+
- `ProcessingResult.resolved_digest` — the digest a caller should pin: `digest or latest_digest`, but deliberately `None` for any outcome other than SUCCESS/SKIP, so a failed publish never hands back a stale-but-plausible digest.
|
|
375
|
+
|
|
376
|
+
The check **fails closed** (an `error`, with nothing published and nothing deprecated) whenever the published state cannot be read completely:
|
|
377
|
+
|
|
378
|
+
- any non-deprecated owner-scoped candidate whose digest is missing, or whose spec/version cannot be fetched or parsed — an unreadable row could be newer than the local version, and must never be deprecated sight-unseen;
|
|
379
|
+
- two or more non-deprecated candidates tied at the selected latest version, where no exact digest can be chosen. The reason names the tied digests instead of guessing from API ordering.
|
|
380
|
+
|
|
381
|
+
Deprecated components are never selected as "latest", and all digest lists in results/logs are sorted, so diagnostics do not depend on API response order.
|
|
382
|
+
|
|
383
|
+
The published state is re-read immediately before create, and the same policy is re-applied to that fresh observation: a version that appeared concurrently since the first check can still turn the publish into a skip or an error. After a successful create, only digests **proven strictly older** in that final observation are deprecated — a row first seen after the publish decision is never deprecated on the strength of the earlier one. A race after the final read is not preventable client-side and needs a server-side conditional/CAS operation.
|
|
384
|
+
|
|
385
|
+
**Contract change:** republishing an older version used to proceed (publishing the older spec and deprecating the newer one). It is now a skip. `--allow-downgrade` publishes the older spec but still never deprecates a strictly newer row; deprecate those explicitly with `published-components deprecate` if that is really intended. Deliberate downgrades must opt in with `--allow-downgrade` on the CLI, or `ComponentPublisher(allow_downgrade=True)` / `allow_downgrade=True` on the `publish_component_to_tangle` / `perform_version_check` wrappers. Republishing the same version is still a skip, as before.
|
|
357
386
|
|
|
358
387
|
There is no separate OSS `publish-all` command. To publish multiple components, pass a YAML/JSON config list, or `_defaults` + `configs`, to the same `published-components publish` command; the command aggregates results and exits nonzero if any component errors. A top-level `_select` node can choose between such documents per environment (see [Environment-selected configs](#environment-selected-configs-_select)).
|
|
359
388
|
|
|
@@ -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.16"
|
|
18
18
|
|
|
19
19
|
__all__ = ["TangleDynamicDiscoveryClient", "__version__"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Resolution of the authenticated account, and the symbolic ``me`` publisher.
|
|
2
|
+
|
|
3
|
+
Compilation is offline, so a compiler cannot write the author's account id into
|
|
4
|
+
a component entry. It writes the symbolic publisher :data:`ME` instead, and the
|
|
5
|
+
account is resolved at hydration time by whoever is actually authenticated.
|
|
6
|
+
|
|
7
|
+
Two callers need the account: the publisher, which scopes its version check and
|
|
8
|
+
deprecation to the owner, and hydration of entries whose publisher is ``me``.
|
|
9
|
+
They must agree, so the parsing lives here once.
|
|
10
|
+
|
|
11
|
+
They differ only in what an *unknown* account means, so that choice is left to
|
|
12
|
+
the caller: the publisher degrades, while resolution must fail closed -- see
|
|
13
|
+
:func:`require_authenticated_user_id`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
#: Symbolic publisher meaning "whoever is authenticated at hydration time".
|
|
22
|
+
#: Matched exactly and case-sensitively, so a literal account id that happens to
|
|
23
|
+
#: differ in case is never mistaken for the sentinel.
|
|
24
|
+
ME = "me"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ME",
|
|
28
|
+
"IdentityUnavailableError",
|
|
29
|
+
"authenticated_user_id",
|
|
30
|
+
"is_symbolic_me",
|
|
31
|
+
"require_authenticated_user_id",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IdentityUnavailableError(RuntimeError):
|
|
36
|
+
"""The authenticated account could not be determined."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_symbolic_me(publisher: Any) -> bool:
|
|
40
|
+
"""Whether ``publisher`` is the symbolic self-reference rather than an id."""
|
|
41
|
+
return publisher == ME
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def authenticated_user_id(client: Any) -> str | None:
|
|
45
|
+
"""Return the current user id, or ``None`` if it cannot be read.
|
|
46
|
+
|
|
47
|
+
An empty or missing id is as unusable as no answer at all.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
user_info = client.users_me()
|
|
51
|
+
except Exception:
|
|
52
|
+
return None
|
|
53
|
+
if user_info is None:
|
|
54
|
+
return None
|
|
55
|
+
if isinstance(user_info, Mapping):
|
|
56
|
+
value = user_info.get("id")
|
|
57
|
+
else:
|
|
58
|
+
value = getattr(user_info, "id", None)
|
|
59
|
+
return str(value) if value else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def require_authenticated_user_id(client: Any) -> str:
|
|
63
|
+
"""Return the current user id, raising when it cannot be determined.
|
|
64
|
+
|
|
65
|
+
For owner-scoped *resolution* an unknown account must never widen the
|
|
66
|
+
search: continuing unscoped is what would let a component published by
|
|
67
|
+
someone else under the same name be resolved as the author's own code.
|
|
68
|
+
"""
|
|
69
|
+
user_id = authenticated_user_id(client)
|
|
70
|
+
if not user_id:
|
|
71
|
+
raise IdentityUnavailableError(
|
|
72
|
+
"Cannot determine the authenticated account, so a component "
|
|
73
|
+
f"published by '{ME}' cannot be resolved. Refusing to fall back to "
|
|
74
|
+
"an unscoped lookup, which could resolve a component owned by "
|
|
75
|
+
"someone else. Re-authenticate and retry."
|
|
76
|
+
)
|
|
77
|
+
return user_id
|
|
@@ -952,12 +952,22 @@ class TangleApiClient(GeneratedTangleApiOperations):
|
|
|
952
952
|
search_digests.add(str(data["digest"]))
|
|
953
953
|
|
|
954
954
|
publisher_filter = published_by_substring or published_by
|
|
955
|
+
# The API parameter is a SUBSTRING match. When the caller asked for an
|
|
956
|
+
# exact owner it is therefore only a prefilter, and accepting its
|
|
957
|
+
# results verbatim would let a superset owner id (``alice`` matching
|
|
958
|
+
# ``alice2``) satisfy an exact request. An owner-scoped lookup is an
|
|
959
|
+
# identity control, so exactness is enforced here on the returned rows
|
|
960
|
+
# and a row with no owner is never accepted. Explicit
|
|
961
|
+
# ``published_by_substring`` callers keep substring semantics.
|
|
962
|
+
exact_owner = published_by if published_by and not published_by_substring else None
|
|
955
963
|
found: dict[str, ComponentInfo] = {}
|
|
956
964
|
|
|
957
965
|
def add(info: ComponentInfo) -> None:
|
|
958
966
|
key = info.digest or info.name
|
|
959
967
|
if not key:
|
|
960
968
|
return
|
|
969
|
+
if exact_owner is not None and info.published_by != exact_owner:
|
|
970
|
+
return
|
|
961
971
|
found[key] = info
|
|
962
972
|
if verbose:
|
|
963
973
|
self.logger.info(f" Found existing component: {info.name} ({key[:16]}...)")
|
{tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_from_func.py
RENAMED
|
@@ -986,8 +986,13 @@ def _is_name_main_test(node: ast.expr) -> bool:
|
|
|
986
986
|
# _strip_authoring_constructs). ``registered`` marks an op published separately
|
|
987
987
|
# via its own gen_config.yaml; when that same op is baked (through its
|
|
988
988
|
# local_from_python entry) the decorator + its authoring import must be stripped
|
|
989
|
-
# too, exactly like @task.
|
|
990
|
-
|
|
989
|
+
# too, exactly like @task. ``Publish`` likewise only records a publication
|
|
990
|
+
# declaration for the compiler; leaving it in the baked program would raise
|
|
991
|
+
# ``NameError`` at container startup, since its import is stripped with the
|
|
992
|
+
# rest of the authoring surface.
|
|
993
|
+
_AUTHORING_DECORATOR_NAMES = frozenset(
|
|
994
|
+
{"task", "pipeline", "subpipeline", "registered", "Publish"}
|
|
995
|
+
)
|
|
991
996
|
|
|
992
997
|
# The python-pipeline authoring modules. ONLY imports of these modules (and
|
|
993
998
|
# their submodules) are authoring-only and stripped from the baked source. We
|
|
@@ -2015,6 +2020,15 @@ def build_component_dict(
|
|
|
2015
2020
|
# ============================================================================
|
|
2016
2021
|
|
|
2017
2022
|
|
|
2023
|
+
def _within(path: Path, root: Path) -> bool:
|
|
2024
|
+
"""Whether *path* lies inside *root*."""
|
|
2025
|
+
try:
|
|
2026
|
+
path.resolve().relative_to(root.resolve())
|
|
2027
|
+
except ValueError:
|
|
2028
|
+
return False
|
|
2029
|
+
return True
|
|
2030
|
+
|
|
2031
|
+
|
|
2018
2032
|
def generate_component_yaml(
|
|
2019
2033
|
file_path: Path,
|
|
2020
2034
|
output_path: Path,
|
|
@@ -2030,12 +2044,21 @@ def generate_component_yaml(
|
|
|
2030
2044
|
emit_generation_annotations: bool = True,
|
|
2031
2045
|
path_annotation_mode: Literal["oss", "td_legacy"] = "oss",
|
|
2032
2046
|
unwrapped_inputs: dict[str, Any] | None = None,
|
|
2047
|
+
logical_output_path: Path | None = None,
|
|
2033
2048
|
) -> bool:
|
|
2034
2049
|
"""Generate a component YAML file from a Python function.
|
|
2035
2050
|
|
|
2036
2051
|
Args:
|
|
2037
2052
|
file_path: Path to the Python source file
|
|
2038
2053
|
output_path: Where to write the generated YAML
|
|
2054
|
+
logical_output_path: Where the component is to be UNDERSTOOD to live,
|
|
2055
|
+
for provenance only. Defaults to ``output_path``. Pass this when
|
|
2056
|
+
the file is written somewhere incidental -- a private staging
|
|
2057
|
+
directory, a scratch area -- so the recorded provenance describes
|
|
2058
|
+
the component rather than the accident of where bytes landed.
|
|
2059
|
+
Provenance is otherwise derived from the physical path, which
|
|
2060
|
+
would embed that location and make the emitted YAML, and therefore
|
|
2061
|
+
its digest, differ between runs of identical source.
|
|
2039
2062
|
container_image: Docker image reference
|
|
2040
2063
|
function_name: Function to extract (auto-detected if None)
|
|
2041
2064
|
dependencies_from: Path to pyproject.toml with pip dependencies
|
|
@@ -2111,7 +2134,6 @@ def generate_component_yaml(
|
|
|
2111
2134
|
deps = read_dependencies(dependencies_from)
|
|
2112
2135
|
|
|
2113
2136
|
# 4. Build annotations
|
|
2114
|
-
directory = file_path.parent.resolve()
|
|
2115
2137
|
module_code = file_path.read_text()
|
|
2116
2138
|
|
|
2117
2139
|
annotations: dict[str, str] = {
|
|
@@ -2143,9 +2165,18 @@ def generate_component_yaml(
|
|
|
2143
2165
|
# basename-only paths outside a git checkout to preserve historical
|
|
2144
2166
|
# snapshots.
|
|
2145
2167
|
resolved_source = file_path.resolve()
|
|
2146
|
-
|
|
2168
|
+
# Provenance describes where the component LIVES, which is not always
|
|
2169
|
+
# where this call happens to write it. Kept UNRESOLVED: ``td_legacy``
|
|
2170
|
+
# annotates the lexical basename, so resolving here would rewrite the
|
|
2171
|
+
# recorded name whenever the output is a symlink.
|
|
2172
|
+
annotation_output = logical_output_path if logical_output_path is not None else output_path
|
|
2173
|
+
resolved_output = annotation_output.resolve()
|
|
2147
2174
|
common_dir = Path(os.path.commonpath([resolved_source, resolved_output]))
|
|
2148
|
-
|
|
2175
|
+
# Discover from the SOURCE's real directory: a symlinked source file
|
|
2176
|
+
# whose link lives outside the checkout would otherwise find no repo
|
|
2177
|
+
# and drop every git annotation.
|
|
2178
|
+
source_dir = resolved_source.parent
|
|
2179
|
+
git_root = get_git_root(source_dir)
|
|
2149
2180
|
use_common_paths = path_annotation_mode == "oss" or git_root is not None
|
|
2150
2181
|
|
|
2151
2182
|
def _path_annotation(path: Path) -> str:
|
|
@@ -2158,7 +2189,7 @@ def generate_component_yaml(
|
|
|
2158
2189
|
|
|
2159
2190
|
if not strip_source_path:
|
|
2160
2191
|
annotations["python_original_code_path"] = _path_annotation(file_path)
|
|
2161
|
-
annotations["component_yaml_path"] = _path_annotation(
|
|
2192
|
+
annotations["component_yaml_path"] = _path_annotation(annotation_output)
|
|
2162
2193
|
if emit_generation_annotations:
|
|
2163
2194
|
if dependencies_from:
|
|
2164
2195
|
annotations["tangle_cli_generation_dependencies_from"] = _path_annotation(dependencies_from)
|
|
@@ -2167,7 +2198,12 @@ def generate_component_yaml(
|
|
|
2167
2198
|
|
|
2168
2199
|
# Git info — use the same common ancestor as git_relative_dir when common paths are active.
|
|
2169
2200
|
if git_root:
|
|
2170
|
-
|
|
2201
|
+
# Read the repository from a directory KNOWN to be inside it. The
|
|
2202
|
+
# common ancestor of source and output need not be: any output
|
|
2203
|
+
# outside the checkout (a configured output_folder, a staging dir)
|
|
2204
|
+
# drags it out, and reading git there returns nothing, silently
|
|
2205
|
+
# publishing a component with no repository of origin.
|
|
2206
|
+
git_info = get_git_info(common_dir if _within(common_dir, git_root) else source_dir)
|
|
2171
2207
|
git_info.pop("_git_root", None)
|
|
2172
2208
|
# Override git_relative_dir to be the common ancestor
|
|
2173
2209
|
try:
|
|
@@ -2176,7 +2212,7 @@ def generate_component_yaml(
|
|
|
2176
2212
|
pass
|
|
2177
2213
|
annotations.update(git_info)
|
|
2178
2214
|
else:
|
|
2179
|
-
git_info = get_git_info(
|
|
2215
|
+
git_info = get_git_info(source_dir)
|
|
2180
2216
|
git_info.pop("_git_root", None)
|
|
2181
2217
|
annotations.update(git_info)
|
|
2182
2218
|
|
{tangle_cli-0.1.14 → tangle_cli-0.1.16}/packages/tangle-cli/src/tangle_cli/component_generator.py
RENAMED
|
@@ -128,6 +128,7 @@ class ComponentGenerator:
|
|
|
128
128
|
resolve_root: Path | None = None,
|
|
129
129
|
emit_generation_annotations: bool = True,
|
|
130
130
|
unwrapped_inputs: dict[str, Any] | None = None,
|
|
131
|
+
logical_output_path: Path | None = None,
|
|
131
132
|
) -> bool:
|
|
132
133
|
"""Generate component YAML from a Python function source file.
|
|
133
134
|
|
|
@@ -147,6 +148,8 @@ class ComponentGenerator:
|
|
|
147
148
|
unwrapped_inputs: Optional persisted unwrap schema. Hydrate forwards
|
|
148
149
|
this ``local_from_python.unwrapped_inputs`` payload so component
|
|
149
150
|
generation expands dict parameters exactly as compile did.
|
|
151
|
+
logical_output_path: Where the component is to be understood to
|
|
152
|
+
live, for provenance only. Defaults to ``output_path``.
|
|
150
153
|
|
|
151
154
|
Returns:
|
|
152
155
|
True when generation succeeds, otherwise False.
|
|
@@ -154,6 +157,12 @@ class ComponentGenerator:
|
|
|
154
157
|
|
|
155
158
|
from tangle_cli.component_from_func import generate_component_yaml
|
|
156
159
|
|
|
160
|
+
# Forwarded only when set. These hops are overridable, and an older
|
|
161
|
+
# override written against the previous signature must keep working
|
|
162
|
+
# for every caller that does not use the seam.
|
|
163
|
+
seam: dict[str, Any] = (
|
|
164
|
+
{} if logical_output_path is None else {"logical_output_path": logical_output_path}
|
|
165
|
+
)
|
|
157
166
|
return generate_component_yaml(
|
|
158
167
|
file_path=file_path,
|
|
159
168
|
output_path=output_path,
|
|
@@ -168,6 +177,7 @@ class ComponentGenerator:
|
|
|
168
177
|
resolve_root=resolve_root,
|
|
169
178
|
emit_generation_annotations=emit_generation_annotations,
|
|
170
179
|
unwrapped_inputs=unwrapped_inputs,
|
|
180
|
+
**seam,
|
|
171
181
|
)
|
|
172
182
|
|
|
173
183
|
def regenerate_yaml(
|
|
@@ -184,6 +194,7 @@ class ComponentGenerator:
|
|
|
184
194
|
resolve_root: Path | None = None,
|
|
185
195
|
emit_generation_annotations: bool = True,
|
|
186
196
|
unwrapped_inputs: dict[str, Any] | None = None,
|
|
197
|
+
logical_output_path: Path | None = None,
|
|
187
198
|
) -> bool:
|
|
188
199
|
"""Regenerate a YAML component from a Python function source file.
|
|
189
200
|
|
|
@@ -202,6 +213,10 @@ class ComponentGenerator:
|
|
|
202
213
|
emit_generation_annotations: Whether to emit regeneration metadata.
|
|
203
214
|
unwrapped_inputs: Optional ``local_from_python.unwrapped_inputs``
|
|
204
215
|
schema to preserve compile-time unwrap expansion at hydrate time.
|
|
216
|
+
logical_output_path: Where the component is to be understood to
|
|
217
|
+
live, for provenance only. Defaults to ``output_path``. Note
|
|
218
|
+
that the image is still read back from the PHYSICAL output, so
|
|
219
|
+
a caller writing to a fresh location supplies ``image``.
|
|
205
220
|
|
|
206
221
|
Returns:
|
|
207
222
|
True when regeneration succeeds, otherwise False.
|
|
@@ -218,6 +233,9 @@ class ComponentGenerator:
|
|
|
218
233
|
self._log(f" Found dependencies: {deps_file}")
|
|
219
234
|
|
|
220
235
|
final_output.parent.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
seam: dict[str, Any] = (
|
|
237
|
+
{} if logical_output_path is None else {"logical_output_path": logical_output_path}
|
|
238
|
+
)
|
|
221
239
|
return self.run_generation(
|
|
222
240
|
python_file=python_file,
|
|
223
241
|
final_output=final_output,
|
|
@@ -231,6 +249,7 @@ class ComponentGenerator:
|
|
|
231
249
|
resolve_root=resolve_root,
|
|
232
250
|
emit_generation_annotations=emit_generation_annotations,
|
|
233
251
|
unwrapped_inputs=unwrapped_inputs,
|
|
252
|
+
**seam,
|
|
234
253
|
)
|
|
235
254
|
|
|
236
255
|
def run_generation(
|
|
@@ -248,6 +267,7 @@ class ComponentGenerator:
|
|
|
248
267
|
resolve_root: Path | None = None,
|
|
249
268
|
emit_generation_annotations: bool = True,
|
|
250
269
|
unwrapped_inputs: dict[str, Any] | None = None,
|
|
270
|
+
logical_output_path: Path | None = None,
|
|
251
271
|
) -> bool:
|
|
252
272
|
"""Execute component generation and clean up partial output on failure.
|
|
253
273
|
|
|
@@ -265,6 +285,8 @@ class ComponentGenerator:
|
|
|
265
285
|
emit_generation_annotations: Whether to emit regeneration metadata.
|
|
266
286
|
unwrapped_inputs: Optional persisted unwrap schema forwarded to the
|
|
267
287
|
low-level generator for compile/hydrate interface parity.
|
|
288
|
+
logical_output_path: Where the component is to be understood to
|
|
289
|
+
live, for provenance only. Defaults to ``final_output``.
|
|
268
290
|
|
|
269
291
|
Returns:
|
|
270
292
|
True when generation succeeds, otherwise False. On failure, any
|
|
@@ -274,6 +296,9 @@ class ComponentGenerator:
|
|
|
274
296
|
try:
|
|
275
297
|
function_detail = f" function {func_name!r}" if func_name else ""
|
|
276
298
|
self._log(f" Generating component from {python_file.name}{function_detail}...")
|
|
299
|
+
seam: dict[str, Any] = (
|
|
300
|
+
{} if logical_output_path is None else {"logical_output_path": logical_output_path}
|
|
301
|
+
)
|
|
277
302
|
success = self.generate_component_yaml(
|
|
278
303
|
file_path=python_file,
|
|
279
304
|
output_path=final_output,
|
|
@@ -287,6 +312,7 @@ class ComponentGenerator:
|
|
|
287
312
|
resolve_root=resolve_root,
|
|
288
313
|
emit_generation_annotations=emit_generation_annotations,
|
|
289
314
|
unwrapped_inputs=unwrapped_inputs,
|
|
315
|
+
**seam,
|
|
290
316
|
)
|
|
291
317
|
if not success:
|
|
292
318
|
self._log(" ❌ Failed to generate component", err=True)
|
|
@@ -340,6 +366,7 @@ def regenerate_yaml(
|
|
|
340
366
|
resolve_root: Path | None = None,
|
|
341
367
|
logger: Any | None = None,
|
|
342
368
|
unwrapped_inputs: dict[str, Any] | None = None,
|
|
369
|
+
logical_output_path: Path | None = None,
|
|
343
370
|
) -> bool:
|
|
344
371
|
"""Regenerate component YAML through the default generator.
|
|
345
372
|
|
|
@@ -358,11 +385,16 @@ def regenerate_yaml(
|
|
|
358
385
|
logger: Optional logger object used by the generator.
|
|
359
386
|
unwrapped_inputs: Optional persisted unwrap schema from
|
|
360
387
|
``local_from_python.unwrapped_inputs``.
|
|
388
|
+
logical_output_path: Where the component is to be understood to live,
|
|
389
|
+
for provenance only. Defaults to ``output_path``.
|
|
361
390
|
|
|
362
391
|
Returns:
|
|
363
392
|
True when regeneration succeeds, otherwise False.
|
|
364
393
|
"""
|
|
365
394
|
|
|
395
|
+
seam: dict[str, Any] = (
|
|
396
|
+
{} if logical_output_path is None else {"logical_output_path": logical_output_path}
|
|
397
|
+
)
|
|
366
398
|
return ComponentGenerator(logger=logger, verbose=verbose).regenerate_yaml(
|
|
367
399
|
python_file=python_file,
|
|
368
400
|
output_path=output_path,
|
|
@@ -375,6 +407,7 @@ def regenerate_yaml(
|
|
|
375
407
|
mode=mode,
|
|
376
408
|
resolve_root=resolve_root,
|
|
377
409
|
unwrapped_inputs=unwrapped_inputs,
|
|
410
|
+
**seam,
|
|
378
411
|
)
|
|
379
412
|
|
|
380
413
|
|