Source code for PyHydroGeophysX.workflows.bundle

"""Export a validated recipe together with the code that reproduces it."""

from __future__ import annotations

from pathlib import Path
from typing import Tuple

from .codegen import generate_python
from .models import WorkflowSpec
from .recipe import save_recipe
from .walkthrough import generate_notebook, generate_walkthrough


[docs] def export_workflow_bundle( spec: WorkflowSpec, directory: str | Path, *, stem: str = "workflow", teaching: bool = True, ) -> Tuple[Path, Path]: """Write the recipe and the runner, plus a walkthrough script and notebook. Four files land in *directory*: ``<stem>_recipe.json`` The run configuration, the single source both generators read from. ``run_<stem>.py`` A runner that calls the workflow engine, for a byte-identical rerun. ``<stem>_walkthrough.py`` and ``<stem>_walkthrough.ipynb`` The same run written as domain-level calls with named parameters and prose, for reading, teaching and editing. Returns ``(recipe_path, script_path)``. The walkthrough files are written as a side effect and a failure to produce them never blocks the reproducible pair: a workflow with no walkthrough definition still exports a valid recipe and runner. Validation occurs before any writer mutates its destination, so a non-serializable or seedless stochastic spec cannot produce a runnable- looking partial export. """ # ``save_recipe`` and ``generate_python`` both validate, but doing it here # first prevents the first file appearing when the second would reject. from .registry import get_workflow descriptor = get_workflow(spec.workflow_id) spec.validate(stochastic=descriptor.stochastic) target = Path(directory) target.mkdir(parents=True, exist_ok=True) recipe_path = save_recipe(spec, target / f"{stem}_recipe.json") script_path = generate_python(spec, target / f"run_{stem}.py") if teaching: try: generate_walkthrough(spec, target / f"{stem}_walkthrough.py") generate_notebook(spec, target / f"{stem}_walkthrough.ipynb") except NotImplementedError: # No walkthrough is defined for this workflow yet. The recipe and # the runner are the contract; the teaching copy is a bonus. pass return recipe_path, script_path
[docs] def teaching_paths(script_path: str | Path) -> Tuple[Path | None, Path | None]: """Locate the walkthrough pair :func:`export_workflow_bundle` wrote. Takes the runner path because that is what a caller already holds, and returns ``(walkthrough_py, walkthrough_ipynb)`` with ``None`` for whichever is absent. The naming convention lives in this module, so resolving by convention here is a lookup rather than a guess. """ runner = Path(script_path) if not runner.name.startswith("run_"): return None, None stem = runner.stem[len("run_"):] script = runner.with_name(f"{stem}_walkthrough.py") notebook = runner.with_name(f"{stem}_walkthrough.ipynb") return (script if script.is_file() else None, notebook if notebook.is_file() else None)
__all__ = ["export_workflow_bundle", "teaching_paths"]