nf-meta 0.2.3__tar.gz → 0.3.0.dev0__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.
Files changed (61) hide show
  1. {nf_meta-0.2.3/src/nf_meta.egg-info → nf_meta-0.3.0.dev0}/PKG-INFO +2 -2
  2. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/README.md +1 -1
  3. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/pyproject.toml +18 -1
  4. nf_meta-0.3.0.dev0/src/nf_meta/__main__.py +135 -0
  5. nf_meta-0.3.0.dev0/src/nf_meta/core/cache.py +39 -0
  6. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/errors.py +24 -11
  7. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/events.py +4 -4
  8. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/graph.py +88 -41
  9. nf_meta-0.3.0.dev0/src/nf_meta/core/models.py +742 -0
  10. nf_meta-0.3.0.dev0/src/nf_meta/core/nf_core_utils.py +521 -0
  11. nf_meta-0.3.0.dev0/src/nf_meta/core/nf_param_validation.py +124 -0
  12. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/session.py +18 -9
  13. nf_meta-0.3.0.dev0/src/nf_meta/editor/__init__.py +41 -0
  14. nf_meta-0.3.0.dev0/src/nf_meta/editor/backend/__init__.py +1 -0
  15. nf_meta-0.3.0.dev0/src/nf_meta/editor/backend/api.py +232 -0
  16. nf_meta-0.3.0.dev0/src/nf_meta/editor/backend/serializers.py +40 -0
  17. nf_meta-0.3.0.dev0/src/nf_meta/editor/base_editor.py +46 -0
  18. nf_meta-0.3.0.dev0/src/nf_meta/editor/browser_editor.py +99 -0
  19. nf_meta-0.3.0.dev0/src/nf_meta/editor/editor.py +20 -0
  20. nf_meta-0.3.0.dev0/src/nf_meta/editor/frontend_dist/assets/index-BeQMBiE2.css +1 -0
  21. nf_meta-0.3.0.dev0/src/nf_meta/editor/frontend_dist/assets/index-C23FgNmk.js +202 -0
  22. nf_meta-0.3.0.dev0/src/nf_meta/editor/frontend_dist/assets/nfcore-BTUAiOeh.svg +76 -0
  23. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/index.html +2 -2
  24. nf_meta-0.3.0.dev0/src/nf_meta/editor/utils.py +20 -0
  25. nf_meta-0.3.0.dev0/src/nf_meta/runner/__init__.py +41 -0
  26. nf_meta-0.3.0.dev0/src/nf_meta/runner/base_runner.py +54 -0
  27. nf_meta-0.3.0.dev0/src/nf_meta/runner/python_runner.py +308 -0
  28. nf_meta-0.3.0.dev0/src/nf_meta/runner/runner.py +50 -0
  29. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/runner/utils.py +59 -4
  30. nf_meta-0.3.0.dev0/src/nf_meta/runner/workflow_run.py +165 -0
  31. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0/src/nf_meta.egg-info}/PKG-INFO +2 -2
  32. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta.egg-info/SOURCES.txt +12 -3
  33. nf_meta-0.2.3/src/nf_meta/__main__.py +0 -85
  34. nf_meta-0.2.3/src/nf_meta/core/models.py +0 -391
  35. nf_meta-0.2.3/src/nf_meta/core/nf_core_utils.py +0 -70
  36. nf_meta-0.2.3/src/nf_meta/editor/__init__.py +0 -66
  37. nf_meta-0.2.3/src/nf_meta/editor/backend/__init__.py +0 -1
  38. nf_meta-0.2.3/src/nf_meta/editor/backend/api.py +0 -141
  39. nf_meta-0.2.3/src/nf_meta/editor/backend/serializers.py +0 -25
  40. nf_meta-0.2.3/src/nf_meta/editor/frontend_dist/assets/index-CUFGOfKn.js +0 -202
  41. nf_meta-0.2.3/src/nf_meta/editor/frontend_dist/assets/index-ESZ-ptZu.css +0 -1
  42. nf_meta-0.2.3/src/nf_meta/runner/__init__.py +0 -4
  43. nf_meta-0.2.3/src/nf_meta/runner/python_runner.py +0 -256
  44. nf_meta-0.2.3/src/nf_meta/runner/runner.py +0 -65
  45. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/LICENSE +0 -0
  46. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/MANIFEST.in +0 -0
  47. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/setup.cfg +0 -0
  48. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/__init__.py +0 -0
  49. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/__init__.py +0 -0
  50. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/core/history.py +0 -0
  51. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  52. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  53. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  54. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  55. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/editor/frontend_dist/vite.svg +0 -0
  56. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/runner/cascade_runner.py +0 -0
  57. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta/runner/errors.py +0 -0
  58. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta.egg-info/dependency_links.txt +0 -0
  59. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta.egg-info/entry_points.txt +0 -0
  60. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta.egg-info/requires.txt +0 -0
  61. {nf_meta-0.2.3 → nf_meta-0.3.0.dev0}/src/nf_meta.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nf-meta
3
- Version: 0.2.3
3
+ Version: 0.3.0.dev0
4
4
  Summary: nf-meta is a cli tool for building, validating and running meta-pipelines based on the Nextflow workflow language
5
5
  Author-email: Julian Flesch <julian.flesch@qbic.uni-tuebingen.de>
6
6
  License-Expression: Apache-2.0
@@ -85,7 +85,7 @@ This project features a small editor which intends to ease the creation and upda
85
85
  of these config files, by visualizing the config as a graph and
86
86
  offering user-firendly form for entering and validating values.
87
87
 
88
- ![editor](assets/nf-meta-screenshot.png)
88
+ ![editor](https://raw.githubusercontent.com/bmds-tue/nf-meta/main/assets/nf-meta-screenshot.png)
89
89
 
90
90
 
91
91
  ## Metapipeline Runners
@@ -61,7 +61,7 @@ This project features a small editor which intends to ease the creation and upda
61
61
  of these config files, by visualizing the config as a graph and
62
62
  offering user-firendly form for entering and validating values.
63
63
 
64
- ![editor](assets/nf-meta-screenshot.png)
64
+ ![editor](https://raw.githubusercontent.com/bmds-tue/nf-meta/main/assets/nf-meta-screenshot.png)
65
65
 
66
66
 
67
67
  ## Metapipeline Runners
@@ -13,7 +13,7 @@ namespaces = false
13
13
 
14
14
  [project]
15
15
  name = "nf-meta"
16
- version = "0.2.3"
16
+ version = "0.3.0-dev"
17
17
  description = "nf-meta is a cli tool for building, validating and running meta-pipelines based on the Nextflow workflow language"
18
18
  readme = "README.md"
19
19
  authors = [
@@ -45,4 +45,21 @@ nf-meta = "nf_meta.__main__:cli"
45
45
  [dependency-groups]
46
46
  dev = [
47
47
  "twine>=6.2.0",
48
+ "pytest>=9.0.3",
49
+ "pytest-mock>=3.15.1",
50
+ "mypy>=2.1.0",
51
+ "types-networkx>=3.6.1.20260408",
52
+ "types-pyyaml>=6.0.12.20260408",
53
+ "types-requests>=2.33.0.20260503",
48
54
  ]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+
59
+ [tool.mypy]
60
+ python_version = "3.12"
61
+ mypy_path = "src"
62
+
63
+ [[tool.mypy.overrides]]
64
+ module = ["uvicorn", "uvicorn.*"]
65
+ ignore_missing_imports = true
@@ -0,0 +1,135 @@
1
+ import click
2
+ from functools import wraps
3
+ from nf_meta.core.errors import (
4
+ GraphValidationError,
5
+ ValidationError,
6
+ format_errors_for_cli,
7
+ )
8
+ from nf_meta.runner import (
9
+ run_metapipeline,
10
+ get_registered_runners,
11
+ RunOptions,
12
+ NfMetaRunnerError,
13
+ )
14
+ from nf_meta.core.graph import MetaworkflowGraph
15
+ from nf_meta.core.session import start_session
16
+ from nf_meta.editor import get_registered_editors, EditorOptions, run_editor
17
+
18
+
19
+ @click.group()
20
+ @click.version_option()
21
+ def cli() -> None:
22
+ return
23
+
24
+
25
+ @click.command("editor")
26
+ @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode")
27
+ @click.option(
28
+ "--editor",
29
+ "-e",
30
+ type=click.Choice(get_registered_editors()),
31
+ default="browser",
32
+ help="Editor backend to use",
33
+ )
34
+ @click.option("--host", help="Host to bind the editor server to")
35
+ @click.option(
36
+ "--port",
37
+ type=int,
38
+ help="Port for the editor server (auto-assigned if omitted)",
39
+ )
40
+ @click.argument("config", required=False, type=click.Path())
41
+ def edit_browser(config, verbose, editor, host, port):
42
+ try:
43
+ start_session(config)
44
+ opts = EditorOptions(editor_name=editor)
45
+ if host is not None:
46
+ opts.host = host
47
+ if port is not None:
48
+ opts.port = port
49
+ run_editor(opts)
50
+ except (GraphValidationError, ValidationError) as e:
51
+ click.echo(format_errors_for_cli(e))
52
+ raise SystemExit(1)
53
+ except FileNotFoundError as e:
54
+ click.echo(click.style(e, fg="red"))
55
+ raise SystemExit(1)
56
+
57
+
58
+ @click.command("validate")
59
+ @click.argument("config", type=click.Path())
60
+ @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode")
61
+ def validate_config(config, verbose):
62
+ try:
63
+ g = MetaworkflowGraph.from_file(config)
64
+ click.echo(click.style("✓ Config is valid", fg="green"))
65
+ except (GraphValidationError, ValidationError) as e:
66
+ click.echo(format_errors_for_cli(e))
67
+ raise SystemExit(1)
68
+ except FileNotFoundError as e:
69
+ click.echo(click.style(e, fg="red"))
70
+ raise SystemExit(1)
71
+
72
+
73
+ @click.command("run")
74
+ @click.argument("config", type=click.Path())
75
+ @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode")
76
+ @click.option(
77
+ "--runner",
78
+ "-r",
79
+ prompt=True,
80
+ type=click.Choice(get_registered_runners()),
81
+ default="python",
82
+ )
83
+ @click.option("--resume", is_flag=True, help="Resume a previous run")
84
+ @click.option(
85
+ "--output-lines",
86
+ "-l",
87
+ default="25",
88
+ type=int,
89
+ help="Number of lines of workflow output to stream to output window (Only for Python Runner!)",
90
+ )
91
+ @click.option("--start", "-s", type=str, help="ID of workflow to start from")
92
+ @click.option("--target", "-t", type=str, help="ID of workflow to run until")
93
+ @click.option(
94
+ "--profile",
95
+ "-p",
96
+ type=str,
97
+ help="Nextflow profile to apply globally, taking precedent over config values.",
98
+ )
99
+ @click.option(
100
+ "--stub",
101
+ is_flag=True,
102
+ help="Run all workflows as stub runs (passes -stub to Nextflow)",
103
+ )
104
+ def run(config, verbose, runner, resume, output_lines, start, target, profile, stub):
105
+ try:
106
+ run_options = RunOptions(
107
+ runner_name=runner,
108
+ verbose=verbose,
109
+ output_lines=output_lines,
110
+ nf_profile=profile,
111
+ stub=stub,
112
+ resume=resume,
113
+ start=start,
114
+ target=target,
115
+ )
116
+ g = MetaworkflowGraph.from_file(config)
117
+ run_metapipeline(g, run_options)
118
+ except (GraphValidationError, ValidationError) as e:
119
+ click.echo(format_errors_for_cli(e))
120
+ raise SystemExit(1)
121
+ except NfMetaRunnerError as e:
122
+ click.echo(click.style(e.message, fg="red"))
123
+ raise SystemExit(1)
124
+ except FileNotFoundError as e:
125
+ click.echo(click.style(e, fg="red"))
126
+ raise SystemExit(1)
127
+
128
+
129
+ cli.add_command(edit_browser)
130
+ cli.add_command(validate_config)
131
+ cli.add_command(run)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ cli()
@@ -0,0 +1,39 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _ENV_VAR = "NF_META_CACHE_DIR"
10
+ GLOBAL_CACHE_DIR: Path = Path(
11
+ os.environ.get(_ENV_VAR) or Path.home() / ".cache" / "nf-meta"
12
+ )
13
+
14
+
15
+ def schema_cache_path(url: str, version: str) -> Path:
16
+ key = hashlib.sha256(f"{url}@{version}".encode()).hexdigest()[:16]
17
+ return GLOBAL_CACHE_DIR / "schemas" / f"{key}.json"
18
+
19
+
20
+ def read_schema_cache(url: str, version: str) -> dict | None:
21
+ path = schema_cache_path(url, version)
22
+ if not path.exists():
23
+ return None
24
+ try:
25
+ with open(path) as f:
26
+ return json.load(f)
27
+ except Exception as e:
28
+ logger.warning("Failed to read schema cache at %s: %s", path, e)
29
+ return None
30
+
31
+
32
+ def write_schema_cache(url: str, version: str, schema: dict) -> None:
33
+ path = schema_cache_path(url, version)
34
+ try:
35
+ path.parent.mkdir(parents=True, exist_ok=True)
36
+ with open(path, "w") as f:
37
+ json.dump(schema, f)
38
+ except Exception as e:
39
+ logger.warning("Failed to write schema cache to %s: %s", path, e)
@@ -25,7 +25,7 @@ class WorkflowReferenceErrors(GraphValidationError):
25
25
  class SessionCommandError(Exception):
26
26
  @dataclass
27
27
  class FieldError:
28
- workflow_id: str
28
+ workflow_id: Optional[str]
29
29
  field: str
30
30
  message: str
31
31
 
@@ -33,7 +33,13 @@ class SessionCommandError(Exception):
33
33
  graph_errors: list[str]
34
34
 
35
35
  @classmethod
36
- def from_exception(cls, e: WorkflowReferenceError | WorkflowReferenceErrors | GraphValidationError):
36
+ def from_exception(
37
+ cls,
38
+ e: WorkflowReferenceError
39
+ | WorkflowReferenceErrors
40
+ | GraphValidationError
41
+ | ValueError,
42
+ ):
37
43
  if isinstance(e, WorkflowReferenceError):
38
44
  e = WorkflowReferenceErrors([e])
39
45
  if isinstance(e, WorkflowReferenceErrors):
@@ -42,23 +48,30 @@ class SessionCommandError(Exception):
42
48
  cls.FieldError(
43
49
  workflow_id=ref_error.reference.source_wf_id,
44
50
  field="params", # TODO: For now ok. Think about more finegrained errors for Workflow output/input references
45
- message=ref_error.message
51
+ message=ref_error.message,
46
52
  )
47
53
  for ref_error in e.errors
48
54
  ],
49
- graph_errors=[]
55
+ graph_errors=[],
50
56
  )
51
57
  return cls(field_errors=[], graph_errors=[str(e)])
52
-
58
+
53
59
  def to_dict(self):
54
60
  return {
55
- "field_errors": [{"workflow_id": e.workflow_id, "field": e.field, "message": e.message}
56
- for e in self.field_errors],
57
- "graph_errors": self.graph_errors
61
+ "field_errors": [
62
+ {"workflow_id": e.workflow_id, "field": e.field, "message": e.message}
63
+ for e in self.field_errors
64
+ ],
65
+ "graph_errors": self.graph_errors,
58
66
  }
59
-
60
67
 
61
- def format_errors_for_cli(e: WorkflowReferenceError | WorkflowReferenceErrors | GraphValidationError | ValidationError) -> str:
68
+
69
+ def format_errors_for_cli(
70
+ e: WorkflowReferenceError
71
+ | WorkflowReferenceErrors
72
+ | GraphValidationError
73
+ | ValidationError,
74
+ ) -> str:
62
75
  lines = []
63
76
  if isinstance(e, WorkflowReferenceError):
64
77
  e = WorkflowReferenceErrors([e])
@@ -74,5 +87,5 @@ def format_errors_for_cli(e: WorkflowReferenceError | WorkflowReferenceErrors |
74
87
  lines.append(click.style("Validation failed:", fg="red", bold=True))
75
88
  for err in e.errors():
76
89
  field = ".".join(str(l) for l in err["loc"])
77
- lines.append(f" {click.style(field, fg="yellow")}: {err["msg"]}")
90
+ lines.append(f" {click.style(field, fg='yellow')}: {err['msg']}")
78
91
  return "\n".join(lines)
@@ -7,7 +7,6 @@ from .models import Workflow, Transition, GlobalOptions
7
7
  # Event Handler
8
8
  # TODO: Test that Metaworkflow implements GraphEventHandler: isinstance(graph, GraphEventHandler)
9
9
  class GraphEventHandler(Protocol):
10
-
11
10
  def pop_events(self) -> tuple["Event"]: ...
12
11
 
13
12
  def add_workflow(self, w: Workflow) -> None: ...
@@ -22,13 +21,14 @@ class GraphEventHandler(Protocol):
22
21
 
23
22
  def update_global_options(self, g: GlobalOptions) -> None: ...
24
23
 
25
- def deferred_validation(self) -> None: ...
24
+ def deferred_validation(self): ...
26
25
 
27
26
 
28
27
  # ------------------------------------------
29
28
  # ----- Events: State Changes --------------
30
29
  # ------------------------------------------
31
30
 
31
+
32
32
  class Event(Protocol):
33
33
  def get_undo_cmd(self) -> "Command": ...
34
34
 
@@ -87,14 +87,14 @@ class GlobalOptionsUpdated:
87
87
  # ----- COMMANDS: State Change Intents -----
88
88
  # ------------------------------------------
89
89
 
90
- class Command(Protocol):
91
90
 
91
+ class Command(Protocol):
92
92
  def apply(self, graph: GraphEventHandler) -> None: ...
93
93
 
94
94
 
95
95
  @dataclass(frozen=True)
96
96
  class Transaction:
97
- commands: tuple[Command]
97
+ commands: tuple[Command, ...]
98
98
 
99
99
  def apply(self, graph: GraphEventHandler):
100
100
  # TODO: Handle errors?
@@ -1,13 +1,33 @@
1
1
  from contextlib import contextmanager
2
- from typing import Dict, Any, Optional
2
+ from typing import Optional
3
3
  from pathlib import Path
4
4
  import logging
5
5
 
6
6
  import networkx as nx
7
7
 
8
- from .models import MetaworkflowConfig, Workflow, GlobalOptions, Transition, load_config, dump_config, CONFIG_VERSION_MAX
9
- from .events import Event, WorkflowAdded, WorkflowRemoved, WorkflowUpdated, TransitionAdded, TransitionRemoved, GlobalOptionsUpdated
10
- from .errors import GraphValidationError, WorkflowReferenceError, WorkflowReferenceErrors
8
+ from .models import (
9
+ MetaworkflowConfig,
10
+ GlobalOptions,
11
+ Transition,
12
+ Workflow,
13
+ load_config,
14
+ dump_config,
15
+ CONFIG_VERSION_MAX,
16
+ )
17
+ from .events import (
18
+ Event,
19
+ WorkflowAdded,
20
+ WorkflowRemoved,
21
+ WorkflowUpdated,
22
+ TransitionAdded,
23
+ TransitionRemoved,
24
+ GlobalOptionsUpdated,
25
+ )
26
+ from .errors import (
27
+ GraphValidationError,
28
+ WorkflowReferenceError,
29
+ WorkflowReferenceErrors,
30
+ )
11
31
 
12
32
  logger = logging.getLogger()
13
33
 
@@ -22,7 +42,7 @@ class MetaworkflowGraph:
22
42
 
23
43
  def __init__(self):
24
44
  self.G: nx.DiGraph = nx.DiGraph()
25
- self.global_options: Optional[GlobalOptions] = None
45
+ self.global_options = GlobalOptions()
26
46
  self.config_version: str = CONFIG_VERSION_MAX
27
47
  self._events: list[Event] = []
28
48
  self._validation_suspended = False
@@ -41,7 +61,7 @@ class MetaworkflowGraph:
41
61
 
42
62
  if not cfg:
43
63
  raise ValueError(f"No config data loaded from {cfg_file}")
44
-
64
+
45
65
  return cls.from_config(cfg)
46
66
 
47
67
  @classmethod
@@ -82,17 +102,17 @@ class MetaworkflowGraph:
82
102
  if wf.params and not self._validation_suspended:
83
103
  self.validate_param_references(wf)
84
104
 
85
- if not wf.id in self.G.nodes:
105
+ if wf.id not in self.G.nodes:
86
106
  raise ValueError("Workflow has invalid id. Update unsuccessful!")
87
107
 
88
108
  old_wf = self.get_workflow_by_id(wf.id)
89
109
  self.G.nodes[wf.id]["workflow"] = wf.model_copy()
90
- self._emit(WorkflowUpdated(old_workflow=old_wf, new_workflow=wf))
110
+ self._emit(WorkflowUpdated(old_workflow=old_wf, new_workflow=wf)) # type: ignore
91
111
 
92
112
  def remove_workflow(self, wf_id: str, recursive=False):
93
113
  if wf_id not in self.G.nodes:
94
114
  raise ValueError("Invalid wf_id")
95
-
115
+
96
116
  for edge in list(self.G.in_edges(wf_id)):
97
117
  self.remove_transition(*edge)
98
118
 
@@ -101,14 +121,18 @@ class MetaworkflowGraph:
101
121
 
102
122
  removed_wf = self.get_workflow_by_id(wf_id)
103
123
  self.G.remove_node(wf_id)
104
- self._emit(WorkflowRemoved(removed_wf))
124
+ self._emit(WorkflowRemoved(removed_wf)) # type: ignore
105
125
 
106
126
  def add_transition(self, tr: Transition):
107
127
  if tr.source not in self.G.nodes:
108
- raise ValueError(f"Unknown node {tr.source} found in transition {tr.source}->{tr.target}")
128
+ raise ValueError(
129
+ f"Unknown node {tr.source} found in transition {tr.source}->{tr.target}"
130
+ )
109
131
 
110
132
  if tr.target not in self.G.nodes:
111
- raise ValueError(f"Unknown node {tr.source} found in transition {tr.source}->{tr.target}")
133
+ raise ValueError(
134
+ f"Unknown node {tr.source} found in transition {tr.source}->{tr.target}"
135
+ )
112
136
 
113
137
  if self.G.has_edge(tr.source, tr.target):
114
138
  print(f"[warning] Edge already exists: {tr.source}->{tr.target}")
@@ -130,7 +154,7 @@ class MetaworkflowGraph:
130
154
 
131
155
  if not self._validation_suspended:
132
156
  source_wf = self.get_workflow_by_id(source)
133
- self.validate_param_references(source_wf)
157
+ self.validate_param_references(source_wf) # type: ignore
134
158
 
135
159
  def update_global_options(self, glob: GlobalOptions):
136
160
  old_globals = self.global_options
@@ -143,22 +167,41 @@ class MetaworkflowGraph:
143
167
  def validate_param_references(self, wf: Workflow):
144
168
  errors = []
145
169
  for ref in wf.field_refs:
146
- if ref.target_wf_id not in self.G.nodes:
147
- errors.append(WorkflowReferenceError(ref, f"Reference to unknown workflow: {ref.target_wf_id}"))
170
+ if ref.namespace != "params":
171
+ continue # output references not yet validated
172
+ referenced_wf = self.get_workflow_by_id(ref.target_wf_id)
173
+ if not referenced_wf:
174
+ errors.append(
175
+ WorkflowReferenceError(
176
+ ref, f"Reference to unknown workflow: {ref.target_wf_id}"
177
+ )
178
+ )
148
179
  continue
149
-
180
+
150
181
  if ref.target_wf_id not in list(self.G.predecessors(wf.id)):
151
- errors.append(WorkflowReferenceError(ref, f"Reference to workflow {ref.target_wf_id} that is not a predecessor of {wf.id}"))
182
+ errors.append(
183
+ WorkflowReferenceError(
184
+ ref,
185
+ f"Reference to workflow {ref.target_wf_id} that is not a predecessor of {wf.id}",
186
+ )
187
+ )
152
188
  continue
153
189
 
154
- referenced_wf = self.get_workflow_by_id(ref.target_wf_id)
155
190
  if not referenced_wf.params or not ref.target_key:
156
- errors.append(WorkflowReferenceError(ref, f"No referencable params in workflow {referenced_wf.id}"))
191
+ errors.append(
192
+ WorkflowReferenceError(
193
+ ref, f"No referencable params in workflow {referenced_wf.id}"
194
+ )
195
+ )
157
196
  continue
158
-
197
+
159
198
  if not referenced_wf.params.get(ref.target_key):
160
- errors.append(WorkflowReferenceError(ref, f"Reference to unresolvable param: {ref.target_key}"))
161
-
199
+ errors.append(
200
+ WorkflowReferenceError(
201
+ ref, f"Reference to unresolvable param: {ref.target_key}"
202
+ )
203
+ )
204
+
162
205
  if errors:
163
206
  raise WorkflowReferenceErrors(errors)
164
207
 
@@ -191,51 +234,55 @@ class MetaworkflowGraph:
191
234
  # ===========================
192
235
  # EXPORT BACK TO CONFIG
193
236
  # ===========================
194
- def to_config(self) -> Dict[str, Any]:
237
+ def to_config(self) -> MetaworkflowConfig:
195
238
  workflows = self.get_workflows()
196
239
  transitions = self.get_transitions()
197
240
 
198
- return MetaworkflowConfig.model_validate({
199
- "config_version": self.config_version,
200
- "globals": self.global_options,
201
- "workflows": workflows,
202
- "transitions": transitions,
203
- })
241
+ return MetaworkflowConfig.model_validate(
242
+ {
243
+ "config_version": self.config_version,
244
+ "globals": self.global_options,
245
+ "workflows": workflows,
246
+ "transitions": transitions,
247
+ }
248
+ )
204
249
 
205
- def to_file(self, file: Path|str) -> None:
250
+ def to_file(self, file: Path | str) -> None:
206
251
  dump_config(self.to_config(), Path(file))
207
252
 
208
253
  # ===========================
209
254
  # UTILITIES
210
255
  # ===========================
211
- def get_workflow_by_id(self, id: str) -> Workflow:
256
+ def get_workflow_by_id(self, id: str) -> Optional[Workflow]:
212
257
  try:
213
258
  return self.G.nodes[id].get("workflow")
214
259
  except KeyError:
215
260
  return None
216
261
 
217
- def get_transition(self, source: str, target: str) -> Transition:
262
+ def get_transition(self, source: str, target: str) -> Optional[Transition]:
218
263
  try:
219
264
  return self.G.edges[(source, target)].get("transition")
220
265
  except KeyError:
221
266
  return None
222
267
 
223
268
  def get_transitions(self) -> list[Transition]:
224
- return [self.get_transition(*e) for e in self.G.edges]
269
+ return [self.get_transition(*e) for e in self.G.edges] # type: ignore
225
270
 
226
271
  def get_workflows(self) -> list[Workflow]:
227
- return [self.get_workflow_by_id(n) for n in self.G.nodes]
272
+ return [self.get_workflow_by_id(n) for n in self.G.nodes] # type: ignore
228
273
 
229
274
  def get_workflows_sorted(self) -> list[Workflow]:
230
275
  """Returns workflows in valid execution order."""
231
276
  nodes_sorted = list(nx.topological_sort(self.G))
232
277
  workflows = [self.get_workflow_by_id(n) for n in nodes_sorted]
233
- return workflows
278
+ return workflows # type: ignore
234
279
 
235
- def subset_workflows(self,
236
- start: Optional[str] = None,
237
- target: Optional[str] = None,
238
- workflows: Optional[list[Workflow]] = None):
280
+ def subset_workflows(
281
+ self,
282
+ start: Optional[str] = None,
283
+ target: Optional[str] = None,
284
+ workflows: Optional[list[Workflow]] = None,
285
+ ):
239
286
 
240
287
  if workflows is None:
241
288
  workflows = self.get_workflows_sorted()
@@ -264,10 +311,10 @@ class MetaworkflowGraph:
264
311
  return workflows_sorted[0]
265
312
 
266
313
  def successors(self, wf: Workflow) -> list[Workflow]:
267
- return [self.get_workflow_by_id(n) for n in self.G.successors(wf.id)]
314
+ return [self.get_workflow_by_id(n) for n in self.G.successors(wf.id)] # type: ignore
268
315
 
269
316
  def predecessors(self, wf: Workflow) -> list[Workflow]:
270
- return [self.get_workflow_by_id(n) for n in self.G.predecessors(wf.id)]
317
+ return [self.get_workflow_by_id(n) for n in self.G.predecessors(wf.id)] # type: ignore
271
318
 
272
319
  @contextmanager
273
320
  def deferred_validation(self):