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.
- {ceci-2.6 → ceci-2.7}/.github/workflows/ci.yml +3 -3
- {ceci-2.6 → ceci-2.7}/PKG-INFO +1 -1
- {ceci-2.6 → ceci-2.7}/ceci/_version.py +3 -3
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/__init__.py +1 -1
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/dry_run.py +16 -6
- ceci-2.7/ceci/pipeline/flow_chart.py +16 -0
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/graph.py +206 -76
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/mini.py +31 -23
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/parsl.py +6 -3
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/pipeline.py +165 -76
- {ceci-2.6 → ceci-2.7}/ceci/sites/nersc.py +2 -15
- {ceci-2.6 → ceci-2.7}/ceci/stage.py +3 -3
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/PKG-INFO +1 -1
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/SOURCES.txt +1 -1
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/scm_file_list.json +1 -1
- ceci-2.7/ceci.egg-info/scm_version.json +8 -0
- {ceci-2.6 → ceci-2.7}/nb/ceci_interactive_example.ipynb +2 -5
- {ceci-2.6 → ceci-2.7}/tests/test_interactive.py +7 -11
- {ceci-2.6 → ceci-2.7}/tests/test_pipeline.py +4 -0
- ceci-2.7/tests/test_pipeline_graph.py +220 -0
- ceci-2.6/ceci/pipeline/file_manager.py +0 -73
- ceci-2.6/ceci/pipeline/flow_chart.py +0 -8
- ceci-2.6/ceci.egg-info/scm_version.json +0 -8
- {ceci-2.6 → ceci-2.7}/.coveragerc +0 -0
- {ceci-2.6 → ceci-2.7}/.github/workflows/publish.yml +0 -0
- {ceci-2.6 → ceci-2.7}/.gitignore +0 -0
- {ceci-2.6 → ceci-2.7}/.pylintrc +0 -0
- {ceci-2.6 → ceci-2.7}/.readthedocs.yml +0 -0
- {ceci-2.6 → ceci-2.7}/LICENSE +0 -0
- {ceci-2.6 → ceci-2.7}/README.md +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/__init__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/__main__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/config.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/errors.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/file_types.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/main.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/minirunner.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/monitor.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/sec.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/pipeline/templates.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/sites/__init__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/sites/ccin2p3.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/sites/local.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/sites/site.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/tools/__init__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/tools/ancestors.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/update_pipelines_for_ceci_2.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci/utils.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/dependency_links.txt +0 -0
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/entry_points.txt +0 -0
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/requires.txt +0 -0
- {ceci-2.6 → ceci-2.7}/ceci.egg-info/top_level.txt +0 -0
- {ceci-2.6 → ceci-2.7}/ceci.png +0 -0
- {ceci-2.6 → ceci-2.7}/ceci_example/__init__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci_example/__main__.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci_example/example_stages.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci_example/types.py +0 -0
- {ceci-2.6 → ceci-2.7}/ceci_example_namespace/example_stages.py +0 -0
- {ceci-2.6 → ceci-2.7}/do_cover.sh +0 -0
- {ceci-2.6 → ceci-2.7}/docs/Makefile +0 -0
- {ceci-2.6 → ceci-2.7}/docs/conf.py +0 -0
- {ceci-2.6 → ceci-2.7}/docs/config.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/config2.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/index.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/installation.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/launchers.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/logo.png +0 -0
- {ceci-2.6 → ceci-2.7}/docs/overview.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/requirements.txt +0 -0
- {ceci-2.6 → ceci-2.7}/docs/sites.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/stages.rst +0 -0
- {ceci-2.6 → ceci-2.7}/docs/tutorial.rst +0 -0
- {ceci-2.6 → ceci-2.7}/pyproject.toml +0 -0
- {ceci-2.6 → ceci-2.7}/setup.cfg +0 -0
- {ceci-2.6 → ceci-2.7}/setup.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/__init__.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/config.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/config_nm.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/create_registry_entries.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/inputs/dm.txt +0 -0
- {ceci-2.6 → ceci-2.7}/tests/inputs/fiducial_cosmology.txt +0 -0
- {ceci-2.6 → ceci-2.7}/tests/north.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/template.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test.hdf5 +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_cmd.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_dask.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_dataregistry.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_executables.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_flow_chart.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_graph.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_helpers.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_io.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_main.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_minirunner.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_monitor.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_namespace.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_python_paths.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_site.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_snapshot.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_stage.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_update_script.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/test_utils.py +0 -0
- {ceci-2.6 → ceci-2.7}/tests/update_test_files/config.yml +0 -0
- {ceci-2.6 → ceci-2.7}/tests/update_test_files/pipeline1.yml +0 -0
- {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: [
|
|
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.
|
|
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
|
{ceci-2.6 → ceci-2.7}/PKG-INFO
RENAMED
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 7)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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,
|
|
24
|
+
def enqueue_job(self, stage, input_paths):
|
|
22
25
|
outputs = stage.find_outputs(self.run_config["output_dir"])
|
|
23
|
-
sec = self.
|
|
26
|
+
sec = self.get_stage_execution_config(stage.instance_name)
|
|
24
27
|
|
|
25
|
-
cmd = sec.generate_full_command(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
The
|
|
15
|
-
|
|
16
|
-
The
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
24
94
|
graph : `networkx.DiGraph`
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
61
|
-
if
|
|
61
|
+
for stage_name in self.stage_names:
|
|
62
|
+
if stage_name not in jobs:
|
|
62
63
|
continue
|
|
63
|
-
job = jobs[
|
|
64
|
+
job = jobs[stage_name]
|
|
64
65
|
depend[job] = []
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
|
|
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,
|
|
86
|
-
sec = self.
|
|
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(
|
|
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.
|
|
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.
|
|
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,
|
|
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(
|
|
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.
|
|
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
|