Python and Bash authoring parity¶
For runner authors in either language. One table, below, is the normative list of the authoring surface: every operation a step can perform, spelled in Python and in Bash, with the protocol artifact it produces. The prose guides — Native runner helpers for Python and Native Bash runner API for Bash — teach the two languages; this page is what they must both agree with.
The table is enforced. tests/test_docs_parity.py parses it and fails the build
when a listed Python member does not exist, when a listed Bash function is not
defined in the packaged library, or when a public member of Runner or
Attempt, or a function of shell/httk-workflow.sh, is missing a row. An
authoring feature that is not in this table does not exist as far as the
documentation is concerned, and adding one to the code means adding a row here.
Both languages perform their work through exactly one implementation — the Bash
functions are thin calls into httk.workflow.shell_bridge, which drives
the same Attempt object the Python SDK exposes — so a Bash runner and a Python
runner publish the same bytes for the same campaign. That is a tested property,
not an aspiration: tests/test_parity.py runs one campaign twice, once in each
language, and compares everything both left behind.
How to read the table¶
Python names a member of
Runner,Attempt, or a type reached fromhttk.workflow— either exported at its root or from one of its named submodules, in which case the cell spells the submodule (for exampleprotocol.JobSpecorruntime_utils.render_template).—means the operation has no Python member of its own, because Python reaches it some other way (an attribute, an exception, the standard library).Bash names a function of the packaged
shell/httk-workflow.sh, sometimes with the option that selects the behaviour of the row; only the function name is normative.—means the operation has no Bash function, because Bash reaches it through an exported environment variable instead — named in bold in that case.Protocol artifact is what the operation writes or reads on disk.
nonemeans the operation is entirely in-process.
The table¶
Python |
Bash |
Semantics |
Protocol artifact |
|---|---|---|---|
|
|
Declare the workflow, and in Bash its complete step set, before any step runs. |
none; registration is in-process |
|
— |
The name of the workflow this runner implements. |
the |
|
step_<name> function |
Register one handler for one step; the name is the function’s unless overridden. |
none |
|
— |
Every registered step name, against which every step name an outcome publishes is checked. |
|
|
|
Print this runner’s own description and touch nothing else. |
one |
|
|
Dispatch the step the manager asked for, and turn every ending of it into exactly one outcome. |
the published |
|
|
A handler that returns without publishing an outcome is reported as such rather than left ambiguous. |
|
|
|
A step this runner does not implement is reported, with the registered steps named. |
|
|
|
A handler that raises, or dies under set -e, discards its unpublished draft, leaves a breadcrumb, and lets the manager’s retry policy own what happens next. |
|
|
|
Bind this process to its attempt and replay every workdir batch an earlier attempt sealed but did not apply. |
reads the |
|
|
The manager-written identity and restart evidence of this attempt; one field, or the whole object. |
|
|
|
The step this attempt runs. Bash also exports it for the handler. |
|
|
— |
The attempt control directory, where the outcome is published. |
$HTTK_WORKFLOW_CONTROL_DIR |
|
— |
The immutable job payload directory. |
$HTTK_WORKFLOW_JOB_DIR |
|
— |
The directory the step does its work in. |
$HTTK_WORKFLOW_WORKDIR |
|
— |
The workspace root this job belongs to. |
$HTTK_WORKFLOW_WORKSPACE_DIR |
|
— |
The job’s published transactional data, or |
$HTTK_WORKFLOW_DATA_DIR |
|
|
The workspace durability mode, threaded into every artifact the attempt publishes so an outcome, transaction, or child is synchronized before it is renamed authoritative. Neither SDK needs a call to act on it. |
|
|
— |
The immutable job definition as a typed value. |
|
|
|
The application-defined |
|
|
|
One input, with an optional default; without one, a missing input raises |
|
|
|
One application setting, resolved most-specific first: the job’s |
|
|
— |
The job’s private JSON state mapping, surviving every advance, every retry, and every isolated workdir. |
|
|
|
Read the whole state document, or in Bash one key; an unset key exits 1. |
|
|
|
Store one JSON value in one atomic replace. |
|
|
|
Remove one key, reporting whether it was present. |
|
|
|
Write several keys in one atomic replace. |
|
|
— |
The append-only structured run log of this workdir. |
|
|
|
Append one ordinary evidence event. |
one |
|
|
Append one event meant to be read first when the job is inspected. |
one |
|
|
Append one event with whole files attached by content and digest. |
one |
— |
|
Write one timestamped |
the attempt’s captured stderr |
|
|
Record the workflow declaration this job observed, carried verbatim; repeating it overwrites. |
|
|
|
Read one declaration back: observed first, then the one |
|
|
— |
Run an argv array in the workdir and terminate its whole process group on timeout, returning a |
none; the result is in memory |
|
|
Run an argv array under checkers and inactivity watches, writing the authoritative report of what it did. |
|
|
|
Stage one file or tree into the job’s transactional data; the manager applies it exactly once when the outcome commits. |
an operation of |
|
|
Stage one removal from the job’s transactional data. |
an operation of |
|
|
Group workdir changes so that an interrupted apply is replayed on the next attempt instead of being left half-done. |
|
|
|
Describe a synthesized child job by its step and inputs; everything else follows the spawning job. |
one entry of |
|
|
Which runner a synthesized child runs: |
|
|
|
Register one child under a mandatory unique label — a |
|
|
|
The children observed by the join that started this activation; empty when no join did, so it can be read unconditionally. |
|
|
|
One observed child by label: its state, identity, failure, and absolute payload, workdir and data paths. |
|
|
|
Publish a new activation of this job at another step, optionally merging state first so the next step finds what decided to run it. |
outcome action |
|
|
Wait for the children spawned on this attempt, then run a step. |
outcome action |
|
|
Publish the successful completion of this job. |
outcome action |
|
|
Publish a structured terminal failure: |
outcome action |
|
|
Ask for another attempt of this same activation. |
outcome action |
|
|
Pause this job until an operator resumes it. |
outcome action |
|
— |
Whether this attempt already published; a second outcome is refused before anything of the first is disturbed. |
the existence of |
|
|
The complete member set of a job definition, including the runner reference and the inputs. |
|
|
|
Write |
|
— |
|
Run several bridge commands, one per line, in one interpreter start; a batch stops at its first failing line. |
none |
|
|
Evaluate one arithmetic expression without a shell. |
none |
|
|
Render one template file with one JSON value document. |
the named output file |
|
|
Compress named files with |
the compressed files |
|
|
Decompress named files, optionally removing the sources. |
the decompressed files |
Exit-code discipline¶
Python raises and returns typed values; Bash cannot, so every Bash function reports one of three statuses, and an absent answer never looks like a broken call:
Status |
Meaning |
Python equivalent |
|---|---|---|
|
the call succeeded |
the return value |
|
the answer is legitimately absent: an unset state key, a missing input without a default, a null child field, a child that was not observed |
|
|
the call is refused: bad usage, a protocol violation such as a second terminal call, or a corrupt attempt context |
|
The functions that run a program report the classified outcome of that program
instead: 22 for a nonzero exit, 124 for a timeout whose process group was
terminated, 125 when a checker or diagnostic stopped it — which is also what
the manager’s launcher reports for a runner it could not start at all. The VASP
functions add 20, 21, and 3; see Native Bash runner API.
httk_workflow_main owns the exit status of a Bash runner, which is why the
outcome functions return rather than exit. Runner.main owns it in Python, and
returns 0 for every ending it turned into an outcome.
Every ending is an outcome¶
The three rows above about no_outcome, unknown_step, and error.json are the
same guarantee stated three ways, and it is the reason the two SDKs can be
compared byte for byte at all:
A handler that returns without publishing is not silently successful. The attempt is failed with code
no_outcomenaming the step.A step the runner does not implement is not a crash. The attempt is failed with code
unknown_step, naming the steps that are registered.A handler that raises — or, in Bash, dies at a failing command under
set -e— publishes nothing at all. Its unpublished draft is removed, so no half-outcome survives, anderror.jsonrecords the exception, the message, and the traceback (in Bash, the file, line, and command the handler died on). The exception then reaches the manager, whose retry policy decides what happens next.
Because the draft is removed rather than published, a step that spawned three children and then crashed spawned nothing: the children exist only if the outcome that creates them was published.