ceci 2.6__tar.gz → 2.7__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 (106) hide show
  1. {ceci-2.6 → ceci-2.7}/.github/workflows/ci.yml +3 -3
  2. {ceci-2.6 → ceci-2.7}/PKG-INFO +1 -1
  3. {ceci-2.6 → ceci-2.7}/ceci/_version.py +3 -3
  4. {ceci-2.6 → ceci-2.7}/ceci/pipeline/__init__.py +1 -1
  5. {ceci-2.6 → ceci-2.7}/ceci/pipeline/dry_run.py +16 -6
  6. ceci-2.7/ceci/pipeline/flow_chart.py +16 -0
  7. {ceci-2.6 → ceci-2.7}/ceci/pipeline/graph.py +206 -76
  8. {ceci-2.6 → ceci-2.7}/ceci/pipeline/mini.py +31 -23
  9. {ceci-2.6 → ceci-2.7}/ceci/pipeline/parsl.py +6 -3
  10. {ceci-2.6 → ceci-2.7}/ceci/pipeline/pipeline.py +165 -76
  11. {ceci-2.6 → ceci-2.7}/ceci/sites/nersc.py +2 -15
  12. {ceci-2.6 → ceci-2.7}/ceci/stage.py +3 -3
  13. {ceci-2.6 → ceci-2.7}/ceci.egg-info/PKG-INFO +1 -1
  14. {ceci-2.6 → ceci-2.7}/ceci.egg-info/SOURCES.txt +1 -1
  15. {ceci-2.6 → ceci-2.7}/ceci.egg-info/scm_file_list.json +1 -1
  16. ceci-2.7/ceci.egg-info/scm_version.json +8 -0
  17. {ceci-2.6 → ceci-2.7}/nb/ceci_interactive_example.ipynb +2 -5
  18. {ceci-2.6 → ceci-2.7}/tests/test_interactive.py +7 -11
  19. {ceci-2.6 → ceci-2.7}/tests/test_pipeline.py +4 -0
  20. ceci-2.7/tests/test_pipeline_graph.py +220 -0
  21. ceci-2.6/ceci/pipeline/file_manager.py +0 -73
  22. ceci-2.6/ceci/pipeline/flow_chart.py +0 -8
  23. ceci-2.6/ceci.egg-info/scm_version.json +0 -8
  24. {ceci-2.6 → ceci-2.7}/.coveragerc +0 -0
  25. {ceci-2.6 → ceci-2.7}/.github/workflows/publish.yml +0 -0
  26. {ceci-2.6 → ceci-2.7}/.gitignore +0 -0
  27. {ceci-2.6 → ceci-2.7}/.pylintrc +0 -0
  28. {ceci-2.6 → ceci-2.7}/.readthedocs.yml +0 -0
  29. {ceci-2.6 → ceci-2.7}/LICENSE +0 -0
  30. {ceci-2.6 → ceci-2.7}/README.md +0 -0
  31. {ceci-2.6 → ceci-2.7}/ceci/__init__.py +0 -0
  32. {ceci-2.6 → ceci-2.7}/ceci/__main__.py +0 -0
  33. {ceci-2.6 → ceci-2.7}/ceci/config.py +0 -0
  34. {ceci-2.6 → ceci-2.7}/ceci/errors.py +0 -0
  35. {ceci-2.6 → ceci-2.7}/ceci/file_types.py +0 -0
  36. {ceci-2.6 → ceci-2.7}/ceci/main.py +0 -0
  37. {ceci-2.6 → ceci-2.7}/ceci/minirunner.py +0 -0
  38. {ceci-2.6 → ceci-2.7}/ceci/monitor.py +0 -0
  39. {ceci-2.6 → ceci-2.7}/ceci/pipeline/sec.py +0 -0
  40. {ceci-2.6 → ceci-2.7}/ceci/pipeline/templates.py +0 -0
  41. {ceci-2.6 → ceci-2.7}/ceci/sites/__init__.py +0 -0
  42. {ceci-2.6 → ceci-2.7}/ceci/sites/ccin2p3.py +0 -0
  43. {ceci-2.6 → ceci-2.7}/ceci/sites/local.py +0 -0
  44. {ceci-2.6 → ceci-2.7}/ceci/sites/site.py +0 -0
  45. {ceci-2.6 → ceci-2.7}/ceci/tools/__init__.py +0 -0
  46. {ceci-2.6 → ceci-2.7}/ceci/tools/ancestors.py +0 -0
  47. {ceci-2.6 → ceci-2.7}/ceci/update_pipelines_for_ceci_2.py +0 -0
  48. {ceci-2.6 → ceci-2.7}/ceci/utils.py +0 -0
  49. {ceci-2.6 → ceci-2.7}/ceci.egg-info/dependency_links.txt +0 -0
  50. {ceci-2.6 → ceci-2.7}/ceci.egg-info/entry_points.txt +0 -0
  51. {ceci-2.6 → ceci-2.7}/ceci.egg-info/requires.txt +0 -0
  52. {ceci-2.6 → ceci-2.7}/ceci.egg-info/top_level.txt +0 -0
  53. {ceci-2.6 → ceci-2.7}/ceci.png +0 -0
  54. {ceci-2.6 → ceci-2.7}/ceci_example/__init__.py +0 -0
  55. {ceci-2.6 → ceci-2.7}/ceci_example/__main__.py +0 -0
  56. {ceci-2.6 → ceci-2.7}/ceci_example/example_stages.py +0 -0
  57. {ceci-2.6 → ceci-2.7}/ceci_example/types.py +0 -0
  58. {ceci-2.6 → ceci-2.7}/ceci_example_namespace/example_stages.py +0 -0
  59. {ceci-2.6 → ceci-2.7}/do_cover.sh +0 -0
  60. {ceci-2.6 → ceci-2.7}/docs/Makefile +0 -0
  61. {ceci-2.6 → ceci-2.7}/docs/conf.py +0 -0
  62. {ceci-2.6 → ceci-2.7}/docs/config.rst +0 -0
  63. {ceci-2.6 → ceci-2.7}/docs/config2.rst +0 -0
  64. {ceci-2.6 → ceci-2.7}/docs/index.rst +0 -0
  65. {ceci-2.6 → ceci-2.7}/docs/installation.rst +0 -0
  66. {ceci-2.6 → ceci-2.7}/docs/launchers.rst +0 -0
  67. {ceci-2.6 → ceci-2.7}/docs/logo.png +0 -0
  68. {ceci-2.6 → ceci-2.7}/docs/overview.rst +0 -0
  69. {ceci-2.6 → ceci-2.7}/docs/requirements.txt +0 -0
  70. {ceci-2.6 → ceci-2.7}/docs/sites.rst +0 -0
  71. {ceci-2.6 → ceci-2.7}/docs/stages.rst +0 -0
  72. {ceci-2.6 → ceci-2.7}/docs/tutorial.rst +0 -0
  73. {ceci-2.6 → ceci-2.7}/pyproject.toml +0 -0
  74. {ceci-2.6 → ceci-2.7}/setup.cfg +0 -0
  75. {ceci-2.6 → ceci-2.7}/setup.py +0 -0
  76. {ceci-2.6 → ceci-2.7}/tests/__init__.py +0 -0
  77. {ceci-2.6 → ceci-2.7}/tests/config.yml +0 -0
  78. {ceci-2.6 → ceci-2.7}/tests/config_nm.yml +0 -0
  79. {ceci-2.6 → ceci-2.7}/tests/create_registry_entries.py +0 -0
  80. {ceci-2.6 → ceci-2.7}/tests/inputs/dm.txt +0 -0
  81. {ceci-2.6 → ceci-2.7}/tests/inputs/fiducial_cosmology.txt +0 -0
  82. {ceci-2.6 → ceci-2.7}/tests/north.yml +0 -0
  83. {ceci-2.6 → ceci-2.7}/tests/template.yml +0 -0
  84. {ceci-2.6 → ceci-2.7}/tests/test.hdf5 +0 -0
  85. {ceci-2.6 → ceci-2.7}/tests/test.yml +0 -0
  86. {ceci-2.6 → ceci-2.7}/tests/test_cmd.py +0 -0
  87. {ceci-2.6 → ceci-2.7}/tests/test_dask.py +0 -0
  88. {ceci-2.6 → ceci-2.7}/tests/test_dataregistry.yml +0 -0
  89. {ceci-2.6 → ceci-2.7}/tests/test_executables.py +0 -0
  90. {ceci-2.6 → ceci-2.7}/tests/test_flow_chart.py +0 -0
  91. {ceci-2.6 → ceci-2.7}/tests/test_graph.py +0 -0
  92. {ceci-2.6 → ceci-2.7}/tests/test_helpers.py +0 -0
  93. {ceci-2.6 → ceci-2.7}/tests/test_io.py +0 -0
  94. {ceci-2.6 → ceci-2.7}/tests/test_main.py +0 -0
  95. {ceci-2.6 → ceci-2.7}/tests/test_minirunner.py +0 -0
  96. {ceci-2.6 → ceci-2.7}/tests/test_monitor.py +0 -0
  97. {ceci-2.6 → ceci-2.7}/tests/test_namespace.yml +0 -0
  98. {ceci-2.6 → ceci-2.7}/tests/test_python_paths.py +0 -0
  99. {ceci-2.6 → ceci-2.7}/tests/test_site.py +0 -0
  100. {ceci-2.6 → ceci-2.7}/tests/test_snapshot.py +0 -0
  101. {ceci-2.6 → ceci-2.7}/tests/test_stage.py +0 -0
  102. {ceci-2.6 → ceci-2.7}/tests/test_update_script.py +0 -0
  103. {ceci-2.6 → ceci-2.7}/tests/test_utils.py +0 -0
  104. {ceci-2.6 → ceci-2.7}/tests/update_test_files/config.yml +0 -0
  105. {ceci-2.6 → ceci-2.7}/tests/update_test_files/pipeline1.yml +0 -0
  106. {ceci-2.6 → ceci-2.7}/tests/update_test_files/pipeline2.yml +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]
18
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
19
19
 
20
20
  steps:
21
21
  - name: Checkout repository
@@ -72,7 +72,7 @@ jobs:
72
72
 
73
73
  strategy:
74
74
  matrix:
75
- python-version: [3.9]
75
+ python-version: [3.12]
76
76
 
77
77
  steps:
78
78
  - name: Checkout repository
@@ -103,7 +103,7 @@ jobs:
103
103
  echo "sqlalchemy.url : postgresql://postgres:postgres@localhost:5432/desc_data_registry" > $DATAREG_CONFIG
104
104
  # Install the data registry
105
105
  cd dataregistry
106
- python3 -m pip install .
106
+ python3 -m pip install . psycopg
107
107
  # Create the data registry schema
108
108
  python3 scripts/create_registry_db.py --config $DATAREG_CONFIG
109
109
  # Create some dummy data registry entries
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ceci
3
- Version: 2.6
3
+ Version: 2.7
4
4
  Summary: Lightweight pipeline engine for LSST DESC
5
5
  Author: Joe Zuntz
6
6
  License: BSD 3-Clause License
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.6'
22
- __version_tuple__ = version_tuple = (2, 6)
21
+ __version__ = version = '2.7'
22
+ __version_tuple__ = version_tuple = (2, 7)
23
23
 
24
- __commit_id__ = commit_id = 'gcf592d97c'
24
+ __commit_id__ = commit_id = 'g6fabd7fa4'
@@ -11,5 +11,5 @@ from .parsl import ParslPipeline
11
11
  from .dry_run import DryRunPipeline
12
12
  from .flow_chart import FlowChartPipeline
13
13
  from .mini import MiniPipeline
14
- from .file_manager import FileManager
15
14
  from .sec import StageExecutionConfig
15
+ from .graph import StageStatus
@@ -13,16 +13,19 @@ class DryRunPipeline(Pipeline):
13
13
  """
14
14
 
15
15
  def initiate_run(self, overall_inputs):
16
- return []
16
+ return {
17
+ "commands": [],
18
+ "completed_stages": [],
19
+ }
17
20
 
18
21
  def should_skip_stage(self, stage):
19
22
  return False
20
23
 
21
- def enqueue_job(self, stage, pipeline_files):
24
+ def enqueue_job(self, stage, input_paths):
22
25
  outputs = stage.find_outputs(self.run_config["output_dir"])
23
- sec = self.stage_execution_config[stage.instance_name]
26
+ sec = self.get_stage_execution_config(stage.instance_name)
24
27
 
25
- cmd = sec.generate_full_command(pipeline_files, outputs, self.stages_config)
28
+ cmd = sec.generate_full_command(input_paths, outputs, self.stages_config)
26
29
 
27
30
  # Replace the first instance of the stage name with bold
28
31
  # text, but only if we are printing to screen. This helps the
@@ -30,13 +33,20 @@ class DryRunPipeline(Pipeline):
30
33
  if sys.stdout.isatty():
31
34
  cmd = cmd.replace(stage.instance_name, embolden(stage.instance_name), 1)
32
35
 
33
- self.run_info.append(cmd)
36
+ # make a list of complete stages and print at the end
37
+ if self.run_config["resume"] in (True, "resume") and super().should_skip_stage(stage):
38
+ self.run_info["completed_stages"].append(stage.instance_name)
39
+
40
+ self.run_info["commands"].append(cmd)
34
41
  return outputs
35
42
 
36
43
  def run_jobs(self):
37
- for cmd in self.run_info:
44
+ for cmd in self.run_info["commands"]:
38
45
  print(cmd)
39
46
  print("\n")
47
+ print("\n")
48
+ for name in self.run_info["completed_stages"]:
49
+ print(f"# {name} is complete and could be skipped")
40
50
  return 0
41
51
 
42
52
  def find_all_outputs(self):
@@ -0,0 +1,16 @@
1
+ from .dry_run import DryRunPipeline
2
+
3
+ class FlowChartPipeline(DryRunPipeline):
4
+ def run_jobs(self):
5
+ filename = self.run_config["flow_chart"]
6
+ self.make_flow_chart(filename)
7
+ return 0
8
+
9
+ def setup_data_registry(self, registry_config):
10
+ """Flow charts never read data, so do not connect to the data registry."""
11
+ return None
12
+
13
+ def data_registry_lookup(self, info):
14
+ """Return a placeholder path instead of querying the data registry."""
15
+ return f"registry:{info}"
16
+
@@ -1,71 +1,156 @@
1
+ import enum
1
2
  import networkx
2
3
  import collections
3
4
 
4
- def build_graph(stage_names, stage_classes, stage_aliases, overall_inputs):
5
+
6
+ class StageStatus(enum.Enum):
7
+ """The run status of a pipeline stage, stored on its graph node"""
8
+
9
+ PENDING = "pending"
10
+ QUEUED = "queued"
11
+ RUNNING = "running"
12
+ SKIPPED = "skipped"
13
+ COMPLETE = "complete"
14
+ FAILED = "failed"
15
+
16
+
17
+ # Key in the graph attributes dictionary where the static stage ordering
18
+ # is cached. Functions in this module that change the stages or connections
19
+ # in a graph invalidate it.
20
+ STATIC_ORDERING = "static_ordering"
21
+
22
+
23
+ def invalidate_static_ordering(graph):
24
+ """Clear the cached static ordering of a graph, if there is one.
25
+
26
+ This must be called when stages or edges are added to or removed from
27
+ the graph other than through the functions in this module.
28
+
29
+ Parameters
30
+ ----------
31
+ graph : `networkx.DiGraph`
32
+ The graph to modify
5
33
  """
6
- Build a directed graph of the pipeline stages and their inputs and outputs.
34
+ graph.graph.pop(STATIC_ORDERING, None)
35
+
36
+
37
+ def add_stage_node(graph, stage_name, stage_class, aliases, **attrs):
38
+ """
39
+ Add a stage, and the output files it generates, to a pipeline graph.
40
+
41
+ The inputs to the stage are not connected here, since they may not
42
+ exist in the graph yet; use `connect_stage_inputs` for that.
7
43
 
8
- This also checks for various pipeline problems like missing inputs,
9
- multiple stages generating the same output, and cyclic dependencies.
10
-
11
44
  Parameters
12
45
  ----------
13
- stage_names : `list` of `str`
14
- The names of the stages in the pipeline
15
- stage_classes : `list` of `PipelineStage` subclasses
16
- The classes of the stages in the pipeline
17
- stage_aliases : `list` of `dict`
18
- A list of dictionaries mapping tags to aliases for each stage
19
- overall_inputs : `dict` of `str: str`
20
- A list of overall input files that are not generated by any stage but are needed by the pipeline
46
+ graph : `networkx.DiGraph`
47
+ The graph to add to
48
+ stage_name : `str`
49
+ The name of the stage
50
+ stage_class : `PipelineStage` subclass
51
+ The class of the stage
52
+ aliases : `dict`
53
+ Dictionary mapping tags to aliases for this stage
54
+ **attrs
55
+ Any additional attributes to set on the stage node
56
+ """
57
+ if stage_name in graph.nodes:
58
+ raise ValueError(f"Stage {stage_name} is used multiple times in the pipeline")
59
+
60
+ # Check all the outputs before changing anything, so that
61
+ # a failure leaves the graph unchanged
62
+ outputs = []
63
+ for tag1, ftype in stage_class.outputs_():
64
+ tag = aliases.get(tag1, tag1)
65
+ node = graph.nodes.get(tag)
66
+ if node is None:
67
+ pass
68
+ elif node["type"] == "output":
69
+ raise ValueError(f"Output {tag} is generated by multiple stages: {[node['creator'], stage_name]}")
70
+ else:
71
+ raise ValueError(f"Tag {tag} is generated by pipeline stage {stage_name} but is also listed as an overall input to the pipeline")
72
+ outputs.append((tag, tag1, ftype))
73
+
74
+ invalidate_static_ordering(graph)
75
+ graph.add_node(
76
+ stage_name,
77
+ type="stage",
78
+ stage_class=stage_class,
79
+ aliases=aliases,
80
+ status=StageStatus.PENDING,
81
+ **attrs,
82
+ )
83
+ for tag, tag1, ftype in outputs:
84
+ graph.add_node(tag, type="output", creator=stage_name, original_tag=tag1, ftype=ftype)
85
+ graph.add_edge(stage_name, tag)
86
+
87
+
88
+ def add_input_node(graph, tag, **attrs):
89
+ """
90
+ Add (or update) an overall input file in a pipeline graph.
21
91
 
22
- Returns
23
- -------
92
+ Parameters
93
+ ----------
24
94
  graph : `networkx.DiGraph`
25
- A directed graph representing the pipeline stages, their inputs, and outputs
95
+ The graph to add to
96
+ tag : `str`
97
+ The tag for the file
98
+ **attrs
99
+ Any additional attributes, e.g. path, to set on the node
26
100
  """
101
+ node = graph.nodes.get(tag)
102
+ if node is not None and node["type"] != "input":
103
+ creator = node.get("creator", tag)
104
+ raise ValueError(f"Tag {tag} is generated by pipeline stage {creator} but is also listed as an overall input to the pipeline")
105
+ graph.add_node(tag, type="input", **attrs)
27
106
 
28
- # Build a graph with all the stages and files in it
29
- graph = networkx.DiGraph()
30
107
 
31
- # First add all the overall inputs
32
- for inp in overall_inputs:
33
- graph.add_node(inp, type="input")
34
-
35
- # Now go through all the stages. We do this in two passes,
36
- # one to add the stages themselves and their outputs, and one
37
- # to connect in all their inputs
38
- for stage_name, stage_class, aliases in zip(stage_names, stage_classes, stage_aliases):
39
- if stage_name in graph.nodes:
40
- raise ValueError(f"Stage {stage_name} is used multiple times in the pipeline")
41
- graph.add_node(stage_name, type="stage", stage_class=stage_class)
42
-
43
- # Now set up all the outputs of the stage, first checking to see if it has been aliased
44
- for tag1 in stage_class.output_tags():
45
- tag = aliases.get(tag1, tag1)
46
- if tag in overall_inputs:
47
- raise ValueError(f"Tag {tag} is generated by pipeline stage {stage_name} but is also listed as an overall input to the pipeline")
48
- graph.add_node(tag, type="output", creator=stage_class, original_tag=tag1)
49
- graph.add_edge(stage_name, tag)
50
-
51
- # Now check for outputs that are generated by multiple stages
52
- for node in graph.nodes:
53
- if graph.nodes[node]["type"] == "output":
54
- parents = list(graph.predecessors(node))
55
- if len(parents) > 1:
56
- raise ValueError(f"Output {node} is generated by multiple stages: {parents}")
57
-
58
-
59
- # Now go through and add in all the inputs needed for each stage:
108
+ def remove_stage_node(graph, stage_name):
109
+ """
110
+ Remove a stage and all the outputs it generates from a pipeline graph.
111
+
112
+ Parameters
113
+ ----------
114
+ graph : `networkx.DiGraph`
115
+ The graph to modify
116
+ stage_name : `str`
117
+ The name of the stage
118
+ """
119
+ outputs = [n for n in graph.successors(stage_name) if graph.nodes[n]["type"] == "output"]
120
+ invalidate_static_ordering(graph)
121
+ graph.remove_nodes_from(outputs + [stage_name])
122
+
123
+
124
+ def stage_nodes(graph):
125
+ """Return the names of the stage nodes in a graph, in insertion order"""
126
+ return [n for n, t in graph.nodes(data="type") if t == "stage"]
127
+
128
+
129
+ def connect_stage_inputs(graph):
130
+ """
131
+ Connect the input files of each stage in the graph to that stage,
132
+ and check that the result is a valid pipeline, with no missing
133
+ inputs or cycles.
134
+
135
+ This can safely be called more than once.
136
+
137
+ Parameters
138
+ ----------
139
+ graph : `networkx.DiGraph`
140
+ The graph to modify
141
+ """
142
+ invalidate_static_ordering(graph)
60
143
  missing = collections.defaultdict(list)
61
- for stage_name, stage_class, aliases in zip(stage_names, stage_classes, stage_aliases):
62
- for tag in stage_class.input_tags():
144
+ for stage_name in stage_nodes(graph):
145
+ node = graph.nodes[stage_name]
146
+ aliases = node["aliases"]
147
+ for tag in node["stage_class"].input_tags():
63
148
  tag = aliases.get(tag, tag)
64
149
  if tag not in graph.nodes:
65
150
  missing[tag].append(stage_name)
66
151
  else:
67
152
  graph.add_edge(tag, stage_name)
68
-
153
+
69
154
  if missing:
70
155
  msg = []
71
156
  for tag, stages in missing.items():
@@ -74,27 +159,79 @@ def build_graph(stage_names, stage_classes, stage_aliases, overall_inputs):
74
159
  msg = "\n".join(msg)
75
160
  raise ValueError(f"The following files are needed by the pipeline but not generated by any stage:\n{msg}")
76
161
 
77
- # Now check for cycles in the graph
78
- cycles = list(networkx.simple_cycles(graph))
79
- if cycles:
162
+ # Now check for cycles in the graph, only listing them if there are any
163
+ if not networkx.is_directed_acyclic_graph(graph):
80
164
  msg = []
81
- for cycle in cycles:
165
+ for cycle in networkx.simple_cycles(graph):
82
166
  cycle_text = " -> ".join(cycle)
83
167
  cycle_text += " -> " + cycle[0]
84
168
  msg.append(f"- {cycle_text}")
169
+ msg = "\n".join(msg)
85
170
  raise ValueError(f"Your pipeline is cyclic. The following cycles were found:\n{msg}")
86
-
171
+
172
+
173
+ def build_graph(stage_names, stage_classes, stage_aliases, overall_inputs):
174
+ """
175
+ Build a directed graph of the pipeline stages and their inputs and outputs.
176
+
177
+ This also checks for various pipeline problems like missing inputs,
178
+ multiple stages generating the same output, and cyclic dependencies.
179
+
180
+ Parameters
181
+ ----------
182
+ stage_names : `list` of `str`
183
+ The names of the stages in the pipeline
184
+ stage_classes : `list` of `PipelineStage` subclasses
185
+ The classes of the stages in the pipeline
186
+ stage_aliases : `list` of `dict`
187
+ A list of dictionaries mapping tags to aliases for each stage
188
+ overall_inputs : `dict` of `str: str`
189
+ A list of overall input files that are not generated by any stage but are needed by the pipeline
190
+
191
+ Returns
192
+ -------
193
+ graph : `networkx.DiGraph`
194
+ A directed graph representing the pipeline stages, their inputs, and outputs
195
+ """
196
+ graph = networkx.DiGraph()
197
+ for inp in overall_inputs:
198
+ add_input_node(graph, inp)
199
+ for stage_name, stage_class, aliases in zip(stage_names, stage_classes, stage_aliases):
200
+ add_stage_node(graph, stage_name, stage_class, aliases)
201
+ connect_stage_inputs(graph)
87
202
  return graph
88
203
 
204
+
89
205
  def get_static_ordering(graph):
90
- # get a topological ordering of the graph
91
- # this will be a list of all the nodes in the graph, including the input and output files
92
- ordering = list(networkx.topological_sort(graph))
206
+ """
207
+ Get an order in which the stages in a pipeline graph can be run.
93
208
 
94
- # cut down to just the stages
95
- ordering = [node for node in ordering if graph.nodes[node]["type"] == "stage"]
96
-
97
- return ordering
209
+ The result is cached in the graph attributes, and re-used until
210
+ it is invalidated by a change to the graph.
211
+
212
+ Parameters
213
+ ----------
214
+ graph : `networkx.DiGraph`
215
+ The pipeline graph
216
+
217
+ Returns
218
+ -------
219
+ ordering : `list` of `str`
220
+ The names of the stages in a valid execution order
221
+ """
222
+ ordering = graph.graph.get(STATIC_ORDERING)
223
+
224
+ if ordering is None:
225
+ # get a topological ordering of the graph
226
+ # this will be a list of all the nodes in the graph, including the input and output files
227
+ ordering = list(networkx.topological_sort(graph))
228
+
229
+ # cut down to just the stages
230
+ ordering = [node for node in ordering if graph.nodes[node]["type"] == "stage"]
231
+ graph.graph[STATIC_ORDERING] = ordering
232
+
233
+ # return a copy so callers cannot modify the cached version
234
+ return list(ordering)
98
235
 
99
236
  def trim_pipeline_graph(graph, from_=None, to_=None):
100
237
  """
@@ -170,30 +307,23 @@ def trim_pipeline_graph(graph, from_=None, to_=None):
170
307
  # Create a subgraph with just these nodes
171
308
  trimmed_graph = graph.subgraph(nodes_to_keep).copy()
172
309
 
310
+ # The copy includes the graph attributes, so any cached
311
+ # ordering refers to the untrimmed graph
312
+ invalidate_static_ordering(trimmed_graph)
313
+
173
314
  # Some stages that were previously outputs of existing stages
174
315
  # must now be overall inputs to the pipeline instead because
175
316
  # the stage that generated them has been trimmed. We need to keep
176
317
  # track of this
177
318
  converted_inputs = {}
178
- for node_name in list(trimmed_graph.nodes):
179
- node = trimmed_graph.nodes[node_name]
319
+ for node_name, node in trimmed_graph.nodes(data=True):
180
320
 
181
321
  if node["type"] == "output":
182
322
  # The node will be listed as an output but will have no
183
323
  # predecessor creating it now.
184
- predecessors = list(trimmed_graph.predecessors(node_name))
185
- if not predecessors:
186
- creator_stage_class = node["creator"]
187
- original_tag = node["original_tag"]
188
- # use the original tag to find the file type object
189
- for t, typ in creator_stage_class.outputs:
190
- if t == original_tag:
191
- ftype = typ
192
- break
193
- else:
194
- raise RuntimeError("This should never happen - please open an issue")
324
+ if trimmed_graph.in_degree(node_name) == 0:
195
325
  # the node_name is the aliased_tag, so this file name should be correct
196
- file_name = ftype.make_name(node_name)
326
+ file_name = node["ftype"].make_name(node_name)
197
327
  converted_inputs[node_name] = file_name
198
328
 
199
329
  return trimmed_graph, converted_inputs
@@ -1,6 +1,7 @@
1
1
  import sys
2
2
  from .. import minirunner
3
3
  from .pipeline import Pipeline, get_default_site
4
+ from .graph import StageStatus
4
5
 
5
6
  class MiniPipeline(Pipeline):
6
7
  """A pipeline subclass that uses Minirunner, a sub-module
@@ -50,42 +51,49 @@ class MiniPipeline(Pipeline):
50
51
  Parameters
51
52
  ----------
52
53
 
53
- stages: list[PipelineStage]
54
- A list of stages to generate the DAG for
54
+ jobs: dict[str, minirunner.Job]
55
+ The jobs to generate the DAG for, keyed by stage name
55
56
 
56
57
  """
57
58
  depend = {}
58
59
 
59
60
  # for each stage in our pipeline ...
60
- for stage in self.stages[:]:
61
- if stage.instance_name not in jobs:
61
+ for stage_name in self.stage_names:
62
+ if stage_name not in jobs:
62
63
  continue
63
- job = jobs[stage.instance_name]
64
+ job = jobs[stage_name]
64
65
  depend[job] = []
65
- # check for each of the inputs for that stage ...
66
- for tag in stage.input_tags():
67
- aliased_tag = stage.get_aliased_tag(tag)
68
- for potential_parent in self.stages[:]:
69
- # if that stage is supplied by another pipeline stage
70
- if potential_parent.instance_name not in jobs: # pragma: no cover
71
- continue
72
- potential_parent_tags = [
73
- potential_parent.get_aliased_tag(tag_)
74
- for tag_ in potential_parent.output_tags()
75
- ]
76
- if aliased_tag in potential_parent_tags:
77
- depend[job].append(jobs[potential_parent.instance_name])
66
+ # the stages it depends on are the ones that make
67
+ # its input files. Stages that are not being run
68
+ # are not included
69
+ for tag in self.graph.predecessors(stage_name):
70
+ for parent in self.graph.predecessors(tag):
71
+ if parent in jobs:
72
+ depend[job].append(jobs[parent])
78
73
  return depend
79
74
 
75
+ def _status_callback(self, event_type, event_info):
76
+ """Record job events as stage status in the graph, and
77
+ then pass them on to any user callback."""
78
+ status = {
79
+ minirunner.EVENT_LAUNCH: StageStatus.RUNNING,
80
+ minirunner.EVENT_COMPLETED: StageStatus.COMPLETE,
81
+ minirunner.EVENT_FAIL: StageStatus.FAILED,
82
+ }.get(event_type)
83
+ if status is not None:
84
+ self.set_stage_status(event_info["job"].name, status)
85
+ if self.callback is not None:
86
+ self.callback(event_type, event_info)
87
+
80
88
  def initiate_run(self, overall_inputs):
81
89
  jobs = {}
82
90
  stages = []
83
91
  return jobs, stages
84
92
 
85
- def enqueue_job(self, stage, pipeline_files):
86
- sec = self.stage_execution_config[stage.instance_name]
93
+ def enqueue_job(self, stage, input_paths):
94
+ sec = self.get_stage_execution_config(stage.instance_name)
87
95
  outputs = stage.find_outputs(self.run_config["output_dir"])
88
- cmd = sec.generate_full_command(pipeline_files, outputs, self.stages_config)
96
+ cmd = sec.generate_full_command(input_paths, outputs, self.stages_config)
89
97
  job = minirunner.Job(
90
98
  stage.instance_name,
91
99
  cmd,
@@ -106,12 +114,12 @@ class MiniPipeline(Pipeline):
106
114
  # This pipeline, being mainly for testing, can only
107
115
  # run at a single site, so we can assume here that the
108
116
  # sites are all the same
109
- sec = self.stage_execution_config[self.stage_names[0]]
117
+ sec = self.get_stage_execution_config(self.stage_names[0])
110
118
  nodes = sec.site.info["nodes"]
111
119
 
112
120
  # Run under minirununer
113
121
  runner = minirunner.Runner(
114
- nodes, graph, log_dir, callback=self.callback, sleep=self.sleep
122
+ nodes, graph, log_dir, callback=self._status_callback, sleep=self.sleep
115
123
  )
116
124
  interval = self.launcher_config.get("interval", 3)
117
125
  try:
@@ -2,6 +2,7 @@ import sys
2
2
  import os
3
3
 
4
4
  from .pipeline import Pipeline
5
+ from .graph import StageStatus
5
6
 
6
7
 
7
8
  class ParslPipeline(Pipeline):
@@ -10,7 +11,7 @@ class ParslPipeline(Pipeline):
10
11
  def initiate_run(self, overall_inputs):
11
12
  return [] # list of futures
12
13
 
13
- def enqueue_job(self, stage, pipeline_files):
14
+ def enqueue_job(self, stage, input_paths):
14
15
 
15
16
  from parsl.data_provider.files import File
16
17
 
@@ -22,7 +23,7 @@ class ParslPipeline(Pipeline):
22
23
  # parsl wants.
23
24
  # The inputs that exist already need to be converted into Parsl File objects.
24
25
  # The ones that don't stay as data futures
25
- inputs1 = stage.find_inputs(pipeline_files)
26
+ inputs1 = stage.find_inputs(input_paths)
26
27
  inputs = [
27
28
  File(val) if isinstance(val, str) else val for val in inputs1.values()
28
29
  ]
@@ -47,8 +48,10 @@ class ParslPipeline(Pipeline):
47
48
  try:
48
49
  # This waits for b/g pipeline completion.
49
50
  future.result()
51
+ self.set_stage_status(stage_name, StageStatus.COMPLETE)
50
52
  # Parsl emits this on any non-zero status code.
51
53
  except BashExitFailure:
54
+ self.set_stage_status(stage_name, StageStatus.FAILED)
52
55
  stdout_file = f"{log_dir}/{stage_name}.err"
53
56
  stderr_file = f"{log_dir}/{stage_name}.out"
54
57
  sys.stderr.write(
@@ -138,7 +141,7 @@ Standard error:
138
141
  config = f"{{inputs[{config_index}]}}"
139
142
 
140
143
  # This includes all the "mpirun" stuff.
141
- sec = self.stage_execution_config[stage.instance_name]
144
+ sec = self.get_stage_execution_config(stage.instance_name)
142
145
  executor = sec.site.info["executor"]
143
146
 
144
147
  # Construct the command line call