tangle-cli 0.1.13__tar.gz → 0.1.14__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.13 → tangle_cli-0.1.14}/PKG-INFO +5 -1
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/README.md +4 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/__init__.py +1 -1
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py +451 -125
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/pyproject.toml +1 -1
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/pyproject.toml.orig +1 -1
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/api_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/api_schema.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/api_transport.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/args_container.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/artifacts.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/artifacts_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/cli_helpers.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/cli_options.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/client.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_from_func.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_generator.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_inspector.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_publisher.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/components_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/dynamic_discovery_client.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/generated_model_extensions.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/handler.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/hydration_trust.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/logger.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/models.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/module_bundler.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/parser.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_annotations.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipelines.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipelines_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/published_components_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/py.typed +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/__init__.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/compiler_context.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/dynamic_data.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/pipeline.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/placeholders.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/registered.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/subpipeline.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/task_env.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/quickstart.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schema_validation.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schemas/dehydrated_pipeline_schema.json +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schemas/pipeline_schema.json +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/secrets.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/secrets_cli.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/utils.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/version_manager.py +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/OSS-CONVENTIONS.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/PORT-README.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/SKILL.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/auth-wizard.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/builder.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/debugger.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/reporter.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/researcher.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/reviewer.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/agents/scenario-builder.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/data-sources.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/event-log.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/example-scenarios/01-mslr-ranking.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/example-scenarios/02-text-classification.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/example-scenarios/INDEX.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/iterating-on-runs.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/knowledge-corpus.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/secrets.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/setup.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-0-initialize.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-1-analyze.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-2-hypothesize.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-3-submit.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-4-monitor.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-5-evaluate.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-6-synthesize.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/step-7-decide.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/tangle-tools.md +0 -0
- {tangle_cli-0.1.13 → tangle_cli-0.1.14}/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.14
|
|
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>
|
|
@@ -483,6 +483,10 @@ Notes:
|
|
|
483
483
|
|
|
484
484
|
A minimal graph uses `@pipeline` for the graph and `@task` for local Python components. `@task` functions are not executed at compile time; the compiler records call sites, emits a sibling `<output>.components.yaml` with `local_from_python` entries, and rewrites task component refs to that sidecar. Hydrate later regenerates the same component YAML from the Python source.
|
|
485
485
|
|
|
486
|
+
Sidecar entries are deduplicated by the **generated component**, not by function name. The dedup key is the module-qualified function identity — a logical module namespace derived from the project-relative source layout (`package_a/tasks.py` -> `package_a.tasks`, `pkg/__init__.py` -> `pkg`), plus `__qualname__` and the function name — together with every generation-affecting option: image (explicit or resolved `image_id`), `mode`, `resolve_root`, `dependencies_from`, the `unwrap` schema, and the source file. Runtime `__module__` is deliberately not used, because pipeline scripts are imported under throwaway UUID module names.
|
|
487
|
+
|
|
488
|
+
Call sites sharing that whole identity collapse to one entry keyed by the readable hyphenated function name (`run_dbt` -> `run-dbt`). Otherwise every colliding variant — a shared helper re-decorated with different task-level options, or `package_a.tasks.run` alongside `package_b.tasks.run` — is emitted as `run-dbt--<hash>`, where `<hash>` is a content digest of the canonical identity; no variant keeps the unsuffixed name, and each task ref points at its own. Digests are anchored at the project source directory, so fragment names are unchanged by relocating the project, compiling into a different output directory, or reordering the pipeline's calls, and they never embed image, path, or credential-bearing values.
|
|
489
|
+
|
|
486
490
|
```python
|
|
487
491
|
from cloud_pipelines import components
|
|
488
492
|
from tangle_cli.python_pipeline import In, Out, pipeline, task
|
|
@@ -457,6 +457,10 @@ Notes:
|
|
|
457
457
|
|
|
458
458
|
A minimal graph uses `@pipeline` for the graph and `@task` for local Python components. `@task` functions are not executed at compile time; the compiler records call sites, emits a sibling `<output>.components.yaml` with `local_from_python` entries, and rewrites task component refs to that sidecar. Hydrate later regenerates the same component YAML from the Python source.
|
|
459
459
|
|
|
460
|
+
Sidecar entries are deduplicated by the **generated component**, not by function name. The dedup key is the module-qualified function identity — a logical module namespace derived from the project-relative source layout (`package_a/tasks.py` -> `package_a.tasks`, `pkg/__init__.py` -> `pkg`), plus `__qualname__` and the function name — together with every generation-affecting option: image (explicit or resolved `image_id`), `mode`, `resolve_root`, `dependencies_from`, the `unwrap` schema, and the source file. Runtime `__module__` is deliberately not used, because pipeline scripts are imported under throwaway UUID module names.
|
|
461
|
+
|
|
462
|
+
Call sites sharing that whole identity collapse to one entry keyed by the readable hyphenated function name (`run_dbt` -> `run-dbt`). Otherwise every colliding variant — a shared helper re-decorated with different task-level options, or `package_a.tasks.run` alongside `package_b.tasks.run` — is emitted as `run-dbt--<hash>`, where `<hash>` is a content digest of the canonical identity; no variant keeps the unsuffixed name, and each task ref points at its own. Digests are anchored at the project source directory, so fragment names are unchanged by relocating the project, compiling into a different output directory, or reordering the pipeline's calls, and they never embed image, path, or credential-bearing values.
|
|
463
|
+
|
|
460
464
|
```python
|
|
461
465
|
from cloud_pipelines import components
|
|
462
466
|
from tangle_cli.python_pipeline import In, Out, pipeline, task
|
|
@@ -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.14"
|
|
18
18
|
|
|
19
19
|
__all__ = ["TangleDynamicDiscoveryClient", "__version__"]
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_compiler.py
RENAMED
|
@@ -479,17 +479,25 @@ def _compile_pipeline_fn(
|
|
|
479
479
|
)
|
|
480
480
|
if task_refs:
|
|
481
481
|
components_path = output_path.with_name(output_path.stem + ".components.yaml")
|
|
482
|
-
|
|
482
|
+
# ONE plan drives both halves: entries and componentRef fragments must
|
|
483
|
+
# agree, and a fragment depends on how many distinct components the
|
|
484
|
+
# function generates pipeline-wide (a shared helper called with
|
|
485
|
+
# different task-level images emits one entry per configuration).
|
|
486
|
+
sidecar_plan = _plan_task_sidecar(
|
|
483
487
|
task_refs,
|
|
484
488
|
components_yaml_dir=components_path.parent,
|
|
489
|
+
# Identity is anchored at the pipeline SOURCE dir, not the output
|
|
490
|
+
# dir, so fragment names survive compiling elsewhere (e.g. the
|
|
491
|
+
# hidden submit bundle) and relocating the project.
|
|
492
|
+
identity_root=base_dir,
|
|
485
493
|
image_overrides=ctx.image_overrides,
|
|
486
494
|
unwrapped_input_keys=builder.task_unwrapped_input_keys,
|
|
487
495
|
)
|
|
496
|
+
components_entries = sidecar_plan.entries
|
|
488
497
|
_rewrite_task_componentref_urls(
|
|
489
498
|
body_dict=body_dict,
|
|
490
|
-
|
|
499
|
+
fragment_by_task=sidecar_plan.fragment_by_task,
|
|
491
500
|
components_yaml_name=components_path.name,
|
|
492
|
-
unwrapped_input_keys=builder.task_unwrapped_input_keys,
|
|
493
501
|
)
|
|
494
502
|
|
|
495
503
|
# 4b. A CHILD artifact is written under ``<root>.subgraphs/``, away from
|
|
@@ -1155,6 +1163,32 @@ def _unwrapped_schema_for_task(
|
|
|
1155
1163
|
raise CompileError(str(exc)) from exc
|
|
1156
1164
|
|
|
1157
1165
|
|
|
1166
|
+
def _canonical_json(payload: Any) -> str:
|
|
1167
|
+
"""Canonical JSON encoding used for hashing and deterministic ordering.
|
|
1168
|
+
|
|
1169
|
+
Args:
|
|
1170
|
+
payload: Any JSON-serialisable structure. Mapping keys are sorted so
|
|
1171
|
+
the encoding never depends on Python dict insertion order.
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
A compact, key-sorted JSON string.
|
|
1175
|
+
"""
|
|
1176
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _stable_payload_hash(payload: Any) -> str:
|
|
1180
|
+
"""Short deterministic SHA-256 prefix for a JSON-serialisable payload.
|
|
1181
|
+
|
|
1182
|
+
Args:
|
|
1183
|
+
payload: Any JSON-serialisable structure. The digest is stable across
|
|
1184
|
+
processes (unlike :func:`hash`) and independent of dict ordering.
|
|
1185
|
+
|
|
1186
|
+
Returns:
|
|
1187
|
+
A 10-character lowercase hex digest prefix.
|
|
1188
|
+
"""
|
|
1189
|
+
return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()[:10]
|
|
1190
|
+
|
|
1191
|
+
|
|
1158
1192
|
def _unwrapped_schema_hash(schema: Mapping[str, Any]) -> str:
|
|
1159
1193
|
"""Hash a persisted unwrap schema for sidecar fragment disambiguation.
|
|
1160
1194
|
|
|
@@ -1167,26 +1201,46 @@ def _unwrapped_schema_hash(schema: Mapping[str, Any]) -> str:
|
|
|
1167
1201
|
``combine--0123abcd`` so different key sets for the same function do
|
|
1168
1202
|
not collide in the generated components sidecar.
|
|
1169
1203
|
"""
|
|
1170
|
-
|
|
1171
|
-
|
|
1204
|
+
return _stable_payload_hash(schema)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _task_fragment_base(ref: CallableRef) -> str:
|
|
1208
|
+
"""Return the LEGACY, un-disambiguated sidecar fragment for a ``@task``.
|
|
1209
|
+
|
|
1210
|
+
Args:
|
|
1211
|
+
ref: The ``@task`` callable reference.
|
|
1212
|
+
|
|
1213
|
+
Returns:
|
|
1214
|
+
The hyphenated function name, matching the ``local_from_python``
|
|
1215
|
+
resolver's output-filename convention (``my_task`` -> ``my-task``).
|
|
1216
|
+
This is the fragment used verbatim when the function generates exactly
|
|
1217
|
+
ONE component across the whole pipeline; see
|
|
1218
|
+
:func:`_plan_task_sidecar` for the multi-variant disambiguation.
|
|
1219
|
+
"""
|
|
1220
|
+
assert ref._task_function_name is not None # only @task refs reach here
|
|
1221
|
+
return ref._task_function_name.replace("_", "-")
|
|
1172
1222
|
|
|
1173
1223
|
|
|
1174
1224
|
def _fragment_for_task(ref: CallableRef, unwrapped_schema: Mapping[str, Any] | None = None) -> str:
|
|
1175
|
-
"""Return the
|
|
1225
|
+
"""Return the legacy single-variant fragment for a ``@task`` call.
|
|
1176
1226
|
|
|
1177
1227
|
Args:
|
|
1178
1228
|
ref: The ``@task`` callable reference.
|
|
1179
1229
|
unwrapped_schema: Optional persisted unwrap schema for this call site.
|
|
1180
1230
|
|
|
1181
1231
|
Returns:
|
|
1182
|
-
The hyphenated function name for normal tasks
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1232
|
+
The hyphenated function name for normal tasks. For unwrapped tasks,
|
|
1233
|
+
returns the function fragment plus a schema hash so different key sets
|
|
1234
|
+
do not collide into one component schema.
|
|
1235
|
+
|
|
1236
|
+
Note:
|
|
1237
|
+
This is only the SINGLE-VARIANT naming. When one function generates
|
|
1238
|
+
several DIFFERENT components in one pipeline (e.g. a shared ``run_dbt``
|
|
1239
|
+
called with different task-level ``image``/``dependencies_from``),
|
|
1240
|
+
:func:`_plan_task_sidecar` appends a component-identity hash instead so
|
|
1241
|
+
the variants do not collapse into the first one emitted.
|
|
1187
1242
|
"""
|
|
1188
|
-
|
|
1189
|
-
base = ref._task_function_name.replace("_", "-")
|
|
1243
|
+
base = _task_fragment_base(ref)
|
|
1190
1244
|
if unwrapped_schema:
|
|
1191
1245
|
return f"{base}--{_unwrapped_schema_hash(unwrapped_schema)}"
|
|
1192
1246
|
return base
|
|
@@ -1530,163 +1584,435 @@ def _relocate_relative_local_url(url: str, source_dir: Path, sidecar_dir: Path)
|
|
|
1530
1584
|
return None
|
|
1531
1585
|
|
|
1532
1586
|
|
|
1533
|
-
|
|
1587
|
+
@dataclass(frozen=True)
|
|
1588
|
+
class TaskSidecarPlan:
|
|
1589
|
+
"""Resolved ``@task`` sidecar entries plus the per-task fragment map.
|
|
1590
|
+
|
|
1591
|
+
Attributes:
|
|
1592
|
+
entries: The ``<stem>.components.yaml`` content, ``{fragment:
|
|
1593
|
+
{local_from_python: {...}}}``, in first-seen task order.
|
|
1594
|
+
fragment_by_task: Graph ``task_id`` -> the sidecar fragment that task's
|
|
1595
|
+
``componentRef`` must point at. Sidecar emission and componentRef
|
|
1596
|
+
rewriting MUST share this map: fragment naming depends on how many
|
|
1597
|
+
distinct components one function generates across the whole
|
|
1598
|
+
pipeline, so it cannot be recomputed per call site in isolation.
|
|
1599
|
+
"""
|
|
1600
|
+
|
|
1601
|
+
entries: dict[str, Any]
|
|
1602
|
+
fragment_by_task: dict[str, str]
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
def _local_from_python_payload(
|
|
1606
|
+
ref: CallableRef,
|
|
1607
|
+
*,
|
|
1608
|
+
source: Path,
|
|
1609
|
+
components_yaml_dir: Path,
|
|
1610
|
+
image_overrides: Mapping[str, str] | None,
|
|
1611
|
+
unwrapped_schema: Mapping[str, Any],
|
|
1612
|
+
) -> dict[str, Any]:
|
|
1613
|
+
"""Build the ``local_from_python`` block for ONE traced ``@task`` call.
|
|
1614
|
+
|
|
1615
|
+
Every field that changes what ``regenerate_yaml`` produces at hydrate time
|
|
1616
|
+
belongs in this payload — the payload IS the component's identity (see
|
|
1617
|
+
:func:`_plan_task_sidecar`).
|
|
1618
|
+
|
|
1619
|
+
Args:
|
|
1620
|
+
ref: The ``@task`` callable reference for this call site.
|
|
1621
|
+
source: The ``@task`` source file (already existence-checked).
|
|
1622
|
+
components_yaml_dir: Directory the generated sidecar will live in;
|
|
1623
|
+
local paths are emitted relative to it.
|
|
1624
|
+
image_overrides: Optional compile-time ``--image ID=REF`` overrides.
|
|
1625
|
+
unwrapped_schema: Persisted unwrap schema for this call site, or an
|
|
1626
|
+
empty mapping when the task does not use ``@task(unwrap=...)``.
|
|
1627
|
+
|
|
1628
|
+
Returns:
|
|
1629
|
+
The ``local_from_python`` mapping for this call site.
|
|
1630
|
+
|
|
1631
|
+
Raises:
|
|
1632
|
+
CompileError: when ``image_id`` does not resolve, or a referenced
|
|
1633
|
+
``resolve_root`` / ``dependencies_from`` path is unreachable.
|
|
1634
|
+
"""
|
|
1635
|
+
local_from_python: dict[str, Any] = {}
|
|
1636
|
+
if ref._task_image is not None:
|
|
1637
|
+
local_from_python["image"] = ref._task_image
|
|
1638
|
+
elif ref._task_image_id is not None:
|
|
1639
|
+
resolved_image = resolve_image_id(ref._task_image_id, image_overrides)
|
|
1640
|
+
if resolved_image is None:
|
|
1641
|
+
raise CompileError(
|
|
1642
|
+
f"@task image_id={ref._task_image_id!r} on function "
|
|
1643
|
+
f"{ref._task_function_name!r} did not resolve to an image. "
|
|
1644
|
+
f"Pass --image {ref._task_image_id}=IMAGE to `tangle sdk pipelines compile`, "
|
|
1645
|
+
f"or register a default with register_image_id({ref._task_image_id!r}, IMAGE)."
|
|
1646
|
+
)
|
|
1647
|
+
local_from_python["image"] = resolved_image
|
|
1648
|
+
# Always pin the function name. Without it the hydrator defaults
|
|
1649
|
+
# to the file stem and extracts the wrong symbol.
|
|
1650
|
+
assert ref._task_function_name is not None
|
|
1651
|
+
local_from_python["function"] = ref._task_function_name
|
|
1652
|
+
if ref._task_mode is not None:
|
|
1653
|
+
local_from_python["mode"] = ref._task_mode
|
|
1654
|
+
if ref._task_resolve_root is not None:
|
|
1655
|
+
resolve_root = ref._task_resolve_root
|
|
1656
|
+
if not resolve_root.exists():
|
|
1657
|
+
raise CompileError(
|
|
1658
|
+
f"@task resolve_root is unreachable: {resolve_root}. "
|
|
1659
|
+
"Point resolve_root at an existing directory or drop it."
|
|
1660
|
+
)
|
|
1661
|
+
local_from_python["resolve_root"] = _relpath_posix(resolve_root, components_yaml_dir)
|
|
1662
|
+
if ref._task_dependencies_from is not None:
|
|
1663
|
+
deps = ref._task_dependencies_from
|
|
1664
|
+
if not deps.exists():
|
|
1665
|
+
raise CompileError(
|
|
1666
|
+
f"@task dependencies_from file is unreachable: {deps}. "
|
|
1667
|
+
"Point dependencies_from at an existing file or drop it."
|
|
1668
|
+
)
|
|
1669
|
+
local_from_python["dependencies_from"] = _relpath_posix(deps, components_yaml_dir)
|
|
1670
|
+
if unwrapped_schema:
|
|
1671
|
+
local_from_python["unwrapped_inputs"] = dict(unwrapped_schema)
|
|
1672
|
+
local_from_python["file"] = _relpath_posix(source, components_yaml_dir)
|
|
1673
|
+
return local_from_python
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
def _logical_module_id(source: Path, *, identity_root: Path) -> str:
|
|
1677
|
+
"""Derive a STABLE logical module namespace for a ``@task`` source file.
|
|
1678
|
+
|
|
1679
|
+
Runtime ``__module__`` is unusable as an identity: the compile driver
|
|
1680
|
+
imports the pipeline script under a throwaway
|
|
1681
|
+
``_tangle_user_pipeline_<uuid>`` module name, so a ``@task`` defined in the
|
|
1682
|
+
script would get a different "module" on every compile while a ``@task``
|
|
1683
|
+
imported from a sibling helper would get a real dotted name. The namespace
|
|
1684
|
+
is therefore derived from the source LAYOUT instead, which is what an
|
|
1685
|
+
import would have produced anyway:
|
|
1686
|
+
|
|
1687
|
+
* Walk up while the directory is a package (``__init__.py`` present) to
|
|
1688
|
+
build the dotted package chain — ``pkg_a/tasks.py`` -> ``pkg_a.tasks``,
|
|
1689
|
+
and ``pkg_a/__init__.py`` -> ``pkg_a`` (the ``__init__`` segment is the
|
|
1690
|
+
package itself, never a child module).
|
|
1691
|
+
* The first NON-package ancestor is the import root. Two identically named
|
|
1692
|
+
modules under different import roots are genuinely different modules, so
|
|
1693
|
+
that root is recorded as a ``<root>:`` prefix, expressed RELATIVE to
|
|
1694
|
+
``identity_root`` (never an absolute machine path).
|
|
1695
|
+
|
|
1696
|
+
Args:
|
|
1697
|
+
source: Absolute path of the ``@task`` source file.
|
|
1698
|
+
identity_root: Stable project anchor (the pipeline's own source
|
|
1699
|
+
directory) that the import root is expressed relative to.
|
|
1700
|
+
|
|
1701
|
+
Returns:
|
|
1702
|
+
``"<import-root-rel>:<dotted.module>"``, e.g. ``".:pkg_a.tasks"`` or
|
|
1703
|
+
``"./src:pipeline"``. Stable when the project is relocated or compiled
|
|
1704
|
+
into a different output directory.
|
|
1705
|
+
|
|
1706
|
+
Raises:
|
|
1707
|
+
CompileError: when no relative path to ``identity_root`` can be formed
|
|
1708
|
+
(see :func:`_relpath_posix`).
|
|
1709
|
+
"""
|
|
1710
|
+
parts: list[str] = []
|
|
1711
|
+
if source.stem != "__init__":
|
|
1712
|
+
parts.append(source.stem)
|
|
1713
|
+
directory = source.parent
|
|
1714
|
+
while (directory / "__init__.py").exists():
|
|
1715
|
+
parts.append(directory.name)
|
|
1716
|
+
parent = directory.parent
|
|
1717
|
+
if parent == directory: # filesystem root — stop rather than loop
|
|
1718
|
+
break
|
|
1719
|
+
directory = parent
|
|
1720
|
+
dotted = ".".join(reversed(parts))
|
|
1721
|
+
return f"{_relpath_posix(directory, identity_root)}:{dotted}"
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _emitted_form_rank(payload: Mapping[str, Any]) -> tuple[int, str]:
|
|
1725
|
+
"""Sort key selecting the canonical spelling of one generated component.
|
|
1726
|
+
|
|
1727
|
+
Several call sites can describe the SAME component with different emitted
|
|
1728
|
+
text — writing ``mode="inline"`` explicitly versus leaving the implicit
|
|
1729
|
+
default out. Ranking by key count first prefers the leanest spelling, so a
|
|
1730
|
+
pipeline that already omits a redundant default keeps its existing sidecar
|
|
1731
|
+
bytes when a sibling call site spells that default out.
|
|
1732
|
+
|
|
1733
|
+
Args:
|
|
1734
|
+
payload: An emitted ``local_from_python`` block.
|
|
1735
|
+
|
|
1736
|
+
Returns:
|
|
1737
|
+
``(key count, canonical JSON)`` — total and independent of call order.
|
|
1738
|
+
"""
|
|
1739
|
+
return (len(payload), _canonical_json(payload))
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def _task_component_identity(
|
|
1743
|
+
ref: CallableRef,
|
|
1744
|
+
*,
|
|
1745
|
+
source: Path,
|
|
1746
|
+
identity_root: Path,
|
|
1747
|
+
image_overrides: Mapping[str, str] | None,
|
|
1748
|
+
unwrapped_schema: Mapping[str, Any],
|
|
1749
|
+
) -> dict[str, Any]:
|
|
1750
|
+
"""Build the canonical dedup identity for one traced ``@task`` call.
|
|
1751
|
+
|
|
1752
|
+
The identity is the MODULE-QUALIFIED FUNCTION IDENTITY plus every
|
|
1753
|
+
generation-affecting option. Two call sites produce the same component —
|
|
1754
|
+
and therefore share one sidecar entry — exactly when this payload matches.
|
|
1755
|
+
|
|
1756
|
+
Paths inside the identity are anchored at ``identity_root`` (the pipeline's
|
|
1757
|
+
own source directory) rather than at the sidecar directory, so the identity
|
|
1758
|
+
is invariant under relocating the project or compiling into a different
|
|
1759
|
+
output directory, and never embeds an absolute machine path.
|
|
1760
|
+
|
|
1761
|
+
Args:
|
|
1762
|
+
ref: The ``@task`` callable reference for this call site.
|
|
1763
|
+
source: The ``@task`` source file.
|
|
1764
|
+
identity_root: Stable project anchor for path-derived identity parts.
|
|
1765
|
+
image_overrides: Optional compile-time ``--image ID=REF`` overrides.
|
|
1766
|
+
unwrapped_schema: Persisted unwrap schema, or an empty mapping.
|
|
1767
|
+
|
|
1768
|
+
Returns:
|
|
1769
|
+
A JSON-serialisable identity payload. Hash it with
|
|
1770
|
+
:func:`_stable_payload_hash`; render it with
|
|
1771
|
+
:func:`_task_identity_label` for diagnostics.
|
|
1772
|
+
|
|
1773
|
+
Raises:
|
|
1774
|
+
CompileError: see :func:`_local_from_python_payload` and
|
|
1775
|
+
:func:`_logical_module_id`.
|
|
1776
|
+
"""
|
|
1777
|
+
# ``__qualname__`` is forwarded onto the ref by @task. It distinguishes
|
|
1778
|
+
# same-named functions nested in different scopes within ONE module;
|
|
1779
|
+
# it equals the function name for the module-level authoring surface.
|
|
1780
|
+
qualname = getattr(ref, "__qualname__", None) or ref._task_function_name
|
|
1781
|
+
generation = _local_from_python_payload(
|
|
1782
|
+
ref,
|
|
1783
|
+
source=source,
|
|
1784
|
+
components_yaml_dir=identity_root,
|
|
1785
|
+
image_overrides=image_overrides,
|
|
1786
|
+
unwrapped_schema=unwrapped_schema,
|
|
1787
|
+
)
|
|
1788
|
+
# Normalise fields whose OMISSION is defined to mean a specific value, so
|
|
1789
|
+
# two spellings of one component do not hash apart. Only ``mode`` has such
|
|
1790
|
+
# an implicit default in the emitted contract: the hydrator reads
|
|
1791
|
+
# ``gen_config.get("mode", "inline")`` and ``CallableRef`` generates with
|
|
1792
|
+
# ``self._task_mode or "inline"``, so ``@task()`` and ``@task(mode="inline")``
|
|
1793
|
+
# regenerate byte-identical components.
|
|
1794
|
+
#
|
|
1795
|
+
# Audited and deliberately NOT normalised:
|
|
1796
|
+
# * ``dependencies_from`` — omission means "auto-discover next to the
|
|
1797
|
+
# source AT HYDRATE TIME". Compile-time discovery could disagree with the
|
|
1798
|
+
# hydrate-time layout, so an explicit path is not provably the same
|
|
1799
|
+
# component as an omission.
|
|
1800
|
+
# * ``resolve_root`` — not inert in inline mode: ``component_from_func``
|
|
1801
|
+
# emits a ``tangle_cli_generation_resolve_root`` annotation whenever it is
|
|
1802
|
+
# set, so it changes the generated component regardless of mode.
|
|
1803
|
+
# * ``image`` — omission means "whatever the generator defaults to", which
|
|
1804
|
+
# is not a value this layer can canonicalise.
|
|
1805
|
+
generation["mode"] = generation.get("mode") or "inline"
|
|
1806
|
+
return {
|
|
1807
|
+
"module": _logical_module_id(source, identity_root=identity_root),
|
|
1808
|
+
"qualname": qualname,
|
|
1809
|
+
"function": ref._task_function_name,
|
|
1810
|
+
# Generation config, anchored at the stable project root. This mirrors
|
|
1811
|
+
# the emitted block field-for-field so a new generation-affecting
|
|
1812
|
+
# option cannot be added to the sidecar without also splitting dedup.
|
|
1813
|
+
"generation": generation,
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _task_identity_label(identity_payload: Mapping[str, Any]) -> str:
|
|
1818
|
+
"""Human-readable ``<module>::<qualname>`` label for an identity payload.
|
|
1819
|
+
|
|
1820
|
+
Args:
|
|
1821
|
+
identity_payload: A :func:`_task_component_identity` payload.
|
|
1822
|
+
|
|
1823
|
+
Returns:
|
|
1824
|
+
A diagnostic label. Only the logical module/function identity is
|
|
1825
|
+
rendered — never image, path, or credential-bearing generation values.
|
|
1826
|
+
"""
|
|
1827
|
+
module = str(identity_payload.get("module", ""))
|
|
1828
|
+
_root, _sep, dotted = module.partition(":")
|
|
1829
|
+
qualname = identity_payload.get("qualname") or identity_payload.get("function")
|
|
1830
|
+
return f"{dotted or module}::{qualname}"
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
def _plan_task_sidecar(
|
|
1534
1834
|
task_refs: list[tuple[str, CallableRef]],
|
|
1535
1835
|
*,
|
|
1536
1836
|
components_yaml_dir: Path,
|
|
1837
|
+
identity_root: Path,
|
|
1537
1838
|
image_overrides: Mapping[str, str] | None = None,
|
|
1538
1839
|
unwrapped_input_keys: Mapping[str, dict[str, list[str]]] | None = None,
|
|
1539
|
-
) ->
|
|
1540
|
-
"""
|
|
1840
|
+
) -> TaskSidecarPlan:
|
|
1841
|
+
"""Plan the ``<stem>.components.yaml`` entries and per-task fragments.
|
|
1842
|
+
|
|
1843
|
+
Dedup is by GENERATED COMPONENT IDENTITY — module-qualified function
|
|
1844
|
+
identity plus every generation-affecting option, with implicit defaults
|
|
1845
|
+
canonicalised (``@task()`` and ``@task(mode="inline")`` are ONE component)
|
|
1846
|
+
— never by bare function name. Two call sites collapse into one entry only when the whole
|
|
1847
|
+
:func:`_task_component_identity` payload matches: logical module namespace,
|
|
1848
|
+
``__qualname__``, function name, image (explicit or resolved ``image_id``),
|
|
1849
|
+
mode, resolve_root, dependencies_from, the persisted unwrap schema, and the
|
|
1850
|
+
source file. Consequently:
|
|
1851
|
+
|
|
1852
|
+
* A shared helper such as ``run_dbt`` invoked with different task-level
|
|
1853
|
+
images or dependency files emits one entry PER distinct configuration,
|
|
1854
|
+
and each graph task is rewritten to its OWN fragment instead of silently
|
|
1855
|
+
inheriting the first call site's component.
|
|
1856
|
+
* ``pkg_a/tasks.py::run`` and ``pkg_b/tasks.py::run`` are different
|
|
1857
|
+
components and both get emitted. This is NOT an error: the bare function
|
|
1858
|
+
name is not an identity, so two packages may each define ``run``.
|
|
1859
|
+
* Repeated identical calls still dedup to exactly one entry.
|
|
1860
|
+
|
|
1861
|
+
Fragment naming:
|
|
1862
|
+
|
|
1863
|
+
* Exactly ONE identity for a function's readable base -> the legacy
|
|
1864
|
+
fragment (``run_dbt`` -> ``run-dbt``, or ``combine--<schema-hash>`` for
|
|
1865
|
+
an unwrapped task). Existing single-component pipelines keep
|
|
1866
|
+
byte-identical sidecars.
|
|
1867
|
+
* SEVERAL identities sharing one base -> ``<base>--<identity>`` for EVERY
|
|
1868
|
+
colliding variant; no arbitrary "first" variant keeps the unsuffixed
|
|
1869
|
+
name. ``identity`` is a SHA-256 prefix over the canonical JSON identity
|
|
1870
|
+
payload, so it is content-addressed (never :func:`hash`, never
|
|
1871
|
+
dict/API/trace ordering), stable across processes, invariant under
|
|
1872
|
+
project relocation and output-directory changes, and it never leaks
|
|
1873
|
+
image, path, or credential-bearing values into the fragment name.
|
|
1874
|
+
Colliding variants are also EMITTED in sorted fragment order so the
|
|
1875
|
+
sidecar text does not depend on which variant was traced first.
|
|
1541
1876
|
|
|
1542
1877
|
Args:
|
|
1543
1878
|
task_refs: Traced ``(task_id, CallableRef)`` records for ``@task``
|
|
1544
1879
|
calls in the pipeline graph.
|
|
1545
1880
|
components_yaml_dir: Directory where the generated sidecar will live;
|
|
1546
|
-
local paths are written relative to this directory.
|
|
1881
|
+
emitted local paths are written relative to this directory.
|
|
1882
|
+
identity_root: Stable project anchor for path-derived IDENTITY parts —
|
|
1883
|
+
the pipeline's OWN source directory. Required (not defaulted to
|
|
1884
|
+
``components_yaml_dir``) because anchoring identity at the output
|
|
1885
|
+
directory would make fragment names change when the same pipeline
|
|
1886
|
+
is compiled elsewhere, e.g. into the hidden submit bundle.
|
|
1547
1887
|
image_overrides: Optional compile-time ``--image ID=REF`` overrides.
|
|
1548
1888
|
unwrapped_input_keys: Optional trace metadata for ``@task(unwrap=...)``
|
|
1549
|
-
calls. When present,
|
|
1550
|
-
|
|
1551
|
-
|
|
1889
|
+
calls. When present, ``local_from_python.unwrapped_inputs`` is
|
|
1890
|
+
written so hydrate regenerates the same flattened input schema
|
|
1891
|
+
without call-site context.
|
|
1552
1892
|
|
|
1553
1893
|
Returns:
|
|
1554
|
-
|
|
1555
|
-
{
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
The ``function`` field is always emitted so hydrate's
|
|
1566
|
-
``regenerate_yaml`` extracts the right function: it otherwise defaults
|
|
1567
|
-
to the file STEM, which is wrong whenever the @task function name
|
|
1568
|
-
differs from the source filename (the common case).
|
|
1569
|
-
|
|
1570
|
-
Paths in ``local_from_python.{file,dependencies_from}`` are POSIX and
|
|
1571
|
-
relative to ``components_yaml_dir`` so the sidecar is portable: as
|
|
1572
|
-
long as the layout under that directory matches at compile- and
|
|
1573
|
-
hydrate-time, the paths resolve correctly.
|
|
1894
|
+
A :class:`TaskSidecarPlan`. Paths in
|
|
1895
|
+
``local_from_python.{file,dependencies_from,resolve_root}`` are POSIX
|
|
1896
|
+
and relative to ``components_yaml_dir`` so the bundle stays portable.
|
|
1897
|
+
|
|
1898
|
+
The component ``name`` is NOT emitted. A top-level ``name`` on a
|
|
1899
|
+
resolve entry means "resolve a published component by this name" to the
|
|
1900
|
+
hydrator (``PipelineHydrator._resolve_primary``), which would let a
|
|
1901
|
+
same-named library component silently win over this local ``@task``.
|
|
1902
|
+
The component's name comes from its source docstring
|
|
1903
|
+
(``Metadata: Name:``) at hydrate time, read by ``regenerate_yaml``.
|
|
1574
1904
|
|
|
1575
1905
|
Raises:
|
|
1576
|
-
CompileError: when
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
unreachable, or when a relative path cannot be formed (see
|
|
1580
|
-
:func:`_relpath_posix`).
|
|
1906
|
+
CompileError: when a referenced local file (the ``@task`` source or its
|
|
1907
|
+
``dependencies_from`` / ``resolve_root``) is unreachable, or when a
|
|
1908
|
+
relative path cannot be formed (see :func:`_relpath_posix`).
|
|
1581
1909
|
"""
|
|
1582
|
-
|
|
1583
|
-
|
|
1910
|
+
checked_sources: set[Path] = set()
|
|
1911
|
+
# base fragment -> identity digest -> (emitted payload, identity payload).
|
|
1912
|
+
variants: dict[str, dict[str, tuple[dict[str, Any], dict[str, Any]]]] = {}
|
|
1913
|
+
# (base, identity digest) -> the legacy single-variant fragment.
|
|
1914
|
+
legacy_fragments: dict[tuple[str, str], str] = {}
|
|
1915
|
+
task_identities: list[tuple[str, str, str]] = []
|
|
1916
|
+
|
|
1584
1917
|
for task_id, ref in task_refs:
|
|
1585
1918
|
source = ref._task_source_path
|
|
1586
1919
|
if source is None: # defensive — only @task refs are recorded here
|
|
1587
1920
|
continue
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
prior_source = seen_fragments.get(fragment)
|
|
1592
|
-
if prior_source is not None:
|
|
1593
|
-
# Already emitted this fragment. Fine when it is the SAME source
|
|
1594
|
-
# (the same @task called from multiple sites). A DIFFERENT
|
|
1595
|
-
# source sharing the function name would silently collide on the
|
|
1596
|
-
# resolve:// fragment, so reject it loudly.
|
|
1597
|
-
if prior_source != source:
|
|
1921
|
+
if source not in checked_sources:
|
|
1922
|
+
if not source.exists():
|
|
1598
1923
|
raise CompileError(
|
|
1599
|
-
"
|
|
1600
|
-
|
|
1601
|
-
"
|
|
1602
|
-
"name (the function name becomes the resolve:// fragment)."
|
|
1924
|
+
f"@task source file is unreachable: {source}. Compile into "
|
|
1925
|
+
"the pipeline source directory, or reference the component "
|
|
1926
|
+
"with an absolute file://, gs://, or resolve:// URL."
|
|
1603
1927
|
)
|
|
1604
|
-
|
|
1928
|
+
checked_sources.add(source)
|
|
1605
1929
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1930
|
+
unwrapped_schema = _unwrapped_schema_for_task(ref, task_id, unwrapped_input_keys)
|
|
1931
|
+
base = _task_fragment_base(ref)
|
|
1932
|
+
emitted = _local_from_python_payload(
|
|
1933
|
+
ref,
|
|
1934
|
+
source=source,
|
|
1935
|
+
components_yaml_dir=components_yaml_dir,
|
|
1936
|
+
image_overrides=image_overrides,
|
|
1937
|
+
unwrapped_schema=unwrapped_schema,
|
|
1938
|
+
)
|
|
1939
|
+
identity_payload = _task_component_identity(
|
|
1940
|
+
ref,
|
|
1941
|
+
source=source,
|
|
1942
|
+
identity_root=identity_root,
|
|
1943
|
+
image_overrides=image_overrides,
|
|
1944
|
+
unwrapped_schema=unwrapped_schema,
|
|
1945
|
+
)
|
|
1946
|
+
identity = _stable_payload_hash(identity_payload)
|
|
1947
|
+
by_identity = variants.setdefault(base, {})
|
|
1948
|
+
previous = by_identity.get(identity)
|
|
1949
|
+
if previous is None:
|
|
1950
|
+
by_identity[identity] = (emitted, identity_payload)
|
|
1951
|
+
elif _emitted_form_rank(emitted) < _emitted_form_rank(previous[0]):
|
|
1952
|
+
# Same component, different SPELLING (e.g. one call site omits
|
|
1953
|
+
# ``mode`` while another writes ``mode="inline"``). Pick the
|
|
1954
|
+
# canonical representative rather than whichever was traced first,
|
|
1955
|
+
# so the sidecar text does not depend on call order.
|
|
1956
|
+
by_identity[identity] = (emitted, previous[1])
|
|
1957
|
+
legacy_fragments.setdefault((base, identity), _fragment_for_task(ref, unwrapped_schema))
|
|
1958
|
+
task_identities.append((task_id, base, identity))
|
|
1612
1959
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if ref._task_mode is not None:
|
|
1631
|
-
local_from_python["mode"] = ref._task_mode
|
|
1632
|
-
if ref._task_resolve_root is not None:
|
|
1633
|
-
resolve_root = ref._task_resolve_root
|
|
1634
|
-
if not resolve_root.exists():
|
|
1635
|
-
raise CompileError(
|
|
1636
|
-
f"@task resolve_root is unreachable: {resolve_root}. "
|
|
1637
|
-
"Point resolve_root at an existing directory or drop it."
|
|
1638
|
-
)
|
|
1639
|
-
local_from_python["resolve_root"] = _relpath_posix(resolve_root, components_yaml_dir)
|
|
1640
|
-
if ref._task_dependencies_from is not None:
|
|
1641
|
-
deps = ref._task_dependencies_from
|
|
1642
|
-
if not deps.exists():
|
|
1960
|
+
entries: dict[str, Any] = {}
|
|
1961
|
+
fragment_by_identity: dict[tuple[str, str], str] = {}
|
|
1962
|
+
identity_labels: dict[str, str] = {}
|
|
1963
|
+
for base, by_identity in variants.items():
|
|
1964
|
+
if len(by_identity) == 1:
|
|
1965
|
+
identity, (emitted, _identity_payload) = next(iter(by_identity.items()))
|
|
1966
|
+
named = [(legacy_fragments[(base, identity)], identity, emitted)]
|
|
1967
|
+
else:
|
|
1968
|
+
# EVERY colliding variant is suffixed — the trace-order "first" one
|
|
1969
|
+
# does not get to keep the bare name — and they are emitted in
|
|
1970
|
+
# sorted order so the sidecar text is call-order independent.
|
|
1971
|
+
named = sorted(
|
|
1972
|
+
(f"{base}--{identity}", identity, emitted)
|
|
1973
|
+
for identity, (emitted, _identity_payload) in by_identity.items()
|
|
1974
|
+
)
|
|
1975
|
+
for fragment, identity, emitted in named:
|
|
1976
|
+
if fragment in entries: # defensive — digests are content-addressed
|
|
1643
1977
|
raise CompileError(
|
|
1644
|
-
f"
|
|
1645
|
-
"
|
|
1978
|
+
f"internal error: sidecar fragment {fragment!r} was generated twice; "
|
|
1979
|
+
f"colliding @task identities: {identity_labels[fragment]!r} and "
|
|
1980
|
+
f"{_task_identity_label(by_identity[identity][1])!r}."
|
|
1646
1981
|
)
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
# the hydrator (PipelineHydrator._resolve_primary), which would let a
|
|
1656
|
-
# same-named library component silently win over this local @task. The
|
|
1657
|
-
# component's name comes from its source docstring (``Metadata: Name:``)
|
|
1658
|
-
# at hydrate time, read by regenerate_yaml.
|
|
1659
|
-
entry: dict[str, Any] = {"local_from_python": local_from_python}
|
|
1660
|
-
entries[fragment] = entry
|
|
1661
|
-
return entries
|
|
1982
|
+
fragment_by_identity[(base, identity)] = fragment
|
|
1983
|
+
identity_labels[fragment] = _task_identity_label(by_identity[identity][1])
|
|
1984
|
+
entries[fragment] = {"local_from_python": emitted}
|
|
1985
|
+
|
|
1986
|
+
fragment_by_task = {
|
|
1987
|
+
task_id: fragment_by_identity[(base, identity)] for task_id, base, identity in task_identities
|
|
1988
|
+
}
|
|
1989
|
+
return TaskSidecarPlan(entries=entries, fragment_by_task=fragment_by_task)
|
|
1662
1990
|
|
|
1663
1991
|
|
|
1664
1992
|
def _rewrite_task_componentref_urls(
|
|
1665
1993
|
*,
|
|
1666
1994
|
body_dict: dict[str, Any],
|
|
1667
|
-
|
|
1995
|
+
fragment_by_task: Mapping[str, str],
|
|
1668
1996
|
components_yaml_name: str,
|
|
1669
|
-
unwrapped_input_keys: Mapping[str, dict[str, list[str]]] | None = None,
|
|
1670
1997
|
) -> None:
|
|
1671
1998
|
"""Rewrite each @task task's ``componentRef`` to a pure resolve URL.
|
|
1672
1999
|
|
|
1673
2000
|
Args:
|
|
1674
2001
|
body_dict: Emitted dehydrated pipeline body to mutate in place.
|
|
1675
|
-
|
|
1676
|
-
|
|
2002
|
+
fragment_by_task: ``TaskSidecarPlan.fragment_by_task`` — the SAME map
|
|
2003
|
+
used to emit the sidecar, so a task whose component differs from a
|
|
2004
|
+
same-function sibling points at its own fragment.
|
|
1677
2005
|
components_yaml_name: Filename of the generated components sidecar.
|
|
1678
|
-
unwrapped_input_keys: Optional trace metadata for ``@task(unwrap=...)``
|
|
1679
|
-
calls. The same persisted schema used for sidecar emission is used
|
|
1680
|
-
here to compute schema-hashed fragments consistently.
|
|
1681
2006
|
|
|
1682
2007
|
Returns:
|
|
1683
2008
|
None. ``body_dict`` is mutated so each task's componentRef becomes
|
|
1684
2009
|
``{"url": "resolve://./<components_yaml_name>#<fragment>"}``.
|
|
2010
|
+
|
|
2011
|
+
Raises:
|
|
2012
|
+
CompileError: when a traced ``@task`` has no matching graph task.
|
|
1685
2013
|
"""
|
|
1686
2014
|
tasks = body_dict.get("implementation", {}).get("graph", {}).get("tasks", {})
|
|
1687
|
-
for task_id,
|
|
1688
|
-
unwrapped_schema = _unwrapped_schema_for_task(ref, task_id, unwrapped_input_keys)
|
|
1689
|
-
fragment = _fragment_for_task(ref, unwrapped_schema)
|
|
2015
|
+
for task_id, fragment in fragment_by_task.items():
|
|
1690
2016
|
url = f"resolve://./{components_yaml_name}#{fragment}"
|
|
1691
2017
|
if task_id not in tasks:
|
|
1692
2018
|
raise CompileError(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/args_container.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_from_func.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_generator.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_inspector.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/component_publisher.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/components_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/hydration_trust.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/module_bundler.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/__init__.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/codegen.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/openapi/parser.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_dehydrator.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_hydrator.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_details.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_manager.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_run_search.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_runner.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_runs_cli.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_spec_utils.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/pipeline_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/cfg.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/emit.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/errors.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/graph.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/ids.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/raw.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/ref.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/task.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/trace.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/python_pipeline/types.py
RENAMED
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schema_validation.py
RENAMED
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/schemas/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/packages/tangle-cli/src/tangle_cli/version_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tangle_cli-0.1.13 → tangle_cli-0.1.14}/skills/tangent/references/example-scenarios/INDEX.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|