daedalus-flow 0.1.0__py3-none-any.whl
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.
- daedalus/__init__.py +12 -0
- daedalus/cli/__init__.py +9 -0
- daedalus/cli/app.py +86 -0
- daedalus/cli/chrome.py +28 -0
- daedalus/cli/commands/__init__.py +1 -0
- daedalus/cli/commands/_outcome.py +65 -0
- daedalus/cli/commands/example.py +97 -0
- daedalus/cli/commands/flow.py +126 -0
- daedalus/cli/commands/lab/__init__.py +521 -0
- daedalus/cli/commands/lab/_prefect.py +49 -0
- daedalus/cli/commands/lab/_validate.py +381 -0
- daedalus/cli/commands/module.py +315 -0
- daedalus/cli/console.py +85 -0
- daedalus/cli/render/__init__.py +83 -0
- daedalus/cli/render/_ascii_dag.py +425 -0
- daedalus/cli/render/_base.py +97 -0
- daedalus/cli/render/_topology.py +586 -0
- daedalus/cli/render/_workflow.py +107 -0
- daedalus/cli/strings.py +495 -0
- daedalus/core/__init__.py +7 -0
- daedalus/core/dag.py +180 -0
- daedalus/core/engine/__init__.py +23 -0
- daedalus/core/engine/_import_probe.py +45 -0
- daedalus/core/engine/_module_runner.py +95 -0
- daedalus/core/engine/isolation.py +602 -0
- daedalus/core/engine/local/__init__.py +29 -0
- daedalus/core/engine/local/_engine.py +373 -0
- daedalus/core/engine/local/_finalize.py +92 -0
- daedalus/core/engine/local/_instance.py +441 -0
- daedalus/core/engine/nix/flake.lock +99 -0
- daedalus/core/engine/nix/flake.nix +76 -0
- daedalus/core/engine/prefect.py +232 -0
- daedalus/core/engine/protocol.py +92 -0
- daedalus/core/engine/scheduler.py +152 -0
- daedalus/core/engine/step.py +192 -0
- daedalus/core/engine/subprocess_runner.py +526 -0
- daedalus/core/flights.py +268 -0
- daedalus/core/lineage/__init__.py +55 -0
- daedalus/core/lineage/_io.py +238 -0
- daedalus/core/lineage/_schema.py +305 -0
- daedalus/core/outcomes.py +228 -0
- daedalus/core/paths.py +77 -0
- daedalus/core/recipe/__init__.py +61 -0
- daedalus/core/recipe/_parse.py +339 -0
- daedalus/core/recipe/_plan.py +94 -0
- daedalus/core/recipe/_validate.py +130 -0
- daedalus/core/topology.py +161 -0
- daedalus/core/walks/__init__.py +65 -0
- daedalus/core/walks/_arithmetic.py +89 -0
- daedalus/core/walks/_config.py +363 -0
- daedalus/core/walks/_propagation.py +474 -0
- daedalus/core/walks/_shared.py +103 -0
- daedalus/examples/__init__.py +1 -0
- daedalus/examples/complex/input/start.json +3 -0
- daedalus/examples/complex/lab.yaml +64 -0
- daedalus/examples/complex/modules/branch_x/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/branch_x/main.py +29 -0
- daedalus/examples/complex/modules/branch_x/requirements.txt +1 -0
- daedalus/examples/complex/modules/branch_y/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/branch_y/main.py +29 -0
- daedalus/examples/complex/modules/branch_y/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_region/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_region/main.py +32 -0
- daedalus/examples/complex/modules/collect_region/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_sib_a/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_sib_a/main.py +29 -0
- daedalus/examples/complex/modules/collect_sib_a/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_sib_b/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_sib_b/main.py +29 -0
- daedalus/examples/complex/modules/collect_sib_b/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_solo/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_solo/main.py +32 -0
- daedalus/examples/complex/modules/collect_solo/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_x/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_x/main.py +29 -0
- daedalus/examples/complex/modules/collect_x/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_x_deep/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_x_deep/main.py +29 -0
- daedalus/examples/complex/modules/collect_x_deep/requirements.txt +1 -0
- daedalus/examples/complex/modules/collect_y/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/collect_y/main.py +29 -0
- daedalus/examples/complex/modules/collect_y/requirements.txt +1 -0
- daedalus/examples/complex/modules/emit/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/emit/main.py +29 -0
- daedalus/examples/complex/modules/emit/requirements.txt +1 -0
- daedalus/examples/complex/modules/fan/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/fan/main.py +29 -0
- daedalus/examples/complex/modules/fan/requirements.txt +1 -0
- daedalus/examples/complex/modules/join_pair/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/join_pair/main.py +29 -0
- daedalus/examples/complex/modules/join_pair/requirements.txt +1 -0
- daedalus/examples/complex/modules/merge_flight/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/merge_flight/main.py +32 -0
- daedalus/examples/complex/modules/merge_flight/requirements.txt +1 -0
- daedalus/examples/complex/modules/pair_left/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/pair_left/main.py +29 -0
- daedalus/examples/complex/modules/pair_left/requirements.txt +1 -0
- daedalus/examples/complex/modules/pair_right/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/pair_right/main.py +29 -0
- daedalus/examples/complex/modules/pair_right/requirements.txt +1 -0
- daedalus/examples/complex/modules/post/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/post/main.py +29 -0
- daedalus/examples/complex/modules/post/requirements.txt +1 -0
- daedalus/examples/complex/modules/prep/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/prep/main.py +29 -0
- daedalus/examples/complex/modules/prep/requirements.txt +1 -0
- daedalus/examples/complex/modules/report/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/report/main.py +29 -0
- daedalus/examples/complex/modules/report/requirements.txt +1 -0
- daedalus/examples/complex/modules/sib_left/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/sib_left/main.py +29 -0
- daedalus/examples/complex/modules/sib_left/requirements.txt +1 -0
- daedalus/examples/complex/modules/sib_right/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/sib_right/main.py +29 -0
- daedalus/examples/complex/modules/sib_right/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_deep/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_deep/main.py +29 -0
- daedalus/examples/complex/modules/x_deep/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_deep_left/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_deep_left/main.py +29 -0
- daedalus/examples/complex/modules/x_deep_left/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_deep_right/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_deep_right/main.py +29 -0
- daedalus/examples/complex/modules/x_deep_right/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_deep_step/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_deep_step/main.py +29 -0
- daedalus/examples/complex/modules/x_deep_step/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_leaf/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_leaf/main.py +29 -0
- daedalus/examples/complex/modules/x_leaf/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_mid/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_mid/main.py +29 -0
- daedalus/examples/complex/modules/x_mid/requirements.txt +1 -0
- daedalus/examples/complex/modules/x_mid_tail/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/x_mid_tail/main.py +29 -0
- daedalus/examples/complex/modules/x_mid_tail/requirements.txt +1 -0
- daedalus/examples/complex/modules/y_leaf/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/y_leaf/main.py +29 -0
- daedalus/examples/complex/modules/y_leaf/requirements.txt +1 -0
- daedalus/examples/complex/modules/y_mid/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/y_mid/main.py +29 -0
- daedalus/examples/complex/modules/y_mid/requirements.txt +1 -0
- daedalus/examples/complex/modules/y_mid_tail/dae-module.yaml +1 -0
- daedalus/examples/complex/modules/y_mid_tail/main.py +29 -0
- daedalus/examples/complex/modules/y_mid_tail/requirements.txt +1 -0
- daedalus/examples/demo/input/targets.csv +4 -0
- daedalus/examples/demo/lab.yaml +29 -0
- daedalus/examples/demo/modules/compare_methods/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/compare_methods/expected/comparison.json +6 -0
- daedalus/examples/demo/modules/compare_methods/main.py +47 -0
- daedalus/examples/demo/modules/compare_methods/requirements.txt +1 -0
- daedalus/examples/demo/modules/emit_targets/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/emit_targets/expected/targets.json +23 -0
- daedalus/examples/demo/modules/emit_targets/main.py +26 -0
- daedalus/examples/demo/modules/emit_targets/requirements.txt +1 -0
- daedalus/examples/demo/modules/fetch_data/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/fetch_data/expected/folded.json +811 -0
- daedalus/examples/demo/modules/fetch_data/main.py +41 -0
- daedalus/examples/demo/modules/fetch_data/requirements.txt +1 -0
- daedalus/examples/demo/modules/fit_mcmc/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/fit_mcmc/expected/posterior.json +2010 -0
- daedalus/examples/demo/modules/fit_mcmc/main.py +43 -0
- daedalus/examples/demo/modules/fit_mcmc/requirements.txt +1 -0
- daedalus/examples/demo/modules/fit_nested/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/fit_nested/expected/posterior.json +2010 -0
- daedalus/examples/demo/modules/fit_nested/main.py +41 -0
- daedalus/examples/demo/modules/fit_nested/requirements.txt +1 -0
- daedalus/examples/demo/modules/summarize_population/dae-module.yaml +2 -0
- daedalus/examples/demo/modules/summarize_population/expected/summary.json +25 -0
- daedalus/examples/demo/modules/summarize_population/main.py +27 -0
- daedalus/examples/demo/modules/summarize_population/requirements.txt +1 -0
- daedalus/examples/demo/tutorial.ipynb +224 -0
- daedalus/examples/demo/tutorial.py +107 -0
- daedalus/examples/ensemble/input/targets.csv +11 -0
- daedalus/examples/ensemble/lab.yaml +13 -0
- daedalus/examples/ensemble/modules/analyze/dae-module.yaml +2 -0
- daedalus/examples/ensemble/modules/analyze/main.py +19 -0
- daedalus/examples/ensemble/modules/analyze/requirements.txt +1 -0
- daedalus/examples/ensemble/modules/collect/dae-module.yaml +2 -0
- daedalus/examples/ensemble/modules/collect/main.py +21 -0
- daedalus/examples/ensemble/modules/collect/requirements.txt +1 -0
- daedalus/examples/ensemble/modules/emit/dae-module.yaml +2 -0
- daedalus/examples/ensemble/modules/emit/main.py +19 -0
- daedalus/examples/ensemble/modules/emit/requirements.txt +1 -0
- daedalus/examples/isolation-nix/input/word.json +3 -0
- daedalus/examples/isolation-nix/lab.yaml +41 -0
- daedalus/examples/isolation-nix/modules/render_classic/dae-module.yaml +7 -0
- daedalus/examples/isolation-nix/modules/render_classic/main.py +23 -0
- daedalus/examples/isolation-nix/modules/render_classic/requirements.txt +1 -0
- daedalus/examples/isolation-nix/modules/render_modern/dae-module.yaml +7 -0
- daedalus/examples/isolation-nix/modules/render_modern/flake.lock +99 -0
- daedalus/examples/isolation-nix/modules/render_modern/flake.nix +72 -0
- daedalus/examples/isolation-nix/modules/render_modern/main.py +23 -0
- daedalus/examples/isolation-nix/modules/render_modern/pyproject.toml +9 -0
- daedalus/examples/isolation-nix/modules/render_modern/uv.lock +23 -0
- daedalus/examples/isolation-nix/modules/seed/dae-module.yaml +2 -0
- daedalus/examples/isolation-nix/modules/seed/main.py +11 -0
- daedalus/examples/isolation-nix/modules/seed/pyproject.toml +11 -0
- daedalus/examples/isolation-nix/modules/seed/uv.lock +8 -0
- daedalus/examples/isolation-nix/modules/verify/dae-module.yaml +2 -0
- daedalus/examples/isolation-nix/modules/verify/main.py +41 -0
- daedalus/examples/isolation-nix/modules/verify/pyproject.toml +8 -0
- daedalus/examples/isolation-nix/modules/verify/uv.lock +8 -0
- daedalus/examples/minimal/input/raw.csv +6 -0
- daedalus/examples/minimal/lab.yaml +8 -0
- daedalus/examples/minimal/modules/normalize/dae-module.yaml +2 -0
- daedalus/examples/minimal/modules/normalize/expected/normalized.json +18 -0
- daedalus/examples/minimal/modules/normalize/main.py +30 -0
- daedalus/examples/minimal/modules/normalize/requirements.txt +1 -0
- daedalus/examples/minimal/tutorial.ipynb +202 -0
- daedalus/examples/minimal/tutorial.py +90 -0
- daedalus/examples/parallel/input/dataset.json +3 -0
- daedalus/examples/parallel/lab.yaml +37 -0
- daedalus/examples/parallel/modules/combine/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/combine/main.py +17 -0
- daedalus/examples/parallel/modules/combine/requirements.txt +1 -0
- daedalus/examples/parallel/modules/split/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/split/main.py +14 -0
- daedalus/examples/parallel/modules/split/requirements.txt +1 -0
- daedalus/examples/parallel/modules/stat_max/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/stat_max/main.py +16 -0
- daedalus/examples/parallel/modules/stat_max/requirements.txt +1 -0
- daedalus/examples/parallel/modules/stat_mean/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/stat_mean/main.py +17 -0
- daedalus/examples/parallel/modules/stat_mean/requirements.txt +1 -0
- daedalus/examples/parallel/modules/stat_min/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/stat_min/main.py +16 -0
- daedalus/examples/parallel/modules/stat_min/requirements.txt +1 -0
- daedalus/examples/parallel/modules/stat_sum/dae-module.yaml +2 -0
- daedalus/examples/parallel/modules/stat_sum/main.py +19 -0
- daedalus/examples/parallel/modules/stat_sum/requirements.txt +1 -0
- daedalus/flow/__init__.py +11 -0
- daedalus/flow/_api.py +71 -0
- daedalus/py.typed +0 -0
- daedalus_flow-0.1.0.dist-info/METADATA +143 -0
- daedalus_flow-0.1.0.dist-info/RECORD +240 -0
- daedalus_flow-0.1.0.dist-info/WHEEL +4 -0
- daedalus_flow-0.1.0.dist-info/entry_points.txt +3 -0
- daedalus_flow-0.1.0.dist-info/licenses/LICENSE +202 -0
- daedalus_flow-0.1.0.dist-info/licenses/NOTICE +10 -0
daedalus/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Design, validate and run labs of modules."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
# The distribution name; must match [project].name in pyproject.toml.
|
|
6
|
+
_DIST_NAME = "daedalus-flow"
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version(_DIST_NAME)
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
# Bare source tree (package not installed); keep imports working.
|
|
12
|
+
__version__ = "0.0.0.dev0"
|
daedalus/cli/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""daedalus.cli - the command-line interface.
|
|
2
|
+
|
|
3
|
+
Builds the ``dae`` / ``daedalus`` console scripts (both point at :func:`main`).
|
|
4
|
+
This is not a public Python API; script authors should use :mod:`daedalus.flow`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from daedalus.cli.app import app, main
|
|
8
|
+
|
|
9
|
+
__all__ = ["app", "main"]
|
daedalus/cli/app.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""daedalus root command (Typer).
|
|
2
|
+
|
|
3
|
+
Builds the root Typer app, mounts the four command groups, and emits the
|
|
4
|
+
onboarding text when invoked bare. Both console scripts (``dae`` and
|
|
5
|
+
``daedalus``) point at :func:`main`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from daedalus import __version__
|
|
13
|
+
from daedalus.cli import render, strings
|
|
14
|
+
from daedalus.cli.commands._outcome import is_json, resolve, state
|
|
15
|
+
from daedalus.cli.commands.example import example
|
|
16
|
+
from daedalus.cli.commands.flow import flow
|
|
17
|
+
from daedalus.cli.commands.lab import lab
|
|
18
|
+
from daedalus.cli.commands.module import module
|
|
19
|
+
from daedalus.core.outcomes import Outcome
|
|
20
|
+
|
|
21
|
+
# --help track panels. The groups bucket by function so `dae --help`
|
|
22
|
+
# reads as a guided surface rather than a flat list.
|
|
23
|
+
_TRACK_AUTHOR = "Author a Lab"
|
|
24
|
+
_TRACK_EXPLORE = "Start from an example"
|
|
25
|
+
_TRACK_INSPECT = "Inspect a run"
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
help=strings.ROOT_TAGLINE,
|
|
29
|
+
no_args_is_help=False,
|
|
30
|
+
add_completion=True,
|
|
31
|
+
# -h is the conventional help alias; set once on the root, Click's context
|
|
32
|
+
# tree carries it down to every group and leaf without per-command wiring.
|
|
33
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
"""Print ``dae <version>`` and exit; an eager flag outside the outcome codes."""
|
|
39
|
+
if value:
|
|
40
|
+
typer.echo(f"dae {__version__}")
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
app.add_typer(example, name="example", rich_help_panel=_TRACK_EXPLORE)
|
|
45
|
+
app.add_typer(module, name="module", rich_help_panel=_TRACK_AUTHOR)
|
|
46
|
+
app.add_typer(lab, name="lab", rich_help_panel=_TRACK_AUTHOR)
|
|
47
|
+
app.add_typer(flow, name="flow", rich_help_panel=_TRACK_INSPECT)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.callback(invoke_without_command=True)
|
|
51
|
+
def root(
|
|
52
|
+
ctx: typer.Context,
|
|
53
|
+
json_out: Annotated[
|
|
54
|
+
bool, typer.Option("--json", help=strings.JSON_OPTION_HELP)
|
|
55
|
+
] = False,
|
|
56
|
+
_version: Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--version",
|
|
60
|
+
"-V",
|
|
61
|
+
callback=_version_callback,
|
|
62
|
+
is_eager=True,
|
|
63
|
+
help=strings.VERSION_OPTION_HELP,
|
|
64
|
+
),
|
|
65
|
+
] = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Emit the onboarding text when invoked with no subcommand.
|
|
68
|
+
|
|
69
|
+
``--json`` here sets the per-invocation baseline before any subcommand; a
|
|
70
|
+
leaf's shared ``--json`` can then only raise it, so the flag works in
|
|
71
|
+
either position.
|
|
72
|
+
"""
|
|
73
|
+
state["json"] = json_out
|
|
74
|
+
if ctx.invoked_subcommand is None:
|
|
75
|
+
# Bare 'dae' (dae.onboarding.ok); the command list comes from the
|
|
76
|
+
# registered groups.
|
|
77
|
+
if not is_json():
|
|
78
|
+
# Every add_typer above passes name=..., so no group name is None;
|
|
79
|
+
# the filter only narrows the type for mypy.
|
|
80
|
+
groups = [group.name for group in app.registered_groups if group.name]
|
|
81
|
+
render.onboarding(groups)
|
|
82
|
+
resolve(Outcome.DAE_ONBOARDING_OK)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> None:
|
|
86
|
+
app()
|
daedalus/cli/chrome.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The stderr chrome of dae, its teaching notes and Next hints.
|
|
2
|
+
|
|
3
|
+
stdout carries the transcript only; stderr carries the teaching chrome and the
|
|
4
|
+
Next hint. Both helpers print through the shared ``err`` console, so the chrome
|
|
5
|
+
picks up the theme. ``next_line`` takes a fixed command string and frames it
|
|
6
|
+
with one ``[muted]`` tag; ``note`` carries dynamic content that may contain
|
|
7
|
+
``[``, so it prints with markup parsing off.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .console import err
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def next_line(text: str) -> None:
|
|
14
|
+
"""Print the stderr Next hint, e.g. ``Next: dae lab visualize``.
|
|
15
|
+
|
|
16
|
+
Each command ends with one of these pointing at the next step. ``text`` is
|
|
17
|
+
a fixed command string, so its one ``[muted]`` markup tag stays parsed.
|
|
18
|
+
"""
|
|
19
|
+
err.print(f"[muted]Next:[/muted] {text}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def note(text: str) -> None:
|
|
23
|
+
"""Print one line of teaching chrome to stderr.
|
|
24
|
+
|
|
25
|
+
One call is one line. Markup parsing is off, so a literal ``[...]`` in the
|
|
26
|
+
message (a ``[viz]`` hint, a bracket in a path) renders verbatim.
|
|
27
|
+
"""
|
|
28
|
+
err.print(text, style="muted", markup=False)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""daedalus command groups."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Every command names its Outcome through ``resolve``.
|
|
2
|
+
|
|
3
|
+
``resolve`` owns the exit code (0 for ok and dry_run outcomes, 2 for usage
|
|
4
|
+
errors). Under ``--json`` it prints the ``{code, exit, error, data}`` envelope
|
|
5
|
+
to plain stdout rather than the Rich console, so the machine surface stays free
|
|
6
|
+
of ANSI. The root ``--json`` flag sets ``state["json"]``; command bodies guard
|
|
7
|
+
their human output behind ``if not is_json():``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Annotated, Any
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from daedalus.cli import strings
|
|
16
|
+
from daedalus.core.outcomes import Outcome
|
|
17
|
+
|
|
18
|
+
# Process-global flag. The root callback sets the baseline before any command
|
|
19
|
+
# body runs; the per-leaf option below can only raise it to True, so --json
|
|
20
|
+
# works before or after the noun.
|
|
21
|
+
state = {"json": False}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_json() -> bool:
|
|
25
|
+
"""Whether the current invocation requested machine-readable JSON output."""
|
|
26
|
+
return state["json"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _escalate_json(value: bool) -> bool:
|
|
30
|
+
"""Set the json flag when --json is passed; a leaf's False never lowers it."""
|
|
31
|
+
if value:
|
|
32
|
+
state["json"] = True
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# One shared --json option reused on every leaf, so the flag is accepted after
|
|
37
|
+
# the noun (`dae lab visualize --json`) as well as before it.
|
|
38
|
+
JsonOption = Annotated[
|
|
39
|
+
bool,
|
|
40
|
+
typer.Option("--json", callback=_escalate_json, help=strings.JSON_OPTION_HELP),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve(
|
|
45
|
+
outcome: Outcome,
|
|
46
|
+
payload: dict[str, Any] | None = None,
|
|
47
|
+
*,
|
|
48
|
+
error: dict[str, Any] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Make the outcome explicit; exit non-zero only for error outcomes.
|
|
51
|
+
|
|
52
|
+
Under ``--json``, print the ``{code, exit, error, data}`` envelope first,
|
|
53
|
+
with the dotted code, its registry exit, the failure cause (null on success)
|
|
54
|
+
and the command payload (null when there is none). The four-key shape is fixed.
|
|
55
|
+
"""
|
|
56
|
+
if is_json():
|
|
57
|
+
body = {
|
|
58
|
+
"code": str(outcome),
|
|
59
|
+
"exit": outcome.exit_code,
|
|
60
|
+
"error": error,
|
|
61
|
+
"data": payload or None,
|
|
62
|
+
}
|
|
63
|
+
print(json.dumps(body))
|
|
64
|
+
if outcome.exit_code != 0:
|
|
65
|
+
raise typer.Exit(code=outcome.exit_code)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""List the example ladder or scaffold one example (the ``example`` command).
|
|
2
|
+
|
|
3
|
+
A thin command layer over the pure functions in ``strings``; teaching notes and
|
|
4
|
+
the Next hint go to stderr through ``chrome``. ``example`` takes an optional
|
|
5
|
+
positional name, so it is a one-callback Typer mounted with ``add_typer``, which
|
|
6
|
+
makes both ``dae example`` and ``dae example minimal`` work. The bare ladder is
|
|
7
|
+
example.list.ok, a known example example.scaffold.ok and an unknown one
|
|
8
|
+
example.scaffold.not_found (exit 2).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from daedalus.cli import chrome, render, strings
|
|
16
|
+
from daedalus.cli.commands._outcome import JsonOption, is_json, resolve
|
|
17
|
+
from daedalus.cli.console import out
|
|
18
|
+
from daedalus.core import paths
|
|
19
|
+
from daedalus.core.outcomes import Outcome
|
|
20
|
+
|
|
21
|
+
example = typer.Typer(
|
|
22
|
+
help="Show the example ladder, or scaffold a single example by name.",
|
|
23
|
+
invoke_without_command=True,
|
|
24
|
+
no_args_is_help=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# The example names with a bundle directory on disk, from strings so the
|
|
28
|
+
# ladder and the command agree; any other name is refused as unknown.
|
|
29
|
+
_BUNDLED_EXAMPLES = set(strings.AVAILABLE_EXAMPLES)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _scaffold_bundled(name: str) -> None:
|
|
33
|
+
"""Copy the bundled example ``name`` into ``./<name>/``, refusing to clobber."""
|
|
34
|
+
dest = paths.example_dir(name)
|
|
35
|
+
if dest.exists():
|
|
36
|
+
if not is_json():
|
|
37
|
+
chrome.note(f"'{name}' already exists here; refusing to overwrite it.")
|
|
38
|
+
return resolve(Outcome.DAE_EXAMPLE_SCAFFOLD_EXISTS)
|
|
39
|
+
paths.copy_example_bundle(name, dest)
|
|
40
|
+
if not is_json():
|
|
41
|
+
# The result path goes to stdout so an agent capturing stdout alone
|
|
42
|
+
# learns where the example landed; the teaching note + Next hint stay on
|
|
43
|
+
# stderr. The path is the one machine-relevant fact of a scaffold.
|
|
44
|
+
out.print(f"scaffolded ./{name}/")
|
|
45
|
+
chrome.note(f"scaffolded example '{name}' into ./{name}/")
|
|
46
|
+
# Point at the input/ dir when the scaffolded bundle ships one (detected
|
|
47
|
+
# on disk).
|
|
48
|
+
input_dir = dest / "input"
|
|
49
|
+
if input_dir.is_dir():
|
|
50
|
+
inputs = sorted(p for p in input_dir.iterdir() if p.is_file())
|
|
51
|
+
if inputs:
|
|
52
|
+
rel = f"{name}/input/{inputs[0].name}"
|
|
53
|
+
chrome.note(strings.INPUT_DIR_HINT.format(input_file=rel))
|
|
54
|
+
chrome.next_line(strings.NEXT_AFTER_SCAFFOLD[name])
|
|
55
|
+
return resolve(Outcome.EXAMPLE_SCAFFOLD_OK, payload={"paths": [str(dest)]})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _example_entries() -> list[dict[str, object]]:
|
|
59
|
+
"""The example ladder as ``--json`` entries, simplest first."""
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
"name": name,
|
|
63
|
+
"available": name in _BUNDLED_EXAMPLES,
|
|
64
|
+
"description": description,
|
|
65
|
+
}
|
|
66
|
+
for name, description in strings.example_rows()
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@example.callback(invoke_without_command=True)
|
|
71
|
+
def example_main(
|
|
72
|
+
name: Annotated[
|
|
73
|
+
str | None,
|
|
74
|
+
typer.Argument(help="Example to scaffold; omit to list the ladder."),
|
|
75
|
+
] = None,
|
|
76
|
+
json_out: JsonOption = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Show the example ladder, or scaffold a single example by name."""
|
|
79
|
+
if name is None:
|
|
80
|
+
# Bare 'dae example': list the ladder, point at the simplest example.
|
|
81
|
+
if not is_json():
|
|
82
|
+
render.example_ladder()
|
|
83
|
+
chrome.next_line("dae example minimal")
|
|
84
|
+
# Under --json, attach the ladder as an array built from the same
|
|
85
|
+
# strings data as the human render, so the two surfaces agree.
|
|
86
|
+
return resolve(
|
|
87
|
+
Outcome.EXAMPLE_LIST_OK, payload={"examples": _example_entries()}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if name in _BUNDLED_EXAMPLES:
|
|
91
|
+
return _scaffold_bundled(name)
|
|
92
|
+
|
|
93
|
+
# An unknown example gets a stderr note and the usage exit code.
|
|
94
|
+
if not is_json():
|
|
95
|
+
chrome.note(f"unknown example '{name}'.")
|
|
96
|
+
chrome.note("valid examples: " + ", ".join(strings.KNOWN_EXAMPLES))
|
|
97
|
+
return resolve(Outcome.EXAMPLE_SCAFFOLD_NOT_FOUND)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Inspect the latest flow and resume a failed one (the ``flow`` command group).
|
|
2
|
+
|
|
3
|
+
``status`` reads the lineage the engine wrote under ``dae-outputs/flows/`` in
|
|
4
|
+
cwd and renders the latest flow; it returns ``dae.flow.status.ok``, or
|
|
5
|
+
``dae.flow.status.nothing`` (exit 0) when there is none. ``resume`` replays the
|
|
6
|
+
recorded plan of the latest failed flow through ``LocalEngine.resume_flow``,
|
|
7
|
+
skipping completed steps; it returns ``dae.flow.resume.ok``, ``.nothing``
|
|
8
|
+
(exit 0) or ``.failed`` (the re-run failed again).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from daedalus.cli import chrome, render
|
|
16
|
+
from daedalus.cli.commands._outcome import JsonOption, is_json, resolve
|
|
17
|
+
from daedalus.core import lineage, topology
|
|
18
|
+
from daedalus.core.outcomes import Outcome
|
|
19
|
+
|
|
20
|
+
flow = typer.Typer(
|
|
21
|
+
help="Inspect a flow run; resume a failed flow.", no_args_is_help=True
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _note(message: str) -> None:
|
|
26
|
+
"""Write a stderr note only on the human (non-json) surface."""
|
|
27
|
+
if not is_json():
|
|
28
|
+
chrome.note(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@flow.command()
|
|
32
|
+
def status(json_out: JsonOption = False) -> None:
|
|
33
|
+
"""Show the latest flow's lineage from dae-outputs/flows/ in cwd.
|
|
34
|
+
|
|
35
|
+
No flow here is dae.flow.status.nothing (exit 0), not an error. status is
|
|
36
|
+
read-only and exits 0 even for a failed flow, which names its cause in the
|
|
37
|
+
envelope error slot; act on a failure with 'dae flow resume'.
|
|
38
|
+
"""
|
|
39
|
+
output_root = Path.cwd() / topology.OUTPUT_ROOT
|
|
40
|
+
flow_ids = lineage.list_flows(output_root)
|
|
41
|
+
if not flow_ids:
|
|
42
|
+
_note("no flows here yet (run 'dae lab run' first).")
|
|
43
|
+
return resolve(Outcome.DAE_FLOW_STATUS_NOTHING)
|
|
44
|
+
|
|
45
|
+
latest = flow_ids[-1]
|
|
46
|
+
# A run stranded as `running`/`submitted` by a crash is flipped to `failed`
|
|
47
|
+
# on read, so status never reports a dead run as alive.
|
|
48
|
+
record = lineage.read_flow_record_reconciled(output_root / "flows" / latest)
|
|
49
|
+
if not is_json():
|
|
50
|
+
render.flow_status(record)
|
|
51
|
+
# resume only re-runs failed steps, so the hint appears for a failed flow.
|
|
52
|
+
if record.status == "failed":
|
|
53
|
+
chrome.next_line("dae flow resume (re-run only the steps that need it)")
|
|
54
|
+
return resolve(
|
|
55
|
+
Outcome.FLOW_STATUS_OK,
|
|
56
|
+
payload=render.flow_status_payload(record),
|
|
57
|
+
error=render.failure_cause(record),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@flow.command()
|
|
62
|
+
def resume(json_out: JsonOption = False) -> None:
|
|
63
|
+
"""Resume the latest failed flow, re-running the failed step and what follows it.
|
|
64
|
+
|
|
65
|
+
Completed steps are skipped and their artifacts reused. With no flow, or the
|
|
66
|
+
latest completed, there is nothing to resume (dae.flow.resume.nothing, exit 0);
|
|
67
|
+
a re-run that fails again is .failed (exit 1). Edits since are not re-validated.
|
|
68
|
+
"""
|
|
69
|
+
# Lazy (off the bare `dae --help` path): recipe + engine pull networkx.
|
|
70
|
+
from daedalus.core import recipe # noqa: PLC0415
|
|
71
|
+
from daedalus.core.engine import LabConfig, LocalEngine # noqa: PLC0415
|
|
72
|
+
|
|
73
|
+
cwd = Path.cwd()
|
|
74
|
+
output_root = cwd / topology.OUTPUT_ROOT
|
|
75
|
+
flow_ids = lineage.list_flows(output_root)
|
|
76
|
+
if not flow_ids:
|
|
77
|
+
_note("no flows here to resume (run 'dae lab run' first).")
|
|
78
|
+
return resolve(Outcome.DAE_FLOW_RESUME_NOTHING)
|
|
79
|
+
|
|
80
|
+
latest = flow_ids[-1]
|
|
81
|
+
record = lineage.read_flow_record(output_root / "flows" / latest)
|
|
82
|
+
if record.status != "failed":
|
|
83
|
+
_note(f"latest flow '{latest}' is {record.status}; nothing to resume.")
|
|
84
|
+
return resolve(Outcome.DAE_FLOW_RESUME_NOTHING)
|
|
85
|
+
|
|
86
|
+
lab_path = recipe.discover_lab(cwd)
|
|
87
|
+
if lab_path is None:
|
|
88
|
+
_note("no lab.yaml here to resume against.")
|
|
89
|
+
return resolve(Outcome.DAE_FLOW_RESUME_NOTHING)
|
|
90
|
+
lab_dir = lab_path.parent
|
|
91
|
+
try:
|
|
92
|
+
spec = recipe.load_recipe(lab_path)
|
|
93
|
+
plan = recipe.build_plan(spec, lab_dir)
|
|
94
|
+
except recipe.RecipeParseError:
|
|
95
|
+
# The lab no longer parses or builds (a structural edit since the run); it
|
|
96
|
+
# cannot be resumed against, and the note says so.
|
|
97
|
+
_note("this lab no longer builds a valid plan; cannot resume it.")
|
|
98
|
+
return resolve(Outcome.DAE_FLOW_RESUME_NOTHING)
|
|
99
|
+
|
|
100
|
+
config = LabConfig(
|
|
101
|
+
lab_name=spec.name or lab_dir.name,
|
|
102
|
+
lab_dir=lab_dir,
|
|
103
|
+
seed=0,
|
|
104
|
+
output_root=output_root,
|
|
105
|
+
max_workers=spec.max_workers,
|
|
106
|
+
engine=spec.engine,
|
|
107
|
+
isolation=spec.isolation,
|
|
108
|
+
)
|
|
109
|
+
result = LocalEngine().resume_flow(plan, config, latest)
|
|
110
|
+
|
|
111
|
+
record = lineage.read_flow_record(output_root / "flows" / latest)
|
|
112
|
+
payload = render.flow_status_payload(record)
|
|
113
|
+
if not is_json():
|
|
114
|
+
render.flow_status(record)
|
|
115
|
+
_note(
|
|
116
|
+
"resume replayed the recorded plan; module / recipe edits since the "
|
|
117
|
+
"failed run were not re-validated."
|
|
118
|
+
)
|
|
119
|
+
if result.error is not None:
|
|
120
|
+
_note(f"resume still failed at a step: {result.error}")
|
|
121
|
+
return resolve(
|
|
122
|
+
Outcome.DAE_FLOW_RESUME_FAILED,
|
|
123
|
+
payload=payload,
|
|
124
|
+
error=render.failure_cause(record),
|
|
125
|
+
)
|
|
126
|
+
return resolve(Outcome.FLOW_RESUME_OK, payload=payload)
|