feat(python-pipeline): support dynamic_secret runtime-secret arguments#32
Conversation
Adds a narrow Python-pipeline authoring primitive for runtime secret
arguments, porting the reusable infrastructure from the downstream
tangle-deploy repo (discovery #32848) into OSS tangle-cli:
- dynamic_secret(name) → a DynamicData wrapper emitted as
{"dynamicData": {"secret": {"name": ...}}}.
- The emitter dispatches DynamicData before the non-string-constant
rejection, so a dynamicData argument is no longer rejected as a bare
dict constant.
- The dehydrated-pipeline schema gains a strict DynamicDataArgument
(non-empty secret.name only; arbitrary dynamic data stays rejected).
The runnable pipeline_schema.json is left untouched: it already carries
an API-generated, intentionally permissive DynamicDataArgument that
mirrors the real runtime contract (secrets, run IDs, loop indices).
- schema_validation's shape detector recognizes the dynamicData wrapper.
Tests cover emit shape, a compiling fixture, strict-schema rejection of
malformed secret references, and the non-empty-name guard. Full suite:
780 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| @dataclass(frozen=True) | ||
| class DynamicData: | ||
| """A task argument value emitted as ``{"dynamicData": ...}``.""" | ||
|
|
||
| value: Mapping[str, Any] |
There was a problem hiding this comment.
(AI-assisted) [nit] DynamicData is a general wrapper but the schema only accepts the secret.name shape. value: Mapping[str, Any] accepts any mapping, and emit renders it verbatim as {"dynamicData": value} — but the dehydrated schema (DynamicSecretData, additionalProperties:false, required:["secret"]) only allows the secret shape. So a DynamicData({"someOtherKind": ...}) would emit yet fail compile-validation, even though the runtime resolves other dynamic-data kinds (run IDs, loop indices — per this PR's own test docstring). Consider either narrowing the class to secrets-only, or broadening the schema to match the documented runtime generality, so the type doesn't promise more than the schema enforces.
Minor also: @dataclass(frozen=True) with a dict field leaves instances effectively unhashable (unlike sibling Raw, which defines __hash__). Harmless today (never hashed), just inconsistent.
There was a problem hiding this comment.
Good catch — fixed in 4949b4a. Rather than loosen the strict schema, I documented DynamicData on the class as an internal emit primitive (kept out of __all__): its value stays a general mapping since the runtime resolves several kinds, but the only public producer dynamic_secret always builds the schema-valid secret shape, and any other shape fails closed at compile validation until the constructor + schema are extended together. So the public API never promises more than the schema enforces.
On the hash: since it holds a dict and is never hashed, I left it unhashable rather than fabricate a canonical form — happy to add a __hash__ for parity with Raw if you would prefer the consistency.
| "registered", | ||
| "ref", | ||
| "raw", | ||
| "dynamic_secret", |
There was a problem hiding this comment.
(AI-assisted) [nit] Only dynamic_secret is re-exported here, not DynamicData. The PR description says "Both re-exported from tangle_cli.python_pipeline (added to __all__)", but DynamicData — the type users would annotate or isinstance-check against — is reachable only via the .dynamic_data submodule. Either add DynamicData to the import + __all__ too, or adjust the description.
There was a problem hiding this comment.
Kept DynamicData internal (only dynamic_secret is in __all__) and corrected the PR description to match — the "both re-exported" line was the inaccurate part. DynamicData is only an emit primitive, so users never need to annotate or isinstance-check against it.
Volv-G
left a comment
There was a problem hiding this comment.
(AI-assisted) Clean, narrow, well-tested port of the dynamic_secret / DynamicData primitive — emit dispatch, strict dehydrated schema (secret.name, non-empty, no extra props), and good negative tests. Left two nits inline (class generality vs. schema narrowness; DynamicData not top-level exported). Neither blocks. LGTM.
Address review nit: the class read as a general public wrapper whose `value: Mapping` promised more than the strict dehydrated schema (secret.name only) enforces. Clarify on the class that DynamicData is an internal emit primitive (not in __all__), that the only public producer `dynamic_secret` always builds the schema-valid secret shape, and that any other kind fails closed at compile validation until constructor and schema are extended together — so the public API never over-promises. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Ports the
dynamic_secret/DynamicDataauthoring primitive into OSStangle-cliso Python-authored pipelines can express a runtime-resolved secret argument natively, without reaching into a downstream package.Tangle runnable arguments can be literals, graph/task edges, or dynamic data resolved by the runtime (e.g. a secret reference). Until now the Python emitter had no explicit wrapper for the dynamic-data case, so author code couldn't request a
dynamicDataargument without opening support for arbitrary dict constants. This adds a narrow, typed primitive for exactly that.Surface
Emits:
dynamic_secret(name)— the public authoring entry point; raisesValueErroron a non-empty-string name. Re-exported fromtangle_cli.python_pipeline(added to__all__, not# noqa).DynamicData(value)— internal frozen-dataclass emit primitive, emitted as{"dynamicData": <value>}. Deliberately not part of the public surface:valueis a general mapping (the runtime resolves several kinds — secrets, run IDs, loop indices), but the only public producer today isdynamic_secret, which always builds the schema-validsecretshape. Any other shape fails closed at compile validation until the constructor and schema are extended together, so the public API never promises more than the schema enforces.Changes
python_pipeline/dynamic_data.py— newDynamicData+dynamic_secret.python_pipeline/__init__.py— re-exportdynamic_secretvia__all__(DynamicDatastays internal).python_pipeline/emit.py— emitDynamicDataargs as{"dynamicData": ...}.schema_validation.py+schemas/dehydrated_pipeline_schema.json— the dehydrated (strict) schema now recognizesdynamicData.secret.name. The runnable (pipeline_schema.json, API-generated) schema stays intentionally permissive.tests/test_python_pipeline_dynamic_data.py(new, 79 lines) covers the wrapper contract, emit shape, validation, and the non-empty-name guard; one import-surface assertion added totests/test_python_pipeline_dsl.py.Testing
ruffclean.🤖 Generated with Claude Code